diff --git a/packages/core/server/src/commands/index.ts b/packages/core/server/src/commands/index.ts index d7e0c0d882d..3102ef69042 100644 --- a/packages/core/server/src/commands/index.ts +++ b/packages/core/server/src/commands/index.ts @@ -23,6 +23,7 @@ import start from './start'; import stop from './stop'; import upgrade from './upgrade'; import consoleCommand from './console'; +import repair from './repair'; export function registerCli(app: Application) { consoleCommand(app); @@ -39,6 +40,7 @@ export function registerCli(app: Application) { destroy(app); start(app); refresh(app); + repair(app); // development only with @nocobase/cli app.command('build').argument('[packages...]'); diff --git a/packages/core/server/src/commands/repair.ts b/packages/core/server/src/commands/repair.ts new file mode 100644 index 00000000000..110f9fddee3 --- /dev/null +++ b/packages/core/server/src/commands/repair.ts @@ -0,0 +1,28 @@ +/** + * 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 Application from '../application'; + +export default (app: Application) => { + app + .command('repair') + .auth() + .preload() + .action(async (options) => { + app.log.info('start repair data...'); + const Collection = app.db.getCollection('collections'); + if (Collection) { + // @ts-ignore + await Collection.repository.setApp(app); + // @ts-ignore + await Collection.repository.load(); + } + await app.emitAsync('repair', options); + }); +}; diff --git a/packages/plugins/@nocobase/plugin-field-sequence/src/server/Plugin.ts b/packages/plugins/@nocobase/plugin-field-sequence/src/server/Plugin.ts index 0e4ed59f870..02dc6a81f6f 100644 --- a/packages/plugins/@nocobase/plugin-field-sequence/src/server/Plugin.ts +++ b/packages/plugins/@nocobase/plugin-field-sequence/src/server/Plugin.ts @@ -14,6 +14,8 @@ import { promisify } from 'util'; import { Plugin } from '@nocobase/server'; import { Registry } from '@nocobase/utils'; import { Pattern, SequenceField } from './fields/sequence-field'; +import _ from 'lodash'; +import { Field, Model } from '@nocobase/database'; const asyncRandomInt = promisify(randomInt); @@ -108,6 +110,55 @@ export default class PluginFieldSequenceServer extends Plugin { transaction, }); }); + + app.on('repair', async () => { + app.log.info(`app ${app.name} plugin ${this.name} start repair data...`); + const sequencesModel = app.db.getModel('sequences'); + const allSequences = await sequencesModel.findAll(); + const groupedSequences = _.groupBy(allSequences, 'collection'); + + const tasks: (() => Promise)[] = []; + for (const [collectionName, sequencesList] of Object.entries(groupedSequences)) { + tasks.push(async () => { + const collection = app.db.getCollection(collectionName); + const fields: Field[] = collection.getFields(); + const fieldMap = Object.fromEntries(fields.map((field) => [field.name, field])); + + const [autoIncrementField] = fields.filter((field) => field.options.primaryKey && field.type === 'bigInt'); + const [createAtField] = fields.filter((field) => field.options.interface === 'createdAt'); + if (!autoIncrementField && !createAtField) { + app.log.warn( + `Collection [${collection}] does not have autoIncrement or createdAt fields. Skipping sequences refresh`, + ); + return; + } + + const [record] = await collection.model.findAll({ + order: [[autoIncrementField?.name ?? createAtField?.name, 'DESC']], + limit: 1, + }); + if (!record) { + app.log.warn(`Collection [${collection}] has no records. Skipping sequences repair`); + return; + } + + const sequencesFieldSet = _.uniq(sequencesList.map(({ field }) => field)); + for (const sequencesField of sequencesFieldSet) { + const field = fieldMap[sequencesField] as SequenceField; + if (!field) { + app.log.warn( + `Collection [${collection}] field [${sequencesField}] definition not found. Skipping sequences repair`, + ); + continue; + } + + await field.update(record, { overwrite: true }); + } + }); + } + await Promise.all(tasks.map((t) => t())); + app.log.info(`app ${app.name} plugin ${this.name} finish repair data`); + }); } async install() {} diff --git a/packages/plugins/@nocobase/plugin-field-sequence/src/server/__tests__/sequence-field-migration.test.ts b/packages/plugins/@nocobase/plugin-field-sequence/src/server/__tests__/sequence-field-migration.test.ts new file mode 100644 index 00000000000..4562e26a436 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-field-sequence/src/server/__tests__/sequence-field-migration.test.ts @@ -0,0 +1,1137 @@ +/** + * 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 { Database, FieldOptions } from '@nocobase/database'; +import { MockServer, createMockServer } from '@nocobase/test'; + +describe('Should update sequence collection`s current base on business collections', () => { + let app: MockServer; + let db: Database; + + const presetField: FieldOptions[] = [ + { + allowNull: false, + autoIncrement: true, + primaryKey: true, + interface: 'integer', + type: 'bigInt', + name: 'id', + }, + { + field: 'createdAt', + interface: 'createdAt', + type: 'date', + name: 'createdAt', + }, + { + field: 'updatedAt', + interface: 'updatedAt', + type: 'date', + name: 'updatedAt', + }, + ]; + + beforeEach(async () => { + app = await createMockServer({ + plugins: ['field-sequence'], + }); + db = app.db; + }); + + afterEach(async () => { + await db.clean({ drop: true }); + await app.destroy(); + }); + + describe('Integer only pattern sequences', () => { + it('Single integer without cycle in sequences field', async () => { + db.collection({ + name: 'tests', + fields: [ + ...presetField, + { + type: 'sequence', + name: 'sequence', + patterns: [ + { + type: 'integer', + options: { + digits: 4, + start: 1, + cycle: null, + key: 1, + }, + }, + ], + }, + ], + }); + await db.sync(); + + const testModel = db.getModel('tests'); + for (let i = 0; i < 5; i++) { + await testModel.create(); + } + + const sequencesRepository = db.getRepository('sequences'); + + let sequences = await sequencesRepository.findOne({ + filter: { + collection: 'tests', + field: 'sequence', + key: 1, + }, + }); + expect(sequences).toBeDefined(); + expect(sequences.current).toBe(5); + + await sequencesRepository.update({ + filterByTk: sequences.id, + values: { + current: 0, + }, + }); + sequences = await sequencesRepository.findOne({ + filter: { + id: sequences.id, + }, + }); + expect(sequences).toBeDefined(); + expect(sequences.current).toBe(0); + + await app.runCommand('repair'); + + sequences = await sequencesRepository.findOne({ + filter: { + id: sequences.id, + }, + }); + expect(sequences).toBeDefined(); + expect(sequences.current).toBe(5); + }); + + it('Single integer with cycle in sequences field', async () => { + db.collection({ + name: 'tests', + fields: [ + ...presetField, + { + type: 'sequence', + name: 'sequence', + patterns: [ + { + type: 'integer', + options: { + digits: 4, + start: 1, + cycle: '0 0 * * *', + key: 1, + }, + }, + ], + }, + ], + }); + await db.sync(); + + const testModel = db.getModel('tests'); + for (let i = 0; i < 5; i++) { + await testModel.create(); + } + + const sequencesRepository = db.getRepository('sequences'); + + let sequences = await sequencesRepository.findOne({ + filter: { + collection: 'tests', + field: 'sequence', + key: 1, + }, + }); + expect(sequences).toBeDefined(); + expect(sequences.current).toBe(5); + + await sequencesRepository.update({ + filterByTk: sequences.id, + values: { + current: 0, + }, + }); + sequences = await sequencesRepository.findOne({ + filter: { + id: sequences.id, + }, + }); + expect(sequences).toBeDefined(); + expect(sequences.current).toBe(0); + + await app.runCommand('repair'); + + sequences = await sequencesRepository.findOne({ + filter: { + id: sequences.id, + }, + }); + expect(sequences).toBeDefined(); + expect(sequences.current).toBe(5); + + const [record] = await testModel.findAll({ + order: [['id', 'DESC']], + limit: 1, + }); + expect(sequences['lastGeneratedAt']).toStrictEqual(record['createdAt']); + }); + + it('Multiple integer without cycle in sequences field', async () => { + db.collection({ + name: 'tests', + fields: [ + ...presetField, + { + type: 'sequence', + name: 'sequence', + patterns: [ + { + type: 'integer', + options: { + digits: 4, + start: 1, + cycle: null, + key: 1, + }, + }, + { + type: 'integer', + options: { + digits: 5, + start: 10, + cycle: null, + key: 2, + }, + }, + { + type: 'integer', + options: { + digits: 6, + start: 100, + cycle: null, + key: 3, + }, + }, + ], + }, + ], + }); + await db.sync(); + + const testModel = db.getModel('tests'); + for (let i = 0; i < 5; i++) { + await testModel.create(); + } + + const sequencesRepository = db.getRepository('sequences'); + + const keys = [1, 2, 3]; + const startMap = { + 1: 1, + 2: 10, + 3: 100, + }; + for (const key of keys) { + let sequences = await sequencesRepository.findOne({ + filter: { + collection: 'tests', + field: 'sequence', + key, + }, + }); + expect(sequences).toBeDefined(); + expect(sequences.current).toBe(startMap[key] + 4); + + await sequencesRepository.update({ + filterByTk: sequences.id, + values: { + current: 0, + }, + }); + sequences = await sequencesRepository.findOne({ + filter: { + id: sequences.id, + }, + }); + expect(sequences).toBeDefined(); + expect(sequences.current).toBe(0); + + await app.runCommand('repair'); + + sequences = await sequencesRepository.findOne({ + filter: { + id: sequences.id, + }, + }); + expect(sequences).toBeDefined(); + expect(sequences.current).toBe(startMap[key] + 4); + } + }); + + it('Multiple integer with cycle in sequences field', async () => { + db.collection({ + name: 'tests', + fields: [ + ...presetField, + { + type: 'sequence', + name: 'sequence', + patterns: [ + { + type: 'integer', + options: { + digits: 4, + start: 1, + cycle: null, + key: 1, + }, + }, + { + type: 'integer', + options: { + digits: 5, + start: 10, + cycle: null, + key: 2, + }, + }, + { + type: 'integer', + options: { + digits: 6, + start: 100, + cycle: null, + key: 3, + }, + }, + ], + }, + ], + }); + await db.sync(); + + const testModel = db.getModel('tests'); + for (let i = 0; i < 5; i++) { + await testModel.create(); + } + + const sequencesRepository = db.getRepository('sequences'); + + const keys = [1, 2, 3]; + const startMap = { + 1: 1, + 2: 10, + 3: 100, + }; + for (const key of keys) { + let sequences = await sequencesRepository.findOne({ + filter: { + collection: 'tests', + field: 'sequence', + key, + }, + }); + expect(sequences).toBeDefined(); + expect(sequences.current).toBe(startMap[key] + 4); + + await sequencesRepository.update({ + filterByTk: sequences.id, + values: { + current: 0, + }, + }); + sequences = await sequencesRepository.findOne({ + filter: { + id: sequences.id, + }, + }); + expect(sequences).toBeDefined(); + expect(sequences.current).toBe(0); + + await app.runCommand('repair'); + + sequences = await sequencesRepository.findOne({ + filter: { + id: sequences.id, + }, + }); + expect(sequences).toBeDefined(); + expect(sequences.current).toBe(startMap[key] + 4); + + const [record] = await testModel.findAll({ + order: [['id', 'DESC']], + limit: 1, + }); + expect(sequences['lastGeneratedAt']).toStrictEqual(record['createdAt']); + } + }); + }); + + describe('Complex patterns sequences', () => { + it('Single integer without cycle in sequences field', async () => { + db.collection({ + name: 'tests', + fields: [ + ...presetField, + { + type: 'sequence', + name: 'sequence', + patterns: [ + { + type: 'string', + options: { + value: 'RF', + }, + }, + { + type: 'date', + options: { + format: 'YYYYMMDD', + }, + }, + { + type: 'randomChar', + options: { + length: 6, + charsets: ['uppercase'], + }, + }, + { + type: 'integer', + options: { + digits: 4, + start: 1, + cycle: null, + key: 1, + }, + }, + ], + }, + ], + }); + await db.sync(); + + const testModel = db.getModel('tests'); + for (let i = 0; i < 5; i++) { + await testModel.create(); + } + + const sequencesRepository = db.getRepository('sequences'); + + let sequences = await sequencesRepository.findOne({ + filter: { + collection: 'tests', + field: 'sequence', + key: 1, + }, + }); + expect(sequences).toBeDefined(); + expect(sequences.current).toBe(5); + + await sequencesRepository.update({ + filterByTk: sequences.id, + values: { + current: 0, + }, + }); + sequences = await sequencesRepository.findOne({ + filter: { + id: sequences.id, + }, + }); + expect(sequences).toBeDefined(); + expect(sequences.current).toBe(0); + + await app.runCommand('repair'); + + sequences = await sequencesRepository.findOne({ + filter: { + id: sequences.id, + }, + }); + expect(sequences).toBeDefined(); + expect(sequences.current).toBe(5); + }); + + it('Single integer with cycle in sequences field', async () => { + db.collection({ + name: 'tests', + fields: [ + ...presetField, + { + type: 'sequence', + name: 'sequence', + patterns: [ + { + type: 'string', + options: { + value: 'RF', + }, + }, + { + type: 'date', + options: { + format: 'YYYYMMDD', + }, + }, + { + type: 'randomChar', + options: { + length: 6, + charsets: ['uppercase'], + }, + }, + { + type: 'integer', + options: { + digits: 4, + start: 1, + cycle: '0 0 * * *', + key: 1, + }, + }, + ], + }, + ], + }); + await db.sync(); + + const testModel = db.getModel('tests'); + for (let i = 0; i < 5; i++) { + await testModel.create(); + } + + const sequencesRepository = db.getRepository('sequences'); + + let sequences = await sequencesRepository.findOne({ + filter: { + collection: 'tests', + field: 'sequence', + key: 1, + }, + }); + expect(sequences).toBeDefined(); + expect(sequences.current).toBe(5); + + await sequencesRepository.update({ + filterByTk: sequences.id, + values: { + current: 0, + }, + }); + sequences = await sequencesRepository.findOne({ + filter: { + id: sequences.id, + }, + }); + expect(sequences).toBeDefined(); + expect(sequences.current).toBe(0); + + await app.runCommand('repair'); + + sequences = await sequencesRepository.findOne({ + filter: { + id: sequences.id, + }, + }); + expect(sequences).toBeDefined(); + expect(sequences.current).toBe(5); + + const [record] = await testModel.findAll({ + order: [['id', 'DESC']], + limit: 1, + }); + expect(sequences['lastGeneratedAt']).toStrictEqual(record['createdAt']); + }); + + it('Multiple integer without cycle in sequences field', async () => { + db.collection({ + name: 'tests', + fields: [ + ...presetField, + { + type: 'sequence', + name: 'sequence', + patterns: [ + { + type: 'string', + options: { + value: 'RF', + }, + }, + { + type: 'integer', + options: { + digits: 4, + start: 1, + cycle: null, + key: 1, + }, + }, + { + type: 'date', + options: { + format: 'YYYYMMDD', + }, + }, + { + type: 'integer', + options: { + digits: 5, + start: 10, + cycle: null, + key: 2, + }, + }, + { + type: 'randomChar', + options: { + length: 6, + charsets: ['uppercase'], + }, + }, + { + type: 'integer', + options: { + digits: 6, + start: 100, + cycle: null, + key: 3, + }, + }, + ], + }, + ], + }); + await db.sync(); + + const testModel = db.getModel('tests'); + for (let i = 0; i < 5; i++) { + await testModel.create(); + } + + const sequencesRepository = db.getRepository('sequences'); + + const keys = [1, 2, 3]; + const startMap = { + 1: 1, + 2: 10, + 3: 100, + }; + for (const key of keys) { + let sequences = await sequencesRepository.findOne({ + filter: { + collection: 'tests', + field: 'sequence', + key, + }, + }); + expect(sequences).toBeDefined(); + expect(sequences.current).toBe(startMap[key] + 4); + + await sequencesRepository.update({ + filterByTk: sequences.id, + values: { + current: 0, + }, + }); + sequences = await sequencesRepository.findOne({ + filter: { + id: sequences.id, + }, + }); + expect(sequences).toBeDefined(); + expect(sequences.current).toBe(0); + + await app.runCommand('repair'); + + sequences = await sequencesRepository.findOne({ + filter: { + id: sequences.id, + }, + }); + expect(sequences).toBeDefined(); + expect(sequences.current).toBe(startMap[key] + 4); + } + }); + + it('Multiple integer with cycle in sequences field', async () => { + db.collection({ + name: 'tests', + fields: [ + ...presetField, + { + type: 'sequence', + name: 'sequence', + patterns: [ + { + type: 'string', + options: { + value: 'RF', + }, + }, + { + type: 'integer', + options: { + digits: 4, + start: 1, + cycle: '0 0 * * *', + key: 1, + }, + }, + { + type: 'date', + options: { + format: 'YYYYMMDD', + }, + }, + { + type: 'integer', + options: { + digits: 5, + start: 10, + cycle: '0 0 * * *', + key: 2, + }, + }, + { + type: 'randomChar', + options: { + length: 6, + charsets: ['uppercase'], + }, + }, + { + type: 'integer', + options: { + digits: 6, + start: 100, + cycle: '0 0 * * *', + key: 3, + }, + }, + ], + }, + ], + }); + await db.sync(); + + const testModel = db.getModel('tests'); + for (let i = 0; i < 5; i++) { + await testModel.create(); + } + + const sequencesRepository = db.getRepository('sequences'); + + const keys = [1, 2, 3]; + const startMap = { + 1: 1, + 2: 10, + 3: 100, + }; + for (const key of keys) { + let sequences = await sequencesRepository.findOne({ + filter: { + collection: 'tests', + field: 'sequence', + key, + }, + }); + expect(sequences).toBeDefined(); + expect(sequences.current).toBe(startMap[key] + 4); + + await sequencesRepository.update({ + filterByTk: sequences.id, + values: { + current: 0, + }, + }); + sequences = await sequencesRepository.findOne({ + filter: { + id: sequences.id, + }, + }); + expect(sequences).toBeDefined(); + expect(sequences.current).toBe(0); + + await app.runCommand('repair'); + + sequences = await sequencesRepository.findOne({ + filter: { + id: sequences.id, + }, + }); + expect(sequences).toBeDefined(); + expect(sequences.current).toBe(startMap[key] + 4); + + const [record] = await testModel.findAll({ + order: [['id', 'DESC']], + limit: 1, + }); + expect(sequences['lastGeneratedAt']).toStrictEqual(record['createdAt']); + } + }); + + it('Multiple sequences field in one collection', async () => { + db.collection({ + name: 'tests', + fields: [ + ...presetField, + { + type: 'sequence', + name: 'sequence1', + patterns: [ + { + type: 'string', + options: { + value: 'RF', + }, + }, + { + type: 'integer', + options: { + digits: 4, + start: 1, + cycle: '0 0 * * *', + key: 1, + }, + }, + { + type: 'date', + options: { + format: 'YYYYMMDD', + }, + }, + { + type: 'integer', + options: { + digits: 5, + start: 10, + cycle: '0 0 * * *', + key: 2, + }, + }, + { + type: 'randomChar', + options: { + length: 6, + charsets: ['uppercase'], + }, + }, + { + type: 'integer', + options: { + digits: 6, + start: 100, + cycle: '0 0 * * *', + key: 3, + }, + }, + ], + }, + { + type: 'sequence', + name: 'sequence2', + patterns: [ + { + type: 'string', + options: { + value: 'RF', + }, + }, + { + type: 'integer', + options: { + digits: 4, + start: 1, + cycle: '0 0 * * *', + key: 1, + }, + }, + { + type: 'date', + options: { + format: 'YYYYMMDD', + }, + }, + { + type: 'integer', + options: { + digits: 5, + start: 10, + cycle: '0 0 * * *', + key: 2, + }, + }, + { + type: 'randomChar', + options: { + length: 6, + charsets: ['uppercase'], + }, + }, + { + type: 'integer', + options: { + digits: 6, + start: 100, + cycle: '0 0 * * *', + key: 3, + }, + }, + ], + }, + ], + }); + await db.sync(); + + const testModel = db.getModel('tests'); + for (let i = 0; i < 5; i++) { + await testModel.create(); + } + + const sequencesRepository = db.getRepository('sequences'); + + const sequencesFields = ['sequence1', 'sequence2']; + const keys = [1, 2, 3]; + const startMap = { + 1: 1, + 2: 10, + 3: 100, + }; + for (const field of sequencesFields) { + for (const key of keys) { + let sequences = await sequencesRepository.findOne({ + filter: { + collection: 'tests', + field, + key, + }, + }); + expect(sequences).toBeDefined(); + expect(sequences.current).toBe(startMap[key] + 4); + + await sequencesRepository.update({ + filterByTk: sequences.id, + values: { + current: 0, + }, + }); + sequences = await sequencesRepository.findOne({ + filter: { + id: sequences.id, + }, + }); + expect(sequences).toBeDefined(); + expect(sequences.current).toBe(0); + + await app.runCommand('repair'); + + sequences = await sequencesRepository.findOne({ + filter: { + id: sequences.id, + }, + }); + expect(sequences).toBeDefined(); + expect(sequences.current).toBe(startMap[key] + 4); + + const [record] = await testModel.findAll({ + order: [['id', 'DESC']], + limit: 1, + }); + expect(sequences['lastGeneratedAt']).toStrictEqual(record['createdAt']); + } + } + }); + + it('Multiple collection with sequences field', async () => { + const sequencesFieldDefinition = [ + { + type: 'sequence', + name: 'sequence1', + patterns: [ + { + type: 'string', + options: { + value: 'RF', + }, + }, + { + type: 'integer', + options: { + digits: 4, + start: 1, + cycle: '0 0 * * *', + key: 1, + }, + }, + { + type: 'date', + options: { + format: 'YYYYMMDD', + }, + }, + { + type: 'integer', + options: { + digits: 5, + start: 10, + cycle: '0 0 * * *', + key: 2, + }, + }, + { + type: 'randomChar', + options: { + length: 6, + charsets: ['uppercase'], + }, + }, + { + type: 'integer', + options: { + digits: 6, + start: 100, + cycle: '0 0 * * *', + key: 3, + }, + }, + ], + }, + { + type: 'sequence', + name: 'sequence2', + patterns: [ + { + type: 'string', + options: { + value: 'RF', + }, + }, + { + type: 'integer', + options: { + digits: 4, + start: 1, + cycle: '0 0 * * *', + key: 1, + }, + }, + { + type: 'date', + options: { + format: 'YYYYMMDD', + }, + }, + { + type: 'integer', + options: { + digits: 5, + start: 10, + cycle: '0 0 * * *', + key: 2, + }, + }, + { + type: 'randomChar', + options: { + length: 6, + charsets: ['uppercase'], + }, + }, + { + type: 'integer', + options: { + digits: 6, + start: 100, + cycle: '0 0 * * *', + key: 3, + }, + }, + ], + }, + ]; + db.collection({ + name: 'tests1', + fields: [...presetField, ...sequencesFieldDefinition], + }); + db.collection({ + name: 'tests2', + fields: [...presetField, ...sequencesFieldDefinition], + }); + await db.sync(); + + const sequencesRepository = db.getRepository('sequences'); + + const collections = ['tests1', 'tests2']; + const sequencesFields = ['sequence1', 'sequence2']; + const keys = [1, 2, 3]; + const startMap = { + 1: 1, + 2: 10, + 3: 100, + }; + for (const collection of collections) { + const testModel = db.getModel(collection); + for (let i = 0; i < 5; i++) { + await testModel.create(); + } + for (const field of sequencesFields) { + for (const key of keys) { + let sequences = await sequencesRepository.findOne({ + filter: { + collection, + field, + key, + }, + }); + expect(sequences).toBeDefined(); + expect(sequences.current).toBe(startMap[key] + 4); + + await sequencesRepository.update({ + filterByTk: sequences.id, + values: { + current: 0, + }, + }); + sequences = await sequencesRepository.findOne({ + filter: { + id: sequences.id, + }, + }); + expect(sequences).toBeDefined(); + expect(sequences.current).toBe(0); + + await app.runCommand('repair'); + + sequences = await sequencesRepository.findOne({ + filter: { + id: sequences.id, + }, + }); + expect(sequences).toBeDefined(); + expect(sequences.current).toBe(startMap[key] + 4); + + const [record] = await testModel.findAll({ + order: [['id', 'DESC']], + limit: 1, + }); + expect(sequences['lastGeneratedAt']).toStrictEqual(record['createdAt']); + } + } + } + }); + }); +}); diff --git a/packages/plugins/@nocobase/plugin-field-sequence/src/server/fields/sequence-field.ts b/packages/plugins/@nocobase/plugin-field-sequence/src/server/fields/sequence-field.ts index d4d1990126b..21d436bd6c5 100644 --- a/packages/plugins/@nocobase/plugin-field-sequence/src/server/fields/sequence-field.ts +++ b/packages/plugins/@nocobase/plugin-field-sequence/src/server/fields/sequence-field.ts @@ -44,7 +44,7 @@ export interface Pattern { instance: Model, value: string, options, - transactionable: Transactionable, + transactionable: Transactionable & { overwrite?: boolean }, ): Promise; } @@ -235,7 +235,7 @@ sequencePatterns.register('integer', { await lastSeq.save({ transaction }); }, - async update(instance, value, options, { transaction }) { + async update(instance, value, options, { transaction, overwrite }) { const recordTime = instance.get('createdAt') ?? new Date(); const { digits = 1, start = 0, base = 10, cycle, key } = options; const SeqRepo = this.database.getRepository('sequences'); @@ -269,6 +269,15 @@ sequencePatterns.register('integer', { { transaction }, ); } + if (overwrite === true) { + return lastSeq.update( + { + current, + lastGeneratedAt: recordTime, + }, + { transaction }, + ); + } if (cycle) { const interval = parser.parseExpression(cycle, { currentDate: lastSeq.get('lastGeneratedAt') });