import { AttributeNames, Attributes, CreationOptional, FindOptions, Model, ModelStatic, Op, WhereOptions, } from "@sequelize/core" import { searchFieldsByTermsFactory } from "@/utils/search-fields-by-terms-factory" // See api/node_modules/@sequelize/core/lib/model.d.ts -> Model export abstract class BaseModel< // eslint-disable-next-line @typescript-eslint/ban-types, @typescript-eslint/no-explicit-any TModelAttributes extends {} = any, // eslint-disable-next-line @typescript-eslint/ban-types TCreationAttributes extends {} = TModelAttributes, > extends Model { declare id: CreationOptional static addSearchScope(this: ModelStatic, fields: AttributeNames[]) { const searchScopeFunction = searchFieldsByTermsFactory(fields) this.addScope("search", searchScopeFunction) } // static findByPk>( // this: ModelStatic, // identifier: unknown, // options: FindByPkOptions & { raw: true; rejectOnEmpty?: false }, // ): Promise; // static findByPk>( // this: ModelStatic, // identifier: unknown, // options: NonNullFindByPkOptions & { raw: true }, // ): Promise; // static findByPk( // this: ModelStatic, // identifier: unknown, // options: NonNullFindByPkOptions, // ): Promise; // static findByPk( // this: ModelStatic, // identifier: unknown, // options?: FindByPkOptions, // ): Promise; public static async findBySlugOrPk( this: ModelStatic, slugOrPk: unknown, options?: Omit>, "where"> ): Promise { if (typeof slugOrPk === "number" || !isNaN(Number(slugOrPk))) { const primaryKey = slugOrPk return this.findByPk(primaryKey, options) } const slug = slugOrPk if (!("slug" in this.getAttributes())) { throw new Error(`${this.name} does not have a 'slug' attribute.`) } return this.findOne({ ...options, // @ts-expect-error - We know that the model has a slug attribute, and are ignoring the TS error where: { slug }, }) } // See api/node_modules/@sequelize/core/lib/model.d.ts -> findAll // Taken from https://api.rubyonrails.org/v7.1.0/classes/ActiveRecord/Batches.html#method-i-find_each // Enforces sort by id, overwriting any supplied order public static async findEach( this: ModelStatic, processFunction: (record: M) => Promise ): Promise public static async findEach>( this: ModelStatic, options: Omit>, "raw"> & { raw: true batchSize?: number }, processFunction: (record: R) => Promise ): Promise public static async findEach( this: ModelStatic, options: FindOptions> & { batchSize?: number }, processFunction: (record: M) => Promise ): Promise public static async findEach>( this: ModelStatic, optionsOrFunction: | ((record: M) => Promise) | (Omit>, "raw"> & { raw: true; batchSize?: number }) | (FindOptions> & { batchSize?: number }), maybeFunction?: (record: R | M) => Promise ): Promise { let options: | (FindOptions> & { batchSize?: number }) | (Omit>, "raw"> & { raw: true; batchSize?: number }) // TODO: fix types so that process function is M when not raw // and R when raw. Raw is usable, just incorrectly typed. let processFunction: (record: M) => Promise if (typeof optionsOrFunction === "function") { options = {} processFunction = optionsOrFunction } else if (maybeFunction === undefined) { throw new Error("findEach requires a processFunction") } else { options = optionsOrFunction processFunction = maybeFunction } const batchSize = options.batchSize ?? 1000 let lastId = 0 let continueProcessing = true while (continueProcessing) { // TODO: fix where option types so cast is not needed const whereClause = { ...options.where, id: { [Op.gt]: lastId }, } as WhereOptions> const records = await this.findAll({ ...options, where: whereClause, limit: batchSize, order: [["id", "ASC"]], }) for (const record of records) { await processFunction(record) lastId = record.id } if (records.length < batchSize) { continueProcessing = false } } } } export default BaseModel