@decaf-ts/decorator-validation
Version:
simple decorator based validation engine
396 lines • 15.9 kB
JavaScript
import { Model } from "./Model.js";
import { ObjectAccumulator } from "typed-object-accumulator";
import { DecorationKeys, prop, description, } from "@decaf-ts/decoration";
import { model } from "./decorators.js";
import { date, diff, email, eq, gt, gte, list, lt, lte, max, maxlength, min, minlength, option, password, pattern, required, step, type, url, } from "./../validation/decorators.js";
import { ValidationKeys } from "./../validation/Validators/constants.js";
import { Validation } from "./../validation/Validation.js";
export class AttributeBuilder {
constructor(parent, attr, declaredType) {
this.parent = parent;
this.attr = attr;
this.declaredType = declaredType;
this.decorators = [];
}
decorate(...decorators) {
for (const decorator of decorators) {
if (this.decorators.includes(decorator))
throw new Error(`Decorator "${decorator}" has already been used`);
this.decorators.push(decorator);
}
return this.parent;
}
undecorate(...decorators) {
for (const decorator of decorators) {
const index = this.decorators.indexOf(decorator);
if (index < 0)
throw new Error(`Decorator "${decorator}" is not applied to ${this.attr}`);
this.decorators.splice(index, 1);
}
return this.parent;
}
required(messageOrMeta) {
const meta = AttributeBuilder.asMeta(messageOrMeta);
const message = typeof messageOrMeta === "string"
? messageOrMeta
: AttributeBuilder.resolveMessage(meta);
return this.decorate(required(message));
}
min(valueOrMeta, message) {
const meta = AttributeBuilder.asMeta(valueOrMeta);
const value = meta?.[ValidationKeys.MIN] ??
(meta ? undefined : valueOrMeta);
if (value === undefined)
throw new Error(`Missing ${ValidationKeys.MIN} for ${String(this.attr)}`);
return this.decorate(min(value, AttributeBuilder.resolveMessage(meta, message)));
}
max(valueOrMeta, message) {
const meta = AttributeBuilder.asMeta(valueOrMeta);
const value = meta?.[ValidationKeys.MAX] ??
(meta ? undefined : valueOrMeta);
if (value === undefined)
throw new Error(`Missing ${ValidationKeys.MAX} for ${String(this.attr)}`);
return this.decorate(max(value, AttributeBuilder.resolveMessage(meta, message)));
}
step(valueOrMeta, message) {
const meta = AttributeBuilder.asMeta(valueOrMeta);
const value = meta?.[ValidationKeys.STEP] ??
(meta ? undefined : valueOrMeta);
if (value === undefined)
throw new Error(`Missing ${ValidationKeys.STEP} for ${String(this.attr)}`);
return this.decorate(step(value, AttributeBuilder.resolveMessage(meta, message)));
}
minlength(valueOrMeta, message) {
const meta = AttributeBuilder.asMeta(valueOrMeta);
const value = meta?.[ValidationKeys.MIN_LENGTH] ??
(meta ? undefined : valueOrMeta);
if (value === undefined)
throw new Error(`Missing ${ValidationKeys.MIN_LENGTH} for ${String(this.attr)}`);
return this.decorate(minlength(value, AttributeBuilder.resolveMessage(meta, message)));
}
maxlength(valueOrMeta, message) {
const meta = AttributeBuilder.asMeta(valueOrMeta);
const value = meta?.[ValidationKeys.MAX_LENGTH] ??
(meta ? undefined : valueOrMeta);
if (value === undefined)
throw new Error(`Missing ${ValidationKeys.MAX_LENGTH} for ${String(this.attr)}`);
return this.decorate(maxlength(value, AttributeBuilder.resolveMessage(meta, message)));
}
pattern(valueOrMeta, message) {
const meta = AttributeBuilder.asMeta(valueOrMeta);
const rawPattern = meta?.[ValidationKeys.PATTERN] ??
(meta ? undefined : valueOrMeta);
const regex = AttributeBuilder.patternFromString(rawPattern) ?? /.*/;
return this.decorate(pattern(regex, AttributeBuilder.resolveMessage(meta, message)));
}
email(messageOrMeta) {
const meta = AttributeBuilder.asMeta(messageOrMeta);
const message = typeof messageOrMeta === "string"
? messageOrMeta
: AttributeBuilder.resolveMessage(meta);
return this.decorate(email(message));
}
url(messageOrMeta) {
const meta = AttributeBuilder.asMeta(messageOrMeta);
const message = typeof messageOrMeta === "string"
? messageOrMeta
: AttributeBuilder.resolveMessage(meta);
return this.decorate(url(message));
}
type(valueOrMeta, message) {
const meta = AttributeBuilder.asMeta(valueOrMeta);
const types = meta?.customTypes ?? meta?.type ?? (meta ? undefined : valueOrMeta);
return this.decorate(type(types, AttributeBuilder.resolveMessage(meta, message)));
}
date(formatOrMeta, message) {
const meta = AttributeBuilder.asMeta(formatOrMeta);
const format = meta?.[ValidationKeys.FORMAT] ??
(meta ? undefined : formatOrMeta);
return this.decorate(date(format, AttributeBuilder.resolveMessage(meta, message)));
}
password(valueOrMeta, message) {
const meta = AttributeBuilder.asMeta(valueOrMeta);
const rawPattern = meta?.[ValidationKeys.PATTERN] ??
(meta ? undefined : valueOrMeta);
const regex = AttributeBuilder.patternFromString(rawPattern);
return this.decorate(password(regex, AttributeBuilder.resolveMessage(meta, message)));
}
list(clazzOrMeta, collection, message) {
const meta = AttributeBuilder.asMeta(clazzOrMeta);
const clazz = meta?.clazz ?? (meta ? undefined : clazzOrMeta);
const typeOfCollection = meta?.type ?? collection;
return this.decorate(list(clazz, typeOfCollection, AttributeBuilder.resolveMessage(meta, message)));
}
set(clazzOrMeta, message) {
if (AttributeBuilder.isMetadataPayload(clazzOrMeta))
return this.list(clazzOrMeta);
return this.list(clazzOrMeta, "Set", message);
}
enum(valueOrMeta, message) {
const meta = AttributeBuilder.asMeta(valueOrMeta);
const values = meta?.[ValidationKeys.ENUM] ?? (meta ? undefined : valueOrMeta);
return this.decorate(option(values, AttributeBuilder.resolveMessage(meta, message)));
}
option(value, message) {
return this.enum(value, message);
}
static isMetadataPayload(value) {
if (!value)
return false;
if (value instanceof Date)
return false;
if (value instanceof RegExp)
return false;
if (Array.isArray(value))
return false;
return typeof value === "object";
}
static asMeta(value) {
return AttributeBuilder.isMetadataPayload(value)
? value
: undefined;
}
static resolveMessage(meta, fallback) {
return meta?.message ?? fallback;
}
static patternFromString(pattern) {
if (!pattern)
return undefined;
if (pattern instanceof RegExp)
return pattern;
const match = pattern.match(/^\/(.+)\/([gimsuy]*)$/);
if (match)
return new RegExp(match[1], match[2]);
return new RegExp(pattern);
}
resolveComparison(propertyOrMeta, key, options) {
const meta = AttributeBuilder.asMeta(propertyOrMeta);
if (meta) {
return {
target: meta[key],
options: {
label: meta.label,
message: meta.message,
},
};
}
return { target: propertyOrMeta, options };
}
equals(propertyOrMeta, options) {
const { target, options: resolvedOptions } = this.resolveComparison(propertyOrMeta, ValidationKeys.EQUALS, options);
return this.decorate(eq(target, resolvedOptions));
}
eq(propertyOrMeta, options) {
return this.equals(propertyOrMeta, options);
}
different(propertyOrMeta, options) {
const { target, options: resolvedOptions } = this.resolveComparison(propertyOrMeta, ValidationKeys.DIFF, options);
return this.decorate(diff(target, resolvedOptions));
}
diff(propertyOrMeta, options) {
return this.different(propertyOrMeta, options);
}
lessThan(propertyOrMeta, options) {
const { target, options: resolvedOptions } = this.resolveComparison(propertyOrMeta, ValidationKeys.LESS_THAN, options);
return this.decorate(lt(target, resolvedOptions));
}
lt(propertyOrMeta, options) {
return this.lessThan(propertyOrMeta, options);
}
lessThanOrEqual(propertyOrMeta, options) {
const { target, options: resolvedOptions } = this.resolveComparison(propertyOrMeta, ValidationKeys.LESS_THAN_OR_EQUAL, options);
return this.decorate(lte(target, resolvedOptions));
}
lte(propertyOrMeta, options) {
return this.lessThanOrEqual(propertyOrMeta, options);
}
greaterThan(propertyOrMeta, options) {
const { target, options: resolvedOptions } = this.resolveComparison(propertyOrMeta, ValidationKeys.GREATER_THAN, options);
return this.decorate(gt(target, resolvedOptions));
}
gt(propertyOrMeta, options) {
return this.greaterThan(propertyOrMeta, options);
}
greaterThanOrEqual(propertyOrMeta, options) {
const { target, options: resolvedOptions } = this.resolveComparison(propertyOrMeta, ValidationKeys.GREATER_THAN_OR_EQUAL, options);
return this.decorate(gte(target, resolvedOptions));
}
gte(propertyOrMeta, options) {
return this.greaterThanOrEqual(propertyOrMeta, options);
}
description(desc) {
return this.decorate(description(desc));
}
/**
* Applies the attribute metadata and decorators to the provided constructor.
*/
build(constructor) {
const target = constructor.prototype;
const propKey = this.attr;
if (!Object.getOwnPropertyDescriptor(target, propKey)) {
Object.defineProperty(target, propKey, {
configurable: true,
enumerable: true,
writable: true,
value: undefined,
});
}
if (this.declaredType) {
Reflect.defineMetadata(DecorationKeys.DESIGN_TYPE, this.declaredType, target, propKey);
}
prop()(target, propKey);
this.decorators.forEach((decorator) => {
try {
decorator(target, propKey);
}
catch (e) {
throw new Error(`Failed to apply decorator to property "${this.attr}": ${e}`);
}
});
}
}
class ListAttributeBuilder {
constructor(parent, attribute, collection) {
this.parent = parent;
this.attribute = attribute;
this.collection = collection;
}
ofPrimitives(clazz, message) {
this.attribute.list(clazz, this.collection, message);
return this.parent;
}
ofModel() {
const nestedBuilder = ModelBuilder.builder();
const originalBuild = nestedBuilder.build;
let cachedConstructor;
const factory = (() => {
return function () {
if (!cachedConstructor) {
cachedConstructor = Reflect.apply(originalBuild, nestedBuilder, []);
}
return cachedConstructor;
};
})();
this.attribute.list(factory, this.collection);
nestedBuilder.build = new Proxy(originalBuild, {
apply: (target, thisArg, argArray) => {
cachedConstructor = Reflect.apply(target, thisArg, argArray);
return this.parent;
},
});
return nestedBuilder;
}
}
export class ModelBuilder extends ObjectAccumulator {
constructor() {
super(...arguments);
this.attributes = new Map();
}
setName(name) {
this._name = name;
return this;
}
description(desc) {
this._description = desc;
return this;
}
attribute(attr, type) {
const existing = this.attributes.get(attr);
if (existing) {
if (existing.declaredType !== type)
throw new Error(`Attribute "${String(attr)}" already exists with a different type`);
return existing;
}
const attributeBuilder = new AttributeBuilder(this, attr, type);
this.attributes.set(attr, attributeBuilder);
return attributeBuilder;
}
string(attr) {
return this.attribute(attr, String);
}
number(attr) {
return this.attribute(attr, Number);
}
date(attr) {
return this.attribute(attr, Date);
}
bigint(attr) {
return this.attribute(attr, BigInt);
}
instance(clazz, attr) {
return this.attribute(attr, clazz);
}
model(attr) {
const mm = new ModelBuilder();
mm.build = new Proxy(mm.build, {
apply: (target, thisArg, argArray) => {
const built = Reflect.apply(target, thisArg, argArray);
return this.instance(built, attr);
},
});
return mm;
}
listOf(attr, collection = "Array") {
const listType = (collection === "Set" ? Set : Array);
const attribute = this.attribute(attr, listType);
return new ListAttributeBuilder(this, attribute, collection);
}
build() {
if (!this._name)
throw new Error("name is required");
const Parent = this._parent ?? Model;
class DynamicBuiltClass extends Parent {
constructor(arg) {
super(arg);
}
}
Object.defineProperty(DynamicBuiltClass, "name", {
value: this._name,
writable: false,
});
for (const attribute of this.attributes.values()) {
attribute.build(DynamicBuiltClass);
}
let result = model()(DynamicBuiltClass);
if (this._description)
result = description(this._description)(result);
return result;
}
static builder() {
return new ModelBuilder();
}
static from(meta, name) {
if (!meta)
throw new Error("metadata is required");
const builder = ModelBuilder.builder();
const derivedName = name ?? `GeneratedModel${Date.now()}`;
builder.setName(derivedName);
const properties = meta.properties || {};
const validations = meta.validation || {};
for (const [prop, designType] of Object.entries(properties)) {
const attribute = builder.attribute(prop, designType || Object);
const propValidation = validations[prop];
if (propValidation) {
for (const [key, validationMeta] of Object.entries(propValidation)) {
const handler = attribute[key];
if (typeof handler === "function") {
handler.call(attribute, validationMeta);
continue;
}
try {
const decoratorFactory = Validation.decoratorFromKey(key);
const decorator = typeof decoratorFactory === "function"
? decoratorFactory(validationMeta)
: decoratorFactory;
attribute.decorate(decorator);
}
catch {
// ignore unknown decorators
}
}
}
}
return builder.build();
}
}
//# sourceMappingURL=Builder.js.map