import { Association, BulkCreateOptions, CreateOptions as SequelizeCreateOptions, FindAndCountOptions as SequelizeAndCountOptions, 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 PrimaryKey = string | number; export type PK = PrimaryKey | PrimaryKey[]; 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 = { [key: string]: string | number | Values | Array; }; export interface CountOptions extends Omit, TransactionAble { fields?: Fields; filter?: Filter; } export interface FilterByPK { filterByPk?: PrimaryKey; } export interface FindOptions extends SequelizeFindOptions, CommonFindOptions, FilterByPK {} export interface CommonFindOptions { filter?: Filter; fields?: Fields; appends?: Appends; except?: Except; sort?: Sort; } interface FindOneOptions extends FindOptions, CommonFindOptions {} export interface DestroyOptions extends TransactionAble { filter?: Filter; filterByPk?: PrimaryKey | PrimaryKey[]; truncate?: boolean; } interface FindAndCountOptions extends Omit { // 数据过滤 filter?: Filter; // 输出结果显示哪些字段 fields?: Fields; // 输出结果不显示哪些字段 except?: Except; // 附加字段,用于控制关系字段的输出 appends?: Appends; // 排序,字段前面加上 “-” 表示降序 sort?: Sort; } export interface CreateOptions extends TransactionAble { values?: Values; whitelist?: WhiteList; blacklist?: BlackList; updateAssociationValues?: AssociationKeysToBeUpdate; } export interface UpdateOptions extends TransactionAble { values: Values; filter?: Filter; filterByPk?: PrimaryKey; whitelist?: WhiteList; blacklist?: BlackList; updateAssociationValues?: AssociationKeysToBeUpdate; } 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< TModelAttributes extends {} = any, TCreationAttributes extends {} = TModelAttributes, > 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 as any; } /** * 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) { const ids = ( await model.findAll({ ...opts, includeIgnoreAttributes: false, attributes: [model.primaryKeyAttribute], group: `${model.name}.${model.primaryKeyAttribute}`, transaction, }) ).map((row) => row.get(model.primaryKeyAttribute)); const where = { [model.primaryKeyAttribute]: { [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: PrimaryKey) { 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); const values = guard.sanitize(options.values || {}); const instance = await this.model.create(values, { transaction }); if (!instance) { return; } await updateAssociations(instance, values, { transaction, }); 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], { 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, { sanitized: true, transaction, }); } return true; } @transaction((args, transaction) => { return { filterByPk: args[0], transaction, }; }) async destroy(options?: PrimaryKey | PrimaryKey[] | DestroyOptions) { const transaction = await this.getTransaction(options); options = options; let filterByPk = options.filterByPk; if (filterByPk) { if (!Array.isArray(filterByPk)) { filterByPk = [filterByPk]; } return await this.model.destroy({ where: { [this.model.primaryKeyAttribute]: { [Op.in]: filterByPk, }, }, transaction, }); } if (options.filter) { const instances = await this.find({ filter: options.filter, transaction, }); return await this.destroy({ filterByPk: instances.map( (instance) => instance[this.model.primaryKeyAttribute], ), transaction, }); } if (options.truncate) { return await this.model.destroy({ 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( this.collection.model, this.collection.context.database, options, ); const params = parser.toSequelizeParams(); debug('sequelize query params %o', params); return { ...options, ...params }; } protected parseFilter(filter: Filter) { const parser = new FilterParser( this.collection.model, this.collection.context.database, filter, ); 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; } }