import { BelongsToMany, Model, Op, Transaction } from 'sequelize'; import { updateThroughTableValue } from '../update-associations'; import { FindAndCountOptions, FindOneOptions, MultipleRelationRepository } from './multiple-relation-repository'; import { CreateOptions, DestroyOptions, FindOptions, TargetKey, UpdateOptions } from '../repository'; import { AssociatedOptions, PrimaryKeyWithThroughValues } from './types'; import lodash from 'lodash'; import { transaction } from './relation-repository'; type CreateBelongsToManyOptions = CreateOptions; interface IBelongsToManyRepository { find(options?: FindOptions): Promise; findAndCount(options?: FindAndCountOptions): Promise<[M[], number]>; findOne(options?: FindOneOptions): Promise; // 新增并关联,存在中间表数据 create(options?: CreateBelongsToManyOptions): Promise; // 更新,存在中间表数据 update(options?: UpdateOptions): Promise; // 删除 destroy(options?: number | string | number[] | string[] | DestroyOptions): Promise; // 建立关联 set(options: TargetKey | TargetKey[] | AssociatedOptions): Promise; // 附加关联,存在中间表数据 add(options: TargetKey | TargetKey[] | AssociatedOptions): Promise; // 移除关联 remove(options: TargetKey | TargetKey[] | AssociatedOptions): Promise; toggle(options: TargetKey | { pk?: TargetKey; transaction?: Transaction }): Promise; } export class BelongsToManyRepository extends MultipleRelationRepository implements IBelongsToManyRepository { @transaction() async create(options?: CreateBelongsToManyOptions): Promise { const transaction = await this.getTransaction(options); const createAccessor = this.accessors().create; const values = options.values; const sourceModel = await this.getSourceModel(transaction); const createOptions = { ...options, through: values[this.throughName()], transaction, }; return sourceModel[createAccessor](values, createOptions); } @transaction((args, transaction) => { return { filterByTk: args[0], transaction, }; }) async destroy(options?: TargetKey | TargetKey[] | DestroyOptions): Promise { const transaction = await this.getTransaction(options); const association = this.association; const instancesToIds = (instances) => { return instances.map((instance) => instance.get(this.targetKey())); }; // Through Table const throughTableWhere: Array = [ { [association.foreignKey]: this.sourceKeyValue, }, ]; let ids; if (options && options['filter']) { const instances = await this.find({ filter: options['filter'], transaction, }); ids = instancesToIds(instances); } if (options && options['filterByTk']) { const instances = (this.association).toInstanceArray(options['filterByTk']); ids = ids ? lodash.intersection(ids, instancesToIds(instances)) : instancesToIds(instances); } if (options && !options['filterByTk'] && !options['filter']) { const sourceModel = await this.getSourceModel(transaction); const instances = await sourceModel[this.accessors().get]({ transaction, }); ids = instancesToIds(instances); } throughTableWhere.push({ [association.otherKey]: { [Op.in]: ids, }, }); // delete through table data await this.throughModel().destroy({ where: throughTableWhere, transaction, }); await this.targetModel.destroy({ where: { [this.targetKey()]: { [Op.in]: ids, }, }, transaction, }); return true; } protected async setTargets( call: 'add' | 'set', options: TargetKey | TargetKey[] | PrimaryKeyWithThroughValues | PrimaryKeyWithThroughValues[] | AssociatedOptions, ) { let handleKeys: TargetKey[] | PrimaryKeyWithThroughValues[]; const transaction = await this.getTransaction(options, false); if (lodash.isPlainObject(options)) { options = (options).tk || []; } if (lodash.isString(options) || lodash.isNumber(options)) { handleKeys = [options]; } // if it is type primaryKeyWithThroughValues else if (lodash.isArray(options) && options.length == 2 && lodash.isPlainObject(options[0][1])) { handleKeys = [options]; } else { handleKeys = options; } const sourceModel = await this.getSourceModel(transaction); const setObj = (handleKeys).reduce((carry, item) => { if (Array.isArray(item)) { carry[item[0]] = item[1]; } else { carry[item] = true; } return carry; }, {}); await sourceModel[this.accessors()[call]](Object.keys(setObj), { transaction, }); for (const [id, throughValues] of Object.entries(setObj)) { if (typeof throughValues === 'object') { const instance = await this.targetModel.findByPk(id, { transaction, }); await updateThroughTableValue(instance, this.throughName(), throughValues, sourceModel, transaction); } } } @transaction((args, transaction) => { return { tk: args[0], transaction, }; }) async add( options: TargetKey | TargetKey[] | PrimaryKeyWithThroughValues | PrimaryKeyWithThroughValues[] | AssociatedOptions, ): Promise { await this.setTargets('add', options); } @transaction((args, transaction) => { return { tk: args[0], transaction, }; }) async set( options: TargetKey | TargetKey[] | PrimaryKeyWithThroughValues | PrimaryKeyWithThroughValues[] | AssociatedOptions, ): Promise { await this.setTargets('set', options); } @transaction((args, transaction) => { return { tk: args[0], transaction, }; }) async toggle(options: TargetKey | { tk?: TargetKey; transaction?: Transaction }): Promise { const transaction = await this.getTransaction(options); const sourceModel = await this.getSourceModel(transaction); const has = await sourceModel[this.accessors().hasSingle](options['tk'], { transaction, }); if (has) { await this.remove({ ...(options), transaction, }); } else { await this.add({ ...(options), transaction, }); } return; } extendFindOptions(findOptions) { let joinTableAttributes; if (lodash.get(findOptions, 'fields')) { joinTableAttributes = []; } return { ...findOptions, joinTableAttributes, }; } throughName() { return this.throughModel().name; } throughModel() { return (this.association).through.model; } }