Compare commits

...

6 Commits

Author SHA1 Message Date
nocobase[bot]
543736b110 Merge branch 'next' into develop 2025-05-01 09:54:05 +00:00
nocobase[bot]
6e188b9571 Merge branch 'main' into next 2025-05-01 09:53:42 +00:00
Junyi
cefb1ead55
feat(cli): add benchmark script (#6623)
* feat(cli): add benchmark script

* chore(cli): remove useless code

* fix(plugin-workflow): fix validate logic

* chore(package): remove package manager

* fix(cli): fix glob

* fix(cli): change to fast-glob

* fix(plugin-workflow): add dependency

* fix(build): change server build target to node

* test(build): change ncc build target to es2023

* fix: tinybench

---------

Co-authored-by: chenos <chenlinxh@gmail.com>
2025-05-01 17:53:14 +08:00
nocobase[bot]
85a2aee6c4 Merge branch 'next' into develop 2025-05-01 03:48:13 +00:00
nocobase[bot]
21a60d5e12 Merge branch 'main' into next 2025-05-01 03:47:50 +00:00
Junyi
15bb8ac97b
fix(database): fix test cases (#6811)
* fix(database): fix test cases

* fix(database): fix unique and null
2025-05-01 11:47:17 +08:00
22 changed files with 271 additions and 35 deletions

View File

@ -32,6 +32,7 @@
"e2e": "nocobase e2e", "e2e": "nocobase e2e",
"ts": "nocobase test:server", "ts": "nocobase test:server",
"tc": "nocobase test:client", "tc": "nocobase test:client",
"benchmark": "nocobase benchmark",
"doc": "nocobase doc", "doc": "nocobase doc",
"doc:cn": "nocobase doc --lang=zh-CN", "doc:cn": "nocobase doc --lang=zh-CN",
"postinstall": "nocobase postinstall", "postinstall": "nocobase postinstall",

View File

@ -25,6 +25,7 @@ export const buildDeclaration = (cwd: string, targetDir: string) => {
`!${path.join(srcPath, '**/demos{,/**}')}`, `!${path.join(srcPath, '**/demos{,/**}')}`,
`!${path.join(srcPath, '**/__test__{,/**}')}`, `!${path.join(srcPath, '**/__test__{,/**}')}`,
`!${path.join(srcPath, '**/__tests__{,/**}')}`, `!${path.join(srcPath, '**/__tests__{,/**}')}`,
`!${path.join(srcPath, '**/__benchmarks__{,/**}')}`,
`!${path.join(srcPath, '**/__e2e__{,/**}')}`, `!${path.join(srcPath, '**/__e2e__{,/**}')}`,
`!${path.join(srcPath, '**/*.mdx')}`, `!${path.join(srcPath, '**/*.mdx')}`,
`!${path.join(srcPath, '**/*.md')}`, `!${path.join(srcPath, '**/*.md')}`,

View File

@ -15,6 +15,7 @@ import fs from 'fs-extra';
import path from 'path'; import path from 'path';
import { build as tsupBuild } from 'tsup'; import { build as tsupBuild } from 'tsup';
import { RsdoctorRspackPlugin } from '@rsdoctor/rspack-plugin';
import { EsbuildSupportExts, globExcludeFiles } from './constant'; import { EsbuildSupportExts, globExcludeFiles } from './constant';
import { PkgLog, UserConfig, getPackageJson } from './utils'; import { PkgLog, UserConfig, getPackageJson } from './utils';
import { import {
@ -26,12 +27,15 @@ import {
getSourcePackages, getSourcePackages,
} from './utils/buildPluginUtils'; } from './utils/buildPluginUtils';
import { getDepPkgPath, getDepsConfig } from './utils/getDepsConfig'; import { getDepPkgPath, getDepsConfig } from './utils/getDepsConfig';
import { RsdoctorRspackPlugin } from '@rsdoctor/rspack-plugin';
const validExts = ['.ts', '.tsx', '.js', '.jsx', '.mjs']; const validExts = ['.ts', '.tsx', '.js', '.jsx', '.mjs'];
const serverGlobalFiles: string[] = ['src/**', '!src/client/**', ...globExcludeFiles]; const serverGlobalFiles: string[] = ['src/**', '!src/client/**', ...globExcludeFiles];
const clientGlobalFiles: string[] = ['src/**', '!src/server/**', ...globExcludeFiles]; const clientGlobalFiles: string[] = ['src/**', '!src/server/**', ...globExcludeFiles];
const sourceGlobalFiles: string[] = ['src/**/*.{ts,js,tsx,jsx,mjs}', '!src/**/__tests__']; const sourceGlobalFiles: string[] = [
'src/**/*.{ts,js,tsx,jsx,mjs}',
'!src/**/__tests__',
'!src/**/__benchmarks__',
];
const external = [ const external = [
// nocobase // nocobase

View File

@ -12,6 +12,7 @@ import path from 'path';
export const globExcludeFiles = [ export const globExcludeFiles = [
'!src/**/__tests__', '!src/**/__tests__',
'!src/**/__benchmarks__',
'!src/**/__test__', '!src/**/__test__',
'!src/**/__e2e__', '!src/**/__e2e__',
'!src/**/demos', '!src/**/demos',

View File

@ -0,0 +1,73 @@
/**
* 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.
*/
const glob = require('fast-glob');
const { Command } = require('commander');
const { run } = require('../util');
/**
*
* @param {Command} cli
*/
module.exports = (cli) => {
return (
cli
.command('benchmark')
.description('Run benchmark tests')
// .option('--single-thread [singleThread]')
.option('-a, --all [all]', 'Run all benchmark files which ends with .benchmark.{js,ts}')
.arguments('[paths...]')
.allowUnknownOption()
.action(async (paths, opts) => {
process.env.NODE_ENV = 'test';
process.env.LOGGER_LEVEL = 'error';
const cliArgs = ['--max_old_space_size=14096'];
// if (process.argv.includes('-h') || process.argv.includes('--help')) {
// await run('node', cliArgs);
// return;
// }
// if (!opts.singleThread) {
// process.argv.splice(process.argv.indexOf('--single-thread=false'), 1);
// } else {
// process.argv.push('--poolOptions.threads.singleThread=true');
// }
if (!paths.length) {
if (opts.all) {
paths.push('**/*.benchmark.ts');
} else {
console.warn(
'No benchmark files specified. Please provide at least 1 benchmark file or path to run. Or use --all to run all "*.benchmark.ts".',
);
return;
}
}
const files = [];
for (const pattern of paths) {
for (const file of glob.sync(pattern)) {
files.push(file);
}
}
if (!files.length) {
console.log('No benchmark files found');
return;
}
for (const file of files) {
await run('tsx', [...cliArgs, file]);
}
})
);
};

View File

@ -29,6 +29,7 @@ module.exports = (cli) => {
require('./pm2')(cli); require('./pm2')(cli);
require('./test')(cli); require('./test')(cli);
require('./test-coverage')(cli); require('./test-coverage')(cli);
require('./benchmark')(cli);
require('./umi')(cli); require('./umi')(cli);
require('./update-deps')(cli); require('./update-deps')(cli);
require('./upgrade')(cli); require('./upgrade')(cli);

View File

@ -389,7 +389,7 @@ exports.initEnv = function initEnv() {
if ( if (
!process.env.APP_ENV_PATH && !process.env.APP_ENV_PATH &&
process.argv[2] && process.argv[2] &&
['test', 'test:client', 'test:server'].includes(process.argv[2]) ['test', 'test:client', 'test:server', 'benchmark'].includes(process.argv[2])
) { ) {
if (fs.existsSync(resolve(process.cwd(), '.env.test'))) { if (fs.existsSync(resolve(process.cwd(), '.env.test'))) {
process.env.APP_ENV_PATH = '.env.test'; process.env.APP_ENV_PATH = '.env.test';

View File

@ -9,8 +9,10 @@
import { Collection, Database, createMockDatabase } from '@nocobase/database'; import { Collection, Database, createMockDatabase } from '@nocobase/database';
import { IdentifierError } from '../errors/identifier-error'; import { IdentifierError } from '../errors/identifier-error';
import { isPg } from '@nocobase/test';
const pgOnly = () => (isPg() ? it : it.skip);
const pgOnly = () => (process.env.DB_DIALECT == 'postgres' ? it : it.skip);
describe('collection', () => { describe('collection', () => {
let db: Database; let db: Database;

View File

@ -115,9 +115,7 @@ describe('string field', () => {
const model = await collection.model.create({ const model = await collection.model.create({
name: ' n1\n ', name: ' n1\n ',
}); });
expect(model.toJSON()).toMatchObject({ expect(model.get('name')).toBe('n1');
name: 'n1',
});
}); });
it('trim when value is null should be null', async () => { it('trim when value is null should be null', async () => {
@ -129,9 +127,7 @@ describe('string field', () => {
const model = await collection.model.create({ const model = await collection.model.create({
name: null, name: null,
}); });
expect(model.toJSON()).toMatchObject({ expect(model.get('name')).toBeFalsy();
name: null,
});
}); });
it('when value is number should be convert to string', async () => { it('when value is number should be convert to string', async () => {
@ -143,8 +139,6 @@ describe('string field', () => {
const model = await collection.model.create({ const model = await collection.model.create({
name: 123, name: 123,
}); });
expect(model.toJSON()).toMatchObject({ expect(model.get('name')).toBe('123');
name: '123',
});
}); });
}); });

View File

@ -62,9 +62,7 @@ describe('text field', () => {
const model = await collection.model.create({ const model = await collection.model.create({
name: ' n1\n ', name: ' n1\n ',
}); });
expect(model.toJSON()).toMatchObject({ expect(model.get('name')).toBe('n1');
name: 'n1',
});
}); });
it('trim when value is null should be null', async () => { it('trim when value is null should be null', async () => {
@ -76,9 +74,7 @@ describe('text field', () => {
const model = await collection.model.create({ const model = await collection.model.create({
name: null, name: null,
}); });
expect(model.toJSON()).toMatchObject({ expect(model.get('name')).toBeFalsy();
name: null,
});
}); });
it('when value is number should be convert to string', async () => { it('when value is number should be convert to string', async () => {
@ -90,8 +86,6 @@ describe('text field', () => {
const model = await collection.model.create({ const model = await collection.model.create({
name: 123, name: 123,
}); });
expect(model.toJSON()).toMatchObject({ expect(model.get('name')).toBe('123');
name: '123',
});
}); });
}); });

View File

@ -8,7 +8,9 @@
*/ */
import { BelongsToManyRepository, Collection, createMockDatabase, Database } from '@nocobase/database'; import { BelongsToManyRepository, Collection, createMockDatabase, Database } from '@nocobase/database';
import { pgOnly } from '@nocobase/test'; import { isPg } from '@nocobase/test';
const pgOnly = () => (isPg() ? describe : describe.skip);
pgOnly()('belongs to many with targetCollection', () => { pgOnly()('belongs to many with targetCollection', () => {
let db: Database; let db: Database;

View File

@ -8,8 +8,10 @@
*/ */
import { createMockDatabase, Database, ViewFieldInference } from '@nocobase/database'; import { createMockDatabase, Database, ViewFieldInference } from '@nocobase/database';
import { pgOnly } from '@nocobase/test';
import { uid } from '@nocobase/utils'; import { uid } from '@nocobase/utils';
import { isPg } from '@nocobase/test';
const pgOnly = () => (isPg() ? describe : describe.skip);
pgOnly()('view with association', () => { pgOnly()('view with association', () => {
let db: Database; let db: Database;

View File

@ -20,12 +20,16 @@ export class StringField extends Field {
} }
additionalSequelizeOptions() { additionalSequelizeOptions() {
const { name, trim } = this.options; const { name, trim, unique } = this.options;
return { return {
set(value) { set(value) {
if (unique && value === '') {
value = null;
}
if (value == null) { if (value == null) {
return value; this.setDataValue(name, null);
return;
} }
if (typeof value !== 'string') { if (typeof value !== 'string') {
value = value.toString(); value = value.toString();

View File

@ -25,12 +25,16 @@ export class TextField extends Field {
} }
additionalSequelizeOptions() { additionalSequelizeOptions() {
const { name, trim } = this.options; const { name, trim, unique } = this.options;
return { return {
set(value) { set(value) {
if (unique && value === '') {
value = null;
}
if (value == null) { if (value == null) {
return value; this.setDataValue(name, null);
return;
} }
if (typeof value !== 'string') { if (typeof value !== 'string') {
value = value.toString(); value = value.toString();

View File

@ -36,6 +36,7 @@
"react-dom": "^18.0.0", "react-dom": "^18.0.0",
"rimraf": "^3.0.0", "rimraf": "^3.0.0",
"serve": "^14.2.4", "serve": "^14.2.4",
"tinybench": "^4.0.1",
"ts-loader": "^7.0.4", "ts-loader": "^7.0.4",
"ts-node": "9.1.1", "ts-node": "9.1.1",
"ts-node-dev": "1.1.8", "ts-node-dev": "1.1.8",

View File

@ -7,7 +7,6 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { describe } from 'vitest';
import ws from 'ws'; import ws from 'ws';
export { createMockDatabase, MockDatabase, mockDatabase } from '@nocobase/database'; export { createMockDatabase, MockDatabase, mockDatabase } from '@nocobase/database';
@ -16,7 +15,6 @@ export * from './memory-pub-sub-adapter';
export * from './mock-isolated-cluster'; export * from './mock-isolated-cluster';
export * from './mock-server'; export * from './mock-server';
export const pgOnly: () => any = () => (process.env.DB_DIALECT == 'postgres' ? describe : describe.skip);
export const isPg = () => process.env.DB_DIALECT == 'postgres'; export const isPg = () => process.env.DB_DIALECT == 'postgres';
export const isMysql = () => process.env.DB_DIALECT == 'mysql'; export const isMysql = () => process.env.DB_DIALECT == 'mysql';

View File

@ -7,9 +7,11 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { MockServer, pgOnly } from '@nocobase/test'; import { isPg, MockServer } from '@nocobase/test';
import { createApp } from '..'; import { createApp } from '..';
const pgOnly = () => (isPg() ? describe : describe.skip);
pgOnly()('Inherited Collection', () => { pgOnly()('Inherited Collection', () => {
let app: MockServer; let app: MockServer;
let agent; let agent;

View File

@ -9,8 +9,10 @@
import Database, { Repository } from '@nocobase/database'; import Database, { Repository } from '@nocobase/database';
import Application from '@nocobase/server'; import Application from '@nocobase/server';
import { isPg } from '@nocobase/test';
import { createApp } from '..'; import { createApp } from '..';
import { pgOnly } from '@nocobase/test';
const pgOnly = () => (isPg() ? describe : describe.skip);
pgOnly()('Inherited Collection with schema options', () => { pgOnly()('Inherited Collection with schema options', () => {
let db: Database; let db: Database;

View File

@ -14,9 +14,11 @@ import Database, {
Repository, Repository,
} from '@nocobase/database'; } from '@nocobase/database';
import Application from '@nocobase/server'; import Application from '@nocobase/server';
import { pgOnly } from '@nocobase/test'; import { isPg } from '@nocobase/test';
import { createApp } from '..'; import { createApp } from '..';
const pgOnly = () => (isPg() ? describe : describe.skip);
pgOnly()('Inherited Collection', () => { pgOnly()('Inherited Collection', () => {
let db: Database; let db: Database;
let app: Application; let app: Application;

View File

@ -8,10 +8,12 @@
*/ */
import { Database, MigrationContext } from '@nocobase/database'; import { Database, MigrationContext } from '@nocobase/database';
import { MockServer, pgOnly } from '@nocobase/test'; import { MockServer, isPg } from '@nocobase/test';
import Migrator from '../../migrations/20230918024546-set-collection-schema'; import Migrator from '../../migrations/20230918024546-set-collection-schema';
import { createApp } from '../index'; import { createApp } from '../index';
const pgOnly = () => (isPg() ? describe : describe.skip);
pgOnly()('set collection schema', () => { pgOnly()('set collection schema', () => {
let app: MockServer; let app: MockServer;
let db: Database; let db: Database;

View File

@ -29,7 +29,8 @@
"react-i18next": "^11.15.1", "react-i18next": "^11.15.1",
"react-js-cron": "^3.1.0", "react-js-cron": "^3.1.0",
"react-router-dom": "^6.11.2", "react-router-dom": "^6.11.2",
"sequelize": "^6.26.0" "sequelize": "^6.26.0",
"tinybench": "4.x"
}, },
"peerDependencies": { "peerDependencies": {
"@nocobase/actions": "1.x", "@nocobase/actions": "1.x",

View File

@ -0,0 +1,145 @@
/**
* 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.
*/
import { Bench } from 'tinybench';
import { getApp } from '@nocobase/plugin-workflow-test';
import Plugin from '..';
async function run(app, fn, args) {
const db = app.db;
const WorkflowRepo = db.getCollection('workflows').repository;
const workflow = await WorkflowRepo.create({
values: {
enabled: true,
sync: true,
type: 'syncTrigger',
},
});
await fn({ app, workflow }, args);
}
async function loopEcho({ app, workflow }, target) {
const plugin = app.pm.get(Plugin) as Plugin;
const loopNode = await workflow.createNode({
type: 'loop',
config: {
target,
},
});
await workflow.createNode({
type: 'echo',
upstreamId: loopNode.id,
branchIndex: 0,
});
await plugin.trigger(workflow, {});
}
async function loopQuery({ app, workflow }, target) {
const plugin = app.pm.get(Plugin) as Plugin;
const loopNode = await workflow.createNode({
type: 'loop',
config: {
target,
},
});
await workflow.createNode({
type: 'query',
config: {
collection: 'posts',
params: {
filterByTk: Math.ceil(Math.random() * 1000),
},
},
upstreamId: loopNode.id,
branchIndex: 0,
});
await plugin.trigger(workflow, {});
}
async function loopCreate({ app, workflow }, target) {
const plugin = app.pm.get(Plugin) as Plugin;
const loopNode = await workflow.createNode({
type: 'loop',
config: {
target,
},
});
await workflow.createNode({
type: 'create',
config: {
collection: 'posts',
params: {
values: {},
},
},
upstreamId: loopNode.id,
branchIndex: 0,
});
await plugin.trigger(workflow, {});
}
async function benchmark() {
const app = await getApp({
plugins: ['workflow-loop'],
});
const PostModel = app.db.getCollection('posts').model;
await PostModel.bulkCreate(Array.from({ length: 1000 }, (_, i) => ({ title: `test-${i}` })));
const bench = new Bench()
.add('1 node', async () => {
await run(app, loopEcho, 0);
})
.add('20 nodes: loop 10 echos', async () => {
await run(app, loopEcho, 10);
})
.add('200 nodes: loop 100 echos', async () => {
await run(app, loopEcho, 100);
})
.add('20 nodes: loop 10 queries', async () => {
await run(app, loopQuery, 10);
})
.add('200 nodes: loop 100 queries', async () => {
await run(app, loopQuery, 100);
})
.add('20 nodes: loop 10 creates', async () => {
await run(app, loopCreate, 10);
})
.add('200 nodes: loop 100 creates', async () => {
await run(app, loopCreate, 100);
});
await bench.run();
await app.cleanDb();
await app.destroy();
console.table(bench.table());
}
benchmark()
.then(() => {
process.exit(0);
})
.catch((err) => {
console.error(err);
process.exit(1);
});