From e56e52ad494a3196b46e4e66a0986cc97ec63798 Mon Sep 17 00:00:00 2001 From: ChengLei Shao Date: Fri, 14 Jun 2024 12:02:03 +0800 Subject: [PATCH] chore(action-import): validate association & select field value (#4643) * chore: validate association value in import * chore: validate value in select field * fix: test --- .../interfaces/multiple-select-interface.ts | 12 +- .../src/interfaces/select-interface.ts | 15 +- .../src/interfaces/to-many-interface.ts | 7 + .../src/interfaces/to-one-interface.ts | 13 +- .../server/__tests__/xlsx-importer.test.ts | 322 +++++++++++++++++- .../src/server/services/xlsx-importer.ts | 2 +- 6 files changed, 362 insertions(+), 9 deletions(-) diff --git a/packages/core/database/src/interfaces/multiple-select-interface.ts b/packages/core/database/src/interfaces/multiple-select-interface.ts index bec77363ef..66548c3e35 100644 --- a/packages/core/database/src/interfaces/multiple-select-interface.ts +++ b/packages/core/database/src/interfaces/multiple-select-interface.ts @@ -16,12 +16,22 @@ export class MultipleSelectInterface extends BaseInterface { const enumConfig = this.options.uiSchema?.enum || []; return items.map((item) => { const option = enumConfig.find((option) => option.label === item); - return option ? option.value : item; + if (option) { + return option.value; + } + + const valueOption = enumConfig.find((option) => option.value === item); + if (valueOption) { + return valueOption.value; + } + + throw new Error(`"${item}" is not a valid option in ${ctx.field.name} field.`); }); } toString(value: any, ctx?: any) { const enumConfig = this.options.uiSchema?.enum || []; + return lodash .castArray(value) .map((value) => { diff --git a/packages/core/database/src/interfaces/select-interface.ts b/packages/core/database/src/interfaces/select-interface.ts index a904a150be..e05f283f49 100644 --- a/packages/core/database/src/interfaces/select-interface.ts +++ b/packages/core/database/src/interfaces/select-interface.ts @@ -11,9 +11,22 @@ import { BaseInterface } from './base-interface'; export class SelectInterface extends BaseInterface { async toValue(str: string, ctx?: any): Promise { + if (!str) { + return null; + } + const enumConfig = this.options.uiSchema?.enum || []; const option = enumConfig.find((item) => item.label === str); - return option?.value || str; + if (option) { + return option.value; + } + + const valueOption = enumConfig.find((item) => item.value === str); + if (valueOption) { + return valueOption.value; + } + + throw new Error(`"${str}" is not a valid option in ${ctx.field.name} field.`); } toString(value: any, ctx?: any) { diff --git a/packages/core/database/src/interfaces/to-many-interface.ts b/packages/core/database/src/interfaces/to-many-interface.ts index 7c3645fbc6..664ff692f7 100644 --- a/packages/core/database/src/interfaces/to-many-interface.ts +++ b/packages/core/database/src/interfaces/to-many-interface.ts @@ -26,6 +26,13 @@ export class ToManyInterface extends BaseInterface { transaction, }); + // check if all items are found + items.forEach((item) => { + if (!targetInstances.find((targetInstance) => targetInstance[filterKey] === item)) { + throw new Error(`"${item}" not found in ${targetCollection.model.name} ${filterKey}`); + } + }); + const primaryKeyAttribute = targetCollection.model.primaryKeyAttribute; return targetInstances.map((targetInstance) => targetInstance[primaryKeyAttribute]); diff --git a/packages/core/database/src/interfaces/to-one-interface.ts b/packages/core/database/src/interfaces/to-one-interface.ts index 4dffbc0977..e2e58b6c8e 100644 --- a/packages/core/database/src/interfaces/to-one-interface.ts +++ b/packages/core/database/src/interfaces/to-one-interface.ts @@ -15,6 +15,10 @@ export class ToOneInterface extends BaseInterface { } async toValue(str: string, ctx?: any) { + if (!str) { + return null; + } + const { filterKey, targetCollection, transaction } = ctx; const targetInstance = await targetCollection.repository.findOne({ @@ -24,12 +28,11 @@ export class ToOneInterface extends BaseInterface { transaction, }); + if (!targetInstance) { + throw new Error(`"${str}" not found in ${targetCollection.model.name} ${filterKey}`); + } const primaryKeyAttribute = targetCollection.model.primaryKeyAttribute; - if (targetInstance) { - return targetInstance[primaryKeyAttribute]; - } - - return null; + return targetInstance[primaryKeyAttribute]; } } diff --git a/packages/plugins/@nocobase/plugin-action-import/src/server/__tests__/xlsx-importer.test.ts b/packages/plugins/@nocobase/plugin-action-import/src/server/__tests__/xlsx-importer.test.ts index 6856058d69..e3eafb395b 100644 --- a/packages/plugins/@nocobase/plugin-action-import/src/server/__tests__/xlsx-importer.test.ts +++ b/packages/plugins/@nocobase/plugin-action-import/src/server/__tests__/xlsx-importer.test.ts @@ -25,6 +25,326 @@ describe('xlsx importer', () => { await app.destroy(); }); + describe('import with select fields', () => { + let User; + beforeEach(async () => { + User = app.db.collection({ + name: 'users', + fields: [ + { type: 'string', name: 'name', title: '姓名' }, + { + uiSchema: { + enum: [ + { + value: '123', + label: 'Label123', + color: 'orange', + }, + { + value: '223', + label: 'Label223', + color: 'lime', + }, + ], + type: 'array', + 'x-component': 'Select', + 'x-component-props': { + mode: 'multiple', + }, + title: 'multi-select', + }, + defaultValue: [], + name: 'multiSelect', + type: 'array', + interface: 'multipleSelect', + }, + { + uiSchema: { + enum: [ + { + value: '123', + label: 'Label123', + color: 'orange', + }, + { + value: '223', + label: 'Label223', + color: 'lime', + }, + ], + type: 'string', + 'x-component': 'Select', + title: 'select', + }, + name: 'select', + type: 'string', + interface: 'select', + }, + ], + }); + + await app.db.sync(); + }); + + it('should import select field with label and value', async () => { + const columns = [ + { + dataIndex: ['name'], + defaultTitle: '姓名', + }, + { + dataIndex: ['multiSelect'], + defaultTitle: '多选', + }, + { + dataIndex: ['select'], + defaultTitle: '单选', + }, + ]; + + const templateCreator = new TemplateCreator({ + collection: User, + columns, + }); + + const template = await templateCreator.run(); + + const worksheet = template.Sheets[template.SheetNames[0]]; + + XLSX.utils.sheet_add_aoa(worksheet, [['test', 'Label123,223', 'Label123']], { + origin: 'A2', + }); + + const importer = new XlsxImporter({ + collectionManager: app.mainDataSource.collectionManager, + collection: User, + columns, + workbook: template, + }); + + await importer.run(); + + expect(await User.repository.count()).toBe(1); + + const user = await User.repository.findOne(); + + expect(user.get('multiSelect')).toEqual(['123', '223']); + expect(user.get('select')).toBe('123'); + }); + + it('should validate values in multiple select field', async () => { + const columns = [ + { + dataIndex: ['name'], + defaultTitle: '姓名', + }, + { + dataIndex: ['multiSelect'], + defaultTitle: '多选', + }, + ]; + + const templateCreator = new TemplateCreator({ + collection: User, + columns, + }); + + const template = await templateCreator.run(); + + const worksheet = template.Sheets[template.SheetNames[0]]; + + XLSX.utils.sheet_add_aoa(worksheet, [['test', 'abc']], { + origin: 'A2', + }); + + const importer = new XlsxImporter({ + collectionManager: app.mainDataSource.collectionManager, + collection: User, + columns, + workbook: template, + }); + + let error; + + try { + await importer.run(); + } catch (e) { + error = e; + } + + expect(error).toBeTruthy(); + }); + it('should validate values in select field', async () => { + const columns = [ + { + dataIndex: ['name'], + defaultTitle: '姓名', + }, + { + dataIndex: ['select'], + defaultTitle: '单选', + }, + ]; + + const templateCreator = new TemplateCreator({ + collection: User, + columns, + }); + + const template = await templateCreator.run(); + + const worksheet = template.Sheets[template.SheetNames[0]]; + + XLSX.utils.sheet_add_aoa(worksheet, [['test', 'abc']], { + origin: 'A2', + }); + + const importer = new XlsxImporter({ + collectionManager: app.mainDataSource.collectionManager, + collection: User, + columns, + workbook: template, + }); + + let error; + + try { + await importer.run(); + } catch (e) { + error = e; + } + + expect(error).toBeTruthy(); + }); + }); + + describe('import with associations', () => { + let User; + let Post; + beforeEach(async () => { + User = app.db.collection({ + name: 'users', + fields: [ + { + type: 'string', + name: 'name', + }, + { + type: 'hasMany', + name: 'posts', + target: 'posts', + interface: 'o2m', + foreignKey: 'userId', + }, + ], + }); + + Post = app.db.collection({ + name: 'posts', + fields: [ + { + type: 'string', + name: 'title', + }, + { + type: 'belongsTo', + name: 'user', + target: 'users', + interface: 'm2o', + }, + ], + }); + + await app.db.sync(); + }); + + it('should validate to many association', async () => { + const columns = [ + { + dataIndex: ['name'], + defaultTitle: '名称', + }, + { + dataIndex: ['posts', 'title'], + defaultTitle: '标题', + }, + ]; + + const templateCreator = new TemplateCreator({ + collection: User, + columns, + }); + + const template = await templateCreator.run(); + + const worksheet = template.Sheets[template.SheetNames[0]]; + + XLSX.utils.sheet_add_aoa(worksheet, [['test', '测试标题']], { + origin: 'A2', + }); + + const importer = new XlsxImporter({ + collectionManager: app.mainDataSource.collectionManager, + collection: User, + columns, + workbook: template, + }); + + let error; + + try { + await importer.run(); + } catch (e) { + error = e; + } + + expect(error).toBeTruthy(); + }); + + it('should validate to one association', async () => { + const columns = [ + { + dataIndex: ['title'], + defaultTitle: '标题', + }, + { + dataIndex: ['user', 'name'], + defaultTitle: '用户名', + }, + ]; + + const templateCreator = new TemplateCreator({ + collection: Post, + columns, + }); + + const template = await templateCreator.run(); + + const worksheet = template.Sheets[template.SheetNames[0]]; + + XLSX.utils.sheet_add_aoa(worksheet, [['test title', 'test user']], { + origin: 'A2', + }); + + const importer = new XlsxImporter({ + collectionManager: app.mainDataSource.collectionManager, + collection: Post, + columns, + workbook: template, + }); + + let error; + + try { + await importer.run(); + } catch (e) { + error = e; + } + + expect(error).toBeTruthy(); + }); + }); + it('should import china region field', async () => { const Post = app.db.collection({ name: 'posts', @@ -652,7 +972,7 @@ describe('xlsx importer', () => { [ ['Post1', 'Content1', 'User1', 'Tag1,Tag2', 'Comment1,Comment2'], ['Post2', 'Content2', 'User1', 'Tag2,Tag3', 'Comment3'], - ['Post3', 'Content3', 'UserNotExist', 'Tag3,TagNotExist', ''], + ['Post3', 'Content3', 'User1', 'Tag3', ''], ['Post4', '', '', ''], ['Post5', null, null, null], ], diff --git a/packages/plugins/@nocobase/plugin-action-import/src/server/services/xlsx-importer.ts b/packages/plugins/@nocobase/plugin-action-import/src/server/services/xlsx-importer.ts index 8ca6711655..4aea097821 100644 --- a/packages/plugins/@nocobase/plugin-action-import/src/server/services/xlsx-importer.ts +++ b/packages/plugins/@nocobase/plugin-action-import/src/server/services/xlsx-importer.ts @@ -188,7 +188,7 @@ export class XlsxImporter extends EventEmitter { await new Promise((resolve) => setTimeout(resolve, 5)); } catch (error) { throw new Error( - `failed to import row ${handingRowIndex}, rowData: ${JSON.stringify(rowValues)} message: ${error.message}`, + `failed to import row ${handingRowIndex}, message: ${error.message}, rowData: ${JSON.stringify(rowValues)}`, { cause: error }, ); }