@decaf-ts/db-decorators
Version:
Agnostic database decorators and repository
1,303 lines (1,284 loc) • 243 kB
JavaScript
(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