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:
parent
3b7143a282
commit
bf6bf00047
@ -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';
|
||||||
|
@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user