mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-07 14:39:25 +08:00
Merge branch 'next' into develop
This commit is contained in:
commit
21f6ed56b3
@ -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)}`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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,
|
||||
|
@ -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.": "查询结果小数位数"
|
||||
}
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
@ -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();
|
||||
|
Loading…
x
Reference in New Issue
Block a user