feat: add repository

This commit is contained in:
chenos 2021-09-25 23:56:26 +08:00
parent 9d67ecaff0
commit 4651d3dddd
5 changed files with 363 additions and 9 deletions

View File

@ -20,7 +20,7 @@ export function getConfig(config = {}, options?: any): DatabaseOptions {
host: process.env.DB_HOST,
port: process.env.DB_PORT,
dialect: process.env.DB_DIALECT,
logging: process.env.DB_LOG_SQL === 'on',
// logging: process.env.DB_LOG_SQL === 'on',
sync: {
force: true,
alter: {

View File

@ -0,0 +1,156 @@
import { Collection } from '../collection';
import { Database } from '../database';
import { updateAssociation, updateAssociations } from '../update-associations';
import { mockDatabase } from './';
describe('repository', () => {
let db: Database;
let User: Collection;
let Post: Collection;
let Comment: Collection;
beforeEach(async () => {
db = mockDatabase();
User = db.collection({
name: 'users',
schema: [
{ type: 'string', name: 'name' },
{ type: 'hasMany', name: 'posts' },
],
});
Post = db.collection({
name: 'posts',
schema: [
{ type: 'string', name: 'name' },
{ type: 'hasMany', name: 'comments' },
],
});
Comment = db.collection({
name: 'comments',
schema: [{ type: 'string', name: 'name' }],
});
await db.sync();
await User.repository.bulkCreate([
{
name: 'user1',
posts: [
{
name: 'post11',
comments: [
{ name: 'comment111' },
{ name: 'comment112' },
{ name: 'comment113' },
],
},
{
name: 'post12',
comments: [
{ name: 'comment121' },
{ name: 'comment122' },
{ name: 'comment123' },
],
},
{
name: 'post13',
comments: [
{ name: 'comment131' },
{ name: 'comment132' },
{ name: 'comment133' },
],
},
{
name: 'post14',
comments: [
{ name: 'comment141' },
{ name: 'comment142' },
{ name: 'comment143' },
],
},
],
},
{
name: 'user2',
posts: [
{
name: 'post21',
comments: [
{ name: 'comment211' },
{ name: 'comment212' },
{ name: 'comment213' },
],
},
{
name: 'post22',
comments: [
{ name: 'comment221' },
{ name: 'comment222' },
{ name: 'comment223' },
],
},
{
name: 'post23',
comments: [
{ name: 'comment231' },
{ name: 'comment232' },
{ name: 'comment233' },
],
},
{ name: 'post24' },
],
},
{
name: 'user3',
posts: [
{
name: 'post31',
comments: [
{ name: 'comment311' },
{ name: 'comment312' },
{ name: 'comment313' },
],
},
{ name: 'post32' },
{
name: 'post33',
comments: [
{ name: 'comment331' },
{ name: 'comment332' },
{ name: 'comment333' },
],
},
{ name: 'post34' },
],
},
]);
});
afterEach(async () => {
await db.close();
});
it.only('findAll', async () => {
const data = await User.repository.findAll({
filter: {
'posts.comments.id': null,
},
page: 1,
pageSize: 1,
});
console.log(data.count, JSON.stringify(data.rows.map(row => row.toJSON()), null, 2));
// expect(data.toJSON()).toMatchObject({
// name: 'user3',
// });
});
it('findOne', async () => {
const data = await User.repository.findOne({
filter: {
'posts.comments.name': 'comment331',
},
});
// expect(data.toJSON()).toMatchObject({
// name: 'user3',
// });
});
});

View File

@ -3,6 +3,7 @@ import { Database } from './database';
import { Schema } from './schema';
import { RelationField } from './schema-fields';
import _ from 'lodash';
import { Repository } from './repository';
export interface CollectionOptions {
schema?: any;
@ -16,6 +17,7 @@ export interface CollectionContext {
export class Collection {
schema: Schema;
model: ModelCtor<Model>;
repository: Repository;
options: CollectionOptions;
context: CollectionContext;
@ -44,6 +46,7 @@ export class Collection {
});
this.schema2model();
this.context.database.emit('collection.init', this);
this.repository = new Repository(this);
}
schema2model() {

View File

@ -1,4 +1,4 @@
import { Sequelize, ModelCtor, Model, Options, SyncOptions } from 'sequelize';
import { Sequelize, ModelCtor, Model, Options, SyncOptions, Op, Utils } from 'sequelize';
import { EventEmitter } from 'events';
import { Collection, CollectionOptions } from './collection';
import {
@ -24,6 +24,7 @@ export class Database extends EventEmitter {
schemaTypes = new Map();
models = new Map();
repositories = new Map();
operators = new Map();
collections: Map<string, Collection>;
pendingFields = new Map<string, RelationField[]>();
@ -50,6 +51,18 @@ export class Database extends EventEmitter {
belongsTo: BelongsToField,
belongsToMany: BelongsToManyField,
});
const operators = new Map();
// Sequelize 内置
for (const key in Op) {
operators.set('$' + key, Op[key]);
const val = Utils.underscoredIf(key, true);
operators.set('$' + val, Op[key]);
operators.set('$' + val.replace(/_/g, ''), Op[key]);
}
this.operators = operators;
}
collection(options: CollectionOptions) {
@ -101,6 +114,12 @@ export class Database extends EventEmitter {
}
}
registerOperators(operators) {
for (const [key, operator] of Object.entries(operators)) {
this.operators.set(key, operator);
}
}
buildSchemaField(options, context) {
const { type } = options;
const Field = this.schemaTypes.get(type);

View File

@ -1,23 +1,199 @@
import { ModelCtor, Model } from 'sequelize';
import {
ModelCtor,
Model,
BulkCreateOptions,
FindOptions,
Op,
} from 'sequelize';
import { flatten } from 'flat';
import { Collection } from './collection';
import _ from 'lodash';
import { Database } from './database';
import { updateAssociations } from './update-associations';
export interface IRepository {
export interface IRepository {}
interface FindAllOptions extends FindOptions {
filter?: any;
fields?: any;
page?: any;
pageSize?: any;
sort?: any;
}
interface FindOneOptions extends FindOptions {
filter?: any;
fields?: any;
sort?: any;
}
export class Repository implements IRepository {
model: ModelCtor<Model>;
collection: Collection;
database: Database;
constructor(model: ModelCtor<Model>) {
this.model = model;
constructor(collection: Collection) {
this.database = collection.context.database;
this.collection = collection;
}
findAll() {}
async findAll(options?: FindAllOptions) {
const model = this.collection.model;
const opts = {
subQuery: false,
...this.parseApiJson(options),
};
let rows = [];
if (opts.include) {
const ids = (
await model.findAll({
...opts,
includeIgnoreAttributes: false,
attributes: [model.primaryKeyAttribute],
group: `${model.name}.${model.primaryKeyAttribute}`,
})
).map((item) => item[model.primaryKeyAttribute]);
rows = await model.findAll({
...opts,
where: {
[model.primaryKeyAttribute]: {
[Op.in]: ids,
},
},
});
} else {
rows = await model.findAll({
...opts,
});
}
const count = await model.count({
...opts,
distinct: opts.include ? true : undefined,
});
return { count, rows };
}
findOne() {}
async findOne(options?: FindOneOptions) {
const opts = this.parseApiJson(options);
console.log({ opts });
const data = await this.collection.model.findOne(opts);
return data;
}
create() {}
update() {}
destroy() {}
async bulkCreate(records: any[], options?: BulkCreateOptions) {
const instances = await this.collection.model.bulkCreate(records, options);
const promises = instances.map((instance, index) => {
return updateAssociations(instance, records[index]);
});
return Promise.all(promises);
}
parseApiJson(options: any) {
const filter = options.filter || {};
const model = this.collection.model;
const operators = this.database.operators;
const obj = flatten(filter || {});
const include = {};
const where = {};
let skipPrefix = null;
const filter2 = {};
for (const [key, value] of Object.entries(obj)) {
_.set(filter2, key, value);
}
for (let [key, value] of Object.entries(obj)) {
if (skipPrefix && key.startsWith(skipPrefix)) {
continue;
}
let keys = key.split('.');
const associations = model.associations;
const paths = [];
const origins = [];
while (keys.length) {
const k = keys.shift();
origins.push(k);
if (k.startsWith('$')) {
if (operators.has(k)) {
const opKey = operators.get(k);
if (typeof opKey === 'symbol') {
paths.push(opKey);
continue;
} else if (typeof opKey === 'function') {
skipPrefix = origins.join('.');
// console.log({ skipPrefix }, filter2, _.get(filter2, origins));
value = opKey(_.get(filter2, origins));
break;
}
} else {
paths.push(k);
continue;
}
}
if (/\d+/.test(k)) {
paths.push(k);
continue;
}
if (!associations[k]) {
paths.push(k);
continue;
}
const associationKeys = [];
associationKeys.push(k);
_.set(include, k, {
association: k,
});
let target = associations[k].target;
while (target) {
const attr = keys.shift();
if (target.rawAttributes[attr]) {
associationKeys.push(attr);
target = null;
} else if (target.associations[attr]) {
associationKeys.push(attr);
const assoc = [];
associationKeys.forEach((associationKey, index) => {
if (index > 0) {
assoc.push('include');
}
assoc.push(associationKey);
});
_.set(include, assoc, {
association: attr,
});
target = target.associations[attr].target;
}
}
if (associationKeys.length > 1) {
paths.push(`$${associationKeys.join('.')}$`);
} else {
paths.push(k);
}
}
console.log(paths, value);
const values = _.get(where, paths);
if (
values &&
typeof values === 'object' &&
value &&
typeof value === 'object'
) {
value = { ...value, ...values };
}
_.set(where, paths, value);
}
const toInclude = (items) => {
return Object.values(items).map((item: any) => {
if (item.include) {
item.include = toInclude(item.include);
}
return item;
});
};
console.log(JSON.stringify({ include: toInclude(include) }, null, 2));
return { ...options, where, include: toInclude(include) };
}
}