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,
|
rootAttributes: findOptions.attributes,
|
||||||
includeOption: findOptions.include,
|
includeOption: findOptions.include,
|
||||||
db: db,
|
db: db,
|
||||||
|
rootQueryOptions: findOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
await eagerLoadingTree.load(users.map((item) => item.id));
|
await eagerLoadingTree.load();
|
||||||
const root = eagerLoadingTree.root;
|
const root = eagerLoadingTree.root;
|
||||||
|
|
||||||
const u1 = root.instances.find((item) => item.get('name') === 'u1');
|
const u1 = root.instances.find((item) => item.get('name') === 'u1');
|
||||||
@ -250,9 +251,10 @@ describe('Eager loading tree', () => {
|
|||||||
rootAttributes: findOptions.attributes,
|
rootAttributes: findOptions.attributes,
|
||||||
includeOption: findOptions.include,
|
includeOption: findOptions.include,
|
||||||
db: db,
|
db: db,
|
||||||
|
rootQueryOptions: findOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
await eagerLoadingTree.load([1, 2]);
|
await eagerLoadingTree.load();
|
||||||
|
|
||||||
const root = eagerLoadingTree.root;
|
const root = eagerLoadingTree.root;
|
||||||
const u1 = root.instances.find((item) => item.get('name') === 'u1');
|
const u1 = root.instances.find((item) => item.get('name') === 'u1');
|
||||||
@ -301,9 +303,10 @@ describe('Eager loading tree', () => {
|
|||||||
rootAttributes: findOptions.attributes,
|
rootAttributes: findOptions.attributes,
|
||||||
includeOption: findOptions.include,
|
includeOption: findOptions.include,
|
||||||
db: db,
|
db: db,
|
||||||
|
rootQueryOptions: findOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
await eagerLoadingTree.load(users.map((item) => item.id));
|
await eagerLoadingTree.load();
|
||||||
|
|
||||||
const root = eagerLoadingTree.root;
|
const root = eagerLoadingTree.root;
|
||||||
const u1 = root.instances.find((item) => item.get('name') === 'u1');
|
const u1 = root.instances.find((item) => item.get('name') === 'u1');
|
||||||
@ -357,9 +360,10 @@ describe('Eager loading tree', () => {
|
|||||||
rootAttributes: findOptions.attributes,
|
rootAttributes: findOptions.attributes,
|
||||||
includeOption: findOptions.include,
|
includeOption: findOptions.include,
|
||||||
db: db,
|
db: db,
|
||||||
|
rootQueryOptions: findOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
await eagerLoadingTree.load([1, 2]);
|
await eagerLoadingTree.load();
|
||||||
|
|
||||||
const root = eagerLoadingTree.root;
|
const root = eagerLoadingTree.root;
|
||||||
const p1 = root.instances.find((item) => item.get('title') === 'p1');
|
const p1 = root.instances.find((item) => item.get('title') === 'p1');
|
||||||
@ -420,8 +424,10 @@ describe('Eager loading tree', () => {
|
|||||||
rootAttributes: findOptions.attributes,
|
rootAttributes: findOptions.attributes,
|
||||||
includeOption: findOptions.include,
|
includeOption: findOptions.include,
|
||||||
db: db,
|
db: db,
|
||||||
|
rootQueryOptions: findOptions,
|
||||||
});
|
});
|
||||||
await eagerLoadingTree.load([1, 2]);
|
|
||||||
|
await eagerLoadingTree.load();
|
||||||
const root = eagerLoadingTree.root;
|
const root = eagerLoadingTree.root;
|
||||||
|
|
||||||
const p1 = root.instances.find((item) => item.get('title') === 'p1');
|
const p1 = root.instances.find((item) => item.get('title') === 'p1');
|
||||||
@ -523,6 +529,7 @@ describe('Eager loading tree', () => {
|
|||||||
rootAttributes: findOptions.attributes,
|
rootAttributes: findOptions.attributes,
|
||||||
includeOption: findOptions.include,
|
includeOption: findOptions.include,
|
||||||
db: db,
|
db: db,
|
||||||
|
rootQueryOptions: findOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(eagerLoadingTree.root.children).toHaveLength(1);
|
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].model).toBe(Tag.model);
|
||||||
expect(eagerLoadingTree.root.children[0].children[0].children[0].model).toBe(TagCategory.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);
|
expect(eagerLoadingTree.root.instances).toHaveLength(2);
|
||||||
const u1 = eagerLoadingTree.root.instances.find((item) => item.get('name') === 'u1');
|
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);
|
const PostTagRepository = new BelongsToManyRepository(Post, 'tags', p1.id);
|
||||||
await PostTagRepository.set([t1.id, t2.id, t3.id]);
|
await PostTagRepository.set([t1.id, t2.id, t3.id]);
|
||||||
|
|
||||||
const posts = await UserPostRepository.find({
|
const post = await UserPostRepository.findOne({
|
||||||
filter: {
|
filter: {
|
||||||
'tags.name': 't1',
|
'tags.name': 't1',
|
||||||
},
|
},
|
||||||
appends: ['tags'],
|
appends: ['tags'],
|
||||||
});
|
});
|
||||||
|
|
||||||
const post = posts[0];
|
|
||||||
|
|
||||||
expect(post.tags.length).toEqual(3);
|
expect(post.tags.length).toEqual(3);
|
||||||
|
|
||||||
const findAndCount = await UserPostRepository.findAndCount({
|
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();
|
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 () => {
|
it('should filter by association array field', async () => {
|
||||||
const User = db.collection({
|
const User = db.collection({
|
||||||
name: 'users',
|
name: 'users',
|
||||||
|
@ -47,6 +47,7 @@ const EagerLoadingNodeProto = {
|
|||||||
export class EagerLoadingTree {
|
export class EagerLoadingTree {
|
||||||
public root: EagerLoadingNode;
|
public root: EagerLoadingNode;
|
||||||
db: Database;
|
db: Database;
|
||||||
|
private rootQueryOptions: any = {};
|
||||||
|
|
||||||
constructor(root: EagerLoadingNode) {
|
constructor(root: EagerLoadingNode) {
|
||||||
this.root = root;
|
this.root = root;
|
||||||
@ -56,10 +57,11 @@ export class EagerLoadingTree {
|
|||||||
model: ModelStatic<any>;
|
model: ModelStatic<any>;
|
||||||
rootAttributes: Array<string>;
|
rootAttributes: Array<string>;
|
||||||
rootOrder?: any;
|
rootOrder?: any;
|
||||||
|
rootQueryOptions?: any;
|
||||||
includeOption: Includeable | Includeable[];
|
includeOption: Includeable | Includeable[];
|
||||||
db: Database;
|
db: Database;
|
||||||
}): EagerLoadingTree {
|
}): EagerLoadingTree {
|
||||||
const { model, rootAttributes, includeOption, db } = options;
|
const { model, rootAttributes, includeOption, db, rootQueryOptions } = options;
|
||||||
|
|
||||||
const buildNode = (node) => {
|
const buildNode = (node) => {
|
||||||
Object.setPrototypeOf(node, EagerLoadingNodeProto);
|
Object.setPrototypeOf(node, EagerLoadingNodeProto);
|
||||||
@ -137,10 +139,11 @@ export class EagerLoadingTree {
|
|||||||
|
|
||||||
const tree = new EagerLoadingTree(root);
|
const tree = new EagerLoadingTree(root);
|
||||||
tree.db = db;
|
tree.db = db;
|
||||||
|
tree.rootQueryOptions = rootQueryOptions;
|
||||||
return tree;
|
return tree;
|
||||||
}
|
}
|
||||||
|
|
||||||
async load(pks: Array<string | number>, transaction?: Transaction) {
|
async load(transaction?: Transaction) {
|
||||||
const result = {};
|
const result = {};
|
||||||
|
|
||||||
const orderOption = (association) => {
|
const orderOption = (association) => {
|
||||||
@ -154,15 +157,59 @@ export class EagerLoadingTree {
|
|||||||
return order;
|
return order;
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadRecursive = async (node, ids) => {
|
const loadRecursive = async (node, ids = []) => {
|
||||||
const modelPrimaryKey = node.model.primaryKeyField || node.model.primaryKeyAttribute;
|
|
||||||
|
|
||||||
let instances = [];
|
let instances = [];
|
||||||
|
|
||||||
// load instances from database
|
|
||||||
if (!node.parent) {
|
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 = {
|
const findOptions = {
|
||||||
where: { [modelPrimaryKey]: ids },
|
where: { [primaryKeyField]: ids.map((i) => i.pk) },
|
||||||
attributes: node.attributes,
|
attributes: node.attributes,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -174,6 +221,16 @@ export class EagerLoadingTree {
|
|||||||
...findOptions,
|
...findOptions,
|
||||||
transaction,
|
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) {
|
} else if (ids.length > 0) {
|
||||||
const association = node.association;
|
const association = node.association;
|
||||||
const associationType = association.associationType;
|
const associationType = association.associationType;
|
||||||
@ -241,8 +298,8 @@ export class EagerLoadingTree {
|
|||||||
node.instances = instances;
|
node.instances = instances;
|
||||||
|
|
||||||
for (const child of node.children) {
|
for (const child of node.children) {
|
||||||
|
const modelPrimaryKey = node.model.primaryKeyField || node.model.primaryKeyAttribute;
|
||||||
const nodeIds = instances.map((instance) => instance.get(modelPrimaryKey));
|
const nodeIds = instances.map((instance) => instance.get(modelPrimaryKey));
|
||||||
|
|
||||||
await loadRecursive(child, nodeIds);
|
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);
|
const appendChildCollectionName = appendChildCollectionNameAfterRepositoryFind(this.db);
|
||||||
|
|
||||||
@ -377,8 +434,7 @@ export class EagerLoadingTree {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const instance of node.instances) {
|
for (const instance of node.instances) {
|
||||||
const attributes = lodash.pick(instance.dataValues, includeAttributes);
|
instance.dataValues = lodash.pick(instance.dataValues, includeAttributes);
|
||||||
instance.dataValues = attributes;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -391,44 +391,16 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
|
|||||||
let rows;
|
let rows;
|
||||||
|
|
||||||
if (opts.include && opts.include.length > 0) {
|
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({
|
const eagerLoadingTree = EagerLoadingTree.buildFromSequelizeOptions({
|
||||||
model,
|
model,
|
||||||
rootAttributes: opts.attributes,
|
rootAttributes: opts.attributes,
|
||||||
includeOption: opts.include,
|
includeOption: opts.include,
|
||||||
rootOrder: opts.order,
|
rootOrder: opts.order,
|
||||||
|
rootQueryOptions: opts,
|
||||||
db: this.database,
|
db: this.database,
|
||||||
});
|
});
|
||||||
|
|
||||||
await eagerLoadingTree.load(
|
await eagerLoadingTree.load(transaction);
|
||||||
ids.map((i) => i.pk),
|
|
||||||
transaction,
|
|
||||||
);
|
|
||||||
|
|
||||||
rows = eagerLoadingTree.root.instances;
|
rows = eagerLoadingTree.root.instances;
|
||||||
} else {
|
} else {
|
||||||
|
Loading…
Reference in New Issue
Block a user