mirror of
https://gitee.com/nocobase/nocobase.git
synced 2024-11-30 03:08:31 +08:00
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 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';
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -14,6 +14,13 @@ export interface Pattern {
|
||||
opts: { [key: string]: any },
|
||||
options: Transactionable,
|
||||
): Promise<string> | string;
|
||||
batchGenerate(
|
||||
this: SequenceField,
|
||||
instances: Model[],
|
||||
values: string[],
|
||||
opts: { [key: string]: any },
|
||||
options: Transactionable,
|
||||
): Promise<void> | 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 = <Date>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 = <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 }) {
|
||||
const recordTime = <Date>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);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user