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
This commit is contained in:
Junyi 2023-01-18 22:54:08 +08:00 committed by GitHub
parent 3b7143a282
commit bf6bf00047
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 239 additions and 30 deletions

View File

@ -16,6 +16,10 @@ export type ModelCreateWithAssociationsEventType = 'afterCreateWithAssociations'
export type ModelUpdateWithAssociationsEventType = 'afterUpdateWithAssociations'; export type ModelUpdateWithAssociationsEventType = 'afterUpdateWithAssociations';
export type ModelSaveWithAssociationsEventType = 'afterSaveWithAssociations'; 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 ModelValidateEventTypes = ModelValidateEventType | `${CollectionNameType}.${ModelValidateEventType}`;
export type ModelCreateEventTypes = ModelCreateEventType | `${CollectionNameType}.${ModelCreateEventType}`; export type ModelCreateEventTypes = ModelCreateEventType | `${CollectionNameType}.${ModelCreateEventType}`;
export type ModelUpdateEventTypes = ModelUpdateEventType | `${CollectionNameType}.${ModelUpdateEventType}`; export type ModelUpdateEventTypes = ModelUpdateEventType | `${CollectionNameType}.${ModelUpdateEventType}`;
@ -25,6 +29,10 @@ export type ModelCreateWithAssociationsEventTypes = ModelCreateWithAssociationsE
export type ModelUpdateWithAssociationsEventTypes = ModelUpdateWithAssociationsEventType | `${CollectionNameType}.${ModelUpdateWithAssociationsEventType}`; export type ModelUpdateWithAssociationsEventTypes = ModelUpdateWithAssociationsEventType | `${CollectionNameType}.${ModelUpdateWithAssociationsEventType}`;
export type ModelSaveWithAssociationsEventTypes = ModelSaveWithAssociationsEventType | `${CollectionNameType}.${ModelSaveWithAssociationsEventType}`; 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 export type ModelEventTypes = ModelSyncEventType
| ModelValidateEventTypes | ModelValidateEventTypes
| ModelCreateEventTypes | ModelCreateEventTypes
@ -33,7 +41,10 @@ export type ModelEventTypes = ModelSyncEventType
| ModelDestroyEventTypes | ModelDestroyEventTypes
| ModelCreateWithAssociationsEventTypes | ModelCreateWithAssociationsEventTypes
| ModelUpdateWithAssociationsEventTypes | ModelUpdateWithAssociationsEventTypes
| ModelSaveWithAssociationsEventTypes; | ModelSaveWithAssociationsEventTypes
| ModelBulkCreateEvnetTypes
| ModelBulkUpdateEvnetTypes
| ModelBulkDestroyEvnetTypes;
export type DatabaseBeforeDefineCollectionEventType = 'beforeDefineCollection'; export type DatabaseBeforeDefineCollectionEventType = 'beforeDefineCollection';
export type DatabaseAfterDefineCollectionEventType = 'afterDefineCollection'; export type DatabaseAfterDefineCollectionEventType = 'afterDefineCollection';

View File

@ -754,4 +754,83 @@ describe('sequence field', () => {
expect(item4.name).toBe('0'); 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');
});
});
}); });

View File

@ -14,6 +14,13 @@ export interface Pattern {
opts: { [key: string]: any }, opts: { [key: string]: any },
options: Transactionable, options: Transactionable,
): Promise<string> | string; ): Promise<string> | string;
batchGenerate(
this: SequenceField,
instances: Model[],
values: string[],
opts: { [key: string]: any },
options: Transactionable,
): Promise<void> | void;
getLength(options): number; getLength(options): number;
getMatcher(options): string; getMatcher(options): string;
update?( update?(
@ -37,6 +44,11 @@ sequencePatterns.register('string', {
generate(instance, options) { generate(instance, options) {
return options.value; return options.value;
}, },
batchGenerate(instances, values, options) {
instances.forEach((instance, i) => {
values[i] = options.value;
});
},
getLength(options) { getLength(options) {
return options.value.length; return options.value.length;
}, },
@ -55,20 +67,27 @@ sequencePatterns.register('integer', {
async generate(this: SequenceField, instance: Model, options, { transaction }) { async generate(this: SequenceField, instance: Model, options, { transaction }) {
const recordTime = <Date>instance.get('createdAt'); const recordTime = <Date>instance.get('createdAt');
const { digits = 1, start = 0, base = 10, cycle, key } = options; const { digits = 1, start = 0, base = 10, cycle, key } = options;
const max = Math.pow(base, digits) - 1; const { repository: SeqRepo, model: SeqModel } = this.database.getCollection('sequences');
const SeqRepo = this.database.getRepository('sequences'); const lastSeq = (await SeqRepo.findOne({
const lastSeq = await SeqRepo.findOne({
filter: { filter: {
collection: this.collection.name, collection: this.collection.name,
field: this.name, field: this.name,
key, key,
}, },
transaction, transaction,
})) || SeqModel.build({
collection: this.collection.name,
field: this.name,
key,
}); });
let next; let next = start;
if (lastSeq && lastSeq.get('current') != null) { if (lastSeq.get('current') != null) {
next = Math.max(lastSeq.get('current') + 1, start); next = Math.max(lastSeq.get('current') + 1, start);
const max = Math.pow(base, digits) - 1;
if (next > max) {
next = start;
}
// cycle as cron string // cycle as cron string
if (cycle) { if (cycle) {
@ -78,29 +97,13 @@ sequencePatterns.register('integer', {
next = start; next = start;
} }
} }
} else {
next = start;
} }
if (next > max) { lastSeq.set({
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, current: next,
lastGeneratedAt: recordTime, lastGeneratedAt: recordTime,
},
transaction,
}); });
} await lastSeq.save({ transaction });
return next.toString(base).padStart(digits, '0'); return next.toString(base).padStart(digits, '0');
}, },
@ -110,11 +113,84 @@ sequencePatterns.register('integer', {
}, },
getMatcher(options = {}) { getMatcher(options = {}) {
const { digits = 1, start = 0, base = 10 } = options; const { digits = 1, base = 10 } = options;
const chars = '0123456789abcdefghijklmnopqrstuvwxyz'.slice(0, base); const chars = '0123456789abcdefghijklmnopqrstuvwxyz'.slice(0, base);
return `[${chars}]{${digits}}`; 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 = <Date>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: <Date>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 }) { async update(instance, value, options, { transaction }) {
const recordTime = <Date>instance.get('createdAt'); const recordTime = <Date>instance.get('createdAt');
const { digits = 1, start = 0, base = 10, cycle, key } = options; const { digits = 1, start = 0, base = 10, cycle, key } = options;
@ -183,6 +259,14 @@ sequencePatterns.register('date', {
generate(this: SequenceField, instance, options) { generate(this: SequenceField, instance, options) {
return moment(instance.get(options?.field ?? 'createdAt')).format(options?.format ?? 'YYYYMMDD'); 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) { getLength(options) {
return options.format?.length ?? 8; return options.format?.length ?? 8;
}, },
@ -250,14 +334,17 @@ export class SequenceField extends Field {
}; };
setValue = async (instance: Model, options) => { 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); const value = instance.get(name);
if (value != null && inputable) { if (value != null && inputable) {
return this.update(instance, options); return this.update(instance, options);
} }
const results = await patterns.reduce( const results = await patterns.reduce(
(promise, p, i) => (promise, p) =>
promise.then(async (result) => { promise.then(async (result) => {
const item = await sequencePatterns.get(p.type).generate.call(this, instance, p.options, options); const item = await sequencePatterns.get(p.type).generate.call(this, instance, p.options, options);
return result.concat(item); return result.concat(item);
@ -267,6 +354,34 @@ export class SequenceField extends Field {
instance.set(name, results.join('')); 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) { match(value) {
return typeof value === 'string' ? value.match(this.matcher) : null; return typeof value === 'string' ? value.match(this.matcher) : null;
} }
@ -295,11 +410,15 @@ export class SequenceField extends Field {
super.bind(); super.bind();
this.on('beforeValidate', this.validate); this.on('beforeValidate', this.validate);
this.on('beforeCreate', this.setValue); this.on('beforeCreate', this.setValue);
this.on('beforeBulkCreate', this.setGroupValue);
this.on('afterBulkCreate', this.cleanHook);
} }
unbind() { unbind() {
super.unbind(); super.unbind();
this.off('beforeValidate', this.validate); this.off('beforeValidate', this.validate);
this.off('beforeCreate', this.setValue); this.off('beforeCreate', this.setValue);
this.off('beforeBulkCreate', this.setGroupValue);
this.off('afterBulkCreate', this.cleanHook);
} }
} }