import { ModelStatic, Model, Attributes, FindOptions, ScopeOptions, literal } from "@sequelize/core" import { User } from "@/models" import { Path, deepPick } from "@/utils/deep-pick" import { isInteger, isNil } from "lodash" export type Actions = "show" | "create" | "update" | "destroy" export const NO_RECORDS_SCOPE = { where: literal("1 = 0") } export const ALL_RECORDS_SCOPE = {} /** * See PolicyFactory below for policy with scope helpers */ export class BasePolicy { protected user: User protected record: M constructor(user: User, record: M) { this.user = user this.record = record } show(): boolean { return false } create(): boolean { return false } update(): boolean { return false } destroy(): boolean { return false } // eslint-disable-next-line @typescript-eslint/no-unused-vars static policyScope(user: User, ...args: unknown[]): FindOptions> { throw new Error("Derived classes must implement policyScope method") } permitAttributes(record: Partial): Partial { return deepPick(record, this.permittedAttributes()) } permitAttributesForCreate(record: Partial): Partial { if (this.permittedAttributesForCreate !== BasePolicy.prototype.permittedAttributesForCreate) { return deepPick(record, this.permittedAttributesForCreate()) } else { return deepPick(record, this.permittedAttributes()) } } permitAttributesForUpdate(record: Partial): Partial { if (this.permittedAttributesForUpdate !== BasePolicy.prototype.permittedAttributesForUpdate) { return deepPick(record, this.permittedAttributesForUpdate()) } else { return deepPick(record, this.permittedAttributes()) } } permittedAttributes(): Path[] { throw new Error("Not Implemented") } permittedAttributesForCreate(): Path[] { throw new Error("Not Implemented") } permittedAttributesForUpdate(): Path[] { throw new Error("Not Implemented") } setNumberNullIfEmpty(input: number | null | undefined): number | null | undefined { let output = input if (!isNil(input) && !isInteger(input)) output = undefined if (!isNil(input) && input == 0) output = undefined return output } setNumberZeroIfEmpty(input: number | null | undefined): number { let output = input if (!isNil(input) && !isInteger(input)) output = 0 if (!isNil(input) && input == 0) output = 0 return output ?? 0 } /** * Add to support return policy information via this.reponse.json({ someObject, policy }) * * If this method becomes complex, it should be broken out into a serializer. * * @returns a JSON representation of the policy */ toJSON(): Record { return { show: this.show(), create: this.create(), update: this.update(), destroy: this.destroy(), } } } // See api/node_modules/sequelize/types/model.d.ts -> Model -> scope export type BaseScopeOptions = string | ScopeOptions export const POLICY_SCOPE_NAME = "policyScope" // eslint-disable-next-line @typescript-eslint/no-explicit-any type AllArgsButFirstOne = T extends [any, ...infer Rest] ? Rest : never export function PolicyFactory(modelClass: ModelStatic) { const policyClass = class Policy extends BasePolicy { static applyScope

( this: P, scopes: BaseScopeOptions[], user: User, ...extraPolicyScopeArgs: AllArgsButFirstOne> ): ModelStatic { this.ensurePolicyScope() return modelClass.withScope([ ...scopes, { method: [POLICY_SCOPE_NAME, user, ...extraPolicyScopeArgs] }, ]) } /** * Just in time scope creation for model class. * TODO: to have scope creation occur at definition time, instead of execution time. */ static ensurePolicyScope() { if (Object.prototype.hasOwnProperty.call(modelClass.options.scopes, POLICY_SCOPE_NAME)) { return } modelClass.addScope(POLICY_SCOPE_NAME, this.policyScope.bind(modelClass)) } } return policyClass } export default PolicyFactory