From bf6bf000478c68721e49b7e4c669aab5e94c8ab0 Mon Sep 17 00:00:00 2001 From: Junyi Date: Wed, 18 Jan 2023 22:54:08 +0800 Subject: [PATCH] Fix(plugin-sequence): support sequence field in m2m through table (#1383) * fix(plugin-sequence): support sequence field in m2m through table * fix(plugin-sequence): fix update last sequence * fix(plugin-sequence): fix hooks --- packages/core/database/src/types.ts | 13 +- .../server/__tests__/sequence-field.test.ts | 79 ++++++++ .../src/server/fields/sequence-field.ts | 177 +++++++++++++++--- 3 files changed, 239 insertions(+), 30 deletions(-) diff --git a/packages/core/database/src/types.ts b/packages/core/database/src/types.ts index 4b4f6bef4..c6cf0ec67 100644 --- a/packages/core/database/src/types.ts +++ b/packages/core/database/src/types.ts @@ -16,6 +16,10 @@ export type ModelCreateWithAssociationsEventType = 'afterCreateWithAssociations' export type ModelUpdateWithAssociationsEventType = 'afterUpdateWithAssociations'; export type ModelSaveWithAssociationsEventType = 'afterSaveWithAssociations'; +export type ModelBulkCreateEvnetType = 'beforeBulkCreate' | 'afterBulkCreate'; +export type ModelBulkUpdateEvnetType = 'beforeBulkUpdate' | 'afterBulkUpdate'; +export type ModelBulkDestroyEvnetType = 'beforeBulkDestroy' | 'afterBulkDestroy'; + export type ModelValidateEventTypes = ModelValidateEventType | `${CollectionNameType}.${ModelValidateEventType}`; export type ModelCreateEventTypes = ModelCreateEventType | `${CollectionNameType}.${ModelCreateEventType}`; export type ModelUpdateEventTypes = ModelUpdateEventType | `${CollectionNameType}.${ModelUpdateEventType}`; @@ -25,6 +29,10 @@ export type ModelCreateWithAssociationsEventTypes = ModelCreateWithAssociationsE export type ModelUpdateWithAssociationsEventTypes = ModelUpdateWithAssociationsEventType | `${CollectionNameType}.${ModelUpdateWithAssociationsEventType}`; export type ModelSaveWithAssociationsEventTypes = ModelSaveWithAssociationsEventType | `${CollectionNameType}.${ModelSaveWithAssociationsEventType}`; +export type ModelBulkCreateEvnetTypes = ModelBulkCreateEvnetType | `${CollectionNameType}.${ModelBulkCreateEvnetType}`; +export type ModelBulkUpdateEvnetTypes = ModelBulkUpdateEvnetType | `${CollectionNameType}.${ModelBulkUpdateEvnetType}`; +export type ModelBulkDestroyEvnetTypes = ModelBulkDestroyEvnetType | `${CollectionNameType}.${ModelBulkDestroyEvnetType}`; + export type ModelEventTypes = ModelSyncEventType | ModelValidateEventTypes | ModelCreateEventTypes @@ -33,7 +41,10 @@ export type ModelEventTypes = ModelSyncEventType | ModelDestroyEventTypes | ModelCreateWithAssociationsEventTypes | ModelUpdateWithAssociationsEventTypes - | ModelSaveWithAssociationsEventTypes; + | ModelSaveWithAssociationsEventTypes + | ModelBulkCreateEvnetTypes + | ModelBulkUpdateEvnetTypes + | ModelBulkDestroyEvnetTypes; export type DatabaseBeforeDefineCollectionEventType = 'beforeDefineCollection'; export type DatabaseAfterDefineCollectionEventType = 'afterDefineCollection'; diff --git a/packages/plugins/sequence-field/src/server/__tests__/sequence-field.test.ts b/packages/plugins/sequence-field/src/server/__tests__/sequence-field.test.ts index a70da74f9..a271b1fd9 100644 --- a/packages/plugins/sequence-field/src/server/__tests__/sequence-field.test.ts +++ b/packages/plugins/sequence-field/src/server/__tests__/sequence-field.test.ts @@ -754,4 +754,83 @@ describe('sequence field', () => { expect(item4.name).toBe('0'); }); }); + + describe('associations', () => { + it('sequence field in m2m through table', async () => { + const postsTagsCollection = db.collection({ + name: 'posts_tags', + fields: [ + { + type: 'sequence', + name: 'seq', + patterns: [ + { type: 'integer', options: { key: 1 } } + ] + } + ] + }); + const postsCollection = db.collection({ + name: 'posts', + fields: [ + { + type: 'string', + name: 'title' + }, + { + type: 'belongsToMany', + name: 'tags', + through: 'posts_tags' + } + ] + }); + const tagsCollection = db.collection({ + name: 'tags', + fields: [ + { + type: 'string', + name: 'title' + }, + { + type: 'belongsToMany', + name: 'posts', + through: 'posts_tags' + } + ] + }); + + await db.sync(); + + const tagsRepo = db.getRepository('tags'); + const tags = await tagsRepo.create({ + values: [ + { title: 't1' }, + { title: 't2' }, + { title: 't3' }, + ] + }); + const postsTagsRepo = db.getRepository('posts_tags'); + const postTag = await postsTagsRepo.create({ + values: { + postId: 1, + tagId: 1 + } + }); + + const postsRepo = db.getRepository('posts'); + await postsRepo.create({ + values: { + title: 'p1', + tags + } + }); + + const postsTags = await postsTagsRepo.find({ + order: [['seq', 'ASC']] + }); + + expect(postsTags[0].seq).toBe('0'); + expect(postsTags[1].seq).toBe('1'); + expect(postsTags[2].seq).toBe('2'); + }); + }); }); diff --git a/packages/plugins/sequence-field/src/server/fields/sequence-field.ts b/packages/plugins/sequence-field/src/server/fields/sequence-field.ts index 127d4f379..e30c8d85c 100644 --- a/packages/plugins/sequence-field/src/server/fields/sequence-field.ts +++ b/packages/plugins/sequence-field/src/server/fields/sequence-field.ts @@ -14,6 +14,13 @@ export interface Pattern { opts: { [key: string]: any }, options: Transactionable, ): Promise | string; + batchGenerate( + this: SequenceField, + instances: Model[], + values: string[], + opts: { [key: string]: any }, + options: Transactionable, + ): Promise | void; getLength(options): number; getMatcher(options): string; update?( @@ -37,6 +44,11 @@ sequencePatterns.register('string', { generate(instance, options) { return options.value; }, + batchGenerate(instances, values, options) { + instances.forEach((instance, i) => { + values[i] = options.value; + }); + }, getLength(options) { return options.value.length; }, @@ -55,20 +67,27 @@ sequencePatterns.register('integer', { async generate(this: SequenceField, instance: Model, options, { transaction }) { const recordTime = instance.get('createdAt'); const { digits = 1, start = 0, base = 10, cycle, key } = options; - const max = Math.pow(base, digits) - 1; - const SeqRepo = this.database.getRepository('sequences'); - const lastSeq = await SeqRepo.findOne({ + const { repository: SeqRepo, model: SeqModel } = this.database.getCollection('sequences'); + const lastSeq = (await SeqRepo.findOne({ filter: { collection: this.collection.name, field: this.name, key, }, transaction, + })) || SeqModel.build({ + collection: this.collection.name, + field: this.name, + key, }); - let next; - if (lastSeq && lastSeq.get('current') != null) { + let next = start; + if (lastSeq.get('current') != null) { next = Math.max(lastSeq.get('current') + 1, start); + const max = Math.pow(base, digits) - 1; + if (next > max) { + next = start; + } // cycle as cron string if (cycle) { @@ -78,29 +97,13 @@ sequencePatterns.register('integer', { next = start; } } - } else { - next = start; } - if (next > max) { - next = start; - } - - // update options - if (lastSeq) { - await lastSeq.update({ current: next, lastGeneratedAt: recordTime }, { transaction }); - } else { - await SeqRepo.create({ - values: { - collection: this.collection.name, - field: this.name, - key, - current: next, - lastGeneratedAt: recordTime, - }, - transaction, - }); - } + lastSeq.set({ + current: next, + lastGeneratedAt: recordTime, + }); + await lastSeq.save({ transaction }); return next.toString(base).padStart(digits, '0'); }, @@ -110,11 +113,84 @@ sequencePatterns.register('integer', { }, getMatcher(options = {}) { - const { digits = 1, start = 0, base = 10 } = options; + const { digits = 1, base = 10 } = options; const chars = '0123456789abcdefghijklmnopqrstuvwxyz'.slice(0, base); return `[${chars}]{${digits}}`; }, + async batchGenerate(instances, values, options, { transaction }) { + const { name, patterns } = this.options; + const { digits = 1, start = 0, base = 10, cycle, key } = options; + + const { repository: SeqRepo, model: SeqModel } = this.database.getCollection('sequences'); + const lastSeq = (await SeqRepo.findOne({ + filter: { + collection: this.collection.name, + field: this.name, + key, + }, + transaction, + })) || SeqModel.build({ + collection: this.collection.name, + field: this.name, + key, + }); + + instances.forEach((instance, i) => { + const recordTime = instance.get('createdAt'); + const value = instance.get(name); + if (value != null && this.options.inputable) { + const matcher = this.match(value); + // 如果匹配到了,需要检查是否要更新 current 值 + if (matcher) { + const patternIndex = patterns.indexOf(options); + const number = Number.parseInt(matcher[patternIndex + 1], base); + // 如果当前值大于 lastSeq.current,则更新 lastSeq.current + if (lastSeq.get('current') == null) { + lastSeq.set({ + current: number, + lastGeneratedAt: recordTime, + }); + } else { + if (number > lastSeq.get('current')) { + lastSeq.set({ + current: number, + lastGeneratedAt: recordTime, + }); + } + } + } + // 否则交给 validate 检查是否要求 match,如果要求,则相应报错 + } else { + // 自动生成 + let next = start; + if (lastSeq.get('current') != null) { + next = Math.max(lastSeq.get('current') + 1, start); + const max = Math.pow(base, digits) - 1; + if (next > max) { + next = start; + } + + // cycle as cron string + if (cycle) { + const interval = parser.parseExpression(cycle, { currentDate: lastSeq.get('lastGeneratedAt') }); + const nextTime = interval.next(); + if (recordTime.getTime() >= nextTime.getTime()) { + next = start; + } + } + } + lastSeq.set({ + current: next, + lastGeneratedAt: recordTime, + }); + values[i] = next.toString(base).padStart(digits, '0'); + } + }); + + await lastSeq.save({ transaction }); + }, + async update(instance, value, options, { transaction }) { const recordTime = instance.get('createdAt'); const { digits = 1, start = 0, base = 10, cycle, key } = options; @@ -183,6 +259,14 @@ sequencePatterns.register('date', { generate(this: SequenceField, instance, options) { return moment(instance.get(options?.field ?? 'createdAt')).format(options?.format ?? 'YYYYMMDD'); }, + batchGenerate(instances, values, options) { + const { name, inputable } = options; + instances.forEach((instance, i) => { + if (!inputable || instance.get(name) == null) { + values[i] = sequencePatterns.get('date').generate.call(this, instance, options); + } + }); + }, getLength(options) { return options.format?.length ?? 8; }, @@ -250,14 +334,17 @@ export class SequenceField extends Field { }; setValue = async (instance: Model, options) => { - const { name, patterns, inputable, match } = this.options; + if (options.skipIndividualHooks?.has(`${this.collection.name}.beforeCreate.${this.name}`)) { + return; + } + const { name, patterns, inputable } = this.options; const value = instance.get(name); if (value != null && inputable) { return this.update(instance, options); } const results = await patterns.reduce( - (promise, p, i) => + (promise, p) => promise.then(async (result) => { const item = await sequencePatterns.get(p.type).generate.call(this, instance, p.options, options); return result.concat(item); @@ -267,6 +354,34 @@ export class SequenceField extends Field { instance.set(name, results.join('')); }; + setGroupValue = async (instances: Model[], options) => { + if (!instances.length) { + return; + } + if (!options.skipIndividualHooks) { + options.skipIndividualHooks = new Set(); + } + options.skipIndividualHooks.add(`${this.collection.name}.beforeCreate.${this.name}`); + + const { name, patterns, inputable } = this.options; + const array = Array(patterns.length).fill(null).map(() => Array(instances.length)); + + await patterns.reduce((promise, p, i) => promise.then(() => + sequencePatterns.get(p.type).batchGenerate.call(this, instances, array[i], p.options, options)), + Promise.resolve()); + + instances.forEach((instance, i) => { + const value = instance.get(name); + if (!inputable || value == null) { + instance.set(this.name, array.map((a) => a[i]).join('')); + } + }); + }; + + cleanHook = (_, options) => { + options.skipIndividualHooks.delete(`${this.collection.name}.beforeCreate.${this.name}`); + } + match(value) { return typeof value === 'string' ? value.match(this.matcher) : null; } @@ -295,11 +410,15 @@ export class SequenceField extends Field { super.bind(); this.on('beforeValidate', this.validate); this.on('beforeCreate', this.setValue); + this.on('beforeBulkCreate', this.setGroupValue); + this.on('afterBulkCreate', this.cleanHook); } unbind() { super.unbind(); this.off('beforeValidate', this.validate); this.off('beforeCreate', this.setValue); + this.off('beforeBulkCreate', this.setGroupValue); + this.off('afterBulkCreate', this.cleanHook); } }