import {
Model,
ModelErrorDefinition,
ModelErrors,
ModelKeys,
ReservedModels,
sf,
Validatable,
Validation,
ValidationKeys,
ValidationPropertyDecoratorDefinition,
} from "@decaf-ts/decorator-validation";
import { DecoratorMetadata, Reflection } from "@decaf-ts/reflection";
import { UpdateValidationKeys, UpdateValidator } from "../validation";
import { findModelId } from "../identity";
/**
* @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 {...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<M extends Model>(
oldModel: M,
newModel: M,
...exceptions: string[]
): ModelErrorDefinition | undefined {
const decoratedProperties: ValidationPropertyDecoratorDefinition[] = [];
for (const prop in newModel)
if (
Object.prototype.hasOwnProperty.call(newModel, prop) &&
exceptions.indexOf(prop) === -1
)
decoratedProperties.push(
Reflection.getPropertyDecorators(
UpdateValidationKeys.REFLECT,
newModel,
prop
) as ValidationPropertyDecoratorDefinition
);
let result: ModelErrors | undefined = undefined;
for (const decoratedProperty of decoratedProperties) {
const { prop, decorators } = decoratedProperty;
decorators.shift(); // remove the design:type decorator, since the type will already be checked
if (!decorators || !decorators.length) continue;
let errs: Record<string, string | undefined> | undefined = undefined;
for (const decorator of decorators) {
const validator: UpdateValidator = Validation.get(
decorator.key
) as UpdateValidator;
if (!validator) {
console.error(
`Could not find Matching validator for ${decorator.key} for property ${String(decoratedProperty.prop)}`
);
continue;
}
const err: string | undefined = validator.updateHasErrors(
(newModel as any)[prop.toString()],
(oldModel as any)[prop.toString()],
...Object.values(decorator.props)
);
if (err) {
errs = errs || {};
errs[decorator.key] = err;
}
}
if (errs) {
result = result || {};
result[decoratedProperty.prop.toString()] = errs;
}
}
// tests nested classes
for (const prop of Object.keys(newModel).filter((k) => {
if (exceptions.includes(k)) return false;
return !result || !result[k];
})) {
let err: string | undefined;
// if a nested Model
const allDecorators = Reflection.getPropertyDecorators(
ValidationKeys.REFLECT,
newModel,
prop
).decorators;
const decorators = Reflection.getPropertyDecorators(
ValidationKeys.REFLECT,
newModel,
prop
).decorators.filter(
(d) => [ModelKeys.TYPE, ValidationKeys.TYPE].indexOf(d.key as any) !== -1
);
if (!decorators || !decorators.length) continue;
const dec = decorators.pop() as DecoratorMetadata;
const clazz = dec.props.name
? [dec.props.name]
: Array.isArray(dec.props.customTypes)
? dec.props.customTypes
: [dec.props.customTypes];
const reserved = Object.values(ReservedModels).map((v) =>
v.toLowerCase()
) as string[];
for (const c of clazz) {
if (reserved.indexOf(c.toLowerCase()) === -1) {
switch (c) {
case Array.name:
case Set.name:
if (allDecorators.length) {
const listDec = allDecorators.find(
(d) => d.key === ValidationKeys.LIST
);
if (listDec) {
let currentList, oldList;
switch (c) {
case Array.name:
currentList = (newModel as Record<string, any>)[prop];
oldList = (oldModel as Record<string, any>)[prop];
break;
case Set.name:
currentList = (newModel as Record<string, any>)[
prop
].values();
oldList = (oldModel as Record<string, any>)[prop].values();
break;
default:
throw new Error(`Invalid attribute type ${c}`);
}
err = currentList
.map((v: Validatable) => {
const id = findModelId(v as any, true);
if (!id) return "Failed to find model id";
const oldModel = oldList.find(
(el: any) => id === findModelId(el, true)
);
if (!oldModel) return; // nothing to compare with
return v.hasErrors(oldModel);
})
.filter((e: any) => !!e) as any;
if (!err?.length) {
// if the result is an empty list...
err = undefined;
}
}
}
break;
default:
try {
if (
(newModel as Record<string, any>)[prop] &&
(oldModel as Record<string, any>)[prop]
)
err = (newModel as Record<string, any>)[prop].hasErrors(
(oldModel as Record<string, any>)[prop]
);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e: any) {
console.warn(sf("Model should be validatable but its not"));
}
}
}
if (err) {
result = result || {};
result[prop] = err as any;
}
}
}
return result ? new ModelErrorDefinition(result) : undefined;
}
Source