UNPKG

@decaf-ts/decorator-validation

Version:
396 lines 15.9 kB
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