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,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);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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