UNPKG

@decaf-ts/core

Version:

Core persistence module for the decaf framework

198 lines 8.93 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Dispatch = void 0; const db_decorators_1 = require("@decaf-ts/db-decorators"); const decorator_validation_1 = require("@decaf-ts/decorator-validation"); const Adapter_1 = require("./Adapter.cjs"); const errors_1 = require("./errors.cjs"); const ContextualLoggedClass_1 = require("./../utils/ContextualLoggedClass.cjs"); /** * @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 extends ContextualLoggedClass_1.ContextualLoggedClass { /** * @description Creates a new Dispatch instance * @summary Initializes a new Dispatch instance without any adapter */ constructor() { super(); } /** * @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) { // Gracefully skip initialization when no adapter is observed yet. // Some tests or setups may construct a Dispatch before calling observe(). // Instead of throwing, we no-op so that later observe() can proceed. this.log .for(this.initialize) .verbose(`No adapter observed for dispatch; skipping initialization`); return; } const adapter = this.adapter; [ db_decorators_1.OperationKeys.CREATE, db_decorators_1.OperationKeys.UPDATE, db_decorators_1.OperationKeys.DELETE, db_decorators_1.BulkCrudOperationKeys.CREATE_ALL, db_decorators_1.BulkCrudOperationKeys.UPDATE_ALL, db_decorators_1.BulkCrudOperationKeys.DELETE_ALL, ].forEach((toWrap) => { if (!adapter[toWrap]) throw new db_decorators_1.InternalError(`Method ${toWrap} not found in ${adapter.alias} adapter to bind Observables Dispatch`); let descriptor = Object.getOwnPropertyDescriptor(adapter, toWrap); let proto = adapter; while (!descriptor && proto !== Object.prototype) { proto = Object.getPrototypeOf(proto); descriptor = Object.getOwnPropertyDescriptor(proto, toWrap); } if (!descriptor || !descriptor.writable) { this.log.error(`Could not find method ${toWrap} to bind Observables Dispatch`); return; } function bulkToSingle(method) { switch (method) { case db_decorators_1.BulkCrudOperationKeys.CREATE_ALL: return db_decorators_1.OperationKeys.CREATE; case db_decorators_1.BulkCrudOperationKeys.UPDATE_ALL: return db_decorators_1.OperationKeys.UPDATE; case db_decorators_1.BulkCrudOperationKeys.DELETE_ALL: return db_decorators_1.OperationKeys.DELETE; default: return method; } } // @ts-expect-error because there are read only properties adapter[toWrap] = new Proxy(adapter[toWrap], { apply: async (target, thisArg, argArray) => { const { log, ctxArgs } = thisArg["logCtx"](argArray, target); const [tableName, ids] = argArray; const result = await target.apply(thisArg, ctxArgs); this.updateObservers(tableName, bulkToSingle(toWrap), ids, result, ...ctxArgs.slice(argArray.length)) .then(() => { log.verbose(`Observer refresh dispatched by ${toWrap} for ${tableName}`); log.debug(`pks: ${ids}`); }) .catch((e) => log.error(`Failed to dispatch observer refresh for ${toWrap} 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<any, any, any, any>} observer - The adapter to observe * @return {void} */ observe(observer) { if (!(observer instanceof Adapter_1.Adapter)) throw new errors_1.UnsupportedError("Only Adapters can be observed by dispatch"); this.adapter = observer; this.models = Adapter_1.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 errors_1.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(model, event, id, ...args) { const table = typeof model === "string" ? model : decorator_validation_1.Model.tableName(model); const { log, ctxArgs } = this.logCtx(args, this.updateObservers); if (!this.adapter) { log.verbose(`No adapter observed for dispatch; skipping observer update for ${table}:${event}`); return; } try { log.debug(`Dispatching ${event} from table ${table} for ${event} with id: ${JSON.stringify(id)}`); await this.adapter.refresh(model, event, id, ...ctxArgs); } catch (e) { throw new db_decorators_1.InternalError(`Failed to refresh dispatch: ${e}`); } } } exports.Dispatch = Dispatch; if (Adapter_1.Adapter) Adapter_1.Adapter["_baseDispatch"] = Dispatch; //# sourceMappingURL=Dispatch.js.map