UNPKG

@decaf-ts/core

Version:

Core persistence module for the decaf framework

850 lines 42.9 kB
"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 @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. */ 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