diff --git a/packages/core/database/src/collection.ts b/packages/core/database/src/collection.ts index e529bf8fa2..04f52aa3cc 100644 --- a/packages/core/database/src/collection.ts +++ b/packages/core/database/src/collection.ts @@ -392,7 +392,6 @@ export class Collection< } const fieldName = options.field || snakeCase(name); - const field = this.findField((f) => { if (f.name === name) { return false; @@ -406,10 +405,20 @@ export class Collection< if (!field) { return; } - - if (options.type !== field.type) { - throw new Error(`fields with same column must be of the same type ${JSON.stringify(options)}`); + if (options.type === field.type) { + return; } + const isContextTypeMatch = (data, dataType: string): boolean => { + return [data.dataType?.key, data.type?.toUpperCase()].includes(dataType?.toUpperCase()); + }; + if (options.type === 'context' && isContextTypeMatch(field, options.dataType)) { + return; + } + if (field.type === 'context' && isContextTypeMatch(options, field.dataType.key)) { + return; + } + + throw new Error(`fields with same column must be of the same type ${JSON.stringify(options)}`); } /** diff --git a/packages/core/database/src/relation-repository/belongs-to-many-repository.ts b/packages/core/database/src/relation-repository/belongs-to-many-repository.ts index eca56eafa3..1d75c03723 100644 --- a/packages/core/database/src/relation-repository/belongs-to-many-repository.ts +++ b/packages/core/database/src/relation-repository/belongs-to-many-repository.ts @@ -22,7 +22,7 @@ export class BelongsToManyRepository extends MultipleRelationRepository { async aggregate(options: AggregateOptions) { const targetRepository = this.targetCollection.repository; - const sourceModel = await this.getSourceModel(); + const sourceModel = await this.getSourceModel(await this.getTransaction(options)); const association = this.association as any; diff --git a/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/index.ts b/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/index.ts index 8cd8450ded..3a8a1150b9 100644 --- a/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/index.ts +++ b/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/index.ts @@ -13,7 +13,7 @@ export async function createApp(options: any = {}) { const app = await createMockServer({ acl: false, ...options, - plugins: ['error-handler', 'field-sort', 'data-source-main', 'ui-schema-storage'], + plugins: (options.plugins || []).concat(['error-handler', 'field-sort', 'data-source-main', 'ui-schema-storage']), }); return app; } diff --git a/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/view/view-collection.test.ts b/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/view/view-collection.test.ts index ac45d9077b..b03e0866e2 100644 --- a/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/view/view-collection.test.ts +++ b/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/view/view-collection.test.ts @@ -11,10 +11,11 @@ import Database, { Repository, ViewCollection, ViewFieldInference } from '@nocob import Application from '@nocobase/server'; import { uid } from '@nocobase/utils'; import { createApp } from '../index'; +import { MockServer } from '@nocobase/test'; describe('view collection', function () { let db: Database; - let app: Application; + let app: MockServer; let collectionRepository: Repository; @@ -25,6 +26,7 @@ describe('view collection', function () { database: { tablePrefix: '', }, + plugins: ['users'], }); db = app.db; @@ -48,7 +50,7 @@ describe('view collection', function () { await collectionRepository.create({ values: { - name: 'users', + name: 'users_1', fields: [ { name: 'name', type: 'string' }, { type: 'belongsTo', name: 'group', foreignKey: 'group_id' }, @@ -57,7 +59,7 @@ describe('view collection', function () { context: {}, }); - const User = db.getCollection('users'); + const User = db.getCollection('users_1'); const assoc = User.model.associations.group; const foreignKey = assoc.foreignKey; @@ -67,7 +69,7 @@ describe('view collection', function () { await db.sequelize.query(`DROP VIEW IF EXISTS ${viewName}`); const createSQL = `CREATE VIEW ${viewName} AS SELECT id, ${foreignField}, name FROM ${db - .getCollection('users') + .getCollection('users_1') .quotedTableName()}`; await db.sequelize.query(createSQL); @@ -107,7 +109,7 @@ describe('view collection', function () { await collectionRepository.create({ values: { - name: 'users', + name: 'users_1', fields: [ { name: 'name', type: 'string' }, { type: 'belongsTo', name: 'group', foreignKey: 'group_id' }, @@ -116,7 +118,7 @@ describe('view collection', function () { context: {}, }); - const User = db.getCollection('users'); + const User = db.getCollection('users_1'); const assoc = User.model.associations.group; const foreignKey = assoc.foreignKey; @@ -126,7 +128,7 @@ describe('view collection', function () { await db.sequelize.query(`DROP VIEW IF EXISTS ${viewName}`); const createSQL = `CREATE VIEW ${viewName} AS SELECT id, ${foreignField}, name FROM ${db - .getCollection('users') + .getCollection('users_1') .quotedTableName()}`; await db.sequelize.query(createSQL); @@ -161,7 +163,7 @@ describe('view collection', function () { it('should load view collection belongs to field', async () => { await collectionRepository.create({ values: { - name: 'users', + name: 'users_1', fields: [ { type: 'string', @@ -190,14 +192,14 @@ describe('view collection', function () { type: 'belongsTo', name: 'user', foreignKey: 'userId', - target: 'users', + target: 'users_1', }, ], }, context: {}, }); - await db.getRepository('users').create({ + await db.getRepository('users_1').create({ values: [ { name: 'u1', @@ -216,7 +218,7 @@ describe('view collection', function () { await db.sequelize.query(`DROP VIEW IF EXISTS ${viewName}`); const viewSQL = ` - CREATE VIEW ${viewName} as SELECT users.* FROM ${Post.quotedTableName()} as users + CREATE VIEW ${viewName} as SELECT users_1.* FROM ${Post.quotedTableName()} as users_1 `; await db.sequelize.query(viewSQL); @@ -256,7 +258,7 @@ describe('view collection', function () { it('should use view collection as through collection', async () => { const User = await collectionRepository.create({ values: { - name: 'users', + name: 'users_1', fields: [{ name: 'name', type: 'string' }], }, context: {}, @@ -270,9 +272,9 @@ describe('view collection', function () { context: {}, }); - const UserCollection = db.getCollection('users'); + const UserCollection = db.getCollection('users_1'); - await db.getRepository('users').create({ + await db.getRepository('users_1').create({ values: [{ name: 'u1' }, { name: 'u2' }], }); @@ -324,7 +326,7 @@ describe('view collection', function () { await fieldsRepository.create({ values: { - collectionName: 'users', + collectionName: 'users_1', name: 'roles', type: 'belongsToMany', target: 'my_roles', @@ -335,14 +337,14 @@ describe('view collection', function () { context: {}, }); - const users = await db.getRepository('users').find({ + const users_1 = await db.getRepository('users_1').find({ appends: ['roles'], filter: { name: 'u1', }, }); - const roles = users[0].get('roles'); + const roles = users_1[0].get('roles'); expect(roles).toHaveLength(2); await collectionRepository.destroy({ @@ -355,7 +357,7 @@ describe('view collection', function () { expect( await fieldsRepository.count({ filter: { - collectionName: 'users', + collectionName: 'users_1', name: 'roles', }, }), @@ -366,7 +368,6 @@ describe('view collection', function () { if (!db.inDialect('postgres')) { return; } - const viewName = 'test_view'; const dbSchema = db.options.schema || 'public'; const randomSchema = `s_${uid(6)}`; @@ -541,4 +542,72 @@ describe('view collection', function () { expect(db.getCollection('view_collection')).toBeUndefined(); }); + + it('should create view collection successfully when underscored env and DB_DIALECT=mysql', async () => { + if (!db.options.underscored) { + return; + } + const tableName = db.inDialect('postgres') ? `${process.env.DB_SCHEMA}.users` : 'users'; + const dropViewSQL = `DROP VIEW IF EXISTS test_view`; + await db.sequelize.query(dropViewSQL); + const viewSQL = `CREATE VIEW test_view AS select * from ${tableName}`; + await db.sequelize.query(viewSQL); + + const response = await app + .agent() + .resource('collections') + .create({ + values: { + name: 'fff1', + template: 'view', + view: true, + fields: [ + { + name: 'id', + rawType: 'BIGINT', + field: 'id', + type: 'bigInt', + source: 'users.id', + uiSchema: { title: 'id' }, + }, + { + name: 'createdBy', + type: 'belongsTo', + source: 'users.createdBy', + uiSchema: { title: 'createdBy' }, + }, + { + name: 'created_by_id', + rawType: 'BIGINT', + field: 'created_by_id', + type: 'bigInt', + possibleTypes: ['bigInt', 'unixTimestamp', 'sort'], + uiSchema: { title: 'created_by_id' }, + }, + { + name: 'updatedBy', + type: 'belongsTo', + source: 'users.updatedBy', + uiSchema: { title: 'updatedBy' }, + }, + { + name: 'updated_by_id', + rawType: 'BIGINT', + field: 'updated_by_id', + type: 'bigInt', + possibleTypes: ['bigInt', 'unixTimestamp', 'sort'], + uiSchema: { title: 'updated_by_id' }, + }, + ], + schema: null, + writableView: false, + sources: ['users'], + title: 'view_collection_display_name', + databaseView: 'test_view', + viewName: 'test_view', + }, + }); + + expect(response.status).toBe(200); + }); }); diff --git a/packages/plugins/@nocobase/plugin-workflow-aggregate/src/client/AggregateInstruction.tsx b/packages/plugins/@nocobase/plugin-workflow-aggregate/src/client/AggregateInstruction.tsx index d4dd846fea..bc6096aeb4 100644 --- a/packages/plugins/@nocobase/plugin-workflow-aggregate/src/client/AggregateInstruction.tsx +++ b/packages/plugins/@nocobase/plugin-workflow-aggregate/src/client/AggregateInstruction.tsx @@ -387,6 +387,21 @@ export default class extends Instruction { }, }, }, + precision: { + type: 'number', + title: `{{t("Result precision", { ns: "${NAMESPACE}" })}}`, + description: `{{t("Number of decimal places for query result.", { ns: "${NAMESPACE}" })}}`, + 'x-decorator': 'FormItem', + 'x-component': 'InputNumber', + 'x-component-props': { + min: 0, + max: 14, + step: 1, + precision: 0, + className: 'auto-width', + }, + default: 2, + }, }; scope = { useCollectionDataSource, diff --git a/packages/plugins/@nocobase/plugin-workflow-aggregate/src/locale/zh-CN.json b/packages/plugins/@nocobase/plugin-workflow-aggregate/src/locale/zh-CN.json index f3b9df4362..12cdb3181f 100644 --- a/packages/plugins/@nocobase/plugin-workflow-aggregate/src/locale/zh-CN.json +++ b/packages/plugins/@nocobase/plugin-workflow-aggregate/src/locale/zh-CN.json @@ -8,5 +8,7 @@ "Data of associated collection": "关联数据表数据", "Field to aggregate": "聚合字段", "Distinct": "去重", - "Query result": "查询结果" + "Query result": "查询结果", + "Result precision": "结果精度", + "Number of decimal places for query result.": "查询结果小数位数" } diff --git a/packages/plugins/@nocobase/plugin-workflow-aggregate/src/server/AggregateInstruction.ts b/packages/plugins/@nocobase/plugin-workflow-aggregate/src/server/AggregateInstruction.ts index 7517611809..e81da5f374 100644 --- a/packages/plugins/@nocobase/plugin-workflow-aggregate/src/server/AggregateInstruction.ts +++ b/packages/plugins/@nocobase/plugin-workflow-aggregate/src/server/AggregateInstruction.ts @@ -23,7 +23,7 @@ const aggregators = { export default class extends Instruction { async run(node: FlowNodeModel, input, processor: Processor) { - const { aggregator, associated, collection, association = {}, params = {} } = node.config; + const { aggregator, associated, collection, association = {}, params = {}, precision = 2 } = node.config; const options = processor.getParsedValue(params, node.id); const [dataSourceName, collectionName] = parseCollectionName(collection); const { collectionManager } = this.workflow.app.dataSourceManager.dataSources.get(dataSourceName); @@ -49,7 +49,10 @@ export default class extends Instruction { }); return { - result: round((options.dataType === DataTypes.DOUBLE ? Number(result) : result) || 0, 14), + result: round( + (options.dataType === DataTypes.DOUBLE ? Number(result) : result) || 0, + Math.max(0, Math.min(precision, 14)), + ), status: JOB_STATUS.RESOLVED, }; } diff --git a/packages/plugins/@nocobase/plugin-workflow-aggregate/src/server/__tests__/instruction.test.ts b/packages/plugins/@nocobase/plugin-workflow-aggregate/src/server/__tests__/instruction.test.ts index 4a3102cd1d..a75d3ba606 100644 --- a/packages/plugins/@nocobase/plugin-workflow-aggregate/src/server/__tests__/instruction.test.ts +++ b/packages/plugins/@nocobase/plugin-workflow-aggregate/src/server/__tests__/instruction.test.ts @@ -36,6 +36,7 @@ describe('workflow > instructions > aggregate', () => { TagRepo = db.getCollection('tags').repository; workflow = await WorkflowModel.create({ + sync: true, enabled: true, type: 'collection', config: { @@ -62,8 +63,6 @@ describe('workflow > instructions > aggregate', () => { const post = await PostRepo.create({ values: { title: 't1' } }); - await sleep(500); - const [execution] = await workflow.getExecutions(); const [job] = await execution.getJobs(); expect(job.result).toBe(1); @@ -86,8 +85,6 @@ describe('workflow > instructions > aggregate', () => { const post = await PostRepo.create({ values: { title: 't1' } }); - await sleep(500); - const [execution] = await workflow.getExecutions(); const [job] = await execution.getJobs(); expect(job.result).toBe(0); @@ -107,16 +104,12 @@ describe('workflow > instructions > aggregate', () => { const p1 = await PostRepo.create({ values: { title: 't1', read: 1 } }); - await sleep(500); - const [e1] = await workflow.getExecutions(); const [j1] = await e1.getJobs(); expect(j1.result).toBe(1); const p2 = await PostRepo.create({ values: { title: 't2', read: 2 } }); - await sleep(500); - const [e2] = await workflow.getExecutions({ order: [['id', 'desc']] }); const [j2] = await e2.getJobs(); expect(j2.result).toBe(3); @@ -136,22 +129,18 @@ describe('workflow > instructions > aggregate', () => { const p1 = await PostRepo.create({ values: { title: 't1', score: 0.1 } }); - await sleep(500); - const [e1] = await workflow.getExecutions(); const [j1] = await e1.getJobs(); expect(j1.result).toBe(0.1); const p2 = await PostRepo.create({ values: { title: 't2', score: 0.2 } }); - await sleep(500); - const [e2] = await workflow.getExecutions({ order: [['id', 'desc']] }); const [j2] = await e2.getJobs(); expect(j2.result).toBe(0.3); }); - it('sum number will be rounded', async () => { + it('sum number will be rounded to 2 decimal places by default', async () => { const n1 = await workflow.createNode({ type: 'aggregate', config: { @@ -163,9 +152,59 @@ describe('workflow > instructions > aggregate', () => { }, }); - const p1 = await PostRepo.create({ values: { title: 't1', score: 0.100000000000001 } }); + const p1 = await PostRepo.create({ values: { title: 't1', score: 0.123 } }); - await sleep(500); + const [e1] = await workflow.getExecutions(); + const [j1] = await e1.getJobs(); + expect(j1.result).toBe(0.12); + + const p2 = await PostRepo.create({ values: { title: 't2', score: 0.456 } }); + + const [e2] = await workflow.getExecutions({ order: [['id', 'desc']] }); + const [j2] = await e2.getJobs(); + expect(j2.result).toBe(0.58); + }); + + it('sum precision configured -1 as 0', async () => { + const n1 = await workflow.createNode({ + type: 'aggregate', + config: { + aggregator: 'sum', + collection: 'posts', + params: { + field: 'score', + }, + precision: -1, + }, + }); + + const p1 = await PostRepo.create({ values: { title: 't1', score: 0.123 } }); + + const [e1] = await workflow.getExecutions(); + const [j1] = await e1.getJobs(); + expect(j1.result).toBe(0); + + const p2 = await PostRepo.create({ values: { title: 't2', score: 0.456 } }); + + const [e2] = await workflow.getExecutions({ order: [['id', 'desc']] }); + const [j2] = await e2.getJobs(); + expect(j2.result).toBe(1); + }); + + it('sum precision configured over 14 as 14', async () => { + const n1 = await workflow.createNode({ + type: 'aggregate', + config: { + aggregator: 'sum', + collection: 'posts', + params: { + field: 'score', + }, + precision: 15, + }, + }); + + const p1 = await PostRepo.create({ values: { title: 't1', score: 0.100000000000001 } }); const [e1] = await workflow.getExecutions(); const [j1] = await e1.getJobs(); @@ -173,8 +212,6 @@ describe('workflow > instructions > aggregate', () => { const p2 = await PostRepo.create({ values: { title: 't2', score: 0.200000000000001 } }); - await sleep(500); - const [e2] = await workflow.getExecutions({ order: [['id', 'desc']] }); const [j2] = await e2.getJobs(); expect(j2.result).toBe(0.3); @@ -194,8 +231,6 @@ describe('workflow > instructions > aggregate', () => { const p2 = await PostRepo.create({ values: { title: 't2' } }); - await sleep(500); - const [e1] = await workflow.getExecutions(); const [j1] = await e1.getJobs(); expect(j1.result).toBe(0); @@ -215,16 +250,12 @@ describe('workflow > instructions > aggregate', () => { const p1 = await PostRepo.create({ values: { title: 't1', read: 1 } }); - await sleep(500); - const [e1] = await workflow.getExecutions(); const [j1] = await e1.getJobs(); expect(j1.result).toBe(1); const p2 = await PostRepo.create({ values: { title: 't2', read: 2 } }); - await sleep(500); - const [e2] = await workflow.getExecutions({ order: [['id', 'desc']] }); const [j2] = await e2.getJobs(); expect(j2.result).toBe(1.5); @@ -244,16 +275,12 @@ describe('workflow > instructions > aggregate', () => { const p1 = await PostRepo.create({ values: { title: 't1', read: 1 } }); - await sleep(500); - const [e1] = await workflow.getExecutions(); const [j1] = await e1.getJobs(); expect(j1.result).toBe(1); const p2 = await PostRepo.create({ values: { title: 't2', read: 2 } }); - await sleep(500); - const [e2] = await workflow.getExecutions({ order: [['id', 'desc']] }); const [j2] = await e2.getJobs(); expect(j2.result).toBe(1); @@ -273,16 +300,12 @@ describe('workflow > instructions > aggregate', () => { const p1 = await PostRepo.create({ values: { title: 't1', read: 1 } }); - await sleep(500); - const [e1] = await workflow.getExecutions(); const [j1] = await e1.getJobs(); expect(j1.result).toBe(1); const p2 = await PostRepo.create({ values: { title: 't2', read: 2 } }); - await sleep(500); - const [e2] = await workflow.getExecutions({ order: [['id', 'desc']] }); const [j2] = await e2.getJobs(); expect(j2.result).toBe(2); @@ -333,8 +356,6 @@ describe('workflow > instructions > aggregate', () => { const p1 = await PostRepo.create({ values: { title: 't1', comments: [{}, { status: 1 }] } }); - await sleep(500); - const [e1] = await workflow.getExecutions(); const [j1, j2] = await e1.getJobs({ order: [['id', 'ASC']] }); expect(j1.result).toBe(2); @@ -343,7 +364,7 @@ describe('workflow > instructions > aggregate', () => { it('sum', async () => { const PostModel = db.getCollection('posts').model; - const p1 = await PostModel.create({ title: 't1', read: 1 }); + const p1 = await PostModel.create({ title: 't1', read: 1 }, { hooks: false }); const n1 = await workflow.createNode({ type: 'create', @@ -400,8 +421,6 @@ describe('workflow > instructions > aggregate', () => { const p2 = await PostRepo.create({ values: { title: 't2', read: 2 } }); - await sleep(500); - const [e1] = await workflow.getExecutions(); const [j1, j2, j3] = await e1.getJobs({ order: [['id', 'ASC']] }); expect(j2.result).toBe(3); @@ -429,8 +448,6 @@ describe('workflow > instructions > aggregate', () => { await PostRepo.create({ values: { title: 't1' } }); - await sleep(500); - const [execution] = await workflow.getExecutions(); expect(execution.status).toBe(EXECUTION_STATUS.RESOLVED); const [job] = await execution.getJobs();