diff --git a/packages/collections/src/__tests__/index.ts b/packages/collections/src/__tests__/index.ts index bc70cdd80..248c3791f 100644 --- a/packages/collections/src/__tests__/index.ts +++ b/packages/collections/src/__tests__/index.ts @@ -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: { diff --git a/packages/collections/src/__tests__/repository.test.ts b/packages/collections/src/__tests__/repository.test.ts new file mode 100644 index 000000000..bfd45848d --- /dev/null +++ b/packages/collections/src/__tests__/repository.test.ts @@ -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', + // }); + }); + +}); diff --git a/packages/collections/src/collection.ts b/packages/collections/src/collection.ts index e9e1998ee..e7da3051a 100644 --- a/packages/collections/src/collection.ts +++ b/packages/collections/src/collection.ts @@ -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; + 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() { diff --git a/packages/collections/src/database.ts b/packages/collections/src/database.ts index 80bd2b729..b7b8e07d6 100644 --- a/packages/collections/src/database.ts +++ b/packages/collections/src/database.ts @@ -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; pendingFields = new Map(); @@ -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); diff --git a/packages/collections/src/repository.ts b/packages/collections/src/repository.ts index ae605d0fb..9647ae1b4 100644 --- a/packages/collections/src/repository.ts +++ b/packages/collections/src/repository.ts @@ -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; + collection: Collection; + database: Database; - constructor(model: ModelCtor) { - 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) }; + } }