@decaf-ts/decorator-validation
Version:
simple decorator based validation engine
401 lines • 16.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ModelBuilder = exports.AttributeBuilder = void 0;
const Model_1 = require("./Model.cjs");
const typed_object_accumulator_1 = require("typed-object-accumulator");
const decoration_1 = require("@decaf-ts/decoration");
const decorators_1 = require("./decorators.cjs");
const decorators_2 = require("./../validation/decorators.cjs");
const constants_1 = require("./../validation/Validators/constants.cjs");
const Validation_1 = require("./../validation/Validation.cjs");
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((0, decorators_2.required)(message));
}
min(valueOrMeta, message) {
const meta = AttributeBuilder.asMeta(valueOrMeta);
const value = meta?.[constants_1.ValidationKeys.MIN] ??
(meta ? undefined : valueOrMeta);
if (value === undefined)
throw new Error(`Missing ${constants_1.ValidationKeys.MIN} for ${String(this.attr)}`);
return this.decorate((0, decorators_2.min)(value, AttributeBuilder.resolveMessage(meta, message)));
}
max(valueOrMeta, message) {
const meta = AttributeBuilder.asMeta(valueOrMeta);
const value = meta?.[constants_1.ValidationKeys.MAX] ??
(meta ? undefined : valueOrMeta);
if (value === undefined)
throw new Error(`Missing ${constants_1.ValidationKeys.MAX} for ${String(this.attr)}`);
return this.decorate((0, decorators_2.max)(value, AttributeBuilder.resolveMessage(meta, message)));
}
step(valueOrMeta, message) {
const meta = AttributeBuilder.asMeta(valueOrMeta);
const value = meta?.[constants_1.ValidationKeys.STEP] ??
(meta ? undefined : valueOrMeta);
if (value === undefined)
throw new Error(`Missing ${constants_1.ValidationKeys.STEP} for ${String(this.attr)}`);
return this.decorate((0, decorators_2.step)(value, AttributeBuilder.resolveMessage(meta, message)));
}
minlength(valueOrMeta, message) {
const meta = AttributeBuilder.asMeta(valueOrMeta);
const value = meta?.[constants_1.ValidationKeys.MIN_LENGTH] ??
(meta ? undefined : valueOrMeta);
if (value === undefined)
throw new Error(`Missing ${constants_1.ValidationKeys.MIN_LENGTH} for ${String(this.attr)}`);
return this.decorate((0, decorators_2.minlength)(value, AttributeBuilder.resolveMessage(meta, message)));
}
maxlength(valueOrMeta, message) {
const meta = AttributeBuilder.asMeta(valueOrMeta);
const value = meta?.[constants_1.ValidationKeys.MAX_LENGTH] ??
(meta ? undefined : valueOrMeta);
if (value === undefined)
throw new Error(`Missing ${constants_1.ValidationKeys.MAX_LENGTH} for ${String(this.attr)}`);
return this.decorate((0, decorators_2.maxlength)(value, AttributeBuilder.resolveMessage(meta, message)));
}
pattern(valueOrMeta, message) {
const meta = AttributeBuilder.asMeta(valueOrMeta);
const rawPattern = meta?.[constants_1.ValidationKeys.PATTERN] ??
(meta ? undefined : valueOrMeta);
const regex = AttributeBuilder.patternFromString(rawPattern) ?? /.*/;
return this.decorate((0, decorators_2.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((0, decorators_2.email)(message));
}
url(messageOrMeta) {
const meta = AttributeBuilder.asMeta(messageOrMeta);
const message = typeof messageOrMeta === "string"
? messageOrMeta
: AttributeBuilder.resolveMessage(meta);
return this.decorate((0, decorators_2.url)(message));
}
type(valueOrMeta, message) {
const meta = AttributeBuilder.asMeta(valueOrMeta);
const types = meta?.customTypes ?? meta?.type ?? (meta ? undefined : valueOrMeta);
return this.decorate((0, decorators_2.type)(types, AttributeBuilder.resolveMessage(meta, message)));
}
date(formatOrMeta, message) {
const meta = AttributeBuilder.asMeta(formatOrMeta);
const format = meta?.[constants_1.ValidationKeys.FORMAT] ??
(meta ? undefined : formatOrMeta);
return this.decorate((0, decorators_2.date)(format, AttributeBuilder.resolveMessage(meta, message)));
}
password(valueOrMeta, message) {
const meta = AttributeBuilder.asMeta(valueOrMeta);
const rawPattern = meta?.[constants_1.ValidationKeys.PATTERN] ??
(meta ? undefined : valueOrMeta);
const regex = AttributeBuilder.patternFromString(rawPattern);
return this.decorate((0, decorators_2.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((0, decorators_2.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?.[constants_1.ValidationKeys.ENUM] ?? (meta ? undefined : valueOrMeta);
return this.decorate((0, decorators_2.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, constants_1.ValidationKeys.EQUALS, options);
return this.decorate((0, decorators_2.eq)(target, resolvedOptions));
}
eq(propertyOrMeta, options) {
return this.equals(propertyOrMeta, options);
}
different(propertyOrMeta, options) {
const { target, options: resolvedOptions } = this.resolveComparison(propertyOrMeta, constants_1.ValidationKeys.DIFF, options);
return this.decorate((0, decorators_2.diff)(target, resolvedOptions));
}
diff(propertyOrMeta, options) {
return this.different(propertyOrMeta, options);
}
lessThan(propertyOrMeta, options) {
const { target, options: resolvedOptions } = this.resolveComparison(propertyOrMeta, constants_1.ValidationKeys.LESS_THAN, options);
return this.decorate((0, decorators_2.lt)(target, resolvedOptions));
}
lt(propertyOrMeta, options) {
return this.lessThan(propertyOrMeta, options);
}
lessThanOrEqual(propertyOrMeta, options) {
const { target, options: resolvedOptions } = this.resolveComparison(propertyOrMeta, constants_1.ValidationKeys.LESS_THAN_OR_EQUAL, options);
return this.decorate((0, decorators_2.lte)(target, resolvedOptions));
}
lte(propertyOrMeta, options) {
return this.lessThanOrEqual(propertyOrMeta, options);
}
greaterThan(propertyOrMeta, options) {
const { target, options: resolvedOptions } = this.resolveComparison(propertyOrMeta, constants_1.ValidationKeys.GREATER_THAN, options);
return this.decorate((0, decorators_2.gt)(target, resolvedOptions));
}
gt(propertyOrMeta, options) {
return this.greaterThan(propertyOrMeta, options);
}
greaterThanOrEqual(propertyOrMeta, options) {
const { target, options: resolvedOptions } = this.resolveComparison(propertyOrMeta, constants_1.ValidationKeys.GREATER_THAN_OR_EQUAL, options);
return this.decorate((0, decorators_2.gte)(target, resolvedOptions));
}
gte(propertyOrMeta, options) {
return this.greaterThanOrEqual(propertyOrMeta, options);
}
description(desc) {
return this.decorate((0, decoration_1.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(decoration_1.DecorationKeys.DESIGN_TYPE, this.declaredType, target, propKey);
}
(0, decoration_1.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}`);
}
});
}
}
exports.AttributeBuilder = AttributeBuilder;
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;
}
}
class ModelBuilder extends typed_object_accumulator_1.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_1.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 = (0, decorators_1.model)()(DynamicBuiltClass);
if (this._description)
result = (0, decoration_1.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_1.Validation.decoratorFromKey(key);
const decorator = typeof decoratorFactory === "function"
? decoratorFactory(validationMeta)
: decoratorFactory;
attribute.decorate(decorator);
}
catch {
// ignore unknown decorators
}
}
}
}
return builder.build();
}
}
exports.ModelBuilder = ModelBuilder;
//# sourceMappingURL=Builder.js.map