@decaf-ts/db-decorators
Version:
Agnostic database decorators and repository
278 lines • 13.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.getValidatableUpdateProps = getValidatableUpdateProps;
exports.validateDecorator = validateDecorator;
exports.validateDecorators = validateDecorators;
exports.validateCompare = validateCompare;
const decorator_validation_1 = require("@decaf-ts/decorator-validation");
const decoration_1 = require("@decaf-ts/decoration");
/**
* @description
* Retrieves validation decorator definitions from a model for update operations, including
* support for special handling of list decorators.
*
* @summary
* Iterates over the model's own enumerable properties and filters out those specified in the
* `propsToIgnore` array. For each remaining property, retrieves validation decorators specific
* to update operations using the `UpdateValidationKeys.REFLECT` key. Additionally, it explicitly
* checks for and appends any `LIST` type decorators to ensure proper validation of collection types.
*
* @template M - A generic parameter extending the `Model` class, representing the model type being inspected.
*
* @param {M} model - The model instance whose properties are being inspected for update-related validations.
* @param {string[]} propsToIgnore - A list of property names to exclude from the validation decorator retrieval process.
*
* @return {ValidationPropertyDecoratorDefinition[]} An array of validation decorator definitions, including both
* update-specific and list-type decorators, excluding those for ignored properties.
*
* @function getValidatableUpdateProps
*/
function getValidatableUpdateProps(model, propsToIgnore) {
const decoratedProperties = [];
for (const prop in model) {
if (!Object.prototype.hasOwnProperty.call(model, prop) ||
propsToIgnore.includes(prop))
continue;
const decorators = decoration_1.Metadata.validationFor(model.constructor, prop) || {};
// Intentionally leaving this part commented out until all tests are complete
// const listDecorator = getValidationDecorators(model, prop).decorators.find(
// ({ key }) => key === ValidationKeys.LIST
// );
// if (listDecorator)
// validationPropertyDefinition.decorators.push(listDecorator);
decoratedProperties.push({ prop, decorators });
}
return decoratedProperties;
}
function validateDecorator(newModel, oldModel, prop, decorator, async) {
const validator = decorator_validation_1.Validation.get(decorator.key);
if (!validator) {
throw new Error(`Missing validator for ${decorator.key}`);
}
// Skip validators that aren't UpdateValidators
if (!validator.updateHasErrors)
return (0, decorator_validation_1.toConditionalPromise)(undefined, async);
// skip async decorators if validateDecorators is called synchronously (async = false)
if (!async && decorator.async)
return (0, decorator_validation_1.toConditionalPromise)(undefined, async);
const decoratorProps = Object.values(decorator) || {};
// const context = PathProxyEngine.create(obj, {
// ignoreUndefined: true,
// ignoreNull: true,
// });
const maybeError = validator.updateHasErrors(newModel[prop], oldModel[prop], ...decoratorProps);
return (0, decorator_validation_1.toConditionalPromise)(maybeError, async);
}
function validateDecorators(newModel, oldModel, prop, decorators, async) {
const result = {};
for (const decoratorKey in decorators) {
const decorator = { ...decorators[decoratorKey], key: decoratorKey };
// skip async decorators if validateDecorators is called synchronously (async = false)
if (!async && decorator.async)
continue;
let validationErrors = validateDecorator(newModel, oldModel, prop, decorator, async);
/*
If the decorator is a list, each element must be checked.
When 'async' is true, the 'err' will always be a pending promise initially,
so the '!err' check will evaluate to false (even if the promise later resolves with no errors)
*/
if (decorator.key === decorator_validation_1.ValidationKeys.LIST && (!validationErrors || async)) {
const newPropValue = newModel[prop];
const oldPropValue = oldModel[prop];
const newValues = newPropValue instanceof Set ? [...newPropValue] : newPropValue;
const oldValues = oldPropValue instanceof Set ? [...oldPropValue] : oldPropValue;
if (newValues?.length) {
const types = decorator.class || decorator.clazz || decorator.customTypes;
const allowedTypes = [types].flat().map((t) => {
t = typeof t === "function" && !t.name ? t() : t;
t = t.name ? t.name : t;
return String(t).toLowerCase();
});
const errs = newValues.map((childValue) => {
if (decorator_validation_1.Model.isModel(childValue)) {
// find by id so the list elements order doesn't matter
const id = decorator_validation_1.Model.pk(childValue, true);
if (!id)
return "Failed to find model id";
const oldListModel = oldValues.find((el) => id === decorator_validation_1.Model.pk(el, true));
return childValue.hasErrors(oldListModel);
}
return allowedTypes.includes(typeof childValue)
? undefined
: "Value has no validatable type";
});
if (async) {
validationErrors = Promise.all(errs).then((result) => {
const allEmpty = result.every((r) => !r);
return allEmpty ? undefined : result;
});
}
else {
const allEmpty = errs.every((r) => !r);
validationErrors = errs.length > 0 && !allEmpty ? errs : undefined;
}
}
}
if (validationErrors)
result[decorator.key] = validationErrors;
}
if (!async)
return Object.keys(result).length > 0 ? result : undefined;
const keys = Object.keys(result);
const promises = Object.values(result);
return Promise.all(promises).then((resolvedValues) => {
const res = {};
for (let i = 0; i < resolvedValues.length; i++) {
const val = resolvedValues[i];
if (val !== undefined) {
res[keys[i]] = val;
}
}
return Object.keys(res).length > 0 ? res : undefined;
});
}
/**
* @description Validates changes between two model versions
* @summary Compares an old and new model version to validate update operations
* @template M - Type extending Model
* @param {M} oldModel - The original model version
* @param {M} newModel - The updated model version
* @param {boolean} async - A flag indicating whether validation should be asynchronous.
* @param {...string[]} propsToIgnore - Properties to exclude from validation
* @return {ModelErrorDefinition|undefined} Error definition if validation fails, undefined otherwise
* @function validateCompare
* @memberOf module:db-decorators
* @mermaid
* sequenceDiagram
* participant Caller
* participant validateCompare
* participant Reflection
* participant Validation
*
* Caller->>validateCompare: oldModel, newModel, propsToIgnore
* validateCompare->>Reflection: get decorated properties
* Reflection-->>validateCompare: property decorators
* loop For each decorated property
* validateCompare->>Validation: get validator
* Validation-->>validateCompare: validator
* validateCompare->>validateCompare: validate property update
* end
* loop For nested models
* validateCompare->>validateCompare: validate nested models
* end
* validateCompare-->>Caller: validation errors or undefined
*/
function validateCompare(oldModel, newModel, async, ...propsToIgnore) {
const ValidatableUpdateProps = getValidatableUpdateProps(newModel, propsToIgnore);
const result = {};
const nestedErrors = {};
for (const { prop, decorators } of ValidatableUpdateProps) {
const propKey = String(prop);
const propValue = newModel[prop];
const { designTypes } = decoration_1.Metadata.getPropDesignTypes(newModel.constructor, prop);
if (!designTypes)
continue;
// Handle array or Set types and enforce the presence of @list decorator
if (designTypes.some((t) => [Array.name, Set.name].includes(t))) {
if (!decorators ||
!Object.keys(decorators).includes(decorator_validation_1.ValidationKeys.LIST)) {
result[propKey] = {
[decorator_validation_1.ValidationKeys.TYPE]: `Array or Set property '${propKey}' requires a @list decorator`,
};
continue;
}
if (propValue &&
!(Array.isArray(propValue) || propValue instanceof Set)) {
result[propKey] = {
[decorator_validation_1.ValidationKeys.TYPE]: `Property '${String(prop)}' must be either an Array or a Set`,
};
continue;
}
}
// TODO: Check validateDecorators method partially working. Complete check pending.
const propErrors = validateDecorators(newModel, oldModel, propKey, decorators, async) || {};
// Check for nested model.
// To prevent unnecessary processing, "propValue" must be defined
const isConstr = decorator_validation_1.Model.isPropertyModel(newModel, propKey);
const hasPropValue = propValue !== null && propValue !== undefined;
if (hasPropValue && isConstr) {
const instance = propValue;
const Constr = designTypes
.map((d) => decorator_validation_1.Model.get(d.name || d))
.find((d) => !!d);
const designTypeNames = designTypes.map((d) => {
if (typeof d === "function")
return d.name ? d.name.toLowerCase() : d()?.name.toLowerCase();
return d.toLowerCase();
});
// Ensure instance is of the expected model class.
if (!Constr || !(propValue instanceof Constr)) {
if (designTypeNames.includes(typeof propValue)) {
// do nothing
}
else {
// If types don't match throw an error
propErrors[decorator_validation_1.ValidationKeys.TYPE] = !Constr
? `Unable to verify type consistency, missing model registry for ${designTypes.toString()} on prop ${propKey}`
: `Value must be an instance of ${Constr.name}`;
delete propErrors[decorator_validation_1.ModelKeys.TYPE]; // remove duplicate type error
}
}
else {
const nestedPropsToIgnore = (0, decorator_validation_1.getChildNestedPropsToIgnore)(propKey, ...propsToIgnore);
nestedErrors[propKey] = instance.hasErrors(oldModel[prop], ...nestedPropsToIgnore);
}
}
// Add to the result if we have any errors
// Async mode returns a Promise that resolves to undefined when no errors exist
if (Object.keys(propErrors).length > 0 || async)
result[propKey] = propErrors;
// Then merge any nested errors
if (!async) {
Object.entries(nestedErrors[propKey] || {}).forEach(([key, error]) => {
if (error !== undefined) {
result[`${propKey}.${key}`] = error;
}
});
}
}
// Synchronous return
if (!async) {
return (Object.keys(result).length > 0
? new decorator_validation_1.ModelErrorDefinition(result)
: undefined);
}
const merged = result; // TODO: apply filtering
const keys = Object.keys(merged);
const promises = Object.values(merged);
return Promise.allSettled(promises).then(async (results) => {
const result = {};
for (const [parentProp, nestedErrPromise] of Object.entries(nestedErrors)) {
const nestedPropDecErrors = (await nestedErrPromise);
if (nestedPropDecErrors)
Object.entries(nestedPropDecErrors).forEach(([nestedProp, nestedPropDecError]) => {
if (nestedPropDecError !== undefined) {
const nestedKey = [parentProp, nestedProp].join(".");
result[nestedKey] = nestedPropDecError;
}
});
}
for (let i = 0; i < results.length; i++) {
const key = keys[i];
const res = results[i];
if (res.status === "fulfilled" && res.value !== undefined) {
result[key] = res.value;
}
else if (res.status === "rejected") {
result[key] =
res.reason instanceof Error
? res.reason.message
: String(res.reason || "Validation failed");
}
}
return Object.keys(result).length > 0
? new decorator_validation_1.ModelErrorDefinition(result)
: undefined;
});
}
//# sourceMappingURL=validation.js.map