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:
parent
fa1785316d
commit
ddb6d69676
@ -285,7 +285,9 @@ export class Collection<
|
||||
const [sourceCollectionName, sourceFieldName] = options.source.split('.');
|
||||
const sourceCollection = this.db.collections.get(sourceCollectionName);
|
||||
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);
|
||||
options = { ...sourceField.options, ...options };
|
||||
|
@ -16,6 +16,7 @@ describe('belongsToMany', () => {
|
||||
await Collection.repository.create({
|
||||
values: {
|
||||
name: 'posts',
|
||||
fields: [{ type: 'string', name: 'title' }],
|
||||
},
|
||||
context: {},
|
||||
});
|
||||
@ -23,6 +24,7 @@ describe('belongsToMany', () => {
|
||||
await Collection.repository.create({
|
||||
values: {
|
||||
name: 'tags',
|
||||
fields: [{ type: 'string', name: 'name' }],
|
||||
},
|
||||
context: {},
|
||||
});
|
||||
@ -76,4 +78,47 @@ describe('belongsToMany', () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
@ -28,6 +28,117 @@ describe('view collection', function () {
|
||||
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 () => {
|
||||
if (!db.inDialect('postgres')) {
|
||||
return;
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { Repository } from '@nocobase/database';
|
||||
import { CollectionModel } from '../models/collection';
|
||||
import toposort from 'toposort';
|
||||
import lodash from 'lodash';
|
||||
import { CollectionsGraph } from '@nocobase/utils';
|
||||
|
||||
interface LoadOptions {
|
||||
filter?: any;
|
||||
@ -13,40 +12,62 @@ export class CollectionRepository extends Repository {
|
||||
const { filter, skipExist } = options;
|
||||
const instances = (await this.find({ filter })) as CollectionModel[];
|
||||
|
||||
const inheritedGraph = [];
|
||||
const throughModels = [];
|
||||
const generalModels = [];
|
||||
const graphlib = CollectionsGraph.graphlib();
|
||||
|
||||
const graph = new graphlib.Graph();
|
||||
|
||||
const nameMap = {};
|
||||
|
||||
const viewCollections = [];
|
||||
|
||||
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
|
||||
const fields = await instance.getFields();
|
||||
for (const field of fields) {
|
||||
if (field['type'] === 'belongsToMany') {
|
||||
const throughName = field.options.through;
|
||||
if (throughName) {
|
||||
inheritedGraph.push([throughName, field.options.target]);
|
||||
inheritedGraph.push([throughName, instance.get('name')]);
|
||||
graph.setEdge(throughName, collectionName);
|
||||
graph.setEdge(throughName, field.options.target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (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;
|
||||
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({});
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user