fix: load view collection (#1727)

* fix: load view collection

* refactor: using graphlib at collections load

* test: destory through collection

* feat: throw error when collection manager cycle found

* test: view collection as through collection
This commit is contained in:
ChengLei Shao 2023-04-19 17:59:49 +08:00 committed by GitHub
parent fa1785316d
commit ddb6d69676
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 194 additions and 15 deletions

View File

@ -285,7 +285,9 @@ export class Collection<
const [sourceCollectionName, sourceFieldName] = options.source.split('.'); const [sourceCollectionName, sourceFieldName] = options.source.split('.');
const sourceCollection = this.db.collections.get(sourceCollectionName); const sourceCollection = this.db.collections.get(sourceCollectionName);
if (!sourceCollection) { if (!sourceCollection) {
throw new Error(`source collection "${sourceCollectionName}" not found`); throw new Error(
`source collection "${sourceCollectionName}" not found for field "${name}" at collection "${this.name}"`,
);
} }
const sourceField = sourceCollection.fields.get(sourceFieldName); const sourceField = sourceCollection.fields.get(sourceFieldName);
options = { ...sourceField.options, ...options }; options = { ...sourceField.options, ...options };

View File

@ -16,6 +16,7 @@ describe('belongsToMany', () => {
await Collection.repository.create({ await Collection.repository.create({
values: { values: {
name: 'posts', name: 'posts',
fields: [{ type: 'string', name: 'title' }],
}, },
context: {}, context: {},
}); });
@ -23,6 +24,7 @@ describe('belongsToMany', () => {
await Collection.repository.create({ await Collection.repository.create({
values: { values: {
name: 'tags', name: 'tags',
fields: [{ type: 'string', name: 'name' }],
}, },
context: {}, context: {},
}); });
@ -76,4 +78,47 @@ describe('belongsToMany', () => {
expect(await tableExists(tableName, mainSchema)).toBe(false); expect(await tableExists(tableName, mainSchema)).toBe(false);
} }
}); });
it('should belongs to many fields after through collection destroyed', async () => {
await Field.repository.create({
values: {
name: 'tags',
type: 'belongsToMany',
collectionName: 'posts',
interface: 'm2m',
through: 'post_tags',
},
context: {},
});
const throughCollection = await Collection.repository.findOne({
filter: {
name: 'post_tags',
},
});
await db.getRepository('posts').create({
values: [
{
title: 'p1',
tags: [{ name: 't1' }],
},
{
title: 'p2',
tags: [{ name: 't2' }],
},
],
});
await throughCollection.destroy();
expect(
await Field.repository.count({
filter: {
name: 'tags',
collectionName: 'posts',
},
}),
).toEqual(0);
});
}); });

View File

@ -28,6 +28,117 @@ describe('view collection', function () {
await app.destroy(); await app.destroy();
}); });
it('should use view collection as through collection', async () => {
const User = await collectionRepository.create({
values: {
name: 'users',
fields: [{ name: 'name', type: 'string' }],
},
context: {},
});
const Role = await collectionRepository.create({
values: {
name: 'roles',
fields: [{ name: 'name', type: 'string' }],
},
context: {},
});
const UserCollection = db.getCollection('users');
console.log(UserCollection);
await db.getRepository('users').create({
values: [{ name: 'u1' }, { name: 'u2' }],
});
await db.getRepository('roles').create({
values: [{ name: 'r1' }, { name: 'r2' }],
});
await collectionRepository.create({
values: {
name: 'user_roles',
fields: [
{ type: 'integer', name: 'user_id' },
{ type: 'integer', name: 'role_id' },
],
},
context: {},
});
const throughCollection = db.getCollection('user_roles');
await throughCollection.repository.create({
values: [
{ user_id: 1, role_id: 1 },
{ user_id: 1, role_id: 2 },
{ user_id: 2, role_id: 1 },
],
});
const viewName = 'test_view';
const dropViewSQL = `DROP VIEW IF EXISTS test_view`;
await db.sequelize.query(dropViewSQL);
const viewSQL = `CREATE VIEW test_view AS select * from ${throughCollection.quotedTableName()}`;
await db.sequelize.query(viewSQL);
await collectionRepository.create({
values: {
name: `${viewName}`,
view: true,
viewName,
fields: [
{ type: 'integer', name: 'user_id' },
{ type: 'integer', name: 'role_id' },
],
schema: db.inDialect('postgres') ? 'public' : undefined,
},
context: {},
});
await fieldsRepository.create({
values: {
collectionName: 'users',
name: 'roles',
type: 'belongsToMany',
target: 'roles',
through: 'test_view',
foreignKey: 'user_id',
otherKey: 'role_id',
},
context: {},
});
const users = await db.getRepository('users').find({
appends: ['roles'],
filter: {
name: 'u1',
},
});
const roles = users[0].get('roles');
expect(roles).toHaveLength(2);
await collectionRepository.destroy({
filter: {
name: 'test_view',
},
context: {},
});
expect(
await fieldsRepository.count({
filter: {
collectionName: 'users',
name: 'roles',
},
}),
).toEqual(0);
});
it('should save view collection in difference schema', async () => { it('should save view collection in difference schema', async () => {
if (!db.inDialect('postgres')) { if (!db.inDialect('postgres')) {
return; return;

View File

@ -1,7 +1,6 @@
import { Repository } from '@nocobase/database'; import { Repository } from '@nocobase/database';
import { CollectionModel } from '../models/collection'; import { CollectionModel } from '../models/collection';
import toposort from 'toposort'; import { CollectionsGraph } from '@nocobase/utils';
import lodash from 'lodash';
interface LoadOptions { interface LoadOptions {
filter?: any; filter?: any;
@ -13,40 +12,62 @@ export class CollectionRepository extends Repository {
const { filter, skipExist } = options; const { filter, skipExist } = options;
const instances = (await this.find({ filter })) as CollectionModel[]; const instances = (await this.find({ filter })) as CollectionModel[];
const inheritedGraph = []; const graphlib = CollectionsGraph.graphlib();
const throughModels = [];
const generalModels = []; const graph = new graphlib.Graph();
const nameMap = {}; const nameMap = {};
const viewCollections = [];
for (const instance of instances) { for (const instance of instances) {
nameMap[instance.get('name')] = instance; graph.setNode(instance.get('name'));
if (instance.get('view')) {
viewCollections.push(instance.get('name'));
}
}
for (const instance of instances) {
const collectionName = instance.get('name');
nameMap[collectionName] = instance;
// @ts-ignore // @ts-ignore
const fields = await instance.getFields(); const fields = await instance.getFields();
for (const field of fields) { for (const field of fields) {
if (field['type'] === 'belongsToMany') { if (field['type'] === 'belongsToMany') {
const throughName = field.options.through; const throughName = field.options.through;
if (throughName) { if (throughName) {
inheritedGraph.push([throughName, field.options.target]); graph.setEdge(throughName, collectionName);
inheritedGraph.push([throughName, instance.get('name')]); graph.setEdge(throughName, field.options.target);
} }
} }
} }
if (instance.get('inherits')) { if (instance.get('inherits')) {
for (const parent of instance.get('inherits')) { for (const parent of instance.get('inherits')) {
inheritedGraph.push([parent, instance.get('name')]); graph.setEdge(parent, collectionName);
} }
} else {
generalModels.push(instance.get('name'));
} }
} }
const sortedNames = [...toposort(inheritedGraph), ...lodash.difference(generalModels, throughModels)]; if (graph.nodeCount() === 0) return;
for (const instanceName of lodash.uniq(sortedNames)) { if (!graphlib.alg.isAcyclic(graph)) {
const cycles = graphlib.alg.findCycles(graph);
throw new Error(`Cyclic dependencies: ${cycles.map((cycle) => cycle.join(' -> ')).join(', ')}`);
}
const sortedNames = graphlib.alg.topsort(graph);
for (const instanceName of sortedNames) {
if (!nameMap[instanceName]) continue; if (!nameMap[instanceName]) continue;
await nameMap[instanceName].load({ skipExist }); await nameMap[instanceName].load({ skipExist, skipField: viewCollections.includes(instanceName) });
}
// load view fields
for (const viewCollectionName of viewCollections) {
await nameMap[viewCollectionName].loadFields({});
} }
} }