feat: support load belongs to association with collection that without primary key (#2529)
* test: find without pk * feat: load root models in eager loading tree
This commit is contained in:
		
							parent
							
								
									376a91b8ec
								
							
						
					
					
						commit
						edbd15ab5b
					
				@ -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');
 | 
			
		||||
 | 
			
		||||
@ -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({
 | 
			
		||||
 | 
			
		||||
@ -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');
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@ -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',
 | 
			
		||||
 | 
			
		||||
@ -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<any>;
 | 
			
		||||
    rootAttributes: Array<string>;
 | 
			
		||||
    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<string | number>, transaction?: Transaction) {
 | 
			
		||||
  async load(transaction?: Transaction) {
 | 
			
		||||
    const result = {};
 | 
			
		||||
 | 
			
		||||
    const orderOption = (association) => {
 | 
			
		||||
@ -154,15 +157,59 @@ 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) {
 | 
			
		||||
        // load root instances
 | 
			
		||||
        const rootInclude = this.rootQueryOptions?.include || node.includeOption;
 | 
			
		||||
 | 
			
		||||
        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: { [modelPrimaryKey]: ids },
 | 
			
		||||
            where: { [primaryKeyField]: ids.map((i) => i.pk) },
 | 
			
		||||
            attributes: node.attributes,
 | 
			
		||||
          };
 | 
			
		||||
 | 
			
		||||
@ -174,6 +221,16 @@ export class EagerLoadingTree {
 | 
			
		||||
            ...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);
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -391,44 +391,16 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
 | 
			
		||||
    let rows;
 | 
			
		||||
 | 
			
		||||
    if (opts.include && opts.include.length > 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 {
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user