@decaf-ts/core
Version:
Core persistence module for the decaf framework
850 lines • 42.9 kB
JavaScript
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Repository = void 0;
const db_decorators_1 = require("@decaf-ts/db-decorators");
const logging_1 = require("@decaf-ts/logging");
const Adapter_1 = require("./../persistence/Adapter.cjs");
const Context_1 = require("./../persistence/Context.cjs");
const constants_1 = require("./../persistence/constants.cjs");
const ObserverHandler_1 = require("./../persistence/ObserverHandler.cjs");
const errors_1 = require("./../query/errors.cjs");
const Condition_1 = require("./../query/Condition.cjs");
const constants_2 = require("./constants.cjs");
const decoration_1 = require("@decaf-ts/decoration");
const decorator_validation_1 = require("@decaf-ts/decorator-validation");
const decorators_1 = require("./../query/decorators.cjs");
const constants_3 = require("./../query/constants.cjs");
/**
* @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
*/
class Repository extends db_decorators_1.Repository {
static { this._cache = {}; }
/**
* @description Logger instance for this repository.
* @summary Provides access to the logger for this repository instance.
* @return {Logger} The logger instance.
*/
get log() {
if (!this.logger)
this.logger = this.adapter["log"].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.
*/
get adapter() {
if (!this._adapter)
throw new db_decorators_1.InternalError(`No adapter found for this repository. did you use the 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.
*/
get tableName() {
if (!this._tableName)
this._tableName = decorator_validation_1.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)
*/
get pkProps() {
return super.pkProps;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
constructor(adapter, clazz, ...args) {
super(clazz);
this.observers = [];
this._overrides = {
allowGenerationOverride: false,
allowRawStatements: true,
forcePrepareSimpleQueries: false,
forcePrepareComplexQueries: false,
ignoreDevSafeGuards: false,
mergeForUpdate: true,
applyUpdateValidation: true,
};
if (adapter)
this._adapter = adapter;
if (clazz) {
Repository.register(clazz, this, this.adapter.alias);
if (adapter) {
const flavour = decoration_1.Metadata.get(clazz, decoration_1.DecorationKeys.FLAVOUR);
if (flavour === decoration_1.DefaultFlavour) {
(0, decoration_1.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;
(0, db_decorators_1.wrapMethodWithContext)(self, self[name + "Prefix"], m, self[name + "Suffix"]);
});
(0, db_decorators_1.wrapMethodWithContextForUpdate)(self, self[this.updateAll.name + "Prefix"], this.updateAll, self[this.updateAll.name + "Suffix"]);
}
logCtx(args, method) {
return Adapter_1.Adapter.logCtx(args, method);
}
/**
* @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) {
return new Proxy(this, {
get: (target, p, receiver) => {
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, ...args) {
return new Proxy(this, {
get: (target, p, receiver) => {
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.
*/
ObserverHandler() {
return new ObserverHandler_1.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.
*/
async createPrefix(model, ...args) {
const contextArgs = await Context_1.Context.args(db_decorators_1.OperationKeys.CREATE, this.class, args, this.adapter, this._overrides || {});
const ignoreHandlers = contextArgs.context.get("ignoreHandlers");
const ignoreValidate = contextArgs.context.get("ignoreValidation");
model = new this.class(model);
if (!ignoreHandlers)
await (0, db_decorators_1.enforceDBDecorators)(this, contextArgs.context, model, db_decorators_1.OperationKeys.CREATE, db_decorators_1.OperationKeys.ON);
if (!ignoreValidate) {
const errors = await Promise.resolve(model.hasErrors(...(contextArgs.context.get("ignoredValidationProperties") || [])));
if (errors)
throw new db_decorators_1.ValidationError(errors.toString());
}
return [model, ...contextArgs.args];
}
/**
* @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, ...args) {
const { ctx, log, ctxArgs } = this.logCtx(args, this.create);
log.debug(`Creating new ${this.class.name} in table ${decorator_validation_1.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(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.
*/
async createAll(models, ...args) {
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 ${decorator_validation_1.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, 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.
*/
async createAllPrefix(models, ...args) {
const contextArgs = await Context_1.Context.args(db_decorators_1.OperationKeys.CREATE, this.class, args, this.adapter, this._overrides || {});
const ignoreHandlers = contextArgs.context.get("ignoreHandlers");
const ignoreValidate = contextArgs.context.get("ignoreValidation");
if (!models.length)
return [models, ...contextArgs.args];
const opts = decorator_validation_1.Model.sequenceFor(models[0]);
let ids = [];
if (decorator_validation_1.Model.generatedBySequence(this.class)) {
if (!opts.name)
opts.name = decorator_validation_1.Model.sequenceName(models[0], "pk");
ids = await (await this.adapter.Sequence(opts)).range(models.length, ...contextArgs.args);
}
else if (!decorator_validation_1.Model.generated(this.class, this.pk)) {
ids = models.map((m, i) => {
if (typeof m[this.pk] === "undefined")
throw new db_decorators_1.InternalError(`Primary key is not defined for model in position ${i}`);
return m[this.pk];
});
}
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());
}
if (!ignoreHandlers)
await (0, db_decorators_1.enforceDBDecorators)(this, contextArgs.context, m, db_decorators_1.OperationKeys.CREATE, db_decorators_1.OperationKeys.ON);
return m;
}));
if (!ignoreValidate) {
const ignoredProps = contextArgs.context.get("ignoredValidationProperties") || [];
const errors = await Promise.all(models.map((m) => Promise.resolve(m.hasErrors(...ignoredProps))));
const errorMessages = (0, db_decorators_1.reduceErrorsToPrint)(errors);
if (errorMessages)
throw new db_decorators_1.ValidationError(errorMessages);
}
return [models, ...contextArgs.args];
}
/**
* @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.
*/
async readPrefix(key, ...args) {
const contextArgs = await Context_1.Context.args(db_decorators_1.OperationKeys.READ, this.class, args, this.adapter, this._overrides || {});
const model = new this.class();
model[this.pk] = key;
await (0, db_decorators_1.enforceDBDecorators)(this, contextArgs.context, model, db_decorators_1.OperationKeys.READ, db_decorators_1.OperationKeys.ON);
return [key, ...contextArgs.args];
}
/**
* @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, ...args) {
const { ctx, log, ctxArgs } = this.logCtx(args, this.read);
log.debug(`reading ${this.class.name} from table ${decorator_validation_1.Model.tableName(this.class)} with pk ${this.pk}`);
const m = await this.adapter.read(this.class, id, ...ctxArgs);
return this.adapter.revert(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.
*/
async readAllPrefix(keys, ...args) {
const contextArgs = await Context_1.Context.args(db_decorators_1.OperationKeys.READ, this.class, args, this.adapter, this._overrides || {});
await Promise.all(keys.map(async (k) => {
const m = new this.class();
m[this.pk] = k;
return (0, db_decorators_1.enforceDBDecorators)(this, contextArgs.context, m, db_decorators_1.OperationKeys.READ, db_decorators_1.OperationKeys.ON);
}));
return [keys, ...contextArgs.args];
}
/**
* @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.
*/
async readAll(keys, ...args) {
const { ctx, log, ctxArgs } = this.logCtx(args, this.readAll);
log.debug(`reading ${keys.length} ${this.class.name} in table ${decorator_validation_1.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, ...args) {
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 ${decorator_validation_1.Model.tableName(this.class)} with id ${id}`);
record = await this.adapter.update(this.class, id, record, ...ctxArgs);
return this.adapter.revert(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.
*/
async updatePrefix(model, ...args) {
const contextArgs = await Context_1.Context.args(db_decorators_1.OperationKeys.UPDATE, this.class, args, this.adapter, this._overrides || {});
const ctx = contextArgs.context;
const ignoreHandlers = ctx.get("ignoreHandlers");
const ignoreValidate = ctx.get("ignoreValidation");
const pk = model[this.pk];
if (!pk)
throw new db_decorators_1.InternalError(`No value for the Id is defined under the property ${this.pk}`);
let oldModel;
if (ctx.get("applyUpdateValidation")) {
oldModel = await this.read(pk, ctx);
if (ctx.get("mergeForUpdate"))
model = decorator_validation_1.Model.merge(oldModel, model, this.class);
}
if (!ignoreHandlers)
await (0, db_decorators_1.enforceDBDecorators)(this, contextArgs.context, model, db_decorators_1.OperationKeys.UPDATE, db_decorators_1.OperationKeys.ON, oldModel);
if (!ignoreValidate) {
const errors = await Promise.resolve(model.hasErrors(oldModel, ...(contextArgs.context.get("ignoredValidationProperties") || [])));
if (errors)
throw new db_decorators_1.ValidationError(errors.toString());
}
return [model, ...contextArgs.args, 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.
*/
async updateAll(models, ...args) {
const { ctx, log, ctxArgs } = this.logCtx(args, this.updateAll);
log.debug(`Updating ${models.length} new ${this.class.name} in table ${decorator_validation_1.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.
*/
async updateAllPrefix(models, ...args) {
const contextArgs = await Context_1.Context.args(db_decorators_1.OperationKeys.UPDATE, this.class, args, this.adapter, this._overrides || {});
const context = contextArgs.context;
const ignoreHandlers = context.get("ignoreHandlers");
const ignoreValidate = context.get("ignoreValidation");
const ids = models.map((m) => {
const id = m[this.pk];
if (!id)
throw new db_decorators_1.InternalError("missing id on update operation");
return id;
});
let oldModels;
if (context.get("applyUpdateValidation")) {
oldModels = await this.readAll(ids, context);
if (context.get("mergeForUpdate"))
models = models.map((m, i) => decorator_validation_1.Model.merge(oldModels[i], m, this.class));
}
if (!ignoreHandlers)
await Promise.all(models.map((m, i) => (0, db_decorators_1.enforceDBDecorators)(this, contextArgs.context, m, db_decorators_1.OperationKeys.UPDATE, db_decorators_1.OperationKeys.ON, oldModels ? oldModels[i] : undefined)));
if (!ignoreValidate) {
const ignoredProps = context.get("ignoredValidationProperties") || [];
let modelsValidation;
if (!context.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[i], ...ignoredProps))));
}
const errorMessages = (0, db_decorators_1.reduceErrorsToPrint)(modelsValidation);
if (errorMessages)
throw new db_decorators_1.ValidationError(errorMessages);
}
return [models, ...contextArgs.args, 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.
*/
async deletePrefix(key, ...args) {
const contextArgs = await Context_1.Context.args(db_decorators_1.OperationKeys.DELETE, this.class, args, this.adapter, this._overrides || {});
const model = await this.read(key, ...contextArgs.args);
await (0, db_decorators_1.enforceDBDecorators)(this, contextArgs.context, model, db_decorators_1.OperationKeys.DELETE, db_decorators_1.OperationKeys.ON);
return [key, ...contextArgs.args];
}
/**
* @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, ...args) {
const { ctx, log, ctxArgs } = this.logCtx(args, this.delete);
log.debug(`deleting new ${this.class.name} in table ${decorator_validation_1.Model.tableName(this.class)} with pk ${id}`);
const m = await this.adapter.delete(this.class, id, ...ctxArgs);
return this.adapter.revert(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.
*/
async deleteAllPrefix(keys, ...args) {
const contextArgs = await Context_1.Context.args(db_decorators_1.OperationKeys.DELETE, this.class, args, this.adapter, this._overrides || {});
const models = await this.readAll(keys, ...contextArgs.args);
await Promise.all(models.map(async (m) => {
return (0, db_decorators_1.enforceDBDecorators)(this, Context_1.Context.childFrom(contextArgs.context), m, db_decorators_1.OperationKeys.DELETE, db_decorators_1.OperationKeys.ON);
}));
return [keys, ...contextArgs.args];
}
/**
* @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.
*/
async deleteAll(keys, ...args) {
const { ctx, log, ctxArgs } = this.logCtx(args, this.create);
log.debug(`deleting ${keys.length} ${this.class.name} in table ${decorator_validation_1.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 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(selector) {
return this.adapter
.Statement(this._overrides)
.select(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, orderBy, order = constants_2.OrderDirection.ASC, limit, skip, ...args) {
const contextArgs = await Context_1.Context.args(constants_1.PersistenceKeys.QUERY, this.class, args, this.adapter, this._overrides || {});
const { ctx } = this.logCtx(contextArgs.args, this.query);
const sort = [orderBy, order];
const query = this.select().where(condition).orderBy(sort);
if (limit)
query.limit(limit);
if (skip)
query.offset(skip);
return query.execute(ctx);
}
async listBy(key, order, ...args) {
const contextArgs = await Context_1.Context.args(constants_3.PreparedStatementKeys.LIST_BY, this.class, args, this.adapter, this._overrides || {});
const { log, ctxArgs } = this.logCtx(contextArgs.args, this.listBy);
log.verbose(`listing ${decorator_validation_1.Model.tableName(this.class)} by ${key} ${order}`);
return this.select()
.orderBy([key, order])
.execute(...ctxArgs);
}
async paginateBy(key, order, ref = {
offset: 1,
limit: 10,
}, ...args) {
// eslint-disable-next-line prefer-const
let { offset, bookmark, limit } = ref;
if (!offset && !bookmark)
throw new errors_1.QueryError(`PaginateBy needs a page or a bookmark`);
const contextArgs = await Context_1.Context.args(constants_3.PreparedStatementKeys.PAGE_BY, this.class, args, this.adapter, this._overrides || {});
const { log, ctxArgs } = this.logCtx(contextArgs.args, this.paginateBy);
log.verbose(`paginating ${decorator_validation_1.Model.tableName(this.class)} with page size ${limit}`);
let paginator;
if (bookmark) {
paginator = await this.override({
forcePrepareComplexQueries: false,
forcePrepareSimpleQueries: false,
})
.select()
.where(this.attr(decorator_validation_1.Model.pk(this.class)).gt(bookmark))
.orderBy([key, order])
.paginate(limit, ...ctxArgs);
offset = 1;
}
else if (offset) {
paginator = await this.override({
forcePrepareComplexQueries: false,
forcePrepareSimpleQueries: false,
})
.select()
.orderBy([key, order])
.paginate(limit, ...ctxArgs);
}
else {
throw new errors_1.QueryError(`PaginateBy needs a page or a bookmark`);
}
const paged = await paginator.page(offset, ...ctxArgs);
return paginator.serialize(paged);
}
async findOneBy(key, value, ...args) {
const contextArgs = await Context_1.Context.args(constants_3.PreparedStatementKeys.FIND_ONE_BY, this.class, args, this.adapter, this._overrides || {});
const { log, ctxArgs } = this.logCtx(contextArgs.args, this.findOneBy);
log.verbose(`finding ${decorator_validation_1.Model.tableName(this.class)} with ${key} ${value}`);
const result = await this.select()
.where(this.attr(key).eq(value))
.limit(1)
.execute(...ctxArgs);
if (!result.length)
throw new db_decorators_1.NotFoundError(`No results found`);
return result[0];
}
async findBy(key, value, ...args) {
const contextArgs = await Context_1.Context.args(constants_3.PreparedStatementKeys.FIND_BY, this.class, args, this.adapter, this._overrides || {});
const { log, ctxArgs } = this.logCtx(contextArgs.args, this.findBy);
log.verbose(`finding ${decorator_validation_1.Model.tableName(this.class)} with ${key} ${value}`);
return this.select()
.where(this.attr(key).eq(value))
.execute(...ctxArgs);
}
async statement(name, ...args) {
if (!Repository.statements(this, name))
throw new errors_1.QueryError(`Invalid prepared statement requested ${name}`);
const contextArgs = await Context_1.Context.args(constants_1.PersistenceKeys.STATEMENT, this.class, args, this.adapter, this._overrides || {});
const { log, ctxArgs } = this.logCtx(contextArgs.args, this.statement);
log.verbose(`Executing prepared statement ${name}`);
return this[name](...ctxArgs);
}
attr(prop) {
return Condition_1.Condition.attr(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}
*/
observe(observer, filter) {
if (!this.observerHandler)
Object.defineProperty(this, "observerHandler", {
value: this.ObserverHandler(),
writable: false,
});
const log = this.log.for(this.observe);
const tableName = decorator_validation_1.Model.tableName(this.class);
this.adapter.observe(this, (table,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
event,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
id,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
...args) => {
if (typeof table === "string")
return table === tableName;
return decoration_1.Metadata.constr(table) === decoration_1.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()}`);
}
/**
* @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}
*/
unObserve(observer) {
if (!this.observerHandler)
throw new db_decorators_1.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, event, id, ...args) {
if (!this.observerHandler)
throw new db_decorators_1.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_1.Adapter["_baseSequence"].parseValue(decorator_validation_1.Model.sequenceFor(this.class).type, i))
: Adapter_1.Adapter["_baseSequence"].parseValue(decorator_validation_1.Model.sequenceFor(this.class).type, id), ...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, event, id, ...args) {
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(model, alias, ...args) {
let repo;
const _alias = alias || decoration_1.Metadata.flavourOf(model) || Adapter_1.Adapter.currentFlavour;
try {
repo = this.get(model, _alias);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
}
catch (e) {
repo = undefined;
}
if (repo instanceof Repository)
return repo;
const flavour = alias ||
decoration_1.Metadata.flavourOf(model) ||
(repo && decoration_1.Metadata.get(repo, constants_1.PersistenceKeys.ADAPTER)) ||
Adapter_1.Adapter.currentFlavour;
const adapter = flavour
? Adapter_1.Adapter.get(flavour)
: undefined;
if (!adapter)
throw new db_decorators_1.InternalError(`No registered persistence adapter found flavour ${flavour}`);
repo = repo || adapter.repository();
return new repo(adapter, model, ...args);
}
/**
* @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.
*/
static get(model, alias) {
const name = decorator_validation_1.Model.tableName(model);
let registryName = name;
if (alias) {
registryName = [name, alias].join(db_decorators_1.DefaultSeparator);
}
if (registryName in this._cache)
return this._cache[registryName];
if (name in this._cache)
return this._cache[name];
throw new db_decorators_1.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(model, repo, alias) {
let name = decorator_validation_1.Model.tableName(model);
if (alias) {
name = [name, alias].join(db_decorators_1.DefaultSeparator);
}
if (name in this._cache) {
if (this._cache[name] instanceof Repository)
throw new db_decorators_1.InternalError(`${name} already has a registered instance`);
}
this._cache[name] = repo;
}
static statements(repo, method) {
const contr = repo instanceof Repository ? repo.constructor : repo;
const meta = decoration_1.Metadata.get(contr, method
? decoration_1.Metadata.key(constants_1.PersistenceKeys.STATEMENT, method)
: constants_1.PersistenceKeys.STATEMENT);
return (method ? meta : Object.keys(meta)) || false;
}
static queries(repo, method) {
const contr = repo instanceof Repository ? repo.constructor : repo;
return decoration_1.Metadata.get(contr, method
? decoration_1.Metadata.key(constants_1.PersistenceKeys.QUERY, method)
: constants_1.PersistenceKeys.QUERY);
}
}
exports.Repository = Repository;
__decorate([
(0, decorators_1.prepared)(),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object, String, void 0]),
__metadata("design:returntype", Promise)
], Repository.prototype, "listBy", null);
__decorate([
(0, decorators_1.prepared)(),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object, String, Object, void 0]),
__metadata("design:returntype", Promise)
], Repository.prototype, "paginateBy", null);
__decorate([
(0, decorators_1.prepared)(),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object, Object, void 0]),
__metadata("design:returntype", Promise)
], Repository.prototype, "findOneBy", null);
__decorate([
(0, decorators_1.prepared)(),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object, Object, void 0]),
__metadata("design:returntype", Promise)
], Repository.prototype, "findBy", null);
__decorate([
(0, logging_1.final)(),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object, Function]),
__metadata("design:returntype", void 0)
], Repository.prototype, "observe", null);
__decorate([
(0, logging_1.final)(),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", void 0)
], Repository.prototype, "unObserve", null);
if (Adapter_1.Adapter)
Adapter_1.Adapter["_baseRepository"] = Repository;
//# sourceMappingURL=Repository.js.map