mirror of
https://github.com/nocobase/nocobase
synced 2024-11-15 07:25:15 +00:00
chore(datasource-main): throw error when destory field if field is used by association field (#4833)
* chore(datasource-main): throw error when destory field if field is used by association field * fix: test * chore: destory field * chore: test * Update beforeDestoryField.ts
This commit is contained in:
parent
5f5a482ec7
commit
558bb49f57
@ -0,0 +1,191 @@
|
||||
import Database from '@nocobase/database';
|
||||
import { MockServer } from '@nocobase/test';
|
||||
import { createApp } from '..';
|
||||
|
||||
describe('destory key that used by association field', () => {
|
||||
let db: Database;
|
||||
let app: MockServer;
|
||||
beforeEach(async () => {
|
||||
app = await createApp();
|
||||
db = app.db;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
it('should destroy field cascade', async () => {
|
||||
await db.getRepository('collections').create({
|
||||
values: {
|
||||
name: 'posts',
|
||||
autoGenId: false,
|
||||
fields: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'title',
|
||||
primaryKey: true,
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'content',
|
||||
},
|
||||
],
|
||||
},
|
||||
context: {},
|
||||
});
|
||||
|
||||
await db.getRepository('collections').create({
|
||||
values: {
|
||||
name: 'comments',
|
||||
fields: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'comment',
|
||||
},
|
||||
],
|
||||
},
|
||||
context: {},
|
||||
});
|
||||
|
||||
// add has many field
|
||||
await db.getRepository('fields').create({
|
||||
values: {
|
||||
name: 'post',
|
||||
collectionName: 'comments',
|
||||
type: 'belongsTo',
|
||||
target: 'posts',
|
||||
foreignKey: 'postTitle',
|
||||
targetKey: 'title',
|
||||
},
|
||||
context: {},
|
||||
});
|
||||
|
||||
const CommentCollection = db.getCollection('comments');
|
||||
|
||||
expect(CommentCollection.getField('post')).toBeTruthy();
|
||||
|
||||
await db.getRepository('collections').create({
|
||||
values: {
|
||||
name: 'posts2',
|
||||
autoGenId: false,
|
||||
fields: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'title',
|
||||
primaryKey: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
context: {},
|
||||
});
|
||||
|
||||
await db.getRepository('collections').create({
|
||||
values: {
|
||||
name: 'comments2',
|
||||
fields: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'comment',
|
||||
},
|
||||
],
|
||||
},
|
||||
context: {},
|
||||
});
|
||||
|
||||
await db.getRepository('fields').create({
|
||||
values: {
|
||||
name: 'post',
|
||||
collectionName: 'comments2',
|
||||
type: 'belongsTo',
|
||||
target: 'posts2',
|
||||
foreignKey: 'postTitle',
|
||||
targetKey: 'title',
|
||||
},
|
||||
context: {},
|
||||
});
|
||||
|
||||
// destroy collection cascade
|
||||
await db.getRepository('collections').destroy({
|
||||
filter: {
|
||||
name: 'posts',
|
||||
},
|
||||
cascade: true,
|
||||
});
|
||||
|
||||
expect(CommentCollection.getField('post')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should throw error when destory a source key of hasMany field', async () => {
|
||||
await db.getRepository('collections').create({
|
||||
values: {
|
||||
name: 'posts',
|
||||
autoGenId: false,
|
||||
fields: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'title',
|
||||
primaryKey: true,
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'content',
|
||||
},
|
||||
],
|
||||
},
|
||||
context: {},
|
||||
});
|
||||
|
||||
await db.getRepository('collections').create({
|
||||
values: {
|
||||
name: 'comments',
|
||||
fields: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'comment',
|
||||
},
|
||||
],
|
||||
},
|
||||
context: {},
|
||||
});
|
||||
|
||||
// add has many field
|
||||
await db.getRepository('fields').create({
|
||||
values: {
|
||||
name: 'comments',
|
||||
collectionName: 'posts',
|
||||
type: 'hasMany',
|
||||
target: 'comments',
|
||||
foreignKey: 'postTitle',
|
||||
sourceKey: 'title',
|
||||
},
|
||||
context: {},
|
||||
});
|
||||
|
||||
// it should throw error when destroy title field
|
||||
let error;
|
||||
try {
|
||||
await db.getRepository('fields').destroy({
|
||||
filter: {
|
||||
name: 'title',
|
||||
collectionName: 'posts',
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(error).toBeTruthy();
|
||||
expect(error.message).toBe(
|
||||
`Can't delete field title of posts, it is used by field comments in collection posts as sourceKey`,
|
||||
);
|
||||
|
||||
// it should destroy posts collection
|
||||
await db.getRepository('collections').destroy({
|
||||
filter: {
|
||||
name: 'posts',
|
||||
},
|
||||
cascade: true,
|
||||
context: {},
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,51 @@
|
||||
import { Database } from '@nocobase/database';
|
||||
|
||||
export function beforeDestoryField(db: Database) {
|
||||
return async (model, opts) => {
|
||||
const { transaction } = opts;
|
||||
const { name, type, collectionName } = model.get();
|
||||
|
||||
if (['belongsTo', 'hasOne', 'hasMany', 'belongsToMany'].includes(type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const relatedFields = await db.getRepository('fields').find({
|
||||
filter: {
|
||||
$or: [
|
||||
{
|
||||
['options.sourceKey']: name,
|
||||
collectionName,
|
||||
},
|
||||
{
|
||||
['options.targetKey']: name,
|
||||
['options.target']: collectionName,
|
||||
},
|
||||
],
|
||||
},
|
||||
transaction,
|
||||
});
|
||||
|
||||
for (const field of relatedFields) {
|
||||
const keys = [
|
||||
{
|
||||
name: 'sourceKey',
|
||||
condition: (associationField) =>
|
||||
associationField.options['sourceKey'] === name && associationField.collectionName === collectionName,
|
||||
},
|
||||
{
|
||||
name: 'targetKey',
|
||||
condition: (associationField) =>
|
||||
associationField.options['targetKey'] === name && associationField.options['target'] === collectionName,
|
||||
},
|
||||
];
|
||||
|
||||
const usedAs = keys.find((key) => key.condition(field))['name'];
|
||||
|
||||
throw new Error(
|
||||
`Can't delete field ${name} of ${collectionName}, it is used by field ${field.get(
|
||||
'name',
|
||||
)} in collection ${field.get('collectionName')} as ${usedAs}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
@ -158,6 +158,10 @@ export class CollectionModel extends MagicAttributeModel {
|
||||
} else if (field.get('through') && field.get('through') === name) {
|
||||
await field.destroy({ transaction });
|
||||
}
|
||||
|
||||
if (field.get('collectionName') === name) {
|
||||
await field.destroy({ transaction });
|
||||
}
|
||||
}
|
||||
|
||||
await collection.removeFromDb(options);
|
||||
|
@ -28,6 +28,7 @@ import { CollectionModel, FieldModel } from './models';
|
||||
import collectionActions from './resourcers/collections';
|
||||
import viewResourcer from './resourcers/views';
|
||||
import { FieldNameExistsError } from './errors/field-name-exists-error';
|
||||
import { beforeDestoryField } from './hooks/beforeDestoryField';
|
||||
|
||||
export class PluginDataSourceMainServer extends Plugin {
|
||||
public schema: string;
|
||||
@ -85,7 +86,7 @@ export class PluginDataSourceMainServer extends Plugin {
|
||||
removeOptions['transaction'] = options.transaction;
|
||||
}
|
||||
|
||||
const cascade = lodash.get(options, 'context.action.params.cascade', false);
|
||||
const cascade = options.cascade || lodash.get(options, 'context.action.params.cascade', false);
|
||||
|
||||
if (cascade === true || cascade === 'true') {
|
||||
removeOptions['cascade'] = true;
|
||||
@ -243,6 +244,7 @@ export class PluginDataSourceMainServer extends Plugin {
|
||||
});
|
||||
|
||||
// before field remove
|
||||
this.app.db.on('fields.beforeDestroy', beforeDestoryField(this.app.db));
|
||||
this.app.db.on('fields.beforeDestroy', beforeDestroyForeignKey(this.app.db));
|
||||
|
||||
const mutex = new Mutex();
|
||||
|
Loading…
Reference in New Issue
Block a user