diff --git a/packages/core/server/src/locale/locale.ts b/packages/core/server/src/locale/locale.ts index ac5c99e279f90..29c548b6057ad 100644 --- a/packages/core/server/src/locale/locale.ts +++ b/packages/core/server/src/locale/locale.ts @@ -109,6 +109,7 @@ export class Locale { // empty } } + Object.keys(resources).forEach((name) => { this.app.i18n.addResources(lang, name, resources[name]); }); diff --git a/packages/plugins/@nocobase/plugin-data-source-main/src/locale/en-US.json b/packages/plugins/@nocobase/plugin-data-source-main/src/locale/en-US.json new file mode 100644 index 0000000000000..d3de755cd014b --- /dev/null +++ b/packages/plugins/@nocobase/plugin-data-source-main/src/locale/en-US.json @@ -0,0 +1,3 @@ +{ + "field-name-exists": "Field name \"{{name}}\" already exists in collection \"{{collectionName}}\"" +} diff --git a/packages/plugins/@nocobase/plugin-data-source-main/src/locale/zh-CN.json b/packages/plugins/@nocobase/plugin-data-source-main/src/locale/zh-CN.json new file mode 100644 index 0000000000000..a7025036424f9 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-data-source-main/src/locale/zh-CN.json @@ -0,0 +1,3 @@ +{ + "field-name-exists": "字段标识 \"{{name}}\" 已存在" +} diff --git a/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/beforeInitOptions.test.ts b/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/beforeInitOptions.test.ts index ecebf5e40194d..e9e1b1897ac1d 100644 --- a/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/beforeInitOptions.test.ts +++ b/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/beforeInitOptions.test.ts @@ -66,6 +66,7 @@ describe('collections repository', () => { }, context: {}, }); + expect(field1.toJSON()).toMatchObject({ type: 'belongsToMany', collectionName: 'foos', @@ -81,6 +82,7 @@ describe('collections repository', () => { }, context: {}, }); + expect(field2.toJSON()).toMatchObject({ type: 'belongsTo', collectionName: 'foos', diff --git a/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/fields.repository.test.ts b/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/fields.repository.test.ts index 26fbd8bfc0f71..bc6a7f6be92db 100644 --- a/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/fields.repository.test.ts +++ b/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/fields.repository.test.ts @@ -132,6 +132,35 @@ describe('collections repository', () => { await app.destroy(); }); + it('should throw error when field name already exists', async () => { + await Field.repository.create({ + values: { + name: 'name', + type: 'string', + collectionName: 'tests', + }, + }); + let error; + + try { + await Field.repository.create({ + values: { + name: 'name', + type: 'string', + collectionName: 'tests', + }, + context: {}, + }); + } catch (err) { + error = err; + } + + expect(error).toBeDefined(); + expect(error.name).toBe('FieldNameExistsError'); + expect(error.value).toBe('name'); + expect(error.collectionName).toBe('tests'); + }); + it('should generate the name and key randomly', async () => { const field = await Field.repository.create({ values: { diff --git a/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/http-api/collections.test.ts b/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/http-api/collections.test.ts index d39908b9de09a..317595a6f2fe6 100644 --- a/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/http-api/collections.test.ts +++ b/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/http-api/collections.test.ts @@ -84,6 +84,41 @@ describe('collections repository', () => { await app.destroy(); }); + it('should throw error when create field with same name', async () => { + const response = await agent.resource('collections').create({ + values: { + name: 'test', + autoGenId: false, + sortable: false, + timestamps: false, + }, + }); + + expect(response.statusCode).toBe(200); + + const response2 = await agent.resource('fields').create({ + values: { + name: 'field', + type: 'string', + collectionName: 'test', + }, + }); + + expect(response2.statusCode).toBe(200); + + const response3 = await agent.resource('fields').create({ + values: { + name: 'field', + type: 'string', + collectionName: 'test', + }, + }); + + expect(response3.statusCode).toBe(400); + const responseBody = response3.body; + console.log(responseBody); + }); + it('should skip sync when create empty collection', async () => { const response = await agent.resource('collections').create({ values: { diff --git a/packages/plugins/@nocobase/plugin-data-source-main/src/server/errors/field-name-exists-error.ts b/packages/plugins/@nocobase/plugin-data-source-main/src/server/errors/field-name-exists-error.ts new file mode 100644 index 0000000000000..2acc69223af41 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-data-source-main/src/server/errors/field-name-exists-error.ts @@ -0,0 +1,21 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +export class FieldNameExistsError extends Error { + value: string; + collectionName: string; + + constructor(value: string, collectionName: string) { + super(`Field name "${value}" already exists in collection "${collectionName}"`); + this.value = value; + this.collectionName = collectionName; + + this.name = 'FieldNameExistsError'; + } +} diff --git a/packages/plugins/@nocobase/plugin-data-source-main/src/server/server.ts b/packages/plugins/@nocobase/plugin-data-source-main/src/server/server.ts index cbd4537df83b1..ff1a10b679454 100644 --- a/packages/plugins/@nocobase/plugin-data-source-main/src/server/server.ts +++ b/packages/plugins/@nocobase/plugin-data-source-main/src/server/server.ts @@ -27,6 +27,7 @@ import { beforeCreateForViewCollection } from './hooks/beforeCreateForViewCollec import { CollectionModel, FieldModel } from './models'; import collectionActions from './resourcers/collections'; import viewResourcer from './resourcers/views'; +import { FieldNameExistsError } from './errors/field-name-exists-error'; export class PluginDataSourceMainServer extends Plugin { public schema: string; @@ -128,6 +129,30 @@ export class PluginDataSourceMainServer extends Plugin { this.app.db.on('fields.beforeCreate', beforeCreateForValidateField(this.app.db)); this.app.db.on('fields.afterCreate', afterCreateForReverseField(this.app.db)); + + this.app.db.on('fields.beforeCreate', async (model: FieldModel, options) => { + const { transaction } = options; + // validate field name + const collectionName = model.get('collectionName'); + const name = model.get('name'); + + if (!collectionName || !name) { + return; + } + + const exists = await this.app.db.getRepository('fields').findOne({ + filter: { + collectionName, + name, + }, + transaction, + }); + + if (exists) { + throw new FieldNameExistsError(name, collectionName); + } + }); + this.app.db.on('fields.beforeUpdate', beforeUpdateForValidateField(this.app.db)); this.app.db.on('fields.beforeUpdate', async (model, options) => { @@ -308,6 +333,25 @@ export class PluginDataSourceMainServer extends Plugin { }, ); + errorHandlerPlugin.errorHandler.register( + (err) => err instanceof FieldNameExistsError, + (err, ctx) => { + ctx.status = 400; + + ctx.body = { + errors: [ + { + message: ctx.i18n.t('field-name-exists', { + name: err.value, + collectionName: err.collectionName, + ns: 'data-source-main', + }), + }, + ], + }; + }, + ); + this.app.resourcer.use(async (ctx, next) => { if (ctx.action.resourceName === 'collections.fields' && ['create', 'update'].includes(ctx.action.actionName)) { ctx.action.mergeParams({