import { Static, TObject, TProperties, Type } from '@sinclair/typebox'; import { TypeCheck, TypeCompiler } from '@sinclair/typebox/compiler'; import { tryCoerceToNumber } from './util'; import { TypeboxValidationException } from './exceptions'; export interface TypeboxDto>> { new (): R; isTypeboxDto: true; typeboxSchema: TObject; validator: TypeCheck> | undefined; toJsonSchema(): TObject; beforeValidate(data: unknown): unknown; validate(data: unknown): unknown; transform(data: Static>): R; } export interface DtoOptions>> { transform?: (data: Static>) => R; coerceTypes?: boolean; stripUnknownProps?: boolean; } export abstract class TypeboxModel { abstract readonly data: T; } export const createTypeboxDto = >>(schema: TObject, options?: DtoOptions) => { class AugmentedTypeboxDto extends TypeboxModel>> { public static isTypeboxDto = true; public static schema = schema; public static options = options; public static validator: TypeCheck> | undefined; public static toJsonSchema() { return Type.Strict(this.schema); } public static beforeValidate(data: Record): unknown { const result = this.options?.stripUnknownProps ? ({} as Record) : data; if (this.options?.coerceTypes || this.options?.stripUnknownProps) { const schema = this.toJsonSchema(); for (const [prop, def] of Object.entries(schema.properties)) { if (data[prop] === undefined) continue; switch (def.type) { case 'number': result[prop] = tryCoerceToNumber(data[prop]); break; default: result[prop] = data[prop]; } } } return result; } public static validate(data: unknown): void { if (!this.validator) { this.validator = TypeCompiler.Compile(this.schema); } if (!this.validator.Check(data)) { throw new TypeboxValidationException(this.validator.Errors(data)); } } public static transform(data: Static>): R { return this.options?.transform?.(data) ?? (data as unknown as R); } constructor(readonly data: Static>) { super(); } } return AugmentedTypeboxDto as unknown as TypeboxDto; };