import { Association, BulkCreateOptions, CreateOptions as SequelizeCreateOptions, UpdateOptions as SequelizeUpdateOptions, FindAndCountOptions as SequelizeAndCountOptions, DestroyOptions as SequelizeDestroyOptions, FindOptions as SequelizeFindOptions, Model, ModelCtor, Op, Transaction, } from 'sequelize'; import { Collection } from './collection'; import lodash, { omit } from 'lodash'; import { Database } from './database'; import { updateAssociations, updateModelByValues } from './update-associations'; import { RelationField } from './fields'; import FilterParser from './filter-parser'; import { OptionsParser } from './options-parser'; import { RelationRepository } from './relation-repository/relation-repository'; import { HasOneRepository } from './relation-repository/hasone-repository'; import { BelongsToRepository } from './relation-repository/belongs-to-repository'; import { BelongsToManyRepository } from './relation-repository/belongs-to-many-repository'; import { HasManyRepository } from './relation-repository/hasmany-repository'; import { UpdateGuard } from './update-guard'; import { transactionWrapperBuilder } from './transaction-decorator'; const debug = require('debug')('noco-database'); export interface IRepository {} interface CreateManyOptions extends BulkCreateOptions { records: Values[]; } export interface TransactionAble { transaction?: Transaction; } export interface FilterAble { filter: Filter; } export type TargetKey = string | number; export type TK = TargetKey | TargetKey[]; export type Filter = any; export type Appends = string[]; export type Except = string[]; export type Fields = string[]; export type Sort = string[]; export type WhiteList = string[]; export type BlackList = string[]; export type AssociationKeysToBeUpdate = string[]; export type Values = any; export interface CountOptions extends Omit, TransactionAble { fields?: Fields; filter?: Filter; } export interface FilterByTk { filterByTk?: TargetKey; } export interface FindOptions extends SequelizeFindOptions, CommonFindOptions, FilterByTk {} export interface CommonFindOptions { filter?: Filter; fields?: Fields; appends?: Appends; except?: Except; sort?: Sort; } interface FindOneOptions extends FindOptions, CommonFindOptions {} export interface DestroyOptions extends SequelizeDestroyOptions { filter?: Filter; filterByTk?: TargetKey | TargetKey[]; truncate?: boolean; context?: any; } interface FindAndCountOptions extends Omit { // 数据过滤 filter?: Filter; // 输出结果显示哪些字段 fields?: Fields; // 输出结果不显示哪些字段 except?: Except; // 附加字段,用于控制关系字段的输出 appends?: Appends; // 排序,字段前面加上 “-” 表示降序 sort?: Sort; } export interface CreateOptions extends SequelizeCreateOptions { values?: Values; whitelist?: WhiteList; blacklist?: BlackList; updateAssociationValues?: AssociationKeysToBeUpdate; context?: any; } export interface UpdateOptions extends Omit { values: Values; filter?: Filter; filterByTk?: TargetKey; whitelist?: WhiteList; blacklist?: BlackList; updateAssociationValues?: AssociationKeysToBeUpdate; context?: any; } interface RelatedQueryOptions { database: Database; field: RelationField; source: { idOrInstance: any; collection: Collection; }; target: { association: Association & { accessors: any; }; collection: Collection; }; } const transaction = transactionWrapperBuilder(function () { return (this).collection.model.sequelize.transaction(); }); class RelationRepositoryBuilder { collection: Collection; associationName: string; association: Association; builderMap = { HasOne: HasOneRepository, BelongsTo: BelongsToRepository, BelongsToMany: BelongsToManyRepository, HasMany: HasManyRepository, }; constructor(collection: Collection, associationName: string) { this.collection = collection; this.associationName = associationName; this.association = this.collection.model.associations[this.associationName]; } protected builder() { return this.builderMap; } of(id: string | number): R { const klass = this.builder()[this.association.associationType]; return new klass(this.collection, this.associationName, id); } } export class Repository implements IRepository { database: Database; collection: Collection; model: ModelCtor; constructor(collection: Collection) { this.database = collection.context.database; this.collection = collection; this.model = collection.model; } /** * return count by filter */ async count(countOptions?: CountOptions): Promise { let options = countOptions ? lodash.clone(countOptions) : {}; const transaction = await this.getTransaction(options); if (countOptions?.filter) { options = { ...options, ...this.parseFilter(countOptions.filter), }; } const count = await this.collection.model.count({ ...options, distinct: true, transaction, }); return count; } /** * find * @param options */ async find(options?: FindOptions) { const model = this.collection.model; const transaction = await this.getTransaction(options); const opts = { subQuery: false, ...this.buildQueryOptions(options), }; if (opts.include && opts.include.length > 0) { // @ts-ignore const primaryKeyField = model.primaryKeyField || model.primaryKeyAttribute; const ids = ( await model.findAll({ ...opts, includeIgnoreAttributes: false, attributes: [primaryKeyField], group: `${model.name}.${primaryKeyField}`, transaction, }) ).map((row) => row.get(primaryKeyField)); const where = { [primaryKeyField]: { [Op.in]: ids, }, }; return await model.findAll({ ...omit(opts, ['limit', 'offset']), where, transaction, }); } return await model.findAll({ ...opts, transaction, }); } /** * find and count * @param options */ async findAndCount(options?: FindAndCountOptions): Promise<[Model[], number]> { const transaction = await this.getTransaction(options); options = { ...options, transaction, }; return [await this.find(options), await this.count(options)]; } /** * Find By Id * */ findById(id: string | number) { return this.collection.model.findByPk(id); } /** * Find one record from database * * @param options */ async findOne(options?: FindOneOptions) { const transaction = await this.getTransaction(options); const rows = await this.find({ ...options, limit: 1, transaction }); return rows.length == 1 ? rows[0] : null; } /** * Save instance to database * * @param values * @param options */ @transaction() async create(options: CreateOptions): Promise { const transaction = await this.getTransaction(options); const guard = UpdateGuard.fromOptions(this.model, { ...options, action: 'create' }); const values = guard.sanitize(options.values || {}); const instance = await this.model.create(values, { ...options, transaction, }); if (!instance) { return; } await updateAssociations(instance, values, { ...options, transaction, }); if (options.hooks !== false) { await this.database.emitAsync(`${this.collection.name}.afterCreateWithAssociations`, instance, options); await this.database.emitAsync(`${this.collection.name}.afterSaveWithAssociations`, instance, options); } return instance; } /** * Save Many instances to database * * @param records * @param options */ @transaction() async createMany(options: CreateManyOptions) { const transaction = await this.getTransaction(options); const { records } = options; const instances = await this.collection.model.bulkCreate(records, { ...options, transaction, }); for (let i = 0; i < instances.length; i++) { await updateAssociations(instances[i], records[i], { ...options, transaction }); } return instances; } /** * Update model value * * @param values * @param options */ @transaction() async update(options: UpdateOptions) { const transaction = await this.getTransaction(options); const guard = UpdateGuard.fromOptions(this.model, options); const values = guard.sanitize(options.values); const queryOptions = this.buildQueryOptions(options); const instances = await this.find({ ...queryOptions, transaction, }); for (const instance of instances) { await updateModelByValues(instance, values, { ...options, sanitized: true, transaction, }); } if (options.hooks !== false) { for (const instance of instances) { await this.database.emitAsync(`${this.collection.name}.afterUpdateWithAssociations`, instance, options); await this.database.emitAsync(`${this.collection.name}.afterSaveWithAssociations`, instance, options); } } return instances; } @transaction((args, transaction) => { return { filterByTk: args[0], transaction, }; }) async destroy(options?: TargetKey | TargetKey[] | DestroyOptions) { const transaction = await this.getTransaction(options); const modelFilterKey = this.collection.filterTargetKey; options = options; if (options['individualHooks'] === undefined) { options['individualHooks'] = true; } const filterByTk: TargetKey[] | undefined = options.filterByTk && !lodash.isArray(options.filterByTk) ? [options.filterByTk] : (options.filterByTk as TargetKey[] | undefined); if (filterByTk && !options.filter) { return await this.model.destroy({ ...options, where: { [modelFilterKey]: { [Op.in]: filterByTk, }, }, transaction, }); } if (options.filter) { let pks = ( await this.find({ filter: options.filter, transaction, }) ).map((instance) => instance.get(modelFilterKey) as TargetKey); if (filterByTk) { pks = lodash.intersection(pks, filterByTk); } return await this.destroy({ ...lodash.omit(options, 'filter'), filterByTk: pks, transaction, }); } if (options.truncate) { return await this.model.destroy({ ...options, truncate: true, transaction, }); } } /** * @param association target association */ relation(association: string): RelationRepositoryBuilder { return new RelationRepositoryBuilder(this.collection, association); } protected buildQueryOptions(options: any) { const parser = new OptionsParser(options, { collection: this.collection, }); const params = parser.toSequelizeParams(); debug('sequelize query params %o', params); return { ...options, ...params }; } protected parseFilter(filter: Filter) { const parser = new FilterParser(filter, { collection: this.collection, }); return parser.toSequelizeParams(); } protected async getTransaction(options: any, autoGen = false) { if (lodash.isPlainObject(options) && options.transaction) { return options.transaction; } if (autoGen) { return await this.model.sequelize.transaction(); } return null; } }