UNPKG

@decaf-ts/core

Version:

Core persistence module for the decaf framework

1,227 lines (1,215 loc) 508 kB
import { inject, injectable, InjectableRegistryImp, Injectables } from '@decaf-ts/injectable-decorators'; import { BaseError, InternalError, OperationKeys, BulkCrudOperationKeys, Context, DefaultRepositoryFlags, modelToTransient, NotFoundError, DBKeys, Repository as Repository$1, wrapMethodWithContext, enforceDBDecorators, ValidationError, findPrimaryKey, DefaultSeparator, ConflictError, onCreateUpdate, onCreate, onUpdate, onDelete, afterAny, readonly, timestamp, DBOperations } from '@decaf-ts/db-decorators'; import { apply, metadata, Reflection } from '@decaf-ts/reflection'; import { __decorate, __metadata } from 'tslib'; import { Decoration, DefaultFlavour, Model, sf, required, Validation, ValidationKeys, propMetadata, prop, type, list } from '@decaf-ts/decorator-validation'; import { Logging } from '@decaf-ts/logging'; /** * @description Enumeration of possible sort directions. * @summary Defines the available sort directions for ordering query results. * @enum {string} * @readonly * @memberOf module:core */ var OrderDirection; (function (OrderDirection) { /** Ascending order (A to Z, 0 to 9) */ OrderDirection["ASC"] = "asc"; /** Descending order (Z to A, 9 to 0) */ OrderDirection["DSC"] = "desc"; })(OrderDirection || (OrderDirection = {})); /** * @description Enumeration of cascade operation types. * @summary Defines the available cascade behaviors for entity relationships. * @enum {string} * @readonly * @memberOf module:core */ var Cascade; (function (Cascade) { /** Perform cascade operation on related entities */ Cascade["CASCADE"] = "cascade"; /** Do not perform cascade operation on related entities */ Cascade["NONE"] = "none"; })(Cascade || (Cascade = {})); /** * @description Default cascade configuration for entity relationships. * @summary Provides the default cascade behavior where updates cascade but deletes do not. * @type {CascadeMetadata} * @const DefaultCascade * @memberOf module:core */ const DefaultCascade = { update: Cascade.CASCADE, delete: Cascade.NONE, }; /** * @description Persistence-related constant keys * @summary Enum containing string constants used throughout the persistence layer for metadata, relations, and other persistence-related operations * @enum {string} * @readonly * @memberOf module:core */ var PersistenceKeys; (function (PersistenceKeys) { /** @description Key for index metadata */ PersistenceKeys["INDEX"] = "index"; /** @description Key for unique constraint metadata */ PersistenceKeys["UNIQUE"] = "unique"; /** @description Key for adapter metadata */ PersistenceKeys["ADAPTER"] = "adapter"; /** @description Template for injectable adapter names */ PersistenceKeys["INJECTABLE"] = "decaf_{0}_adapter_for_{1}"; /** @description Key for table name metadata */ PersistenceKeys["TABLE"] = "table"; /** @description Key for column name metadata */ PersistenceKeys["COLUMN"] = "column"; /** @description Key for general metadata storage */ PersistenceKeys["METADATA"] = "__metadata"; // Ownership /** @description Key for created-by ownership metadata */ PersistenceKeys["OWNERSHIP"] = "ownership"; /** @description Key for created-by ownership metadata */ PersistenceKeys["CREATED_BY"] = "ownership.created-by"; /** @description Key for updated-by ownership metadata */ PersistenceKeys["UPDATED_BY"] = "ownership.updated-by"; // Relations /** @description Key for relations metadata storage */ PersistenceKeys["RELATIONS"] = "__relations"; /** @description Key for relations metadata storage */ PersistenceKeys["RELATION"] = "relation"; /** @description Key for one-to-one relation metadata */ PersistenceKeys["ONE_TO_ONE"] = "relation.one-to-one"; /** @description Key for one-to-many relation metadata */ PersistenceKeys["ONE_TO_MANY"] = "relation.one-to-many"; /** @description Key for many-to-one relation metadata */ PersistenceKeys["MANY_TO_ONE"] = "relation.many-to-one"; /** @description Key for many-to-one relation metadata */ PersistenceKeys["MANY_TO_MANY"] = "relation.many-to-one"; /** @description Key for populate metadata */ PersistenceKeys["POPULATE"] = "populate"; })(PersistenceKeys || (PersistenceKeys = {})); /** * @description Creates a decorator that makes a method non-configurable * @summary This decorator prevents a method from being overridden by making it non-configurable. * It throws an error if used on anything other than a method. * @return {Function} A decorator function that can be applied to methods * @function final * @category Method Decorators */ function final() { return (target, propertyKey, descriptor) => { if (!descriptor) throw new Error("final decorator can only be used on methods"); if (descriptor?.configurable) { descriptor.configurable = false; } return descriptor; }; } /** * @description Error thrown when a user is not authorized to perform an action * @summary This error is thrown when a user attempts to access a resource or perform an action without proper authentication * @param {string|Error} msg - The error message or Error object * @class AuthorizationError * @category Errors * @example * ```typescript * // Example of throwing an AuthorizationError * if (!user.isAuthenticated()) { * throw new AuthorizationError('User not authenticated'); * } * ``` */ class AuthorizationError extends BaseError { constructor(msg) { super(AuthorizationError.name, msg, 401); } } /** * @description Error thrown when a user is forbidden from accessing a resource * @summary This error is thrown when an authenticated user attempts to access a resource or perform an action they don't have permission for * @param {string|Error} msg - The error message or Error object * @return {void} * @class ForbiddenError * @category Errors * @example * ```typescript * // Example of throwing a ForbiddenError * if (!user.hasPermission('admin')) { * throw new ForbiddenError('User does not have admin permissions'); * } * ``` */ class ForbiddenError extends BaseError { constructor(msg) { super(ForbiddenError.name, msg, 403); } } /** * @description Error thrown when a connection to a service fails * @summary This error is thrown when the application fails to establish a connection to a required service or resource * @param {string|Error} msg - The error message or Error object * @return {void} * @class ConnectionError * @category Errors * @example * ```typescript * // Example of throwing a ConnectionError * try { * await database.connect(); * } catch (error) { * throw new ConnectionError('Failed to connect to database'); * } * ``` */ class ConnectionError extends BaseError { constructor(msg) { super(ConnectionError.name, msg, 503); } } /** * @description Error thrown when an unsupported operation is attempted * @summary This error is thrown when an operation is requested that is not supported by the current * persistence adapter or configuration. It extends the BaseError class and sets a 500 status code. * @param {string|Error} msg - The error message or an Error object to wrap * @class UnsupportedError * @example * ```typescript * // Throwing an UnsupportedError * if (!adapter.supportsTransactions()) { * throw new UnsupportedError('Transactions are not supported by this adapter'); * } * * // Catching an UnsupportedError * try { * await adapter.beginTransaction(); * } catch (error) { * if (error instanceof UnsupportedError) { * console.error('Operation not supported:', error.message); * } * } * ``` */ class UnsupportedError extends BaseError { constructor(msg) { super(UnsupportedError.name, msg, 500); } } /** * @description Dispatches database operation events to observers * @summary The Dispatch class implements the Observable interface and is responsible for intercepting * database operations from an Adapter and notifying observers when changes occur. It uses proxies to * wrap the adapter's CRUD methods and automatically trigger observer updates after operations complete. * @template Y - The native database driver type * @param {void} - No constructor parameters * @class Dispatch * @example * ```typescript * // Creating and using a Dispatch instance * const dispatch = new Dispatch<PostgresDriver>(); * * // Connect it to an adapter * const adapter = new PostgresAdapter(connection); * dispatch.observe(adapter); * * // Now any CRUD operations on the adapter will automatically * // trigger observer notifications * await adapter.create('users', 123, userModel); * // Observers will be notified about the creation * * // When done, you can disconnect * dispatch.unObserve(adapter); * ``` */ class Dispatch { /** * @description Accessor for the logger * @summary Gets or initializes the logger for this dispatch instance * @return {Logger} The logger instance */ get log() { if (!this.logger) this.logger = Logging.for(this).for(this.adapter); return this.logger; } /** * @description Creates a new Dispatch instance * @summary Initializes a new Dispatch instance without any adapter */ constructor() { } /** * @description Initializes the dispatch by proxying adapter methods * @summary Sets up proxies on the adapter's CRUD methods to intercept operations and notify observers. * This method is called automatically when an adapter is observed. * @return {Promise<void>} A promise that resolves when initialization is complete * @mermaid * sequenceDiagram * participant Dispatch * participant Adapter * participant Proxy * * Dispatch->>Dispatch: initialize() * Dispatch->>Dispatch: Check if adapter exists * alt No adapter * Dispatch-->>Dispatch: Throw InternalError * end * * loop For each CRUD method * Dispatch->>Adapter: Check if method exists * alt Method doesn't exist * Dispatch-->>Dispatch: Throw InternalError * end * * Dispatch->>Adapter: Get property descriptor * loop While descriptor not found * Dispatch->>Adapter: Check prototype chain * end * * alt Descriptor not found or not writable * Dispatch->>Dispatch: Log error and continue * else Descriptor found and writable * Dispatch->>Proxy: Create proxy for method * Dispatch->>Adapter: Replace method with proxy * end * end */ async initialize() { if (!this.adapter) throw new InternalError(`No adapter observed for dispatch`); const adapter = this.adapter; [ OperationKeys.CREATE, OperationKeys.UPDATE, OperationKeys.DELETE, BulkCrudOperationKeys.CREATE_ALL, BulkCrudOperationKeys.UPDATE_ALL, BulkCrudOperationKeys.DELETE_ALL, ].forEach((method) => { if (!adapter[method]) throw new InternalError(`Method ${method} not found in ${adapter.alias} adapter to bind Observables Dispatch`); let descriptor = Object.getOwnPropertyDescriptor(adapter, method); let proto = adapter; while (!descriptor && proto !== Object.prototype) { proto = Object.getPrototypeOf(proto); descriptor = Object.getOwnPropertyDescriptor(proto, method); } if (!descriptor || !descriptor.writable) { this.log.error(`Could not find method ${method} to bind Observables Dispatch`); return; } function bulkToSingle(method) { switch (method) { case BulkCrudOperationKeys.CREATE_ALL: return OperationKeys.CREATE; case BulkCrudOperationKeys.UPDATE_ALL: return OperationKeys.UPDATE; case BulkCrudOperationKeys.DELETE_ALL: return OperationKeys.DELETE; default: return method; } } // @ts-expect-error because there are read only properties adapter[method] = new Proxy(adapter[method], { apply: async (target, thisArg, argArray) => { const [tableName, ids] = argArray; const result = await target.apply(thisArg, argArray); this.updateObservers(tableName, bulkToSingle(method), ids) .then(() => { this.log.verbose(`Observer refresh dispatched by ${method} for ${tableName}`); this.log.debug(`pks: ${ids}`); }) .catch((e) => this.log.error(`Failed to dispatch observer refresh for ${method} on ${tableName}: ${e}`)); return result; }, }); }); } /** * @description Closes the dispatch * @summary Performs any necessary cleanup when the dispatch is no longer needed * @return {Promise<void>} A promise that resolves when closing is complete */ async close() { // to nothing in this instance but may be required for closing connections } /** * @description Starts observing an adapter * @summary Connects this dispatch to an adapter to monitor its operations * @param {Adapter<Y, any, any, any>} observer - The adapter to observe * @return {void} */ observe(observer) { if (!(observer instanceof Adapter)) throw new UnsupportedError("Only Adapters can be observed by dispatch"); this.adapter = observer; this.native = observer.native; this.models = Adapter.models(this.adapter.alias); this.initialize().then(() => this.log.verbose(`Dispatch initialized for ${this.adapter.alias} adapter`)); } /** * @description Stops observing an adapter * @summary Disconnects this dispatch from an adapter * @param {Observer} observer - The adapter to stop observing * @return {void} */ unObserve(observer) { if (this.adapter !== observer) throw new UnsupportedError("Only the adapter that was used to observe can be unobserved"); this.adapter = undefined; } /** * @description Updates observers about a database event * @summary Notifies observers about a change in the database * @param {string} table - The name of the table where the change occurred * @param {OperationKeys|BulkCrudOperationKeys|string} event - The type of operation that occurred * @param {EventIds} id - The identifier(s) of the affected record(s) * @return {Promise<void>} A promise that resolves when all observers have been notified */ async updateObservers(table, event, id) { if (!this.adapter) throw new InternalError(`No adapter observed for dispatch`); try { await this.adapter.refresh(table, event, id); } catch (e) { throw new InternalError(`Failed to refresh dispatch: ${e}`); } } } /** * @description Manages a collection of observers for database events * @summary The ObserverHandler class implements the Observable interface and provides a centralized * way to manage multiple observers. It allows registering observers with optional filters to control * which events they receive notifications for, and handles the process of notifying all relevant * observers when database events occur. * @class ObserverHandler * @example * ```typescript * // Create an observer handler * const handler = new ObserverHandler(); * * // Register an observer * const myObserver = { * refresh: async (table, event, id) => { * console.log(`Change in ${table}: ${event} for ID ${id}`); * } * }; * * // Add observer with a filter for only user table events * handler.observe(myObserver, (table, event, id) => table === 'users'); * * // Notify observers about an event * await handler.updateObservers(logger, 'users', 'CREATE', 123); * * // Remove an observer when no longer needed * handler.unObserve(myObserver); * ``` */ class ObserverHandler { constructor() { /** * @description Collection of registered observers * @summary Array of observer objects along with their optional filters */ this.observers = []; } /** * @description Gets the number of registered observers * @summary Returns the count of observers currently registered with this handler * @return {number} The number of registered observers */ count() { return this.observers.length; } /** * @description Registers a new observer * @summary Adds an observer to the collection with an optional filter function * @param {Observer} observer - The observer to register * @param {ObserverFilter} [filter] - Optional filter function to determine which events the observer receives * @return {void} */ observe(observer, filter) { const index = this.observers.map((o) => o.observer).indexOf(observer); if (index !== -1) throw new InternalError("Observer already registered"); this.observers.push({ observer: observer, filter: filter }); } /** * @description Unregisters an observer * @summary Removes an observer from the collection * @param {Observer} observer - The observer to unregister * @return {void} */ unObserve(observer) { const index = this.observers.map((o) => o.observer).indexOf(observer); if (index === -1) throw new InternalError("Failed to find Observer"); this.observers.splice(index, 1); } /** * @description Notifies all relevant observers about a database event * @summary Filters observers based on their filter functions and calls refresh on each matching observer * @param {Logger} log - Logger for recording notification activities * @param {string} table - The name of the table where the event occurred * @param {OperationKeys|BulkCrudOperationKeys|string} event - The type of operation that occurred * @param {EventIds} id - The identifier(s) of the affected record(s) * @param {...any[]} args - Additional arguments to pass to the observers * @return {Promise<void>} A promise that resolves when all observers have been notified * @mermaid * sequenceDiagram * participant Client * participant ObserverHandler * participant Observer * * Client->>ObserverHandler: updateObservers(log, table, event, id, ...args) * * ObserverHandler->>ObserverHandler: Filter observers * * loop For each observer with matching filter * alt Observer has filter * ObserverHandler->>Observer: Apply filter(table, event, id) * alt Filter throws error * ObserverHandler->>Logger: Log error * ObserverHandler-->>ObserverHandler: Skip observer * else Filter returns true * ObserverHandler->>Observer: refresh(table, event, id, ...args) * else Filter returns false * ObserverHandler-->>ObserverHandler: Skip observer * end * else No filter * ObserverHandler->>Observer: refresh(table, event, id, ...args) * end * end * * ObserverHandler->>ObserverHandler: Process results * loop For each result * alt Result is rejected * ObserverHandler->>Logger: Log error * end * end * * ObserverHandler-->>Client: Return */ async updateObservers(log, table, event, id, ...args) { const results = await Promise.allSettled(this.observers .filter((o) => { const { filter } = o; if (!filter) return true; try { return filter(table, event, id); } catch (e) { log.error(`Failed to filter observer ${o.observer.toString()}: ${e}`); return false; } }) .map((o) => o.observer.refresh(table, event, id, ...args))); results.forEach((result, i) => { if (result.status === "rejected") log.error(`Failed to update observable ${this.observers[i].toString()}: ${result.reason}`); }); } } Decoration.setFlavourResolver((obj) => { try { return (Adapter.flavourOf(Model.isModel(obj) ? obj.constructor : obj) || DefaultFlavour); // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { return DefaultFlavour; } }); /** * @description Abstract base class for database adapters * @summary Provides the foundation for all database adapters in the persistence layer. This class * implements several interfaces to provide a consistent API for database operations, observer * pattern support, and error handling. It manages adapter registration, CRUD operations, and * observer notifications. * @template Y - The underlying database driver type * @template Q - The query object type used by the adapter * @template F - The repository flags type * @template C - The context type * @param {Y} _native - The underlying database driver instance * @param {string} flavour - The identifier for this adapter type * @param {string} [_alias] - Optional alternative name for this adapter * @class Adapter * @example * ```typescript * // Implementing a concrete adapter * class PostgresAdapter extends Adapter<pg.Client, pg.Query, PostgresFlags, PostgresContext> { * constructor(client: pg.Client) { * super(client, 'postgres'); * } * * async initialize() { * // Set up the adapter * await this.native.connect(); * } * * async create(tableName, id, model) { * // Implementation for creating records * const columns = Object.keys(model).join(', '); * const values = Object.values(model); * const placeholders = values.map((_, i) => `$${i+1}`).join(', '); * * const query = `INSERT INTO ${tableName} (${columns}) VALUES (${placeholders}) RETURNING *`; * const result = await this.native.query(query, values); * return result.rows[0]; * } * * // Other required method implementations... * } * * // Using the adapter * const pgClient = new pg.Client(connectionString); * const adapter = new PostgresAdapter(pgClient); * await adapter.initialize(); * * // Set as the default adapter * Adapter.setCurrent('postgres'); * * // Perform operations * const user = await adapter.create('users', 1, { name: 'John', email: 'john@example.com' }); * ``` * @mermaid * classDiagram * class Adapter { * +Y native * +string flavour * +string alias * +create(tableName, id, model) * +read(tableName, id) * +update(tableName, id, model) * +delete(tableName, id) * +observe(observer, filter) * +unObserve(observer) * +static current * +static get(flavour) * +static setCurrent(flavour) * } * * class RawExecutor { * +raw(query) * } * * class Observable { * +observe(observer, filter) * +unObserve(observer) * +updateObservers(table, event, id) * } * * class Observer { * +refresh(table, event, id) * } * * class ErrorParser { * +parseError(err) * } * * Adapter --|> RawExecutor * Adapter --|> Observable * Adapter --|> Observer * Adapter --|> ErrorParser */ class Adapter { static { this._cache = {}; } /** * @description Logger accessor * @summary Gets or initializes the logger for this adapter instance * @return {Logger} The logger instance */ get log() { if (!this.logger) this.logger = Logging.for(this); return this.logger; } /** * @description Gets the native database driver * @summary Provides access to the underlying database driver instance * @return {Y} The native database driver */ get native() { return this._native; } /** * @description Gets the adapter's alias or flavor name * @summary Returns the alias if set, otherwise returns the flavor name * @return {string} The adapter's identifier */ get alias() { return this._alias || this.flavour; } /** * @description Gets the repository constructor for this adapter * @summary Returns the constructor for creating repositories that work with this adapter * @template M - The model type * @return {Constructor<Repository<M, Q, Adapter<Y, Q, F, C>, F, C>>} The repository constructor */ repository() { return Repository; } /** * @description Creates a new adapter instance * @summary Initializes the adapter with the native driver and registers it in the adapter cache */ constructor(_native, flavour, _alias) { this._native = _native; this.flavour = flavour; this._alias = _alias; /** * @description The context constructor for this adapter * @summary Reference to the context class constructor used by this adapter */ this.Context = (Context); if (this.flavour in Adapter._cache) throw new InternalError(`${this.alias} persistence adapter ${this._alias ? `(${this.flavour}) ` : ""} already registered`); Adapter._cache[this.alias] = this; this.log.info(`Created ${this.alias} persistence adapter ${this._alias ? `(${this.flavour}) ` : ""} persistence adapter`); if (!Adapter._current) { this.log.verbose(`Defined ${this.alias} persistence adapter as current`); Adapter._current = this; } } /** * @description Creates a new dispatch instance * @summary Factory method that creates a dispatch instance for this adapter * @return {Dispatch<Y>} A new dispatch instance */ Dispatch() { return new Dispatch(); } /** * @description Creates a new observer handler * @summary Factory method that creates an observer handler for this adapter * @return {ObserverHandler} A new observer handler instance */ ObserverHandler() { return new ObserverHandler(); } /** * @description Checks if an attribute name is reserved * @summary Determines if a given attribute name is reserved and cannot be used as a column name * @param {string} attr - The attribute name to check * @return {boolean} True if the attribute is reserved, false otherwise */ isReserved(attr) { return !attr; } /** * @description Creates repository flags for an operation * @summary Generates a set of flags that describe a database operation, combining default flags with overrides * @template F - The Repository Flags type * @template M - The model type * @param {OperationKeys} operation - The type of operation being performed * @param {Constructor<M>} model - The model constructor * @param {Partial<F>} flags - Custom flag overrides * @param {...any[]} args - Additional arguments * @return {Promise<F>} The complete set of flags */ async flags(operation, model, flags, // eslint-disable-next-line @typescript-eslint/no-unused-vars ...args) { return Object.assign({}, DefaultRepositoryFlags, flags, { affectedTables: Repository.table(model), writeOperation: operation !== OperationKeys.READ, timestamp: new Date(), operation: operation, }); } /** * @description Creates a context for a database operation * @summary Generates a context object that describes a database operation, used for tracking and auditing * @template F - The Repository flags type * @template M - The model type * @param {OperationKeys.CREATE|OperationKeys.READ|OperationKeys.UPDATE|OperationKeys.DELETE} operation - The type of operation * @param {Partial<F>} overrides - Custom flag overrides * @param {Constructor<M>} model - The model constructor * @param {...any[]} args - Additional arguments * @return {Promise<C>} A promise that resolves to the context object */ async context(operation, overrides, model, ...args) { const log = this.log.for(this.context); log.debug(`Creating new context for ${operation} operation on ${model.name} model with flag overrides: ${JSON.stringify(overrides)}`); const flags = await this.flags(operation, model, overrides, ...args); log.debug(`Flags: ${JSON.stringify(flags)}`); return new this.Context().accumulate(flags); } /** * @description Prepares a model for persistence * @summary Converts a model instance into a format suitable for database storage, * handling column mapping and separating transient properties * @template M - The model type * @param {M} model - The model instance to prepare * @param pk - The primary key property name * @return The prepared data */ prepare(model, pk) { const log = this.log.for(this.prepare); log.silly(`Preparing model ${model.constructor.name} before persisting`); const split = modelToTransient(model); const result = Object.entries(split.model).reduce((accum, [key, val]) => { if (typeof val === "undefined") return accum; const mappedProp = Repository.column(model, key); if (this.isReserved(mappedProp)) throw new InternalError(`Property name ${mappedProp} is reserved`); accum[mappedProp] = val; return accum; }, {}); if (model[PersistenceKeys.METADATA]) { log.silly(`Passing along persistence metadata for ${model[PersistenceKeys.METADATA]}`); Object.defineProperty(result, PersistenceKeys.METADATA, { enumerable: false, writable: false, configurable: true, value: model[PersistenceKeys.METADATA], }); } return { record: result, id: model[pk], transient: split.transient, }; } /** * @description Converts database data back into a model instance * @summary Reconstructs a model instance from database data, handling column mapping * and reattaching transient properties * @template M - The model type * @param obj - The database record * @param {string|Constructor<M>} clazz - The model class or name * @param pk - The primary key property name * @param {string|number|bigint} id - The primary key value * @param [transient] - Transient properties to reattach * @return {M} The reconstructed model instance */ revert(obj, clazz, pk, id, transient) { const log = this.log.for(this.revert); const ob = {}; ob[pk] = id; const m = (typeof clazz === "string" ? Model.build(ob, clazz) : new clazz(ob)); log.silly(`Rebuilding model ${m.constructor.name} id ${id}`); const metadata = obj[PersistenceKeys.METADATA]; const result = Object.keys(m).reduce((accum, key) => { if (key === pk) return accum; accum[key] = obj[Repository.column(accum, key)]; return accum; }, m); if (transient) { log.verbose(`re-adding transient properties: ${Object.keys(transient).join(", ")}`); Object.entries(transient).forEach(([key, val]) => { if (key in result) throw new InternalError(`Transient property ${key} already exists on model ${m.constructor.name}. should be impossible`); result[key] = val; }); } if (metadata) { log.silly(`Passing along ${this.flavour} persistence metadata for ${m.constructor.name} id ${id}: ${metadata}`); Object.defineProperty(result, PersistenceKeys.METADATA, { enumerable: false, configurable: false, writable: false, value: metadata, }); } return result; } /** * @description Creates multiple records in the database * @summary Inserts multiple records with the given IDs and data into the specified table * @param {string} tableName - The name of the table to insert into * @param id - The identifiers for the new records * @param model - The data to insert for each record * @param {...any[]} args - Additional arguments specific to the adapter implementation * @return A promise that resolves to an array of created records */ async createAll(tableName, id, model, ...args) { if (id.length !== model.length) throw new InternalError("Ids and models must have the same length"); const log = this.log.for(this.createAll); log.verbose(`Creating ${id.length} entries ${tableName} table`); log.debug(`pks: ${id}`); return Promise.all(id.map((i, count) => this.create(tableName, i, model[count], ...args))); } /** * @description Retrieves multiple records from the database * @summary Fetches multiple records with the given IDs from the specified table * @param {string} tableName - The name of the table to read from * @param id - The identifiers of the records to retrieve * @param {...any[]} args - Additional arguments specific to the adapter implementation * @return A promise that resolves to an array of retrieved records */ async readAll(tableName, id, ...args) { const log = this.log.for(this.readAll); log.verbose(`Reading ${id.length} entries ${tableName} table`); log.debug(`pks: ${id}`); return Promise.all(id.map((i) => this.read(tableName, i, ...args))); } /** * @description Updates multiple records in the database * @summary Modifies multiple existing records with the given IDs in the specified table * @param {string} tableName - The name of the table to update * @param {string[]|number[]} id - The identifiers of the records to update * @param model - The new data for each record * @param {...any[]} args - Additional arguments specific to the adapter implementation * @return A promise that resolves to an array of updated records */ async updateAll(tableName, id, model, ...args) { if (id.length !== model.length) throw new InternalError("Ids and models must have the same length"); const log = this.log.for(this.updateAll); log.verbose(`Updating ${id.length} entries ${tableName} table`); log.debug(`pks: ${id}`); return Promise.all(id.map((i, count) => this.update(tableName, i, model[count], ...args))); } /** * @description Deletes multiple records from the database * @summary Removes multiple records with the given IDs from the specified table * @param {string} tableName - The name of the table to delete from * @param id - The identifiers of the records to delete * @param {...any[]} args - Additional arguments specific to the adapter implementation * @return A promise that resolves to an array of deleted records */ async deleteAll(tableName, id, ...args) { const log = this.log.for(this.createAll); log.verbose(`Deleting ${id.length} entries ${tableName} table`); log.debug(`pks: ${id}`); return Promise.all(id.map((i) => this.delete(tableName, i, ...args))); } /** * @description Registers an observer for database events * @summary Adds an observer to be notified about database changes. The observer can optionally * provide a filter function to receive only specific events. * @param {Observer} observer - The observer to register * @param {ObserverFilter} [filter] - Optional filter function to determine which events the observer receives * @return {void} */ observe(observer, filter) { if (!this.observerHandler) Object.defineProperty(this, "observerHandler", { value: this.ObserverHandler(), writable: false, }); this.observerHandler.observe(observer, filter); this.log .for(this.observe) .verbose(`Registering new observer ${observer.toString()}`); if (!this.dispatch) { this.log.for(this.observe).info(`Creating dispatch for ${this.alias}`); this.dispatch = this.Dispatch(); this.dispatch.observe(this); } } /** * @description Unregisters an observer * @summary Removes a previously registered observer so it no longer receives database event notifications * @param {Observer} observer - The observer to unregister * @return {void} */ unObserve(observer) { 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`); } /** * @description Notifies all observers about a database event * @summary Sends notifications to all registered observers about a change in the database, * filtering based on each observer's filter function * @param {string} table - The name of the table where the change occurred * @param {OperationKeys|BulkCrudOperationKeys|string} event - The type of operation that occurred * @param {EventIds} id - The identifier(s) of the affected record(s) * @param {...any[]} args - Additional arguments to pass to the observers * @return {Promise<void>} A promise that resolves when all observers have been notified */ async updateObservers(table, event, id, ...args) { if (!this.observerHandler) throw new InternalError("ObserverHandler not initialized. Did you register any observables?"); const log = this.log.for(this.updateObservers); log.verbose(`Updating ${this.observerHandler.count()} observers for adapter ${this.alias}`); await this.observerHandler.updateObservers(this.log, table, event, id, ...args); } /** * @description Refreshes data based on a database event * @summary Implementation of the Observer interface method that delegates to updateObservers * @param {string} table - The name of the table where the change occurred * @param {OperationKeys|BulkCrudOperationKeys|string} event - The type of operation that occurred * @param {EventIds} id - The identifier(s) of the affected record(s) * @param {...any[]} args - Additional arguments related to the event * @return {Promise<void>} A promise that resolves when the refresh is complete */ async refresh(table, event, id, ...args) { return this.updateObservers(table, event, id, ...args); } /** * @description Gets a string representation of the adapter * @summary Returns a human-readable string identifying this adapter * @return {string} A string representation of the adapter */ toString() { return `${this.flavour} persistence Adapter`; } /** * @description Gets the adapter flavor associated with a model * @summary Retrieves the adapter flavor that should be used for a specific model class * @template M - The model type * @param {Constructor<M>} model - The model constructor * @return {string} The adapter flavor name */ static flavourOf(model) { return (Reflect.getMetadata(this.key(PersistenceKeys.ADAPTER), model) || this.current.flavour); } /** * @description Gets the current default adapter * @summary Retrieves the adapter that is currently set as the default for operations * @return {Adapter<any, any, any, any>} The current adapter */ static get current() { if (!Adapter._current) throw new InternalError(`No persistence flavour set. Please initialize your adapter`); return Adapter._current; } /** * @description Gets an adapter by flavor * @summary Retrieves a registered adapter by its flavor name * @template Y - The database driver type * @template Q - The query type * @template C - The context type * @template F - The repository flags type * @param {string} flavour - The flavor name of the adapter to retrieve * @return {Adapter<Y, Q, F, C> | undefined} The adapter instance or undefined if not found */ static get(flavour) { if (flavour in this._cache) return this._cache[flavour]; throw new InternalError(`No Adapter registered under ${flavour}.`); } /** * @description Sets the current default adapter * @summary Changes which adapter is used as the default for operations * @param {string} flavour - The flavor name of the adapter to set as current * @return {void} */ static setCurrent(flavour) { const adapter = Adapter.get(flavour); if (!adapter) throw new NotFoundError(`No persistence flavour ${flavour} registered`); this._current = adapter; } /** * @description Creates a metadata key * @summary Generates a standardized metadata key for persistence-related metadata * @param {string} key - The base key name * @return {string} The formatted metadata key */ static key(key) { return Repository.key(key); } /** * @description Gets all models associated with an adapter flavor * @summary Retrieves all model constructors that are configured to use a specific adapter flavor * @template M - The model type * @param {string} flavour - The adapter flavor to find models for * @return An array of model constructors */ static models(flavour) { try { const registry = Model.getRegistry(); const cache = registry.cache; const managedModels = Object.values(cache) .map((m) => { let f = Reflect.getMetadata(Adapter.key(PersistenceKeys.ADAPTER), m); if (f && f === flavour) return m; if (!f) { const repo = Reflect.getMetadata(Repository.key(DBKeys.REPOSITORY), m); if (!repo) return; const repository = Repository.forModel(m); f = Reflect.getMetadata(Adapter.key(PersistenceKeys.ADAPTER), repository); return f; } }) .filter((m) => !!m); return managedModels; } catch (e) { throw new InternalError(e); } } } __decorate([ final(), __metadata("design:type", Function), __metadata("design:paramtypes", [String, Object, Object, Object]), __metadata("design:returntype", Promise) ], Adapter.prototype, "context", null); __decorate([ final(), __metadata("design:type", Function), __metadata("design:paramtypes", [Object, Function]), __metadata("design:returntype", void 0) ], Adapter.prototype, "observe", null); __decorate([ final(), __metadata("design:type", Function), __metadata("design:paramtypes", [Object]), __metadata("design:returntype", void 0) ], Adapter.prototype, "unObserve", null); /** * @description Gets the table name for a model * @summary Retrieves the table name associated with a model by checking metadata or falling back to the constructor name * @template M - Type that extends Model * @param {M | Constructor<M>} model - The model instance or constructor to get the table name for * @return {string} The table name for the model * @function getTableName * @memberOf module:core */ function getTableName(model) { const obj = model instanceof Model ? model.constructor : model; const metadata = Reflect.getOwnMetadata(Adapter.key(PersistenceKeys.TABLE), obj); if (metadata) { return metadata; } if (model instanceof Model) { return model.constructor.name; } return model.name; } /** * @description Generates a sequence name for a model * @summary Creates a standardized sequence name by combining the table name with additional arguments * @template M - Type that extends Model * @param {M | Constructor<M>} model - The model instance or constructor to generate the sequence name for * @param {...string} args - Additional string arguments to append to the sequence name * @return {string} The generated sequence name * @function sequenceNameForModel * @memberOf module:core */ function sequenceNameForModel(model, ...args) { return [getTableName(model), ...args].join("_"); } /** * @description Abstract base class for sequence generation * @summary Provides a framework for generating sequential values (like primary keys) in the persistence layer. * Implementations of this class handle the specifics of how sequences are stored and incremented in different * database systems. * @param {SequenceOptions} options - Configuration options for the sequence generator * @class Sequence * @example * ```typescript * // Example implementation for a specific database * class PostgresSequence extends Sequence { * constructor(options: SequenceOptions) { * super(options); * } * * async next(): Promise<number> { * // Implementation to get next value from PostgreSQL sequence * const result = await this.options.executor.raw(`SELECT nextval('${this.options.name}')`); * return parseInt(result.rows[0].nextval); * } * * async current(): Promise<number> { * // Implementation to get current value from PostgreSQL sequence * const result = await this.options.executor.raw(`SELECT currval('${this.options.name}')`); * return parseInt(result.rows[0].currval); * } * * async range(count: number): Promise<number[]> { * // Implementation to get a range of values * const values: number[] = []; * for (let i = 0; i < count; i++) { * values.push(await this.next()); * } * return values; * } * } * * // Usage * const sequence = new PostgresSequence({ * name: 'user_id_seq', * executor: dbExecutor * }); * * const nextId = await sequence.next(); * ``` */ class Sequence { /** * @description Accessor for the logger instance * @summary Gets or initializes the logger for this sequence * @return {Logger} The logger instance */ get log() { if (!this.logger) this.logger = Logging.for(this); return this.logger; } /** * @description Creates a new sequence instance * @summary Protected constructor that initializes the sequence with the provided options */ constructor(options) { this.options = options; } /** * @description Gets the primary key sequence name for a model * @summary Utility method that returns the standardized sequence name for a model's primary key * @template M - The model type * @param {M|Constructor<M>} model - The model instance or constructor * @return {string} The sequence name for the model's primary key */ static pk(model) { return sequenceNameForModel(model, "pk"); } /** * @description Parses a sequence value to the appropriate type * @summary Converts a sequence value to the specified type (Number or BigInt) * @param {"Number"|"BigInt"|undefined} type - The target type to convert to * @param {string|number|bigint} value - The value to convert * @return {string|number|bigint} The converted value */ static parseValue(type, value) { switch (type) { case "Number": return typeof value === "string" ? parseInt(value) : typeof value === "number" ? value : BigInt(value); case "BigInt": return BigInt(value); default: throw new InternalError("Should never happen"); } } } /** * @description Specifies which persistence adapter flavor a model should use * @summary This decorator applies metadata to a model class to indicate which persistence adapter flavor * should be used when performing database operations on instances of the model. The flavor is a string * identifier that corresponds to a registered adapter configuration. * @param {string} flavour - The identifier of the adapter flavor to use * @return {Function} A decorator function that can be applied to a model class * @function uses * @category Class Decorators */ function uses(flavour) { return apply(metadata(Adapter.key(PersistenceKeys.ADAPTER), flavour)); } /** * @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. * @templa