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 [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 };
|
||||||
|
@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -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;
|
||||||
|
@ -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({});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user