@decaf-ts/core
Version:
Core persistence module for the decaf framework
1,227 lines (1,215 loc) • 508 kB
JavaScript
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