import {
Model,
ModelConditionalAsync,
validate,
} from "@decaf-ts/decorator-validation";
import { validateCompare } from "../model/validation";
import { Constructor, Metadata } from "@decaf-ts/decoration";
import { DBKeys } from "../model/constants";
import { ModelOperations } from "../operations/constants";
import { SerializationError } from "../repository/errors";
import { ComposedFromMetadata } from "../model/decorators";
Model.prototype.isTransient = function (): boolean {
return Metadata.isTransient(this);
};
/**
* @description Validates the model and checks for errors
* @summary Validates the current model state and optionally compares with a previous version
* @template M - Type extending Model
* @param {M|any} [previousVersion] - Optional previous version of the model for comparison
* @param {...any[]} exclusions - Properties to exclude from validation
* @return {ModelErrorDefinition|undefined} Error definition if validation fails, undefined otherwise
* @function hasErrors
* @memberOf module:db-decorators
*/
Model.prototype.hasErrors = function <M extends Model<true | false>>(
this: M,
previousVersion?: M | any,
...exclusions: any[]
): ModelConditionalAsync<M> {
if (previousVersion && !(previousVersion instanceof Model)) {
exclusions.unshift(previousVersion);
previousVersion = undefined;
}
const async = this.isAsync();
const errs = validate(this, async, ...exclusions);
if (async) {
return Promise.resolve(errs).then((resolvedErrs) => {
if (resolvedErrs || !previousVersion) {
return resolvedErrs;
}
return validateCompare(previousVersion, this, async, ...exclusions);
}) as any;
}
if (errs || !previousVersion) return errs as any;
// @ts-expect-error Overriding Model prototype method with dynamic conditional return type.
return validateCompare(previousVersion, this, async, ...exclusions);
};
Model.prototype.segregate = function segregate<M extends Model>(
this: M
): { model: M; transient?: Record<keyof M, M[keyof M]> } {
return Model.segregate(this);
};
(Model as any).segregate = function segregate<M extends Model>(
model: M
): { model: M; transient?: Record<keyof M, M[keyof M]> } {
if (!Metadata.isTransient(model)) return { model: model };
const decoratedProperties = Metadata.validatableProperties(
model.constructor as any
);
const transientProps = Metadata.get(
model.constructor as any,
DBKeys.TRANSIENT
);
const result = {
model: {} as Record<string, any>,
transient: {} as Record<string, any>,
};
for (const key of decoratedProperties) {
const isTransient = Object.keys(transientProps).includes(key);
if (isTransient) {
result.transient = result.transient || {};
try {
result.transient[key] = model[key as keyof M];
} catch (e: unknown) {
throw new SerializationError(
`Failed to serialize transient property ${key}: ${e}`
);
}
} else {
result.model = result.model || {};
result.model[key] = (model as Record<string, any>)[key];
}
}
result.model = Model.build(result.model, model.constructor.name);
return result as { model: M; transient?: Record<keyof M, M[keyof M]> };
};
(Model as any).pk = function <M extends Model>(
model: M | Constructor<M>,
keyValue = false
) {
if (!model) throw new Error("No model was provided");
const constr = model instanceof Model ? model.constructor : model;
const idProp = Metadata.get(constr as Constructor, DBKeys.ID);
if (!idProp) {
throw new Error(
`No Id property defined for model ${constr?.name || "Unknown Model"}`
);
}
const key = Object.keys(idProp)[0] as keyof M;
if (!keyValue) return key;
if (model instanceof Model) return model[key as keyof M];
throw new Error("Cannot get the value of the pk from the constructor");
}.bind(Model);
(Model as any).pkProps = function <M extends Model>(
model: Constructor<M>
): any {
return Metadata.get(
model,
Metadata.key(DBKeys.ID, Model.pk(model) as string)
);
}.bind(Model);
(Model as any).isTransient = function isTransient<M extends Model>(
model: M | Constructor<M>
): boolean {
return Metadata.isTransient(model);
}.bind(Model);
(Model as any).composed = function composed<M extends Model<boolean>>(
model: Constructor<M> | M,
prop?: keyof M
): boolean | ComposedFromMetadata | undefined {
const constr =
model instanceof Model ? (model.constructor as Constructor<M>) : model;
if (prop)
return Metadata.get(constr, Metadata.key(DBKeys.COMPOSED, prop as string));
return !!Metadata.get(constr, DBKeys.COMPOSED);
}.bind(Model);
/**
* @description Merges two model instances into a new instance.
* @summary Creates a new model instance by combining properties from an old model and a new model.
* Properties from the new model override properties from the old model if they are defined.
* @template {M} - Type extending Model
* @param {M} oldModel - The original model instance
* @param {M} model - The new model instance with updated properties
* @return {M} A new model instance with merged properties
*/
(Model as any).merge = function merge<M extends Model>(
oldModel: M,
newModel: M,
constructor?: Constructor<M>
): M {
constructor = constructor || (oldModel.constructor as Constructor<M>);
const extract = (model: M) =>
Object.entries(model).reduce((accum: Record<string, any>, [key, val]) => {
if (typeof val !== "undefined") accum[key] = val;
return accum;
}, {});
return new constructor(
Object.assign({}, extract(oldModel), extract(newModel))
);
}.bind(Model);
(Metadata as any).saveOperation = function <M extends Model>(
model: Constructor<M>,
propertyKey: string,
operation: string,
metadata: any
) {
if (!propertyKey) return;
Metadata.set(
model,
Metadata.key(ModelOperations.OPERATIONS, propertyKey, operation),
metadata
);
}.bind(Metadata);
(Metadata as any).readOperation = function <M extends Model>(
model: Constructor<M>,
propertyKey?: string,
operation?: string
) {
if (!propertyKey || !operation) return;
return Metadata.get(
model,
Metadata.key(ModelOperations.OPERATIONS, propertyKey, operation)
);
}.bind(Metadata);
(Metadata as any).isTransient = function isTransient<M extends Model>(
model: M | Constructor<M>
): boolean {
return !!Metadata.get(
typeof model !== "function" ? (model.constructor as any) : model,
DBKeys.TRANSIENT
);
}.bind(Metadata);
Source