UNPKG

@decaf-ts/db-decorators

Version:

Agnostic database decorators and repository

1,303 lines (1,284 loc) 243 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@decaf-ts/decorator-validation'), require('tslib'), require('@decaf-ts/reflection'), require('typed-object-accumulator')) : typeof define === 'function' && define.amd ? define(['exports', '@decaf-ts/decorator-validation', 'tslib', '@decaf-ts/reflection', 'typed-object-accumulator'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global["db-decorators"] = {}, global.decoratorValidation, global.tslib, global.reflection, global.typedObjectAccumulator)); })(this, (function (exports, decoratorValidation, tslib, reflection, typedObjectAccumulator) { 'use strict'; /** * @summary Holds the Model reflection keys * @const DBKeys * * @memberOf module:db-decorators.Model */ const DBKeys = { REFLECT: `${decoratorValidation.ModelKeys.REFLECT}persistence.`, REPOSITORY: "repository", CLASS: "_class", ID: "id", INDEX: "index", UNIQUE: "unique", SERIALIZE: "serialize", READONLY: "readonly", TIMESTAMP: "timestamp", TRANSIENT: "transient", HASH: "hash", COMPOSED: "composed", VERSION: "version", ORIGINAL: "__originalObj", }; /** * @summary The default separator when concatenating indexes * * @const DefaultIndexSeparator * * @category Managers * @subcategory Constants */ const DefaultSeparator = "_"; /** * @summary Holds the default timestamp date format * @constant DEFAULT_TIMESTAMP_FORMAT * * @memberOf module:db-decorators.Model */ const DEFAULT_TIMESTAMP_FORMAT = "dd/MM/yyyy HH:mm:ss:S"; /** * @summary holds the default error messages * @const DEFAULT_ERROR_MESSAGES * * @memberOf module:db-decorators.Model */ const DEFAULT_ERROR_MESSAGES = { ID: { INVALID: "This Id is invalid", REQUIRED: "The Id is mandatory", }, READONLY: { INVALID: "This cannot be updated", }, TIMESTAMP: { REQUIRED: "Timestamp is Mandatory", DATE: "The Timestamp must the a valid date", INVALID: "This value must always increase", }, }; /** * @summary Update reflection keys * @const UpdateValidationKeys * @memberOf module:db-decorators.Operations */ const UpdateValidationKeys = { REFLECT: "db.update.validation.", TIMESTAMP: DBKeys.TIMESTAMP, READONLY: DBKeys.READONLY, }; /** * @summary Validator for the {@link readonly} decorator * * @class ReadOnlyValidator * @extends Validator * * @category Validators */ exports.ReadOnlyValidator = class ReadOnlyValidator extends decoratorValidation.Validator { constructor() { super(DEFAULT_ERROR_MESSAGES.READONLY.INVALID); } /** * @inheritDoc */ // eslint-disable-next-line @typescript-eslint/no-unused-vars hasErrors(value, ...args) { return undefined; } /** * @summary Validates a value has not changed * @param {any} value * @param {any} oldValue * @param {string} [message] the error message override */ updateHasErrors(value, oldValue, message) { if (value === undefined) return; return reflection.isEqual(value, oldValue) ? undefined : this.getMessage(message || this.message); } }; exports.ReadOnlyValidator = tslib.__decorate([ decoratorValidation.validator(UpdateValidationKeys.READONLY), tslib.__metadata("design:paramtypes", []) ], exports.ReadOnlyValidator); /** * @summary Validates the update of a timestamp * * @class TimestampValidator * @extends Validator * * @category Validators */ exports.TimestampValidator = class TimestampValidator extends decoratorValidation.Validator { constructor() { super(DEFAULT_ERROR_MESSAGES.TIMESTAMP.INVALID); } // eslint-disable-next-line @typescript-eslint/no-unused-vars hasErrors(value, ...args) { return undefined; } updateHasErrors(value, oldValue, message) { if (value === undefined) return; message = message || this.getMessage(message || this.message); try { value = new Date(value); oldValue = new Date(oldValue); // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { return message; } return value <= oldValue ? message : undefined; } }; exports.TimestampValidator = tslib.__decorate([ decoratorValidation.validator(UpdateValidationKeys.TIMESTAMP), tslib.__metadata("design:paramtypes", []) ], exports.TimestampValidator); /** * @summary Base class for an Update validator * * @param {string} [message] error message. defaults to {@link DecoratorMessages#DEFAULT} * @param {string[]} [acceptedTypes] the accepted value types by the decorator * * @class UpdateValidator * @abstract * @extends Validator * * @category Validators */ class UpdateValidator extends decoratorValidation.Validator { constructor(message = decoratorValidation.DEFAULT_ERROR_MESSAGES.DEFAULT, ...acceptedTypes) { super(message, ...acceptedTypes); } } decoratorValidation.Validation.updateKey = function (key) { return UpdateValidationKeys.REFLECT + key; }; /** * @summary Set of constants to define db CRUD operations and their equivalent 'on' and 'after' phases * @const OperationKeys * * @memberOf module:db-decorators.Operations */ exports.OperationKeys = void 0; (function (OperationKeys) { OperationKeys["REFLECT"] = "decaf.model.db.operations."; OperationKeys["CREATE"] = "create"; OperationKeys["READ"] = "read"; OperationKeys["UPDATE"] = "update"; OperationKeys["DELETE"] = "delete"; OperationKeys["ON"] = "on."; OperationKeys["AFTER"] = "after."; })(exports.OperationKeys || (exports.OperationKeys = {})); exports.BulkCrudOperationKeys = void 0; (function (BulkCrudOperationKeys) { BulkCrudOperationKeys["CREATE_ALL"] = "createAll"; BulkCrudOperationKeys["READ_ALL"] = "readAll"; BulkCrudOperationKeys["UPDATE_ALL"] = "updateAll"; BulkCrudOperationKeys["DELETE_ALL"] = "deleteAll"; })(exports.BulkCrudOperationKeys || (exports.BulkCrudOperationKeys = {})); /** * @summary Maps out groups of CRUD operations for easier mapping of decorators * * @constant DBOperations * * @memberOf module:db-decorators.Operations */ const DBOperations = { CREATE: [exports.OperationKeys.CREATE], READ: [exports.OperationKeys.READ], UPDATE: [exports.OperationKeys.UPDATE], DELETE: [exports.OperationKeys.DELETE], CREATE_UPDATE: [exports.OperationKeys.CREATE, exports.OperationKeys.UPDATE], READ_CREATE: [exports.OperationKeys.READ, exports.OperationKeys.CREATE], ALL: [ exports.OperationKeys.CREATE, exports.OperationKeys.READ, exports.OperationKeys.UPDATE, exports.OperationKeys.DELETE, ], }; /** * @summary Holds the registered operation handlers * * @class OperationsRegistry * @implements IRegistry<OperationHandler<any>> * * @see OperationHandler * * @category Operations */ class OperationsRegistry { constructor() { this.cache = {}; } /** * @summary retrieves an {@link OperationHandler} if it exists * @param {string} target * @param {string} propKey * @param {string} operation * @param accum * @return {OperationHandler | undefined} */ get(target, propKey, operation, accum) { accum = accum || []; let name; try { name = typeof target === "string" ? target : target.constructor.name; accum.unshift(...Object.values(this.cache[name][propKey][operation] || [])); // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { if (typeof target === "string" || target === Object.prototype || Object.getPrototypeOf(target) === Object.prototype) return accum; } let proto = Object.getPrototypeOf(target); if (proto.constructor.name === name) proto = Object.getPrototypeOf(proto); return this.get(proto, propKey, operation, accum); } /** * @summary Registers an {@link OperationHandler} * @param {OperationHandler} handler * @param {string} operation * @param {{}} target * @param {string | symbol} propKey */ register(handler, operation, target, propKey) { const name = target.constructor.name; const handlerName = Operations.getHandlerName(handler); if (!this.cache[name]) this.cache[name] = {}; if (!this.cache[name][propKey]) this.cache[name][propKey] = {}; if (!this.cache[name][propKey][operation]) this.cache[name][propKey][operation] = {}; if (this.cache[name][propKey][operation][handlerName]) return; this.cache[name][propKey][operation][handlerName] = handler; } } /** * @summary Static class holding common Operation Functionality * * @class Operations * * @category Operations */ class Operations { constructor() { } static getHandlerName(handler) { if (handler.name) return handler.name; console.warn("Handler name not defined. A name will be generated, but this is not desirable. please avoid using anonymous functions"); return decoratorValidation.Hashing.hash(handler.toString()); } static key(str) { return exports.OperationKeys.REFLECT + str; } static get(targetName, propKey, operation) { return Operations.registry.get(targetName, propKey, operation); } static getOpRegistry() { if (!Operations.registry) Operations.registry = new OperationsRegistry(); return Operations.registry; } static register(handler, operation, target, propKey) { Operations.getOpRegistry().register(handler, operation, target, propKey); } } function handle(op, handler) { return (target, propertyKey) => { Operations.register(handler, op, target, propertyKey); }; } /** * @summary Defines a behaviour to set on the defined {@link DBOperations.CREATE_UPDATE} * * @param {OnOperationHandler<any>} handler The method called upon the operation * @param data * @param {any[]} [args] Arguments that will be passed in order to the handler method * * @see on * * @function onCreateUpdate * * @category Decorators */ function onCreateUpdate(handler, data) { return on(DBOperations.CREATE_UPDATE, handler, data); } /** * @summary Defines a behaviour to set on the defined {@link DBOperations.UPDATE} * * @param {OnOperationHandler<any>} handler The method called upon the operation * @param data * @param {any[]} [args] Arguments that will be passed in order to the handler method * * @see on * * @function onUpdate * * @category Decorators */ function onUpdate(handler, data) { return on(DBOperations.UPDATE, handler, data); } /** * @summary Defines a behaviour to set on the defined {@link DBOperations.CREATE} * * @param {OnOperationHandler<any>} handler The method called upon the operation * @param data * * @see on * * @function onCreate * * @category Decorators */ function onCreate(handler, data) { return on(DBOperations.CREATE, handler, data); } /** * @summary Defines a behaviour to set on the defined {@link DBOperations.READ} * * @param {OnOperationHandler<any>} handler The method called upon the operation * @param data * * @see on * * @function onRead * * @category Decorators */ function onRead(handler, data) { return on(DBOperations.READ, handler, data); } /** * @summary Defines a behaviour to set on the defined {@link DBOperations.DELETE} * * @param {OnOperationHandler<any>} handler The method called upon the operation * @param data * * @see on * * @function onDelete * * @category Decorators */ function onDelete(handler, data) { return on(DBOperations.DELETE, handler, data); } /** * @summary Defines a behaviour to set on the defined {@link DBOperations.DELETE} * * @param {OnOperationHandler<any>} handler The method called upon the operation * @param data * * @see on * * @function onAny * * @category Decorators */ function onAny(handler, data) { return on(DBOperations.ALL, handler, data); } /** * @summary Defines a behaviour to set on the defined {@link DBOperations} * * @param {OperationKeys[] | DBOperations} op One of {@link DBOperations} * @param {OnOperationHandler<any>} handler The method called upon the operation * @param data * * ex: handler(...args, ...props.map(p => target[p])) * * @function on * * @category Decorators */ function on(op = DBOperations.ALL, handler, data) { return operation(exports.OperationKeys.ON, op, handler, data); } /** * @summary Defines a behaviour to set after the defined {@link DBOperations.CREATE_UPDATE} * * @param {AfterOperationHandler<any>} handler The method called upon the operation * @param data * * @see after * * @function afterCreateUpdate * * @category Decorators */ function afterCreateUpdate(handler, data) { return after(DBOperations.CREATE_UPDATE, handler, data); } /** * @summary Defines a behaviour to set after the defined {@link DBOperations.UPDATE} * * @param {AfterOperationHandler<any>} handler The method called upon the operation * @param data * * @see after * * @function afterUpdate * * @category Decorators */ function afterUpdate(handler, data) { return after(DBOperations.UPDATE, handler, data); } /** * @summary Defines a behaviour to set after the defined {@link DBOperations.CREATE} * * @param {AfterOperationHandler<any>} handler The method called upon the operation * @param data * * @see after * * @function afterCreate * * @category Decorators */ function afterCreate(handler, data) { return after(DBOperations.CREATE, handler, data); } /** * @summary Defines a behaviour to set after the defined {@link DBOperations.READ} * * @param {AfterOperationHandler<any>} handler The method called upon the operation * @param data * @param {any[]} [args] Arguments that will be passed in order to the handler method * * @see after * * @function afterRead * * @category Decorators */ function afterRead(handler, data) { return after(DBOperations.READ, handler, data); } /** * @summary Defines a behaviour to set after the defined {@link DBOperations.DELETE} * * @param {AfterOperationHandler<any>} handler The method called upon the operation * @param data * @param {any[]} [args] Arguments that will be passed in order to the handler method * * @see after * * @function afterDelete * * @category Decorators */ function afterDelete(handler, data) { return after(DBOperations.DELETE, handler, data); } /** * @summary Defines a behaviour to set after the defined {@link DBOperations.DELETE} * * @param {AfterOperationHandler<any>} handler The method called upon the operation * @param data * @param {any[]} [args] Arguments that will be passed in order to the handler method * * @see after * * @function afterAny * * @category Decorators */ function afterAny(handler, data) { return after(DBOperations.ALL, handler, data); } /** * @summary Defines a behaviour to set on the defined {@link DBOperations} * * @param {OperationKeys[] | DBOperations} op One of {@link DBOperations} * @param {AfterOperationHandler<any>} handler The method called upon the operation * * ex: handler(...args, ...props.map(p => target[p])) * * @param data * @param args * @function after * * @category Decorators */ function after(op = DBOperations.ALL, handler, data) { return operation(exports.OperationKeys.AFTER, op, handler, data); } function operation(baseOp, operation = DBOperations.ALL, handler, dataToAdd) { return (target, propertyKey) => { const name = target.constructor.name; const decorators = operation.reduce((accum, op) => { const compoundKey = baseOp + op; let data = Reflect.getMetadata(Operations.key(compoundKey), target, propertyKey); if (!data) data = { operation: op, handlers: {}, }; const handlerKey = Operations.getHandlerName(handler); if (!data.handlers[name] || !data.handlers[name][propertyKey] || !(handlerKey in data.handlers[name][propertyKey])) { data.handlers[name] = data.handlers[name] || {}; data.handlers[name][propertyKey] = data.handlers[name][propertyKey] || {}; data.handlers[name][propertyKey][handlerKey] = { data: dataToAdd, }; accum.push(handle(compoundKey, handler), decoratorValidation.propMetadata(Operations.key(compoundKey), data)); } return accum; }, []); return reflection.apply(...decorators)(target, propertyKey); }; } /** * @summary Base Error * * @param {string} msg the error message * * @class BaseDLTError * @extends Error */ class BaseError extends Error { constructor(name, msg, code = 500) { if (msg instanceof BaseError) return msg; const message = `[${name}] ${msg instanceof Error ? msg.message : msg}`; super(message); this.code = code; if (msg instanceof Error) this.stack = msg.stack; } } /** * @summary Represents a failure in the Model details * * @param {string} msg the error message * * @class ValidationError * @extends BaseError */ class ValidationError extends BaseError { constructor(msg) { super(ValidationError.name, msg, 422); } } /** * @summary Represents an internal failure (should mean an error in code) * * @param {string} msg the error message * * @class InternalError * @extends BaseError */ class InternalError extends BaseError { constructor(msg) { super(InternalError.name, msg, 500); } } /** * @summary Represents a failure in the Model de/serialization * * @param {string} msg the error message * * @class SerializationError * @extends BaseError * */ class SerializationError extends BaseError { constructor(msg) { super(SerializationError.name, msg, 422); } } /** * @summary Represents a failure in finding a model * * @param {string} msg the error message * * @class NotFoundError * @extends BaseError * */ class NotFoundError extends BaseError { constructor(msg) { super(NotFoundError.name, msg, 404); } } /** * @summary Represents a conflict in the storage * * @param {string} msg the error message * * @class ConflictError * @extends BaseError * */ class ConflictError extends BaseError { constructor(msg) { super(ConflictError.name, msg, 409); } } /** * @summary retrieves the arguments for the handler * @param {any} dec the decorator * @param {string} prop the property name * @param {{}} m the model * @param {{}} [accum] accumulator used for internal recursiveness * * @function getHandlerArgs * @memberOf module:db-decorators.Repository */ const getHandlerArgs = function (dec, prop, m, accum) { const name = m.constructor.name; if (!name) throw new InternalError("Could not determine model class"); accum = accum || {}; if (dec.props.handlers[name] && dec.props.handlers[name][prop]) accum = { ...dec.props.handlers[name][prop], ...accum }; let proto = Object.getPrototypeOf(m); if (proto === Object.prototype) return accum; if (proto.constructor.name === name) proto = Object.getPrototypeOf(proto); return getHandlerArgs(dec, prop, proto, accum); }; /** * * @param {IRepository<T>} repo * @param context * @param {T} model * @param operation * @param prefix * * @param oldModel * @function enforceDBPropertyDecoratorsAsync * * @memberOf db-decorators.utils */ async function enforceDBDecorators(repo, context, model, operation, prefix, oldModel) { const decorators = getDbDecorators(model, operation, prefix); if (!decorators) return; for (const prop in decorators) { const decs = decorators[prop]; for (const dec of decs) { const { key } = dec; const handlers = Operations.get(model, prop, prefix + key); if (!handlers || !handlers.length) throw new InternalError(`Could not find registered handler for the operation ${prefix + key} under property ${prop}`); const handlerArgs = getHandlerArgs(dec, prop, model); if (!handlerArgs || Object.values(handlerArgs).length !== handlers.length) throw new InternalError("Args and handlers length do not match"); let handler; let data; for (let i = 0; i < handlers.length; i++) { handler = handlers[i]; data = Object.values(handlerArgs)[i]; const args = [context, data.data, prop, model]; if (operation === exports.OperationKeys.UPDATE && prefix === exports.OperationKeys.ON) { if (!oldModel) throw new InternalError("Missing old model for update operation"); args.push(oldModel); } try { await handler.apply(repo, args); } catch (e) { const msg = `Failed to execute handler ${handler.name} for ${prop} on ${model.constructor.name} due to error: ${e}`; if (context.get("breakOnHandlerError")) throw new InternalError(msg); console.log(msg); } } } } } /** * Specific for DB Decorators * @param {T} model * @param {string} operation CRUD {@link OperationKeys} * @param {string} [extraPrefix] * * @function getDbPropertyDecorators * * @memberOf db-decorators.utils */ function getDbDecorators(model, operation, extraPrefix) { const decorators = reflection.Reflection.getAllPropertyDecorators(model, // undefined, exports.OperationKeys.REFLECT + (extraPrefix ? extraPrefix : "")); if (!decorators) return; return Object.keys(decorators).reduce((accum, decorator) => { const dec = decorators[decorator].filter((d) => d.key === operation); if (dec && dec.length) { if (!accum) accum = {}; accum[decorator] = dec; } return accum; }, undefined); } /** * @summary Retrieves the decorators for an object's properties prefixed by {@param prefixes} recursively * @param model * @param accum * @param prefixes * * @function getAllPropertyDecoratorsRecursive * @memberOf module:db-decorators.Repository */ const getAllPropertyDecoratorsRecursive = function (model, accum, ...prefixes) { const accumulator = accum || {}; const mergeDecorators = function (decs) { const pushOrSquash = (key, ...values) => { values.forEach((val) => { let match; if (!(match = accumulator[key].find((e) => e.key === val.key)) || match.props.operation !== val.props.operation) { accumulator[key].push(val); return; } if (val.key === decoratorValidation.ModelKeys.TYPE) return; const { handlers, operation } = val.props; if (!operation || !operation.match(new RegExp(`^(:?${exports.OperationKeys.ON}|${exports.OperationKeys.AFTER})(:?${exports.OperationKeys.CREATE}|${exports.OperationKeys.READ}|${exports.OperationKeys.UPDATE}|${exports.OperationKeys.DELETE})$`))) { accumulator[key].push(val); return; } const accumHandlers = match.props.handlers; Object.entries(handlers).forEach(([clazz, handlerDef]) => { if (!(clazz in accumHandlers)) { accumHandlers[clazz] = handlerDef; return; } Object.entries(handlerDef).forEach(([handlerProp, handler]) => { if (!(handlerProp in accumHandlers[clazz])) { accumHandlers[clazz][handlerProp] = handler; return; } Object.entries(handler).forEach(([handlerKey, argsObj]) => { if (!(handlerKey in accumHandlers[clazz][handlerProp])) { accumHandlers[clazz][handlerProp][handlerKey] = argsObj; return; } console.warn(`Skipping handler registration for ${clazz} under prop ${handlerProp} because handler is the same`); }); }); }); }); }; Object.entries(decs).forEach(([key, value]) => { accumulator[key] = accumulator[key] || []; pushOrSquash(key, ...value); }); }; const decs = reflection.Reflection.getAllPropertyDecorators(model, ...prefixes); if (decs) mergeDecorators(decs); if (Object.getPrototypeOf(model) === Object.prototype) return accumulator; // const name = model.constructor.name; const proto = Object.getPrototypeOf(model); if (!proto) return accumulator; // if (proto.constructor && proto.constructor.name === name) // proto = Object.getPrototypeOf(proto) return getAllPropertyDecoratorsRecursive(proto, accumulator, ...prefixes); }; const DefaultRepositoryFlags = { parentContext: undefined, childContexts: [], ignoredValidationProperties: [], callArgs: [], writeOperation: false, affectedTables: [], operation: undefined, breakOnHandlerError: true, rebuildWithTransient: true, }; const DefaultContextFactory = (arg) => { return new Context().accumulate(Object.assign({}, arg, { timestamp: new Date() })); }; class Context { static { this.factory = DefaultContextFactory; } constructor(obj) { this.cache = new typedObjectAccumulator.ObjectAccumulator(); if (obj) return this.accumulate(obj); } accumulate(value) { Object.defineProperty(this, "cache", { value: this.cache.accumulate(value), writable: false, enumerable: false, configurable: true, }); return this; } get timestamp() { return this.cache.timestamp; } get(key) { try { return this.cache.get(key); } catch (e) { if (this.cache.parentContext) return this.cache.parentContext.get(key); throw e; } } child(operation, model) { return Context.childFrom(this, { operation: operation, affectedTables: model ? [model] : [], }); } static childFrom(context, overrides) { return Context.factory(Object.assign({}, context.cache, overrides || {})); } static async from(operation, overrides, model, // eslint-disable-next-line @typescript-eslint/no-unused-vars ...args) { return Context.factory(Object.assign({}, DefaultRepositoryFlags, overrides, { operation: operation, model: model, })); } static async args(operation, model, args, contextual, overrides) { const last = args.pop(); async function getContext() { if (contextual) return contextual.context(operation, overrides || {}, model, ...args); return Context.from(operation, overrides || {}, model, ...args); } let c; if (last) { if (last instanceof Context) { c = last; args.push(last); } else { c = (await getContext()); args.push(last, c); } } else { c = (await getContext()); args.push(c); } return { context: c, args: args }; } } /** * @summary Util method to change a method of an object prefixing it with another * @param {any} obj The Base Object * @param {Function} after The original method * @param {Function} prefix The Prefix method. The output will be used as arguments in the original method * @param {string} [afterName] When the after function anme cannot be extracted, pass it here * * @function prefixMethod * * @memberOf module:db-decorators.Repository */ function prefixMethod(obj, after, prefix, afterName) { async function wrapper(...args) { const results = await Promise.resolve(prefix.call(this, ...args)); return Promise.resolve(after.apply(this, results)); } const wrapped = wrapper.bind(obj); const name = afterName ? afterName : after.name; Object.defineProperty(wrapped, "name", { enumerable: true, configurable: true, writable: false, value: name, }); obj[name] = wrapped; } /** * @summary Util method to change a method of an object suffixing it with another * @param {any} obj The Base Object * @param {Function} before The original method * @param {Function} suffix The Prefix method. The output will be used as arguments in the original method * @param {string} [beforeName] When the after function anme cannot be extracted, pass it here * * @function suffixMethod * * @memberOf module:db-decorators.Repository */ function suffixMethod(obj, before, suffix, beforeName) { async function wrapper(...args) { const results = await Promise.resolve(before.call(this, ...args)); return suffix.call(this, ...results); } const wrapped = wrapper.bind(obj); const name = beforeName ? beforeName : before.name; Object.defineProperty(wrapped, "name", { enumerable: true, configurable: true, writable: false, value: name, }); obj[name] = wrapped; } /** * @summary Util method to wrap a method of an object with additional logic * * @param {any} obj The Base Object * @param {Function} before the method to be prefixed * @param {Function} method the method to be wrapped * @param {Function} after The method to be suffixed * @param {string} [methodName] When the after function anme cannot be extracted, pass it here * * @function wrapMethodWithContext * * @memberOf module:db-decorators.Repository */ function wrapMethodWithContext(obj, before, method, after, methodName) { const name = methodName ? methodName : method.name; obj[name] = new Proxy(obj[name], { apply: async (target, thisArg, argArray) => { let transformedArgs = before.call(thisArg, ...argArray); if (transformedArgs instanceof Promise) transformedArgs = await transformedArgs; const context = transformedArgs[transformedArgs.length - 1]; if (!(context instanceof Context)) throw new InternalError("Missing a context"); let results = await target.call(thisArg, ...transformedArgs); if (results instanceof Promise) results = await results; results = after.call(thisArg, results, context); if (results instanceof Promise) results = await results; return results; }, }); } /** * @summary Returns the primary key attribute for a {@link Model} * @description searches in all the properties in the object for an {@link id} decorated property * * @param {Model} model * * @throws {InternalError} if no property or more than one properties are {@link id} decorated * or no value is set in that property * * @function findPrimaryKey * * @category managers */ function findPrimaryKey(model) { const decorators = getAllPropertyDecoratorsRecursive(model, undefined, DBKeys.REFLECT + DBKeys.ID); const idDecorators = Object.entries(decorators).reduce((accum, [prop, decs]) => { const filtered = decs.filter((d) => d.key !== decoratorValidation.ModelKeys.TYPE); if (filtered && filtered.length) { accum[prop] = accum[prop] || []; accum[prop].push(...filtered); } return accum; }, {}); if (!idDecorators || !Object.keys(idDecorators).length) throw new InternalError("Could not find ID decorated Property"); if (Object.keys(idDecorators).length > 1) throw new InternalError(decoratorValidation.sf(Object.keys(idDecorators).join(", "))); const idProp = Object.keys(idDecorators)[0]; if (!idProp) throw new InternalError("Could not find ID decorated Property"); return { id: idProp, props: idDecorators[idProp][0].props, }; } /** * @summary Returns the primary key value for a {@link Model} * @description searches in all the properties in the object for an {@link pk} decorated property * * @param {Model} model * @param {boolean} [returnEmpty] * @return {string | number | bigint} primary key * * @throws {InternalError} if no property or more than one properties are {@link pk} decorated * @throws {NotFoundError} returnEmpty is false and no value is set on the {@link pk} decorated property * * @function findModelID * * @category managers */ function findModelId(model, returnEmpty = false) { const idProp = findPrimaryKey(model).id; const modelId = model[idProp]; if (typeof modelId === "undefined" && !returnEmpty) throw new InternalError(`No value for the Id is defined under the property ${idProp}`); return modelId; } class BaseRepository { get class() { if (!this._class) throw new InternalError(`No class definition found for this repository`); return this._class; } get pk() { if (!this._pk) { const { id, props } = findPrimaryKey(new this.class()); this._pk = id; this._pkProps = props; } return this._pk; } get pkProps() { if (!this._pkProps) { // eslint-disable-next-line @typescript-eslint/no-unused-vars this.pk; } return this._pkProps; } constructor(clazz) { if (clazz) this._class = clazz; // eslint-disable-next-line @typescript-eslint/no-this-alias const self = this; [this.create, this.read, this.update, this.delete].forEach((m) => { const name = m.name; wrapMethodWithContext(self, self[name + "Prefix"], m, self[name + "Suffix"]); }); } async createAll(models, ...args) { return Promise.all(models.map((m) => this.create(m, ...args))); } async createPrefix(model, ...args) { const contextArgs = await Context.args(exports.OperationKeys.CREATE, this.class, args); model = new this.class(model); await enforceDBDecorators(this, contextArgs.context, model, exports.OperationKeys.CREATE, exports.OperationKeys.ON); return [model, ...contextArgs.args]; } async createSuffix(model, context) { await enforceDBDecorators(this, context, model, exports.OperationKeys.CREATE, exports.OperationKeys.AFTER); return model; } async createAllPrefix(models, ...args) { const contextArgs = await Context.args(exports.OperationKeys.CREATE, this.class, args); await Promise.all(models.map(async (m) => { m = new this.class(m); await enforceDBDecorators(this, contextArgs.context, m, exports.OperationKeys.CREATE, exports.OperationKeys.ON); return m; })); return [models, ...contextArgs.args]; } async createAllSuffix(models, context) { await Promise.all(models.map((m) => enforceDBDecorators(this, context, m, exports.OperationKeys.CREATE, exports.OperationKeys.AFTER))); return models; } async readAll(keys, ...args) { return await Promise.all(keys.map((id) => this.read(id, ...args))); } async readSuffix(model, context) { await enforceDBDecorators(this, context, model, exports.OperationKeys.READ, exports.OperationKeys.AFTER); return model; } async readPrefix(key, ...args) { const contextArgs = await Context.args(exports.OperationKeys.READ, this.class, args); const model = new this.class(); model[this.pk] = key; await enforceDBDecorators(this, contextArgs.context, model, exports.OperationKeys.READ, exports.OperationKeys.ON); return [key, ...contextArgs.args]; } async readAllPrefix(keys, ...args) { const contextArgs = await Context.args(exports.OperationKeys.READ, this.class, args); await Promise.all(keys.map(async (k) => { const m = new this.class(); m[this.pk] = k; return enforceDBDecorators(this, contextArgs.context, m, exports.OperationKeys.READ, exports.OperationKeys.ON); })); return [keys, ...contextArgs.args]; } async readAllSuffix(models, context) { await Promise.all(models.map((m) => enforceDBDecorators(this, context, m, exports.OperationKeys.READ, exports.OperationKeys.AFTER))); return models; } async updateAll(models, ...args) { return Promise.all(models.map((m) => this.update(m, ...args))); } async updateSuffix(model, context) { await enforceDBDecorators(this, context, model, exports.OperationKeys.UPDATE, exports.OperationKeys.AFTER); return model; } async updatePrefix(model, ...args) { const contextArgs = await Context.args(exports.OperationKeys.UPDATE, this.class, args); const id = model[this.pk]; if (!id) throw new InternalError(`No value for the Id is defined under the property ${this.pk}`); const oldModel = await this.read(id); await enforceDBDecorators(this, contextArgs.context, model, exports.OperationKeys.UPDATE, exports.OperationKeys.ON, oldModel); return [model, ...contextArgs.args]; } async updateAllPrefix(models, ...args) { const contextArgs = await Context.args(exports.OperationKeys.UPDATE, this.class, args); await Promise.all(models.map((m) => { m = new this.class(m); enforceDBDecorators(this, contextArgs.context, m, exports.OperationKeys.UPDATE, exports.OperationKeys.ON); return m; })); return [models, ...contextArgs.args]; } async updateAllSuffix(models, context) { await Promise.all(models.map((m) => enforceDBDecorators(this, context, m, exports.OperationKeys.UPDATE, exports.OperationKeys.AFTER))); return models; } async deleteAll(keys, ...args) { return Promise.all(keys.map((k) => this.delete(k, ...args))); } async deleteSuffix(model, context) { await enforceDBDecorators(this, context, model, exports.OperationKeys.DELETE, exports.OperationKeys.AFTER); return model; } async deletePrefix(key, ...args) { const contextArgs = await Context.args(exports.OperationKeys.DELETE, this.class, args); const model = await this.read(key, ...contextArgs.args); await enforceDBDecorators(this, contextArgs.context, model, exports.OperationKeys.DELETE, exports.OperationKeys.ON); return [key, ...contextArgs.args]; } async deleteAllPrefix(keys, ...args) { const contextArgs = await Context.args(exports.OperationKeys.DELETE, this.class, args); const models = await this.readAll(keys, ...contextArgs.args); await Promise.all(models.map(async (m) => { return enforceDBDecorators(this, contextArgs.context, m, exports.OperationKeys.DELETE, exports.OperationKeys.ON); })); return [keys, ...contextArgs.args]; } async deleteAllSuffix(models, context) { await Promise.all(models.map((m) => enforceDBDecorators(this, context, m, exports.OperationKeys.DELETE, exports.OperationKeys.AFTER))); return models; } merge(oldModel, model) { const extract = (model) => Object.entries(model).reduce((accum, [key, val]) => { if (typeof val !== "undefined") accum[key] = val; return accum; }, {}); return new this.class(Object.assign({}, extract(oldModel), extract(model))); } toString() { return `${this.class.name} Repository`; } } class Repository extends BaseRepository { constructor(clazz) { super(clazz); } async createPrefix(model, ...args) { const contextArgs = await Context.args(exports.OperationKeys.CREATE, this.class, args); model = new this.class(model); await enforceDBDecorators(this, contextArgs.context, model, exports.OperationKeys.CREATE, exports.OperationKeys.ON); const errors = model.hasErrors(); if (errors) throw new ValidationError(errors.toString()); return [model, ...contextArgs.args]; } async createAllPrefix(models, ...args) { const contextArgs = await Context.args(exports.OperationKeys.CREATE, this.class, args); await Promise.all(models.map(async (m) => { m = new this.class(m); await enforceDBDecorators(this, contextArgs.context, m, exports.OperationKeys.CREATE, exports.OperationKeys.ON); return m; })); const errors = models .map((m) => m.hasErrors()) .reduce((accum, e, i) => { if (e) accum = typeof accum === "string" ? accum + `\n - ${i}: ${e.toString()}` : ` - ${i}: ${e.toString()}`; return accum; }, undefined); if (errors) throw new ValidationError(errors); return [models, ...contextArgs.args]; } async updatePrefix(model, ...args) { const contextArgs = await Context.args(exports.OperationKeys.UPDATE, this.class, args); const pk = model[this.pk]; if (!pk) throw new InternalError(`No value for the Id is defined under the property ${this.pk}`); const oldModel = await this.read(pk); model = this.merge(oldModel, model); await enforceDBDecorators(this, contextArgs.context, model, exports.OperationKeys.UPDATE, exports.OperationKeys.ON, oldModel); const errors = model.hasErrors(oldModel); if (errors) throw new ValidationError(errors.toString()); return [model, ...contextArgs.args]; } async updateAllPrefix(models, ...args) { const contextArgs = await Context.args(exports.OperationKeys.UPDATE, this.class, args); const ids = models.map((m) => { const id = m[this.pk]; if (typeof id === "undefined") throw new InternalError(`No value for the Id is defined under the property ${this.pk}`); return id; }); const oldModels = await this.readAll(ids, ...contextArgs.args); models = models.map((m, i) => this.merge(oldModels[i], m)); await Promise.all(models.map((m, i) => enforceDBDecorators(this, contextArgs.context, m, exports.OperationKeys.UPDATE, exports.OperationKeys.ON, oldModels[i]))); const errors = models .map((m, i) => m.hasErrors(oldModels[i])) .reduce((accum, e, i) => { if (e) accum = typeof accum === "string" ? accum + `\n - ${i}: ${e.toString()}` : ` - ${i}: ${e.toString()}`; return accum; }, undefined); if (errors) throw new ValidationError(errors); return [models, ...contextArgs.args]; } static key(key) { return DBKeys.REFLECT + key; } } /** * Marks the property as readonly. * * @param {string} [message] the error message. Defaults to {@link DEFAULT_ERROR_MESSAGES.READONLY.INVALID} * * @decorator readonly * * @category Decorators */ function readonly(message = DEFAULT_ERROR_MESSAGES.READONLY.INVALID) { const key = decoratorValidation.Validation.updateKey(DBKeys.READONLY); return decoratorValidation.Decoration.for(key) .define(decoratorValidation.propMetadata(key, { message: message, })) .apply(); } async function timestampHandler(context, data, key, model) { model[key] = context.timestamp; } /** * Marks the property as timestamp. * Makes it {@li