diff --git a/packages/core/database/src/decorators/transaction-decorator.ts b/packages/core/database/src/decorators/transaction-decorator.ts index 989ec637f..9419f7457 100644 --- a/packages/core/database/src/decorators/transaction-decorator.ts +++ b/packages/core/database/src/decorators/transaction-decorator.ts @@ -43,6 +43,7 @@ export function transactionWrapperBuilder(transactionGenerator) { return results; } catch (err) { + console.error(err); await transaction.rollback(); throw err; } diff --git a/packages/core/database/src/fields/belongs-to-field.ts b/packages/core/database/src/fields/belongs-to-field.ts index 28c7f5ee0..3c95eadc7 100644 --- a/packages/core/database/src/fields/belongs-to-field.ts +++ b/packages/core/database/src/fields/belongs-to-field.ts @@ -93,6 +93,7 @@ export class BelongsToField extends RelationField { const association = collection.model.associations[this.name]; this.database.referenceMap.removeReference(this.reference(association)); + this.clearAccessors(); // 删掉 model 的关联字段 delete collection.model.associations[this.name]; // @ts-ignore diff --git a/packages/core/database/src/fields/belongs-to-many-field.ts b/packages/core/database/src/fields/belongs-to-many-field.ts index d13dcb444..e493e1f45 100644 --- a/packages/core/database/src/fields/belongs-to-many-field.ts +++ b/packages/core/database/src/fields/belongs-to-many-field.ts @@ -23,7 +23,9 @@ export class BelongsToManyField extends RelationField { bind() { const { database, collection } = this.context; + const Target = this.TargetModel; + if (!Target) { database.addPendingField(this); return false; @@ -86,9 +88,12 @@ export class BelongsToManyField extends RelationField { unbind() { const { database, collection } = this.context; const Through = database.getCollection(this.through); + // 如果关系字段还没建立就删除了,也同步删除待建立关联的关系字段 database.removePendingField(this); // 删掉 model 的关联字段 + + this.clearAccessors(); delete collection.model.associations[this.name]; } } diff --git a/packages/core/database/src/fields/field.ts b/packages/core/database/src/fields/field.ts index 115dcd6df..281e48496 100644 --- a/packages/core/database/src/fields/field.ts +++ b/packages/core/database/src/fields/field.ts @@ -5,7 +5,7 @@ import { ModelIndexesOptions, QueryInterfaceOptions, SyncOptions, - Transactionable + Transactionable, } from 'sequelize'; import { Collection } from '../collection'; import { Database } from '../database'; diff --git a/packages/core/database/src/fields/has-many-field.ts b/packages/core/database/src/fields/has-many-field.ts index 5fb345581..c72089437 100644 --- a/packages/core/database/src/fields/has-many-field.ts +++ b/packages/core/database/src/fields/has-many-field.ts @@ -172,6 +172,7 @@ export class HasManyField extends RelationField { this.database.referenceMap.removeReference(this.reference(association)); } + this.clearAccessors(); // 删掉 model 的关联字段 delete collection.model.associations[this.name]; // @ts-ignore diff --git a/packages/core/database/src/fields/has-one-field.ts b/packages/core/database/src/fields/has-one-field.ts index 4c2986ce2..2c1734311 100644 --- a/packages/core/database/src/fields/has-one-field.ts +++ b/packages/core/database/src/fields/has-one-field.ts @@ -168,6 +168,7 @@ export class HasOneField extends RelationField { const association = collection.model.associations[this.name]; this.database.referenceMap.removeReference(this.reference(association)); + this.clearAccessors(); // 删掉 model 的关联字段 delete collection.model.associations[this.name]; // @ts-ignore diff --git a/packages/core/database/src/fields/relation-field.ts b/packages/core/database/src/fields/relation-field.ts index 80e822d35..313f922f6 100644 --- a/packages/core/database/src/fields/relation-field.ts +++ b/packages/core/database/src/fields/relation-field.ts @@ -34,4 +34,20 @@ export abstract class RelationField extends Field { get TargetModel() { 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]; + }); + } } diff --git a/packages/core/database/src/model-hook.ts b/packages/core/database/src/model-hook.ts index 17aa755f1..5464f7321 100644 --- a/packages/core/database/src/model-hook.ts +++ b/packages/core/database/src/model-hook.ts @@ -5,8 +5,6 @@ import { Model } from './model'; const { hooks } = require('sequelize/lib/hooks'); - - export class ModelHook { database: Database; diff --git a/packages/core/database/src/repository.ts b/packages/core/database/src/repository.ts index 8d4240d04..224c22f54 100644 --- a/packages/core/database/src/repository.ts +++ b/packages/core/database/src/repository.ts @@ -338,7 +338,6 @@ export class Repository(values, { ...options, transaction, diff --git a/packages/core/database/src/update-associations.ts b/packages/core/database/src/update-associations.ts index 716cdc209..b1948d8d2 100644 --- a/packages/core/database/src/update-associations.ts +++ b/packages/core/database/src/update-associations.ts @@ -6,7 +6,7 @@ import { HasOne, Hookable, ModelCtor, - Transactionable + Transactionable, } from 'sequelize'; import { Model } from './model'; import { UpdateGuard } from './update-guard'; @@ -375,6 +375,7 @@ export async function updateMultipleAssociation( const setAccessor = association.accessors.set; const createAccessor = association.accessors.create; + if (isUndefinedOrNull(value)) { await model[setAccessor](null, { transaction, context }); model.setDataValue(key, null); diff --git a/packages/plugins/collection-manager/src/__tests__/fields/belongsToMany.test.ts b/packages/plugins/collection-manager/src/__tests__/fields/belongsToMany.test.ts index 88607e20d..509167c75 100644 --- a/packages/plugins/collection-manager/src/__tests__/fields/belongsToMany.test.ts +++ b/packages/plugins/collection-manager/src/__tests__/fields/belongsToMany.test.ts @@ -15,12 +15,12 @@ describe('belongsToMany', () => { Field = db.getCollection('fields'); await Collection.repository.create({ values: { - name: 'tests', + name: 'posts', }, }); await Collection.repository.create({ values: { - name: 'foos', + name: 'tags', }, }); }); @@ -29,7 +29,5 @@ describe('belongsToMany', () => { await app.destroy(); }); - it('a', () => { - expect(true).toBe(true); - }); + it('should create belongsToMany field', async () => {}); }); diff --git a/packages/plugins/collection-manager/src/__tests__/http-api/belongsToMany.test.ts b/packages/plugins/collection-manager/src/__tests__/http-api/belongsToMany.test.ts new file mode 100644 index 000000000..c257d7be0 --- /dev/null +++ b/packages/plugins/collection-manager/src/__tests__/http-api/belongsToMany.test.ts @@ -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); + }); +}); diff --git a/packages/plugins/collection-manager/src/models/collection.ts b/packages/plugins/collection-manager/src/models/collection.ts index 029344c43..99212ef75 100644 --- a/packages/plugins/collection-manager/src/models/collection.ts +++ b/packages/plugins/collection-manager/src/models/collection.ts @@ -54,6 +54,7 @@ export class CollectionModel extends MagicAttributeModel { const { transaction } = options || {}; const name = this.get('name'); const collection = this.db.getCollection(name); + if (!collection) { return; } @@ -72,6 +73,7 @@ export class CollectionModel extends MagicAttributeModel { await field.destroy({ transaction }); } } + await collection.removeFromDb({ transaction, }); diff --git a/packages/plugins/collection-manager/src/server.ts b/packages/plugins/collection-manager/src/server.ts index 275c4f020..d133f5f4b 100644 --- a/packages/plugins/collection-manager/src/server.ts +++ b/packages/plugins/collection-manager/src/server.ts @@ -83,7 +83,7 @@ export class CollectionManagerPlugin extends Plugin { }); } }); - + // after migrate 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) { await model.load({ transaction }); } @@ -120,7 +120,7 @@ export class CollectionManagerPlugin extends Plugin { 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); });