fix: association accessors rebind (#1027)

* chore: test

* chore: test

* fix: association accessors rebind

* fix: test
This commit is contained in:
ChengLei Shao 2022-11-04 09:04:53 +08:00 committed by GitHub
parent f15c67afd5
commit b3f3883435
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 176 additions and 13 deletions

View File

@ -43,6 +43,7 @@ export function transactionWrapperBuilder(transactionGenerator) {
return results; return results;
} catch (err) { } catch (err) {
console.error(err);
await transaction.rollback(); await transaction.rollback();
throw err; throw err;
} }

View File

@ -93,6 +93,7 @@ export class BelongsToField extends RelationField {
const association = collection.model.associations[this.name]; const association = collection.model.associations[this.name];
this.database.referenceMap.removeReference(this.reference(association)); this.database.referenceMap.removeReference(this.reference(association));
this.clearAccessors();
// 删掉 model 的关联字段 // 删掉 model 的关联字段
delete collection.model.associations[this.name]; delete collection.model.associations[this.name];
// @ts-ignore // @ts-ignore

View File

@ -23,7 +23,9 @@ export class BelongsToManyField extends RelationField {
bind() { bind() {
const { database, collection } = this.context; const { database, collection } = this.context;
const Target = this.TargetModel; const Target = this.TargetModel;
if (!Target) { if (!Target) {
database.addPendingField(this); database.addPendingField(this);
return false; return false;
@ -86,9 +88,12 @@ export class BelongsToManyField extends RelationField {
unbind() { unbind() {
const { database, collection } = this.context; const { database, collection } = this.context;
const Through = database.getCollection(this.through); const Through = database.getCollection(this.through);
// 如果关系字段还没建立就删除了,也同步删除待建立关联的关系字段 // 如果关系字段还没建立就删除了,也同步删除待建立关联的关系字段
database.removePendingField(this); database.removePendingField(this);
// 删掉 model 的关联字段 // 删掉 model 的关联字段
this.clearAccessors();
delete collection.model.associations[this.name]; delete collection.model.associations[this.name];
} }
} }

View File

@ -5,7 +5,7 @@ import {
ModelIndexesOptions, ModelIndexesOptions,
QueryInterfaceOptions, QueryInterfaceOptions,
SyncOptions, SyncOptions,
Transactionable Transactionable,
} from 'sequelize'; } from 'sequelize';
import { Collection } from '../collection'; import { Collection } from '../collection';
import { Database } from '../database'; import { Database } from '../database';

View File

@ -172,6 +172,7 @@ export class HasManyField extends RelationField {
this.database.referenceMap.removeReference(this.reference(association)); this.database.referenceMap.removeReference(this.reference(association));
} }
this.clearAccessors();
// 删掉 model 的关联字段 // 删掉 model 的关联字段
delete collection.model.associations[this.name]; delete collection.model.associations[this.name];
// @ts-ignore // @ts-ignore

View File

@ -168,6 +168,7 @@ export class HasOneField extends RelationField {
const association = collection.model.associations[this.name]; const association = collection.model.associations[this.name];
this.database.referenceMap.removeReference(this.reference(association)); this.database.referenceMap.removeReference(this.reference(association));
this.clearAccessors();
// 删掉 model 的关联字段 // 删掉 model 的关联字段
delete collection.model.associations[this.name]; delete collection.model.associations[this.name];
// @ts-ignore // @ts-ignore

View File

@ -34,4 +34,20 @@ export abstract class RelationField extends Field {
get TargetModel() { get TargetModel() {
return this.context.database.sequelize.models[this.target]; return this.context.database.sequelize.models[this.target];
} }
protected clearAccessors() {
const { collection } = this.context;
const association = collection.model.associations[this.name];
if (!association) {
return;
}
// @ts-ignore
const accessors = Object.values(association.accessors);
accessors.forEach((accessor) => {
// @ts-ignore
delete collection.model.prototype[accessor];
});
}
} }

View File

@ -5,8 +5,6 @@ import { Model } from './model';
const { hooks } = require('sequelize/lib/hooks'); const { hooks } = require('sequelize/lib/hooks');
export class ModelHook { export class ModelHook {
database: Database; database: Database;

View File

@ -338,7 +338,6 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
const guard = UpdateGuard.fromOptions(this.model, { ...options, action: 'create' }); const guard = UpdateGuard.fromOptions(this.model, { ...options, action: 'create' });
const values = guard.sanitize(options.values || {}); const values = guard.sanitize(options.values || {});
const instance = await this.model.create<any>(values, { const instance = await this.model.create<any>(values, {
...options, ...options,
transaction, transaction,

View File

@ -6,7 +6,7 @@ import {
HasOne, HasOne,
Hookable, Hookable,
ModelCtor, ModelCtor,
Transactionable Transactionable,
} from 'sequelize'; } from 'sequelize';
import { Model } from './model'; import { Model } from './model';
import { UpdateGuard } from './update-guard'; import { UpdateGuard } from './update-guard';
@ -375,6 +375,7 @@ export async function updateMultipleAssociation(
const setAccessor = association.accessors.set; const setAccessor = association.accessors.set;
const createAccessor = association.accessors.create; const createAccessor = association.accessors.create;
if (isUndefinedOrNull(value)) { if (isUndefinedOrNull(value)) {
await model[setAccessor](null, { transaction, context }); await model[setAccessor](null, { transaction, context });
model.setDataValue(key, null); model.setDataValue(key, null);

View File

@ -15,12 +15,12 @@ describe('belongsToMany', () => {
Field = db.getCollection('fields'); Field = db.getCollection('fields');
await Collection.repository.create({ await Collection.repository.create({
values: { values: {
name: 'tests', name: 'posts',
}, },
}); });
await Collection.repository.create({ await Collection.repository.create({
values: { values: {
name: 'foos', name: 'tags',
}, },
}); });
}); });
@ -29,7 +29,5 @@ describe('belongsToMany', () => {
await app.destroy(); await app.destroy();
}); });
it('a', () => { it('should create belongsToMany field', async () => {});
expect(true).toBe(true);
});
}); });

View File

@ -0,0 +1,140 @@
import { MockServer } from '@nocobase/test';
import { createApp } from '..';
describe('collections repository', () => {
let app: MockServer;
let agent;
let db;
beforeEach(async () => {
app = await createApp();
db = app.db;
agent = app.agent();
});
afterEach(async () => {
await app.destroy();
});
it('should create belongs to many fields', async () => {
await agent.resource('collections').create({
values: {
name: 'posts',
},
});
await agent.resource('collections').create({
values: {
name: 'tags',
},
});
await agent.resource('collections').create({
values: {
name: 'post_tag',
createdBy: true,
updatedBy: true,
sortable: true,
logging: true,
fields: [
{
name: 'id',
type: 'integer',
autoIncrement: true,
primaryKey: true,
allowNull: false,
uiSchema: {
type: 'number',
title: '{{t("ID")}}',
'x-component': 'InputNumber',
'x-read-pretty': true,
},
interface: 'id',
},
],
title: 'post_tag',
},
});
await agent.resource('tags').create({
values: {},
});
const response0 = await agent.resource('collections.fields', 'posts').create({
values: {
foreignKey: 'post_id',
otherKey: 'tag_id',
name: 'tags',
type: 'belongsToMany',
uiSchema: {
'x-component': 'RecordPicker',
'x-component-props': {
multiple: true,
fieldNames: {
label: 'id',
value: 'id',
},
},
title: 'tags',
},
interface: 'm2m',
through: 'post_tag',
target: 'tags',
},
});
expect(response0.status).toBe(200);
const TagRepository = db.getCollection('tags').repository;
const PostRepository = db.getCollection('posts').repository;
const tag = await TagRepository.findOne();
expect(!!tag).toBeTruthy();
const response = await agent.resource('posts').create({
values: {
tags: [tag.id],
},
});
expect(response.status).toBe(200);
const post = await PostRepository.findOne({
appends: ['tags'],
});
expect(post.tags.length).toBe(1);
expect(PostRepository.model.associations['tags']).toBeDefined();
// destroy through table
await agent.resource('collections').destroy({
filterByTk: 'post_tag',
});
// association deleted
expect(PostRepository.model.associations['tags']).not.toBeDefined();
// create association again
await agent.resource('collections.fields', 'posts').create({
values: {
name: 'tags',
type: 'belongsToMany',
target: 'tags',
through: 'random_2', //difference name
foreignKey: 'post_id',
otherKey: 'tag_id',
},
});
const response2 = await agent.resource('posts').create({
values: {
tags: [tag.id],
},
});
expect(response2.status).toBe(200);
});
});

View File

@ -54,6 +54,7 @@ export class CollectionModel extends MagicAttributeModel {
const { transaction } = options || {}; const { transaction } = options || {};
const name = this.get('name'); const name = this.get('name');
const collection = this.db.getCollection(name); const collection = this.db.getCollection(name);
if (!collection) { if (!collection) {
return; return;
} }
@ -72,6 +73,7 @@ export class CollectionModel extends MagicAttributeModel {
await field.destroy({ transaction }); await field.destroy({ transaction });
} }
} }
await collection.removeFromDb({ await collection.removeFromDb({
transaction, transaction,
}); });

View File

@ -83,7 +83,7 @@ export class CollectionManagerPlugin extends Plugin {
}); });
} }
}); });
// after migrate // after migrate
this.app.db.on('fields.afterCreate', afterCreateForForeignKeyField(this.app.db)); this.app.db.on('fields.afterCreate', afterCreateForForeignKeyField(this.app.db));
@ -108,7 +108,7 @@ export class CollectionManagerPlugin extends Plugin {
} }
}); });
this.app.db.on('fields.afterSaveWithAssociations', async (model, { context, transaction }) => { this.app.db.on('fields.afterSaveWithAssociations', async (model: FieldModel, { context, transaction }) => {
if (context) { if (context) {
await model.load({ transaction }); await model.load({ transaction });
} }
@ -120,7 +120,7 @@ export class CollectionManagerPlugin extends Plugin {
await model.remove(options); await model.remove(options);
}); });
this.app.db.on('collections.beforeDestroy', async (model, options) => { this.app.db.on('collections.beforeDestroy', async (model: CollectionModel, options) => {
await model.remove(options); await model.remove(options);
}); });