chore(database): clean invalid associations in collection model when set field failed (#2720)

* chore(database): clean invalid associations in collection model when set field failed

* fix: import

* chore: test
This commit is contained in:
ChengLei Shao 2023-09-27 22:03:04 +08:00 committed by GitHub
parent a57c93d35b
commit dbdd587b71
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 163 additions and 2 deletions

View File

@ -13,6 +13,58 @@ describe('belongs to many field', () => {
await db.close(); await db.close();
}); });
it('should define belongs to many when change alias name', async () => {
const Through = db.collection({
name: 't1',
fields: [
{
type: 'bigInt',
name: 'id',
primaryKey: true,
},
],
});
const A = db.collection({
name: 'a',
});
const B = db.collection({
name: 'b',
});
await db.sync();
let error;
expect(Object.keys(A.model.associations).length).toEqual(0);
try {
A.setField('t1', {
type: 'belongsToMany',
through: 't1',
target: 't1',
});
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(Object.keys(A.model.associations).length).toEqual(0);
let error2;
try {
A.setField('xxx', {
type: 'belongsToMany',
through: 't1',
target: 't1',
});
} catch (e) {
error2 = e;
}
expect(error2).not.toBeDefined();
});
it('should define belongs to many relation through exists pivot collection', async () => { it('should define belongs to many relation through exists pivot collection', async () => {
const PostTag = db.collection({ const PostTag = db.collection({
name: 'postsTags', name: 'postsTags',

View File

@ -22,6 +22,40 @@ export type CollectionSortable = string | boolean | { name?: string; scopeKey?:
type dumpable = 'required' | 'optional' | 'skip'; type dumpable = 'required' | 'optional' | 'skip';
function EnsureAtomicity(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const model = this.model;
const beforeAssociationKeys = Object.keys(model.associations);
const beforeRawAttributes = Object.keys(model.rawAttributes);
try {
return originalMethod.apply(this, args);
} catch (error) {
// remove associations created in this method
const afterAssociationKeys = Object.keys(model.associations);
const createdAssociationKeys = lodash.difference(afterAssociationKeys, beforeAssociationKeys);
for (const key of createdAssociationKeys) {
delete this.model.associations[key];
}
const afterRawAttributes = Object.keys(model.rawAttributes);
const createdRawAttributes = lodash.difference(afterRawAttributes, beforeRawAttributes);
console.log({
beforeRawAttributes,
afterRawAttributes,
});
for (const key of createdRawAttributes) {
delete this.model.rawAttributes[key];
}
throw error;
}
};
return descriptor;
}
export interface CollectionOptions extends Omit<ModelOptions, 'name' | 'hooks'> { export interface CollectionOptions extends Omit<ModelOptions, 'name' | 'hooks'> {
name: string; name: string;
namespace?: string; namespace?: string;
@ -251,6 +285,7 @@ export class Collection<
} }
} }
@EnsureAtomicity
setField(name: string, options: FieldOptions): Field { setField(name: string, options: FieldOptions): Field {
checkIdentifier(name); checkIdentifier(name);
this.checkFieldType(name, options); this.checkFieldType(name, options);

View File

@ -85,12 +85,14 @@ export class BelongsToManyField extends RelationField {
this.options.onDelete = 'CASCADE'; this.options.onDelete = 'CASCADE';
} }
const association = collection.model.belongsToMany(Target, { const belongsToManyOptions = {
constraints: false, constraints: false,
...omit(this.options, ['name', 'type', 'target']), ...omit(this.options, ['name', 'type', 'target']),
as: this.name, as: this.name,
through: Through.model, through: Through.model,
}); };
const association = collection.model.belongsToMany(Target, belongsToManyOptions);
// 建立关系之后从 pending 列表中删除 // 建立关系之后从 pending 列表中删除
database.removePendingField(this); database.removePendingField(this);

View File

@ -34,6 +34,78 @@ describe('belongsToMany', () => {
await app.destroy(); await app.destroy();
}); });
it('should define belongs to many when change alias name', async () => {
await Collection.repository.create({
values: {
name: 'a',
fields: [{ type: 'bigInt', name: 'id', primaryKey: true, autoIncrement: true }],
},
context: {},
});
await Collection.repository.create({
values: {
name: 'b',
fields: [{ type: 'bigInt', name: 'id', primaryKey: true, autoIncrement: true }],
},
context: {},
});
await Collection.repository.create({
values: {
name: 't1',
fields: [{ type: 'bigInt', name: 'id', primaryKey: true, autoIncrement: true }],
},
context: {},
});
const Through = db.getCollection('t1');
let error;
try {
await Field.repository.create({
values: {
name: 't1',
type: 'belongsToMany',
target: 'b',
through: 't1',
collectionName: 'a',
sourceKey: 'id',
targetKey: 'id',
foreignKey: 'a_id',
otherKey: 'b_id',
},
context: {},
});
} catch (e) {
error = e;
}
expect(error).toBeDefined();
let error2;
try {
await Field.repository.create({
values: {
name: 'xxx',
type: 'belongsToMany',
target: 'b',
through: 't1',
collectionName: 'a',
sourceKey: 'id',
targetKey: 'id',
foreignKey: 'a_id',
otherKey: 'b_id',
},
context: {},
});
} catch (e) {
error2 = e;
}
expect(error2).not.toBeDefined();
});
it('should create belongsToMany field', async () => { it('should create belongsToMany field', async () => {
await Field.repository.create({ await Field.repository.create({
values: { values: {