UNPKG

@decaf-ts/db-decorators

Version:

Agnostic database decorators and repository

278 lines 13.6 kB
"use strict"; 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