diff --git a/packages/core/database/src/__tests__/eager-loading/eager-loading-tree.test.ts b/packages/core/database/src/__tests__/eager-loading/eager-loading-tree.test.ts index 69e492299..79267a54c 100644 --- a/packages/core/database/src/__tests__/eager-loading/eager-loading-tree.test.ts +++ b/packages/core/database/src/__tests__/eager-loading/eager-loading-tree.test.ts @@ -199,9 +199,10 @@ describe('Eager loading tree', () => { rootAttributes: findOptions.attributes, includeOption: findOptions.include, db: db, + rootQueryOptions: findOptions, }); - await eagerLoadingTree.load(users.map((item) => item.id)); + await eagerLoadingTree.load(); const root = eagerLoadingTree.root; const u1 = root.instances.find((item) => item.get('name') === 'u1'); @@ -250,9 +251,10 @@ describe('Eager loading tree', () => { rootAttributes: findOptions.attributes, includeOption: findOptions.include, db: db, + rootQueryOptions: findOptions, }); - await eagerLoadingTree.load([1, 2]); + await eagerLoadingTree.load(); const root = eagerLoadingTree.root; const u1 = root.instances.find((item) => item.get('name') === 'u1'); @@ -301,9 +303,10 @@ describe('Eager loading tree', () => { rootAttributes: findOptions.attributes, includeOption: findOptions.include, db: db, + rootQueryOptions: findOptions, }); - await eagerLoadingTree.load(users.map((item) => item.id)); + await eagerLoadingTree.load(); const root = eagerLoadingTree.root; const u1 = root.instances.find((item) => item.get('name') === 'u1'); @@ -357,9 +360,10 @@ describe('Eager loading tree', () => { rootAttributes: findOptions.attributes, includeOption: findOptions.include, db: db, + rootQueryOptions: findOptions, }); - await eagerLoadingTree.load([1, 2]); + await eagerLoadingTree.load(); const root = eagerLoadingTree.root; const p1 = root.instances.find((item) => item.get('title') === 'p1'); @@ -420,8 +424,10 @@ describe('Eager loading tree', () => { rootAttributes: findOptions.attributes, includeOption: findOptions.include, db: db, + rootQueryOptions: findOptions, }); - await eagerLoadingTree.load([1, 2]); + + await eagerLoadingTree.load(); const root = eagerLoadingTree.root; const p1 = root.instances.find((item) => item.get('title') === 'p1'); @@ -523,6 +529,7 @@ describe('Eager loading tree', () => { rootAttributes: findOptions.attributes, includeOption: findOptions.include, db: db, + rootQueryOptions: findOptions, }); expect(eagerLoadingTree.root.children).toHaveLength(1); @@ -530,7 +537,7 @@ describe('Eager loading tree', () => { expect(eagerLoadingTree.root.children[0].children[0].model).toBe(Tag.model); expect(eagerLoadingTree.root.children[0].children[0].children[0].model).toBe(TagCategory.model); - await eagerLoadingTree.load((await User.model.findAll()).map((item) => item[User.model.primaryKeyAttribute])); + await eagerLoadingTree.load(); expect(eagerLoadingTree.root.instances).toHaveLength(2); const u1 = eagerLoadingTree.root.instances.find((item) => item.get('name') === 'u1'); diff --git a/packages/core/database/src/__tests__/relation-repository/has-many-repository.test.ts b/packages/core/database/src/__tests__/relation-repository/has-many-repository.test.ts index a3f53dd59..50b9bf4ad 100644 --- a/packages/core/database/src/__tests__/relation-repository/has-many-repository.test.ts +++ b/packages/core/database/src/__tests__/relation-repository/has-many-repository.test.ts @@ -286,15 +286,13 @@ describe('has many repository', () => { const PostTagRepository = new BelongsToManyRepository(Post, 'tags', p1.id); await PostTagRepository.set([t1.id, t2.id, t3.id]); - const posts = await UserPostRepository.find({ + const post = await UserPostRepository.findOne({ filter: { 'tags.name': 't1', }, appends: ['tags'], }); - const post = posts[0]; - expect(post.tags.length).toEqual(3); const findAndCount = await UserPostRepository.findAndCount({ diff --git a/packages/core/database/src/__tests__/repository/find-with-collection-without-pk.test.ts b/packages/core/database/src/__tests__/repository/find-with-collection-without-pk.test.ts new file mode 100644 index 000000000..daa37e2f4 --- /dev/null +++ b/packages/core/database/src/__tests__/repository/find-with-collection-without-pk.test.ts @@ -0,0 +1,71 @@ +import Database from '../../database'; +import { mockDatabase } from '../index'; + +describe('find collection that without primary key', () => { + let db: Database; + + beforeAll(async () => { + db = mockDatabase({ + tablePrefix: '', + }); + + await db.clean({ drop: true }); + }); + + afterEach(async () => { + await db.close(); + }); + + it('should find collection with belongsTo', async () => { + const B = db.collection({ + name: 'b', + fields: [ + { + type: 'string', + name: 'name', + }, + ], + }); + + const A = db.collection({ + name: 'a', + autoGenId: false, + fields: [ + { + type: 'string', + name: 'name', + }, + { + type: 'belongsTo', + name: 'b', + target: 'b', + foreignKey: 'b_id', + }, + ], + }); + + await db.sync(); + + const b1 = await B.repository.create({ + values: { + name: 'b1', + }, + }); + + await A.repository.create({ + values: { + name: 'a1', + b_id: b1.get('id'), + }, + }); + + const aWithB = await A.repository.find({ + appends: ['b'], + filter: { + 'b.name': 'b1', + }, + }); + + expect(aWithB[0].get('b').get('name')).toBe('b1'); + }); +}); diff --git a/packages/core/database/src/__tests__/repository/find.test.ts b/packages/core/database/src/__tests__/repository/find.test.ts index 693a37f96..000020c4d 100644 --- a/packages/core/database/src/__tests__/repository/find.test.ts +++ b/packages/core/database/src/__tests__/repository/find.test.ts @@ -14,6 +14,65 @@ describe('find with associations', () => { await db.close(); }); + it('should filter has many with limit', async () => { + const User = db.collection({ + name: 'users', + fields: [ + { type: 'string', name: 'name' }, + { + type: 'hasMany', + name: 'posts', + }, + ], + }); + + const Post = db.collection({ + name: 'posts', + fields: [ + { type: 'string', name: 'title' }, + { type: 'belongsTo', name: 'user' }, + ], + }); + + await db.sync(); + + await User.repository.create({ + values: [ + { + name: 'u1', + posts: [ + { + title: 'u1p1', + }, + { + title: 'u1p2', + }, + ], + }, + { + name: 'u2', + posts: [ + { + title: 'u2p1', + }, + { + title: 'u2p2', + }, + ], + }, + ], + }); + + const users = await User.repository.find({ + filter: { + 'posts.title.$includes': 'p', + }, + limit: 2, + }); + + expect(users.length).toEqual(2); + }); + it('should filter by association array field', async () => { const User = db.collection({ name: 'users', diff --git a/packages/core/database/src/eager-loading/eager-loading-tree.ts b/packages/core/database/src/eager-loading/eager-loading-tree.ts index 7b22a2c68..f736f1f76 100644 --- a/packages/core/database/src/eager-loading/eager-loading-tree.ts +++ b/packages/core/database/src/eager-loading/eager-loading-tree.ts @@ -47,6 +47,7 @@ const EagerLoadingNodeProto = { export class EagerLoadingTree { public root: EagerLoadingNode; db: Database; + private rootQueryOptions: any = {}; constructor(root: EagerLoadingNode) { this.root = root; @@ -56,10 +57,11 @@ export class EagerLoadingTree { model: ModelStatic; rootAttributes: Array; rootOrder?: any; + rootQueryOptions?: any; includeOption: Includeable | Includeable[]; db: Database; }): EagerLoadingTree { - const { model, rootAttributes, includeOption, db } = options; + const { model, rootAttributes, includeOption, db, rootQueryOptions } = options; const buildNode = (node) => { Object.setPrototypeOf(node, EagerLoadingNodeProto); @@ -137,10 +139,11 @@ export class EagerLoadingTree { const tree = new EagerLoadingTree(root); tree.db = db; + tree.rootQueryOptions = rootQueryOptions; return tree; } - async load(pks: Array, transaction?: Transaction) { + async load(transaction?: Transaction) { const result = {}; const orderOption = (association) => { @@ -154,26 +157,80 @@ export class EagerLoadingTree { return order; }; - const loadRecursive = async (node, ids) => { - const modelPrimaryKey = node.model.primaryKeyField || node.model.primaryKeyAttribute; - + const loadRecursive = async (node, ids = []) => { let instances = []; - // load instances from database if (!node.parent) { - const findOptions = { - where: { [modelPrimaryKey]: ids }, - attributes: node.attributes, - }; + // load root instances + const rootInclude = this.rootQueryOptions?.include || node.includeOption; - if (node.order) { - findOptions['order'] = node.order; + const includeForFilter = rootInclude.filter((include) => { + return ( + Object.keys(include.where || {}).length > 0 || + JSON.stringify(this.rootQueryOptions?.filter)?.includes(include.association) + ); + }); + + const belongsToAssociationsOnly = includeForFilter.every((include) => { + const association = node.model.associations[include.association]; + if (!association) { + return false; + } + return association.associationType == 'BelongsTo'; + }); + + if (belongsToAssociationsOnly) { + instances = await node.model.findAll({ + ...this.rootQueryOptions, + attributes: node.attributes, + distinct: true, + include: includeForFilter, + transaction, + }); + } else { + const primaryKeyField = node.model.primaryKeyField || node.model.primaryKeyAttribute; + + if (!primaryKeyField) { + throw new Error(`Model ${node.model.name} does not have primary key`); + } + + // find all ids + const ids = ( + await node.model.findAll({ + ...this.rootQueryOptions, + includeIgnoreAttributes: false, + attributes: [primaryKeyField], + group: `${node.model.name}.${primaryKeyField}`, + transaction, + include: includeForFilter, + } as any) + ).map((row) => { + return { row, pk: row.get(primaryKeyField) }; + }); + + const findOptions = { + where: { [primaryKeyField]: ids.map((i) => i.pk) }, + attributes: node.attributes, + }; + + if (node.order) { + findOptions['order'] = node.order; + } + + instances = await node.model.findAll({ + ...findOptions, + transaction, + }); } - instances = await node.model.findAll({ - ...findOptions, - transaction, - }); + // clear filter association value + const associations = node.model.associations; + for (const association of Object.keys(associations)) { + for (const instance of instances) { + delete instance[association]; + delete instance.dataValues[association]; + } + } } else if (ids.length > 0) { const association = node.association; const associationType = association.associationType; @@ -241,8 +298,8 @@ export class EagerLoadingTree { node.instances = instances; for (const child of node.children) { + const modelPrimaryKey = node.model.primaryKeyField || node.model.primaryKeyAttribute; const nodeIds = instances.map((instance) => instance.get(modelPrimaryKey)); - await loadRecursive(child, nodeIds); } @@ -343,7 +400,7 @@ export class EagerLoadingTree { } }; - await loadRecursive(this.root, pks); + await loadRecursive(this.root); const appendChildCollectionName = appendChildCollectionNameAfterRepositoryFind(this.db); @@ -377,8 +434,7 @@ export class EagerLoadingTree { } for (const instance of node.instances) { - const attributes = lodash.pick(instance.dataValues, includeAttributes); - instance.dataValues = attributes; + instance.dataValues = lodash.pick(instance.dataValues, includeAttributes); } }; diff --git a/packages/core/database/src/repository.ts b/packages/core/database/src/repository.ts index 3c9ad81a5..216993332 100644 --- a/packages/core/database/src/repository.ts +++ b/packages/core/database/src/repository.ts @@ -391,44 +391,16 @@ export class Repository 0) { - // @ts-ignore - const primaryKeyField = model.primaryKeyField || model.primaryKeyAttribute; - - // find all ids - const ids = ( - await model.findAll({ - ...opts, - includeIgnoreAttributes: false, - attributes: [primaryKeyField], - group: `${model.name}.${primaryKeyField}`, - transaction, - include: opts.include.filter((include) => { - return ( - Object.keys(include.where || {}).length > 0 || JSON.stringify(opts?.filter)?.includes(include.association) - ); - }), - } as any) - ).map((row) => { - return { row, pk: row.get(primaryKeyField) }; - }); - - if (ids.length == 0) { - return []; - } - - // find all rows const eagerLoadingTree = EagerLoadingTree.buildFromSequelizeOptions({ model, rootAttributes: opts.attributes, includeOption: opts.include, rootOrder: opts.order, + rootQueryOptions: opts, db: this.database, }); - await eagerLoadingTree.load( - ids.map((i) => i.pk), - transaction, - ); + await eagerLoadingTree.load(transaction); rows = eagerLoadingTree.root.instances; } else {