UNPKG

@decaf-ts/db-decorators

Version:

Agnostic database decorators and repository

268 lines 12.7 kB
import { getValidationDecorators, Model, ModelErrorDefinition, ModelKeys, toConditionalPromise, Validation, ValidationKeys, } from "@decaf-ts/decorator-validation"; import { Reflection } from "@decaf-ts/reflection"; import { UpdateValidationKeys } from "./../validation/index.js"; import { findModelId } from "./../identity/index.js"; /** * @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 */ export function getValidatableUpdateProps(model, propsToIgnore) { const decoratedProperties = []; for (const prop in model) { if (Object.prototype.hasOwnProperty.call(model, prop) && !propsToIgnore.includes(prop)) { const validationPropertyDefinition = getValidationDecorators(model, prop, UpdateValidationKeys.REFLECT); const listDecorator = getValidationDecorators(model, prop).decorators.find(({ key }) => key === ValidationKeys.LIST); if (listDecorator) validationPropertyDefinition.decorators.push(listDecorator); decoratedProperties.push(validationPropertyDefinition); } } return decoratedProperties; } export function validateDecorator(newModel, oldModel, prop, decorator, async) { const validator = Validation.get(decorator.key); if (!validator) { throw new Error(`Missing validator for ${decorator.key}`); } // Skip validators that aren't UpdateValidators if (!validator.updateHasErrors) return toConditionalPromise(undefined, async); // skip async decorators if validateDecorators is called synchronously (async = false) if (!async && decorator.props.async) return toConditionalPromise(undefined, async); const decoratorProps = Object.values(decorator.props) || {}; // const context = PathProxyEngine.create(obj, { // ignoreUndefined: true, // ignoreNull: true, // }); const maybeError = validator.updateHasErrors(newModel[prop], oldModel[prop], ...decoratorProps); return toConditionalPromise(maybeError, async); } export function validateDecorators(newModel, oldModel, prop, decorators, async) { const result = {}; for (const decorator of decorators) { // skip async decorators if validateDecorators is called synchronously (async = false) if (!async && decorator.props.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 === 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 && newValues.length > 0) { const types = decorator.props.class || decorator.props.clazz || decorator.props.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) => { // find by id so the list elements order doesn't matter const id = findModelId(childValue, true); if (!id) return "Failed to find model id"; const oldModel = oldValues.find((el) => id === findModelId(el, true)); if (Model.isModel(childValue)) { return childValue.hasErrors(oldModel); } 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[]} exceptions - 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, exceptions * 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 */ export function validateCompare(oldModel, newModel, async, ...exceptions) { const decoratedProperties = getValidatableUpdateProps(newModel, exceptions); const result = {}; const nestedErrors = {}; for (const { prop, decorators } of decoratedProperties) { const propKey = String(prop); let propValue = newModel[prop]; if (!decorators?.length) continue; // Get the default type validator const designTypeDec = decorators.find((d) => [ModelKeys.TYPE, ValidationKeys.TYPE].includes(d.key)); if (!designTypeDec) continue; const designType = designTypeDec.props.name; // Handle array or Set types and enforce the presence of @list decorator if ([Array.name, Set.name].includes(designType)) { const { decorators } = Reflection.getPropertyDecorators(ValidationKeys.REFLECT, newModel, propKey); if (!decorators.some((d) => d.key === ValidationKeys.LIST)) { result[propKey] = { [ValidationKeys.TYPE]: `Array or Set property '${propKey}' requires a @list decorator`, }; continue; } if (propValue && !(Array.isArray(propValue) || propValue instanceof Set)) { result[propKey] = { [ValidationKeys.TYPE]: `Property '${String(prop)}' must be either an array or a Set`, }; continue; } // Remove design:type decorator, since @list decorator already ensures type for (let i = decorators.length - 1; i >= 0; i--) { if (decorators[i].key === ModelKeys.TYPE) { decorators.splice(i, 1); } } propValue = propValue instanceof Set ? [...propValue] : propValue; } const propErrors = validateDecorators(newModel, oldModel, propKey, decorators, async) || {}; // Check for nested properties. // To prevent unnecessary processing, "propValue" must be defined and validatable const isConstr = Model.isPropertyModel(newModel, propKey); // if propValue !== undefined, null if (propValue && isConstr) { const instance = propValue; const isInvalidModel = typeof instance !== "object" || !instance.hasErrors || typeof instance.hasErrors !== "function"; if (isInvalidModel) { // propErrors[ValidationKeys.TYPE] = // "Model should be validatable but it's not."; console.warn("Model should be validatable but it's not."); } else { nestedErrors[propKey] = instance.hasErrors(oldModel[prop]); } } // 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 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 ModelErrorDefinition(result) : undefined; }); } //# sourceMappingURL=validation.js.map