import {
BulkCrudOperationKeys,
DefaultSeparator,
enforceDBDecorators,
InternalError,
ValidationError,
IRepository,
OperationKeys,
Repository as Rep,
wrapMethodWithContext,
reduceErrorsToPrint,
PrimaryKeyType,
NotFoundError,
wrapMethodWithContextForUpdate,
} from "@decaf-ts/db-decorators";
import { final, Logger } from "@decaf-ts/logging";
import {
ContextualArgs,
ContextualizedArgs,
MaybeContextualArg,
MethodOrOperation,
} from "../utils/ContextualLoggedClass";
import { Adapter } from "../persistence/Adapter";
import { PersistenceKeys } from "../persistence/constants";
import { ObserverHandler } from "../persistence/ObserverHandler";
import { QueryError } from "../query/errors";
import type { DirectionLimitOffset, QueryOptions } from "../query/types";
import { OrderBySelector, SelectSelector } from "../query/selectors";
import { CountWhereOption, WhereOption } from "../query/options";
import { Condition } from "../query/Condition";
import { Queriable } from "../interfaces/Queriable";
import { SequenceOptions } from "../interfaces/SequenceOptions";
import { OrderDirection } from "./constants";
import type {
AllOperationKeys,
ContextOf,
EventIds,
FlagsOf,
InferredAdapterConfig,
ObserverFilter,
PersistenceObservable,
PersistenceObserver,
} from "../persistence/types";
import type { Observer } from "../interfaces/Observer";
import {
Constructor,
DecorationKeys,
DefaultFlavour,
Metadata,
uses,
} from "@decaf-ts/decoration";
import { Model } from "@decaf-ts/decorator-validation";
import { prepared } from "../query/decorators";
import { PreparedStatementKeys } from "../query/constants";
import { Paginator } from "../query/Paginator";
import { SerializedPage } from "../query/Paginator";
import { getFilters } from "../persistence/event-filters";
/**
* @description Type alias for Repository class with simplified generic parameters.
* @summary Provides a more concise way to reference the Repository class with its generic parameters.
* @template M - The model type that extends Model.
* @template F - The repository flags type.
* @template C - The context type.
* @template Q - The query type.
* @template A - The adapter type.
* @typedef Repo
* @memberOf module:core
*/
export type Repo<M extends Model<boolean>> = Repository<M, any>;
/**
* @description Core repository implementation for database operations on models on a table by table way.
* @summary Provides CRUD operations, querying capabilities, and observer pattern implementation for model persistence.
* @template M - The model type that extends Model.
* @template Q - The query type used by the adapter.
* @template A - The adapter type for database operations.
* @template F - The repository flags type.
* @template C - The context type for operations.
* @param {A} [adapter] - Optional adapter instance for database operations.
* @param {Constructor<M>} [clazz] - Optional constructor for the model class.
* @param {...any[]} [args] - Additional arguments for repository initialization.
* @class Repository
* @example
* // Creating a repository for User model
* const userRepo = Repository.forModel(User);
*
* // Using the repository for CRUD operations
* const user = await userRepo.create(new User({ name: 'John' }));
* const retrievedUser = await userRepo.read(user.id);
* user.name = 'Jane';
* await userRepo.update(user);
* await userRepo.delete(user.id);
*
* // Querying with conditions
* const users = await userRepo
* .select()
* .where({ name: 'Jane' })
* .orderBy('createdAt', OrderDirection.DSC)
* .limit(10)
* .execute();
* @mermaid
* sequenceDiagram
* participant C as Client Code
* participant R as Repository
* participant A as Adapter
* participant DB as Database
* participant O as Observers
*
* C->>+R: create(model)
* R->>R: createPrefix(model)
* R->>+A: prepare(model)
* A-->>-R: prepared data
* R->>+A: create(table, id, record)
* A->>+DB: Insert Operation
* DB-->>-A: Result
* A-->>-R: record
* R->>+A: revert(record)
* A-->>-R: model instance
* R->>R: createSuffix(model)
* R->>+O: updateObservers(table, CREATE, id)
* O-->>-R: Notification complete
* R-->>-C: created model
*/
export class Repository<
M extends Model<boolean>,
A extends Adapter<any, any, any, any>,
>
extends Rep<M, ContextOf<A>>
implements
PersistenceObservable<ContextOf<A>>,
PersistenceObserver<ContextOf<A>>,
Queriable<M>,
IRepository<M, ContextOf<A>>
{
private static _cache: Record<
string,
Constructor<Repo<Model>> | Repo<Model>
> = {};
protected observers: Observer[] = [];
protected observerHandler?: ObserverHandler;
private readonly _adapter!: A;
private _tableName!: string;
protected _overrides: Partial<FlagsOf<ContextOf<A>>> = {
ignoreHandlers: false,
ignoreValidation: false,
allowGenerationOverride: false,
allowRawStatements: true,
forcePrepareSimpleQueries: false,
forcePrepareComplexQueries: false,
ignoreDevSafeGuards: false,
mergeForUpdate: true,
applyUpdateValidation: true,
} as any;
private logger!: Logger;
/**
* @description Logger instance for this repository.
* @summary Provides access to the logger for this repository instance.
* @return {Logger} The logger instance.
*/
get log(): Logger {
if (!this.logger)
this.logger = (
this.adapter["log" as keyof typeof this.adapter] as Logger
).for(this.toString());
return this.logger;
}
/**
* @description Adapter for database operations.
* @summary Provides access to the adapter instance for this repository.
* @template A - The adapter type.
* @return {A} The adapter instance.
* @throws {InternalError} If no adapter is found.
*/
protected get adapter(): A {
if (!this._adapter)
throw new InternalError(
`No adapter found for this repository. did you use the @uses decorator or pass it in the constructor?`
);
return this._adapter;
}
/**
* @description Table name for this repository's model.
* @summary Gets the database table name associated with this repository's model.
* @return {string} The table name.
*/
protected get tableName(): string {
if (!this._tableName) this._tableName = Model.tableName(this.class);
return this._tableName;
}
/**
* @description Primary key properties for this repository's model.
* @summary Gets the sequence options containing primary key information.
* @return {SequenceOptions} The primary key properties.
* @deprecated for Model.sequenceFor(class)
*/
protected override get pkProps(): SequenceOptions {
return super.pkProps;
}
get filters() {
return getFilters(this);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
constructor(adapter?: A, clazz?: Constructor<M>, ...args: any[]) {
super(clazz);
if (adapter) this._adapter = adapter;
if (clazz) {
Repository.register(clazz, this, this.adapter.alias);
if (adapter) {
const flavour = Metadata.get(clazz, DecorationKeys.FLAVOUR);
if (flavour === DefaultFlavour) {
uses(adapter.flavour)(clazz);
}
}
}
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this;
[this.createAll, this.readAll, this.deleteAll].forEach((m) => {
const name = m.name;
wrapMethodWithContext(
self,
(self as any)[name + "Prefix"],
m,
(self as any)[name + "Suffix"]
);
});
wrapMethodWithContextForUpdate(
self,
(self as any)[this.updateAll.name + "Prefix"],
this.updateAll,
(self as any)[this.updateAll.name + "Suffix"]
);
}
protected logCtx<
ARGS extends any[] = any[],
METHOD extends MethodOrOperation = MethodOrOperation,
>(
args: MaybeContextualArg<ContextOf<A>, ARGS>,
operation: METHOD
): ContextualizedArgs<
ContextOf<A>,
ARGS,
METHOD extends string ? true : false
>;
protected logCtx<
ARGS extends any[] = any[],
METHOD extends MethodOrOperation = MethodOrOperation,
>(
args: MaybeContextualArg<ContextOf<A>, ARGS>,
operation: METHOD,
allowCreate: false
): ContextualizedArgs<
ContextOf<A>,
ARGS,
METHOD extends string ? true : false
>;
protected logCtx<
ARGS extends any[] = any[],
METHOD extends MethodOrOperation = MethodOrOperation,
>(
args: MaybeContextualArg<ContextOf<A>, ARGS>,
operation: METHOD,
allowCreate: true
): Promise<
ContextualizedArgs<ContextOf<A>, ARGS, METHOD extends string ? true : false>
>;
protected logCtx<
ARGS extends any[] = any[],
METHOD extends MethodOrOperation = MethodOrOperation,
>(
args: MaybeContextualArg<ContextOf<A>, ARGS>,
operation: METHOD,
allowCreate: true,
overrides?: Partial<FlagsOf<ContextOf<A>>>
): Promise<
ContextualizedArgs<ContextOf<A>, ARGS, METHOD extends string ? true : false>
>;
protected logCtx<
ARGS extends any[] = any[],
METHOD extends MethodOrOperation = MethodOrOperation,
>(
args: MaybeContextualArg<ContextOf<A>, ARGS>,
operation: METHOD,
allowCreate: false,
overrides?: Partial<FlagsOf<ContextOf<A>>>
): ContextualizedArgs<
ContextOf<A>,
ARGS,
METHOD extends string ? true : false
>;
protected logCtx<
ARGS extends any[] = any[],
METHOD extends MethodOrOperation = MethodOrOperation,
>(
args: MaybeContextualArg<ContextOf<A>, ARGS>,
operation: METHOD,
allowCreate: boolean = false,
overrides?: Partial<FlagsOf<ContextOf<A>>>
):
| Promise<
ContextualizedArgs<
ContextOf<A>,
ARGS,
METHOD extends string ? true : false
>
>
| ContextualizedArgs<
ContextOf<A>,
ARGS,
METHOD extends string ? true : false
> {
const ctx = this.adapter["logCtx"](
[this.class as any, ...args] as any,
operation,
allowCreate as any,
Object.assign({}, overrides || {}, this._overrides || {})
) as
| ContextualizedArgs<
ContextOf<A>,
ARGS,
METHOD extends string ? true : false
>
| Promise<
ContextualizedArgs<
ContextOf<A>,
ARGS,
METHOD extends string ? true : false
>
>;
function squashArgs(ctx: ContextualizedArgs<ContextOf<A>>) {
ctx.ctxArgs.shift(); // removes added model to args
return ctx as any;
}
if (!(ctx instanceof Promise)) return squashArgs(ctx);
return ctx.then(squashArgs);
}
/**
* @description Creates a proxy with overridden repository flags.
* @summary Returns a proxy of this repository with the specified flags overridden.
* @param {Partial<F>} flags - The flags to override.
* @return {Repository} A proxy of this repository with overridden flags.
*/
override(flags: Partial<FlagsOf<ContextOf<A>>>): this {
return new Proxy(this, {
get: (target: typeof this, p: string | symbol, receiver: any) => {
const result = Reflect.get(target, p, receiver);
if (p !== "_overrides") return result;
return Object.assign({}, result, flags);
},
});
}
/**
* @description Creates a new instance of the Repository class with a specific adapter and arguments.
*
* @template A - The type of the adapter.
* @template Q - The type of the query builder.
* @template F - The type of the filter.
* @template C - The type of the context.
*
* @param {Partial<InferredAdapterConfig<A>>} conf - adapter configurations to override.
* @param [args] - Additional arguments to be passed to the new instance.
*
* @return A new instance of the Repository class with the specified adapter and arguments.
*/
for(conf: Partial<InferredAdapterConfig<A>>, ...args: any[]): this {
return new Proxy(this, {
get: (target: any, p: string | symbol, receiver: any) => {
if (p === "adapter") {
return this.adapter.for(conf, ...args);
}
return Reflect.get(target, p, receiver);
},
});
}
/**
* @description Creates a new observer handler.
* @summary Factory method for creating an observer handler instance.
* @return {ObserverHandler} A new observer handler instance.
*/
protected ObserverHandler(): ObserverHandler {
return new ObserverHandler();
}
/**
* @description Prepares a model for creation.
* @summary Validates the model and prepares it for creation in the database.
* @template M - The model type.
* @param {M} model - The model to create.
* @param {...any[]} args - Additional arguments.
* @return The prepared model and context arguments.
* @throws {ValidationError} If the model fails validation.
*/
protected override async createPrefix(
model: M,
...args: MaybeContextualArg<ContextOf<A>>
): Promise<[M, ...any[], ContextOf<A>]> {
const { ctx, ctxArgs, log } = (
await this.logCtx(args, OperationKeys.CREATE, true)
).for(this.createPrefix);
const ignoreHandlers = ctx.get("ignoreHandlers");
const ignoreValidate = ctx.get("ignoreValidation");
log.silly(
`handlerSetting: ${ignoreHandlers}, validationSetting: ${ignoreValidate}`
);
model = new this.class(model);
if (!ignoreHandlers)
await enforceDBDecorators<M, Repository<M, A>, any>(
this,
ctx,
model,
OperationKeys.CREATE,
OperationKeys.ON
);
if (!ignoreValidate) {
const propsToIgnore = ctx.get("ignoredValidationProperties") || [];
log.silly(`ignored validation properties: ${propsToIgnore}`);
const errors = await Promise.resolve(model.hasErrors(...propsToIgnore));
if (errors) throw new ValidationError(errors.toString());
}
return [model, ...ctxArgs];
}
/**
* @description Creates a model in the database.
* @summary Persists a model instance to the database.
* @param {M} model - The model to create.
* @param {...any[]} args - Additional arguments.
* @return {Promise<M>} The created model with updated properties.
*/
async create(
model: M,
...args: MaybeContextualArg<ContextOf<A>>
): Promise<M> {
const { ctx, log, ctxArgs } = this.logCtx(args, this.create);
log.debug(
`Creating new ${this.class.name} in table ${Model.tableName(this.class)}`
);
// eslint-disable-next-line prefer-const
let { record, id, transient } = this.adapter.prepare(model, ctx);
record = await this.adapter.create(this.class, id, record, ...ctxArgs);
return this.adapter.revert<M>(record, this.class, id, transient, ctx);
}
/**
* @description Creates multiple models in the database.
* @summary Persists multiple model instances to the database in a batch operation.
* @param {M[]} models - The models to create.
* @param {...any[]} args - Additional arguments.
* @return {Promise<M[]>} The created models with updated properties.
*/
override async createAll(
models: M[],
...args: MaybeContextualArg<ContextOf<A>>
): Promise<M[]> {
if (!models.length) return models;
const { ctx, log, ctxArgs } = this.logCtx(args, this.createAll);
log.debug(
`Creating ${models.length} new ${this.class.name} in table ${Model.tableName(this.class)}`
);
const prepared = models.map((m) => this.adapter.prepare(m, ctx));
const ids = prepared.map((p) => p.id);
let records = prepared.map((p) => p.record);
records = await this.adapter.createAll(
this.class,
ids as PrimaryKeyType[],
records,
...ctxArgs
);
return records.map((r, i) =>
this.adapter.revert(
r,
this.class,
ids[i],
ctx.get("rebuildWithTransient") ? prepared[i].transient : undefined,
ctx
)
);
}
/**
* @description Prepares multiple models for creation.
* @summary Validates multiple models and prepares them for creation in the database.
* @param {M[]} models - The models to create.
* @param {...any[]} args - Additional arguments.
* @return The prepared models and context arguments.
* @throws {ValidationError} If any model fails validation.
*/
protected override async createAllPrefix(
models: M[],
...args: MaybeContextualArg<ContextOf<A>>
): Promise<[M[], ...any[], ContextOf<A>]> {
const { ctx, ctxArgs, log } = (
await this.logCtx(args, BulkCrudOperationKeys.CREATE_ALL, true)
).for(this.createAllPrefix);
const ignoreHandlers = ctx.get("ignoreHandlers");
const ignoreValidate = ctx.get("ignoreValidation");
log.silly(
`handlerSetting: ${ignoreHandlers}, validationSetting: ${ignoreValidate}`
);
if (!models.length) return [models, ...ctxArgs];
const opts = Model.sequenceFor(models[0]);
let ids: (string | number | bigint | undefined)[] = [];
if (Model.generatedBySequence(this.class)) {
if (!opts.name) opts.name = Model.sequenceName(models[0], "pk");
ids = await (
await this.adapter.Sequence(opts)
).range(models.length, ...ctxArgs);
} else if (!Model.generated(this.class, this.pk)) {
ids = models.map((m, i) => {
if (typeof m[this.pk] === "undefined")
throw new InternalError(
`Primary key is not defined for model in position ${i}`
);
return m[this.pk] as string;
});
} else {
// do nothing. The pk is tagged as generated, so it'll be handled by some other decorator
}
models = await Promise.all(
models.map(async (m, i) => {
m = new this.class(m);
if (opts.type) {
m[this.pk] = (
opts.type !== "String"
? ids[i]
: opts.generated
? ids[i]
: `${m[this.pk]}`.toString()
) as M[keyof M];
}
if (!ignoreHandlers)
await enforceDBDecorators<M, Repository<M, A>, any>(
this,
ctx,
m,
OperationKeys.CREATE,
OperationKeys.ON
);
return m;
})
);
if (!ignoreValidate) {
const propsToIgnore = ctx.get("ignoredValidationProperties") || [];
log.silly(`ignored validation properties: ${propsToIgnore}`);
const errors = await Promise.all(
models.map((m) => Promise.resolve(m.hasErrors(...propsToIgnore)))
);
const errorMessages = reduceErrorsToPrint(errors);
if (errorMessages) throw new ValidationError(errorMessages);
}
return [models, ...ctxArgs];
}
/**
* @description Prepares for reading a model by ID.
* @summary Prepares the context and enforces decorators before reading a model.
* @param {string} key - The primary key of the model to read.
* @param {...any[]} args - Additional arguments.
* @return The key and context arguments.
*/
protected override async readPrefix(
key: PrimaryKeyType,
...args: MaybeContextualArg<ContextOf<A>>
): Promise<[PrimaryKeyType, ...any[], ContextOf<A>]> {
const { ctx, ctxArgs, log } = (
await this.logCtx(args, OperationKeys.READ, true)
).for(this.readPrefix);
const ignoreHandlers = ctx.get("ignoreHandlers");
log.silly(`handlerSetting: ${ignoreHandlers}`);
const model: M = new this.class();
model[this.pk] = key as M[keyof M];
if (!ignoreHandlers)
await enforceDBDecorators<M, Repository<M, A>, any>(
this,
ctx,
model,
OperationKeys.READ,
OperationKeys.ON
);
return [key, ...ctxArgs];
}
/**
* @description Reads a model from the database by ID.
* @summary Retrieves a model instance from the database using its primary key.
* @param {PrimaryKeyType} id - The primary key of the model to read.
* @param {...any[]} args - Additional arguments.
* @return {Promise<M>} The retrieved model instance.
*/
async read(
id: PrimaryKeyType,
...args: MaybeContextualArg<ContextOf<A>>
): Promise<M> {
const { ctx, log, ctxArgs } = this.logCtx(args, this.read);
log.debug(
`reading ${this.class.name} from table ${Model.tableName(this.class)} with pk ${this.pk as string}`
);
const m = await this.adapter.read(this.class, id, ...ctxArgs);
return this.adapter.revert<M>(m, this.class, id, undefined, ctx);
}
/**
* @description Prepares for reading multiple models by IDs.
* @summary Prepares the context and enforces decorators before reading multiple models.
* @param {string[]|number[]} keys - The primary keys of the models to read.
* @param {...any[]} args - Additional arguments.
* @return The keys and context arguments.
*/
protected override async readAllPrefix(
keys: PrimaryKeyType[],
...args: MaybeContextualArg<ContextOf<A>>
): Promise<[PrimaryKeyType[], ...any[], ContextOf<A>]> {
const { ctx, ctxArgs, log } = (
await this.logCtx(args, BulkCrudOperationKeys.READ_ALL, true)
).for(this.readAllPrefix);
const ignoreHandlers = ctx.get("ignoreHandlers");
log.silly(`handlerSetting: ${ignoreHandlers}`);
if (!ignoreHandlers)
await Promise.all(
keys.map(async (k) => {
const m = new this.class();
m[this.pk] = k as M[keyof M];
return enforceDBDecorators<M, Repository<M, A>, any>(
this,
ctx,
m,
OperationKeys.READ,
OperationKeys.ON
);
})
);
return [keys, ...ctxArgs];
}
/**
* @description Reads multiple models from the database by IDs.
* @summary Retrieves multiple model instances from the database using their primary keys.
* @param {string[]|number[]} keys - The primary keys of the models to read.
* @param {...any[]} args - Additional arguments.
* @return {Promise<M[]>} The retrieved model instances.
*/
override async readAll(
keys: PrimaryKeyType[],
...args: MaybeContextualArg<ContextOf<A>>
): Promise<M[]> {
const { ctx, log, ctxArgs } = this.logCtx(args, this.readAll);
log.debug(
`reading ${keys.length} ${this.class.name} in table ${Model.tableName(this.class)}`
);
const records = await this.adapter.readAll(this.class, keys, ...ctxArgs);
return records.map((r, i) =>
this.adapter.revert(r, this.class, keys[i], undefined, ctx)
);
}
/**
* @description Updates a model in the database.
* @summary Persists changes to an existing model instance in the database.
* @param {M} model - The model to update.
* @param {...any[]} args - Additional arguments.
* @return {Promise<M>} The updated model with refreshed properties.
*/
async update(
model: M,
...args: MaybeContextualArg<ContextOf<A>>
): Promise<M> {
const { ctxArgs, log, ctx } = this.logCtx(args, this.update);
// eslint-disable-next-line prefer-const
let { record, id, transient } = this.adapter.prepare(model, ctx);
log.debug(
`updating ${this.class.name} in table ${Model.tableName(this.class)} with id ${id}`
);
record = await this.adapter.update(this.class, id, record, ...ctxArgs);
return this.adapter.revert<M>(record, this.class, id, transient, ctx);
}
/**
* @description Prepares a model for update.
* @summary Validates the model and prepares it for update in the database.
* @param {M} model - The model to update.
* @param {...any[]} args - Additional arguments.
* @return The prepared model and context arguments.
* @throws {InternalError} If the model has no primary key value.
* @throws {ValidationError} If the model fails validation.
*/
protected override async updatePrefix(
model: M,
...args: MaybeContextualArg<ContextOf<A>>
): Promise<[M, ...args: any[], ContextOf<A>, M | undefined]> {
const { ctx, ctxArgs, log } = (
await this.logCtx(args, OperationKeys.UPDATE, true)
).for(this.updatePrefix);
const ignoreHandlers = ctx.get("ignoreHandlers");
const ignoreValidate = ctx.get("ignoreValidation");
log.silly(
`handlerSetting: ${ignoreHandlers}, validationSetting: ${ignoreValidate}`
);
const pk = model[this.pk] as string;
if (!pk)
throw new InternalError(
`No value for the Id is defined under the property ${this.pk as string}`
);
let oldModel: M | undefined;
if (ctx.get("applyUpdateValidation")) {
oldModel = await this.read(pk as string, ctx);
if (ctx.get("mergeForUpdate"))
model = Model.merge(oldModel, model, this.class);
}
if (!ignoreHandlers)
await enforceDBDecorators(
this,
ctx as any,
model,
OperationKeys.UPDATE,
OperationKeys.ON,
oldModel
);
if (!ignoreValidate) {
const propsToIgnore = ctx.get("ignoredValidationProperties") || [];
log.silly(`ignored validation properties: ${propsToIgnore}`);
const errors = await Promise.resolve(
model.hasErrors(oldModel, ...propsToIgnore)
);
if (errors) throw new ValidationError(errors.toString());
}
return [model, ...ctxArgs, oldModel];
}
/**
* @description Updates multiple models in the database.
* @summary Persists changes to multiple existing model instances in the database in a batch operation.
* @param {M[]} models - The models to update.
* @param {...any[]} args - Additional arguments.
* @return {Promise<M[]>} The updated models with refreshed properties.
*/
override async updateAll(
models: M[],
...args: MaybeContextualArg<ContextOf<A>>
): Promise<M[]> {
const { ctx, log, ctxArgs } = this.logCtx(args, this.updateAll);
log.verbose(
`Updating ${models.length} new ${this.class.name} in table ${Model.tableName(this.class)}`
);
const records = models.map((m) => this.adapter.prepare(m, ctx));
const updated = await this.adapter.updateAll(
this.class,
records.map((r) => r.id),
records.map((r) => r.record),
...ctxArgs
);
return updated.map((u, i) =>
this.adapter.revert(
u,
this.class,
records[i].id,
ctx.get("rebuildWithTransient") ? records[i].transient : undefined,
ctx
)
);
}
/**
* @description Prepares multiple models for update.
* @summary Validates multiple models and prepares them for update in the database.
* @param {M[]} models - The models to update.
* @param {...any[]} args - Additional arguments.
* @return {Promise<any[]>} The prepared models and context arguments.
* @throws {InternalError} If any model has no primary key value.
* @throws {ValidationError} If any model fails validation.
*/
protected override async updateAllPrefix(
models: M[],
...args: MaybeContextualArg<ContextOf<A>>
): Promise<[M[], ...args: any[], ContextOf<A>, M[] | undefined]> {
const { ctx, ctxArgs, log } = (
await this.logCtx(args, BulkCrudOperationKeys.UPDATE_ALL, true)
).for(this.updateAllPrefix);
const ignoreHandlers = ctx.get("ignoreHandlers");
const ignoreValidate = ctx.get("ignoreValidation");
log.silly(
`handlerSetting: ${ignoreHandlers}, ignoredValidation: ${ignoreValidate}`
);
const ids = models.map((m) => {
const id = m[this.pk] as string;
if (!id) throw new InternalError("missing id on update operation");
return id;
});
let oldModels: M[] | undefined;
if (ctx.get("applyUpdateValidation")) {
oldModels = await this.readAll(ids as string[], ctx);
if (ctx.get("mergeForUpdate"))
models = models.map((m, i) =>
Model.merge((oldModels as any)[i], m, this.class)
);
}
if (!ignoreHandlers)
await Promise.all(
models.map((m, i) =>
enforceDBDecorators<M, Repository<M, A>, any>(
this,
ctx,
m,
OperationKeys.UPDATE,
OperationKeys.ON,
oldModels ? oldModels[i] : undefined
)
)
);
if (!ignoreValidate) {
const ignoredProps = ctx.get("ignoredValidationProperties") || [];
log.silly(`ignored validation properties: ${ignoredProps}`);
let modelsValidation: any;
if (!ctx.get("applyUpdateValidation")) {
modelsValidation = await Promise.resolve(
models.map((m) => m.hasErrors(...ignoredProps))
);
} else {
modelsValidation = await Promise.all(
models.map((m, i) =>
Promise.resolve(
m.hasErrors((oldModels as any)[i] as any, ...ignoredProps)
)
)
);
}
const errorMessages = reduceErrorsToPrint(modelsValidation);
if (errorMessages) throw new ValidationError(errorMessages);
}
return [models, ...ctxArgs, oldModels];
}
/**
* @description Prepares for deleting a model by ID.
* @summary Prepares the context and enforces decorators before deleting a model.
* @param {any} key - The primary key of the model to delete.
* @param {...any[]} args - Additional arguments.
* @return The key and context arguments.
*/
protected override async deletePrefix(
key: PrimaryKeyType,
...args: MaybeContextualArg<ContextOf<A>>
): Promise<[PrimaryKeyType, ...any[], ContextOf<A>]> {
const { ctx, ctxArgs, log } = (
await this.logCtx(args, OperationKeys.DELETE, true)
).for(this.deletePrefix);
const ignoreHandlers = ctx.get("ignoreHandlers");
log.silly(`handlerSetting: ${ignoreHandlers}`);
if (!ignoreHandlers) {
const model = await this.read(key, ...ctxArgs);
await enforceDBDecorators<M, Repository<M, A>, any>(
this,
ctx,
model,
OperationKeys.DELETE,
OperationKeys.ON
);
}
return [key, ...ctxArgs];
}
/**
* @description Deletes a model from the database by ID.
* @summary Removes a model instance from the database using its primary key.
* @param {string|number|bigint} id - The primary key of the model to delete.
* @param {...any[]} args - Additional arguments.
* @return {Promise<M>} The deleted model instance.
*/
async delete(
id: PrimaryKeyType,
...args: MaybeContextualArg<ContextOf<A>>
): Promise<M> {
const { ctx, log, ctxArgs } = this.logCtx(args, this.delete);
log.debug(
`deleting new ${this.class.name} in table ${Model.tableName(this.class)} with pk ${id}`
);
const m = await this.adapter.delete(this.class, id, ...ctxArgs);
return this.adapter.revert<M>(m, this.class, id, undefined, ctx);
}
/**
* @description Prepares for deleting multiple models by IDs.
* @summary Prepares the context and enforces decorators before deleting multiple models.
* @param {string[]|number[]} keys - The primary keys of the models to delete.
* @param {...any[]} args - Additional arguments.
* @return The keys and context arguments.
*/
protected override async deleteAllPrefix(
keys: PrimaryKeyType[],
...args: MaybeContextualArg<ContextOf<A>>
): Promise<[PrimaryKeyType[], ...any[], ContextOf<A>]> {
const { ctx, ctxArgs, log } = (
await this.logCtx(args, BulkCrudOperationKeys.DELETE_ALL, true)
).for(this.deleteAllPrefix);
const ignoreHandlers = ctx.get("ignoreHandlers");
log.silly(`handlerSetting: ${ignoreHandlers}`);
if (!ignoreHandlers) {
const models = await this.readAll(keys, ...ctxArgs);
await Promise.all(
models.map(async (m) => {
return enforceDBDecorators<M, Repository<M, A>, any>(
this,
ctx,
m,
OperationKeys.DELETE,
OperationKeys.ON
);
})
);
}
return [keys, ...ctxArgs];
}
/**
* @description Deletes multiple models from the database by IDs.
* @summary Removes multiple model instances from the database using their primary keys.
* @param {string[]|number[]} keys - The primary keys of the models to delete.
* @param {...any[]} args - Additional arguments.
* @return {Promise<M[]>} The deleted model instances.
*/
override async deleteAll(
keys: PrimaryKeyType[],
...args: MaybeContextualArg<ContextOf<A>>
): Promise<M[]> {
const { ctx, log, ctxArgs } = this.logCtx(args, this.create);
log.debug(
`deleting ${keys.length} ${this.class.name} in table ${Model.tableName(this.class)}`
);
const results = await this.adapter.deleteAll(this.class, keys, ...ctxArgs);
return results.map((r, i) =>
this.adapter.revert(r, this.class, keys[i], undefined, ctx)
);
}
/**
* @description Creates a select query without specifying fields.
* @summary Starts building a query that will return all fields of the model.
* @template S - The array type of select selectors.
* @return A query builder for the model.
*/
select<
// eslint-disable-next-line @typescript-eslint/no-unused-vars
S extends readonly SelectSelector<M>[],
>(): WhereOption<M, M[]>;
/**
* @description Creates a select query with specific fields.
* @summary Starts building a query that will return only the specified fields of the model.
* @template S - The array type of select selectors.
* @param selector - The fields to select.
* @return A query builder for the selected fields.
*/
select<S extends readonly SelectSelector<M>[]>(
selector: readonly [...S]
): WhereOption<M, Pick<M, S[number]>[]>;
/**
* @description Implementation of the select method.
* @summary Creates a query builder for the model with optional field selection.
* @template S - The array type of select selectors.
* @param [selector] - Optional fields to select.
* @return A query builder.
*/
select<S extends readonly SelectSelector<M>[]>(
selector?: readonly [...S]
): WhereOption<M, M[]> | WhereOption<M, Pick<M, S[number]>[]> {
return this.adapter
.Statement<M>(this._overrides)
.select(selector as readonly [...S])
.from(this.class);
}
/**
* @description Creates a count query
* @summary Starts building a query that will count records
* @template S - The select selector type
* @param {S} [selector] - Optional field to count (counts all if not specified)
* @return A count query builder with distinct() available
*/
count<S extends SelectSelector<M>>(selector?: S): CountWhereOption<M> {
return this.adapter
.Statement<M>(this._overrides)
.count(selector)
.from(this.class) as unknown as CountWhereOption<M>;
}
/**
* @description Creates a min query
* @summary Starts building a query that will find the minimum value
* @template S - The select selector type
* @param {S} selector - Field to find minimum value of
* @return A min query builder
*/
min<S extends SelectSelector<M>>(selector: S) {
return this.adapter
.Statement<M>(this._overrides)
.min(selector)
.from(this.class);
}
/**
* @description Creates a max query
* @summary Starts building a query that will find the maximum value
* @template S - The select selector type
* @param {S} selector - Field to find maximum value of
* @return A max query builder
*/
max<S extends SelectSelector<M>>(selector: S) {
return this.adapter
.Statement<M>(this._overrides)
.max(selector)
.from(this.class);
}
/**
* @description Creates a sum query
* @summary Starts building a query that will sum values
* @template S - The select selector type
* @param {S} selector - Field to sum
* @return A sum query builder
*/
sum<S extends SelectSelector<M>>(selector: S) {
return this.adapter
.Statement<M>(this._overrides)
.sum(selector)
.from(this.class);
}
/**
* @description Creates an average query
* @summary Starts building a query that will compute the average value
* @template S - The select selector type
* @param {S} selector - Field to average
* @return An average query builder
*/
avg<S extends SelectSelector<M>>(selector: S) {
return this.adapter
.Statement<M>(this._overrides)
.avg(selector)
.from(this.class);
}
/**
* @description Creates a distinct query
* @summary Starts building a query that will find distinct values
* @template S - The select selector type
* @param {S} selector - Field to find distinct values of
* @return A distinct query builder
*/
distinct<S extends SelectSelector<M>>(selector: S) {
return this.adapter
.Statement<M>(this._overrides)
.distinct(selector)
.from(this.class);
}
/**
* @description Executes a query with the specified conditions and options.
* @summary Provides a simplified way to query the database with common query parameters.
* @param {Condition<M>} condition - The condition to filter records.
* @param orderBy - The field to order results by.
* @param {OrderDirection} [order=OrderDirection.ASC] - The sort direction.
* @param {number} [limit] - Optional maximum number of results to return.
* @param {number} [skip] - Optional number of results to skip.
* @return {Promise<M[]>} The query results as model instances.
*/
async query(
condition: Condition<M>,
orderBy: keyof M,
order: OrderDirection = OrderDirection.ASC,
limit?: number,
skip?: number,
...args: MaybeContextualArg<ContextOf<A>>
): Promise<M[]> {
const { ctxArgs } = (
await this.logCtx(args, PersistenceKeys.QUERY, true)
).for(this.query);
const sort: OrderBySelector<M> = [orderBy, order as OrderDirection];
const query = this.select().where(condition).orderBy(sort);
if (limit) query.limit(limit);
if (skip) query.offset(skip);
return query.execute(...ctxArgs);
}
@prepared()
async listBy(
key: keyof M,
order: OrderDirection,
...args: MaybeContextualArg<ContextOf<A>>
) {
const { log, ctxArgs } = (
await this.logCtx(args, PreparedStatementKeys.LIST_BY, true)
).for(this.listBy);
log.verbose(
`listing ${Model.tableName(this.class)} by ${key as string} ${order}`
);
return this.select()
.orderBy([key, order])
.execute(...ctxArgs);
}
@prepared()
async paginateBy(
key: keyof M,
order: OrderDirection,
ref: Omit<DirectionLimitOffset, "direction"> = {
offset: 1,
limit: 10,
},
...args: MaybeContextualArg<ContextOf<A>>
): Promise<SerializedPage<M>> {
const requestedPage = ref.offset || 1;
const { offset, bookmark, limit } = ref;
if (!offset && !bookmark)
throw new QueryError(`PaginateBy needs a page or a bookmark`);
const { log, ctx, ctxArgs } = (
await this.logCtx(args, PreparedStatementKeys.PAGE_BY, true)
).for(this.paginateBy);
log.verbose(
`paginating ${Model.tableName(this.class)} with page size ${limit}`
);
let paginator: Paginator<M>;
if (bookmark && ctx.get("paginateByBookmark")) {
paginator = await this.override({
forcePrepareComplexQueries: false,
forcePrepareSimpleQueries: false,
} as any)
.select()
.where(
(() => {
return order === OrderDirection.ASC
? this.attr(Model.pk(this.class)).gt(bookmark)
: this.attr(Model.pk(this.class)).lt(bookmark);
})()
)
.orderBy([key, order])
.paginate(limit as number, ...ctxArgs);
} else if (offset) {
paginator = await this.override({
forcePrepareComplexQueries: false,
forcePrepareSimpleQueries: false,
} as any)
.select()
.orderBy([key, order])
.paginate(limit as number, ...ctxArgs);
} else {
throw new QueryError(`PaginateBy needs a page or a bookmark`);
}
const paged = await paginator.page(requestedPage, bookmark, ...ctxArgs);
const serialization = paginator.serialize(paged) as SerializedPage<M>;
// if (bookmark) serialization.current = requestedPage;
return serialization;
}
@prepared()
async find(
value: string,
order: OrderDirection = OrderDirection.ASC,
...args: MaybeContextualArg<ContextOf<A>>
): Promise<M[]> {
if (typeof value !== "string")
throw new QueryError("Find value must be a string");
const attrs = this.getDefaultQueryAttributes();
const condition = this.buildDefaultStartsWithCondition(value, attrs);
const { log, ctxArgs } = (
await this.logCtx(args, PreparedStatementKeys.FIND, true)
).for(this.find);
log.verbose(
`finding ${Model.tableName(this.class)} by default attributes ${attrs.join(
", "
)}`
);
return this.select()
.where(condition)
.orderBy([attrs[0], order])
.execute(...ctxArgs);
}
@prepared()
async page(
value: string,
direction: OrderDirection = OrderDirection.ASC,
ref: Omit<DirectionLimitOffset, "direction"> = {
offset: 1,
limit: 10,
},
...args: MaybeContextualArg<ContextOf<A>>
): Promise<SerializedPage<M>> {
if (typeof value !== "string")
throw new QueryError("Page value must be a string");
const requestedPage = ref.offset || 1;
const { offset, bookmark, limit } = ref;
if (!offset && !bookmark)
throw new QueryError(`PaginateBy needs a page or a bookmark`);
const attrs = this.getDefaultQueryAttributes();
const condition = this.buildDefaultStartsWithCondition(value, attrs);
const { log, ctx, ctxArgs } = (
await this.logCtx(args, PreparedStatementKeys.PAGE, true)
).for(this.page);
log.verbose(
`paging ${Model.tableName(this.class)} by default attributes ${attrs.join(
", "
)}`
);
const limitValue = limit ?? 10;
const orderKey = attrs[0];
const overrideControls = this.override({
forcePrepareComplexQueries: false,
forcePrepareSimpleQueries: false,
} as any);
let paginator: Paginator<M>;
if (bookmark && ctx.get("paginateByBookmark")) {
const pk = Model.pk(this.class) as keyof M;
const bookmarkCondition =
direction === OrderDirection.ASC
? this.attr(pk).gt(bookmark)
: this.attr(pk).lt(bookmark);
paginator = await overrideControls
.select()
.where(condition.and(bookmarkCondition))
.orderBy([orderKey, direction])
.paginate(limitValue, ...ctxArgs);
} else if (offset) {
paginator = await overrideControls
.select()
.where(condition)
.orderBy([orderKey, direction])
.paginate(limitValue, ...ctxArgs);
} else {
throw new QueryError(`PaginateBy needs a page or a bookmark`);
}
const paged = await paginator.page(requestedPage, bookmark, ...ctxArgs);
const serialization = paginator.serialize(paged) as SerializedPage<M>;
return serialization;
}
@prepared()
async findOneBy(
key: keyof M,
value: any,
...args: MaybeContextualArg<ContextOf<A>>
) {
const { log, ctxArgs } = (
await this.logCtx(args, PreparedStatementKeys.FIND_ONE_BY, true)
).for(this.findOneBy);
log.verbose(
`finding ${Model.tableName(this.class)} with ${key as string} ${value}`
);
const result = await this.select()
.where(this.attr(key).eq(value))
.limit(1)
.execute(...ctxArgs);
if (!result.length) throw new NotFoundError(`No results found`);
return result[0];
}
@prepared()
async findBy(
key: keyof M,
value: any,
...args: MaybeContextualArg<ContextOf<A>>
) {
const { log, ctxArgs } = (
await this.logCtx(args, PreparedStatementKeys.FIND_BY, true)
).for(this.findBy);
log.verbose(
`finding ${Model.tableName(this.class)} with ${key as string} ${value}`
);
return this.select()
.where(this.attr(key).eq(value))
.execute(...ctxArgs);
}
/**
* @description Counts records, optionally filtered by a key value
* @summary Returns the count of records matching the optional key condition
* @param {string} key - The field to count (optional, counts all if not specified)
* @param {...any[]} args - Additional arguments including context
* @return {Promise<number>} The count of matching records
*/
@prepared()
async countOf(
key?: keyof M,
...args: MaybeContextualArg<ContextOf<A>>
): Promise<number> {
const { log, ctxArgs } = (
await this.logCtx(args, PreparedStatementKeys.COUNT_OF, true)
).for(this.countOf);
log.verbose(
`counting ${Model.tableName(this.class)}${key ? ` by ${key as string}` : ""}`
);
return this.count(key as any).execute(...ctxArgs);
}
/**
* @description Finds the maximum value of a field
* @summary Returns the maximum value for the specified field across all records
* @param {string} key - The field to find the maximum value of
* @param {...any[]} args - Additional arguments including context
* @return {Promise<any>} The maximum value
*/
@prepared()
async maxOf<K extends keyof M>(
key: K,
...args: MaybeContextualArg<ContextOf<A>>
): Promise<M[K]> {
const { log, ctxArgs } = (
await this.logCtx(args, PreparedStatementKeys.MAX_OF, true)
).for(this.maxOf);
log.verbose(
`finding max of ${key as string} in ${Model.tableName(this.class)}`
);
return this.max(key as any).execute(...ctxArgs);
}
/**
* @description Finds the minimum value of a field
* @summary Returns the minimum value for the specified field across all records
* @param {string} key - The field to find the minimum value of
* @param {...any[]} args - Additional arguments including context
* @return {Promise<any>} The minimum value
*/
@prepared()
async minOf<K extends keyof M>(
key: K,
...args: MaybeContextualArg<ContextOf<A>>
): Promise<M[K]> {
const { log, ctxArgs } = (
await this.logCtx(args, PreparedStatementKeys.MIN_OF, true)
).for(this.minOf);
log.verbose(
`finding min of ${key as string} in ${Model.tableName(this.class)}`
);
return this.min(key as any).execute(...ctxArgs);
}
/**
* @description Calculates the average value of a field
* @summary Returns the average value for the specified numeric field across all records
* @param {string} key - The field to calculate the average of
* @param {...any[]} args - Additional arguments including context
* @return {Promise<number>} The average value
*/
@prepared()
async avgOf<K extends keyof M>(
key: K,
...args: MaybeContextualArg<ContextOf<A>>
): Promise<number> {
const { log, ctxArgs } = (
await this.logCtx(args, PreparedStatementKeys.AVG_OF, true)
).for(this.avgOf);
log.verbose(
`calculating average of ${key as string} in ${Model.tableName(this.class)}`
);
return this.avg(key as any).execute(...ctxArgs);
}
/**
* @description Calculates the sum of a field
* @summary Returns the sum of values for the specified numeric field across all records
* @param {string} key - The field to sum
* @param {...any[]} args - Additional arguments including context
* @return {Promise<number>} The sum
*/
@prepared()
async sumOf<K extends keyof M>(
key: K,
...args: MaybeContextualArg<ContextOf<A>>
): Promise<number> {
const { log, ctxArgs } = (
await this.logCtx(args, PreparedStatementKeys.SUM_OF, true)
).for(this.sumOf);
log.verbose(
`calculating sum of ${key as string} in ${Model.tableName(this.class)}`
);
return this.sum(key as any).execute(...ctxArgs);
}
/**
* @description Finds distinct values of a field
* @summary Returns an array of unique values for the specified field
* @param {string} key - The field to get distinct values of
* @param {...any[]} args - Additional arguments including context
* @return {Promise<any[]>} An array of distinct values
*/
@prepared()
async distinctOf<K extends keyof M>(
key: K,
...args: MaybeContextualArg<ContextOf<A>>
): Promise<M[K][]> {
const { log, ctxArgs } = (
await this.logCtx(args, PreparedStatementKeys.DISTINCT_OF, true)
).for(this.distinctOf);
log.verbose(
`finding distinct values of ${key as string} in ${Model.tableName(this.class)}`
);
return this.distinct(key as any).execute(...ctxArgs);
}
/**
* @description Groups records by a field
* @summary Returns records grouped by the specified field
* @param {string} key - The field to group by
* @param {...any[]} args - Additional arguments including context
* @return {Promise<Record<string, M[]>>} Records grouped by the field value
*/
@prepared()
async groupOf<K extends keyof M>(
key: K,
...args: MaybeContextualArg<ContextOf<A>>
): Promise<Record<string, M[]>> {
const { log, ctxArgs } = (
await this.logCtx(args, PreparedStatementKeys.GROUP_OF, true)
).for(this.groupOf);
log.verbose(`grouping ${Model.tableName(this.class)} by ${key as string}`);
return this.select()
.groupBy(key as any)
.execute(...ctxArgs) as Promise<Record<string, M[]>>;
}
async statement(name: string, ...args: MaybeContextualArg<ContextOf<A>>) {
if (!Repository.statements(this, name as keyof typeof this))
throw new QueryError(`Invalid prepared statement requested ${name}`);
const { log, ctxArgs } = (
await this.logCtx(args, PersistenceKeys.STATEMENT, true)
).for(this.statement);
log.verbose(`Executing prepared statement ${name}`);
return (this as any)[name](...ctxArgs);
}
private getDefaultQueryAttributes(): (keyof M)[] {
const attrs = Model.defaultQueryAttributes(this.class);
if (!attrs || !attrs.length)
throw new QueryError(
`No default query attributes defined for ${Model.tableName(this.class)}`
);
return attrs as (keyof M)[];
}
private buildDefaultStartsWithCondition(
value: string,
attrs: (keyof M)[]
): Condition<M> {
if (typeof value !== "string")
throw new QueryError("Default query value must be a string");
let condition: Condition<M> | undefined;
for (const attr of attrs) {
const attrCondition = this.attr(attr).startsWith(value);
condition = condition ? condition.or(attrCondition) : attrCondition;
}
if (!condition)
throw new QueryError(
`No default query attributes available for ${Model.tableName(this.class)}`
);
return condition;
}
attr(prop: keyof M) {
return Condition.attr<M>(prop);
}
/**
* @description Registers an observer for this repository.
* @summary Adds an observer that will be notified of changes to models in this repository.
* @param {Observer} observer - The observer to register.
* @param {ObserverFilter} [filter] - Optional filter to limit which events the observer receives.
* @return {void}
* @see {Observable#observe}
*/
@final()
observe(observer: Observer, filter?: ObserverFilter): () => void {
if (!this.observerHandler)
Object.defineProperty(this, "observerHandler", {
value: this.ObserverHandler(),
writable: false,
});
const log = this.log.for(this.observe);
const tableName = this.class.name; // Model.tableName(this.class);
this.adapter.observe(
this,
(
table: Constructor | string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
event: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
id: EventIds,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
...args: [...any[], ContextOf<any>]
) => {
if (typeof table === "string") return table === tableName;
return Metadata.constr(table) === Metadata.constr(this.class);
}
);
log.verbose(
`now observing ${this.adapter} filtering on table === ${tableName}`
);
this.observerHandler!.observe(observer, filter);
log.verbose(`Registered new observer ${observer.toString()}`);
return () => this.unObserve(observer);
}
/**
* @description Unregisters an observer from this repository.
* @summary Removes an observer so it will no longer receive notifications of changes.
* @param {Observer} observer - The observer to unregister.
* @return {void}
* @throws {InternalError} If the observer handler is not initialized.
* @see {Observable#unObserve}
*/
@final()
unObserve(observer: Observer): void {
if (!this.observerHandler)
throw new InternalError(
"ObserverHandler not initialized. Did you register any observables?"
);
this.observerHandler.unObserve(observer);
this.log
.for(this.unObserve)
.verbose(`Observer ${observer.toString()} removed`);
if (!this.observerHandler.count()) {
this.log.verbose(
`No more observers registered for ${this.adapter}, unsubscribing`
);
this.adapter.unObserve(this);
this.log.verbose(`No longer observing adapter ${this.adapter.flavour}`);
}
}
/**
* @description Notifies all observers of an event.
* @summary Updates all registered observers with information about a database event.
* @param {string} table - The table name where the event occurred.
* @param {OperationKeys|BulkCrudOperationKeys|string} event - The type of event that occurred.
* @param {EventIds} id - The ID or IDs of the affected records.
* @param {...any[]} args - Additional arguments.
* @return {Promise<void>} A promise that resolves when all observers have been notified.
* @throws {InternalError} If the observer handler is not initialized.
*/
async updateObservers(
table: Constructor<M> | string,
event: AllOperationKeys,
id: EventIds,
...args: ContextualArgs<ContextOf<A>>
): Promise<void> {
if (!this.observerHandler)
throw new InternalError(
"ObserverHandler not initialized. Did you register any observables?"
);
const { log, ctxArgs } = this.logCtx(args, this.updateObservers);
log.verbose(
`Updating ${this.observerHandler.count()} observers for ${this}`
);
await this.observerHandler.updateObservers(
table,
event,
Array.isArray(id)
? id.map(
(i) =>
(Adapter["_baseSequence"] as any).parseValue(
Model.sequenceFor(this.class).type,
i
) as string
)
: ((Adapter["_baseSequence"] as any).parseValue(
Model.sequenceFor(this.class).type,
id
) as string),
...ctxArgs
);
}
/**
* @description Alias for updateObservers.
* @summary Notifies all observers of an event (alias for updateObservers).
* @param {string} table - The table name where the event occurred.
* @param {OperationKeys|BulkCrudOperationKeys|string} event - The type of event that occurred.
* @param {EventIds} id - The ID or IDs of the affected records.
* @param {...any[]} args - Additional arguments.
* @return {Promise<void>} A promise that resolves when all observers have been notified.
*/
async refresh(
table: Constructor<M> | string,
event: AllOperationKeys,
id: EventIds,
...args: ContextualArgs<ContextOf<A>>
): Promise<void> {
return this.updateObservers(table, event, id, ...args);
}
/**
* @description Creates or retrieves a repository for a model.
* @summary Factory method that returns a repository instance for the specified model.
* @template M - The model type that extends Model.
* @template R - The repository type that extends Repo<M>.
* @param {Constructor<M>} model - The model constructor.
* @param {string} [alias] - Optional default adapter flavour if not specified on the model.
* @param {...any[]} [args] - Additional arguments to pass to the repository constructor.
* @return {R} A repository instance for the model.
* @throws {InternalError} If no adapter is registered for the flavour.
*/
static forModel<M extends Model, R extends Repo<M>>(
model: Constructor<M>,
alias?: string,
...args: any[]
): R {
let repo: R | Constructor<R> | undefined;
const _alias: string | undefined =
alias || Metadata.flavourOf(model) || Adapter.currentFlavour;
try {
repo = this.get(model, _alias) as Constructor<R> | R;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e: any) {
repo = undefined;
}
if (repo instanceof Repository) return repo as R;
const flavour: string | undefined =
alias ||
Metadata.flavourOf(model) ||
(repo && Metadata.get(repo, PersistenceKeys.ADAPTER)) ||
Adapter.currentFlavour;
const adapter: Adapter<any, any, any, any> | undefined = flavour
? Adapter.get(flavour)
: undefined;
if (!adapter)
throw new InternalError(
`No registered persistence adapter found flavour ${flavour}`
);
repo = repo || (adapter.repository() as Constructor<R>);
return new repo(adapter, model, ...args) as R;
}
/**
* @description Retrieves a repository for a model from the cache.
* @summary Gets a repository constructor or instance for the specified model from the internal cache.
* @template M - The model type that extends Model.
* @param {Constructor<M>} model - The model constructor.
* @param {string} [alias] - The adapter alias.
* @return {Constructor<Repo<M>> | Repo<M>} The repository constructor or instance.
* @throws {InternalError} If no repository is registered for the model.
*/
private static get<M extends Model>(
model: Constructor<M>,
alias?: string
): Constructor<Repo<M>> | Repo<M> {
const name: string = Model.tableName(model);
let registryName: string = name;
if (alias) {
registryName = [name, alias].join(DefaultSeparator);
}
if (registryName in this._cache)
return this._cache[registryName] as unknown as
| Constructor<Repo<M>>
| Repo<M>;
if (name in this._cache)
return this._cache[name] as unknown as Constructor<Repo<M>> | Repo<M>;
throw new InternalError(
`Could not find repository registered under ${name}`
);
}
/**
* @description Registers a repository for a model.
* @summary Associates a repository constructor or instance with a model in the internal cache.
* @template M - The model type that extends Model.
* @param {Constructor<M>} model - The model constructor.
* @param {Constructor<Repo<M>> | Repo<M>} repo - The repository constructor or instance.
* @param {string} [alias] the adapter alias/flavour.
* @throws {InternalError} If a repository is already registered for the model.
*/
static register<M extends Model>(
model: Constructor<M>,
repo: Constructor<Repo<M>> | Repo<M>,
alias?: string
) {
let name = Model.tableName(model);
if (alias) {
name = [name, alias].join(DefaultSeparator);
}
if (name in this._cache) {
if (this._cache[name] instanceof Repository)
throw new InternalError(`${name} already has a registered instance`);
}
this._cache[name] = repo as any;
}
static statements<R extends Repository<any, any>, K extends keyof R>(
repo: Constructor<R> | R,
method?: K
): undefined | (K extends keyof R ? boolean : (keyof R)[]) {
const contr: Constructor<R> =
repo instanceof Repository ? (repo.constructor as Constructor<R>) : repo;
const meta = Metadata.get(
contr,
method
? Metadata.key(PersistenceKeys.STATEMENT, method as string)
: PersistenceKeys.STATEMENT
);
return (method ? meta : Object.keys(meta)) || false;
}
static queries<R extends Repository<any, any>, K extends keyof R>(
repo: Constructor<R> | R,
method?: K
):
| undefined
| (K extends keyof R ? QueryOptions : Record<keyof R, QueryOptions>) {
const contr: Constructor<R> =
repo instanceof Repository ? (repo.constructor as Constructor<R>) : repo;
return Metadata.get(
contr,
method
? Metadata.key(PersistenceKeys.QUERY, method as string)
: PersistenceKeys.QUERY
);
}
}
if (Adapter) Adapter["_baseRepository"] = Repository;
Source