UNPKG

nestjs-typebox

Version:

This library provides helper utilities for writing and validating NestJS APIs using [TypeBox](https://github.com/sinclairzx81/typebox) as an alternative to class-validator/class-transformer. It also includes a patch for @nestjs/swagger allowing OpenAPI ge

211 lines (201 loc) 8.57 kB
import { Type } from '@sinclair/typebox'; import { TypeCompiler } from '@sinclair/typebox/compiler'; import { BadRequestException, HttpStatus, assignMetadata, Injectable } from '@nestjs/common'; import { RouteParamtypes } from '@nestjs/common/enums/route-paramtypes.enum'; import { ROUTE_ARGS_METADATA } from '@nestjs/common/constants'; import { DECORATORS } from '@nestjs/swagger/dist/constants'; import { Format } from '@sinclair/typebox/format'; import { Reflector } from '@nestjs/core'; import { map } from 'rxjs/operators'; import { SchemaObjectFactory } from '@nestjs/swagger/dist/services/schema-object-factory'; // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types function isTypeboxDto(metatype) { return typeof metatype === 'function' && metatype?.isTypeboxDto; } const tryCoerceToNumber = (val)=>{ switch(typeof val){ case 'number': return val; case 'boolean': return val === true ? 1 : 0; case 'string': { const v = Number(val); if (Number.isFinite(v)) { return v; } break; } case 'object': { if (val === null) return 0; break; } } return val; }; class TypeboxValidationException extends BadRequestException { constructor(errors){ super({ statusCode: HttpStatus.BAD_REQUEST, message: 'Validation failed', errors: [ ...errors ] }); } } class TypeboxModel { } const createTypeboxDto = (schema, options)=>{ let AugmentedTypeboxDto = class AugmentedTypeboxDto extends TypeboxModel { static isTypeboxDto = true; static schema = schema; static options = options; static toJsonSchema() { return Type.Strict(this.schema); } static beforeValidate(data) { const result = this.options?.stripUnknownProps ? {} : 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; } static validate(data) { if (!this.validator) { this.validator = TypeCompiler.Compile(this.schema); } if (!this.validator.Check(data)) { throw new TypeboxValidationException(this.validator.Errors(data)); } } static transform(data) { return this.options?.transform?.(data) ?? data; } constructor(data){ super(); this.data = data; } }; return AugmentedTypeboxDto; }; const Params = ()=>{ return (target, key, index)=>{ const args = Reflect.getMetadata(ROUTE_ARGS_METADATA, target.constructor, key) || {}; const [type] = Reflect.getMetadata('design:paramtypes', target, key); if (isTypeboxDto(type)) { const objSchema = type.toJsonSchema(); if (objSchema.type === 'object') { const parameters = Object.entries(objSchema.properties).map(([name, { description , examples , ...schema }])=>({ in: 'path', name, description, examples, schema, required: objSchema.required?.includes(name) })); // eslint-disable-next-line @typescript-eslint/no-explicit-any Reflect.defineMetadata(DECORATORS.API_PARAMETERS, parameters, target[key]); } } Reflect.defineMetadata(ROUTE_ARGS_METADATA, assignMetadata(args, RouteParamtypes.PARAM, index), target.constructor, key); }; }; const emailRegex = /.+\@.+\..+/; const emailFormat = (value)=>value.match(emailRegex) !== null; const applyFormats = ()=>{ Format.Set('email', emailFormat); }; var __decorate$1 = globalThis && globalThis.__decorate || function(decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = globalThis && globalThis.__metadata || function(k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; let TypeboxTransformInterceptor = class TypeboxTransformInterceptor { constructor(reflector){ this.reflector = reflector; } intercept(context, next) { return next.handle().pipe(map((data)=>{ const responseMeta = this.reflector.get(DECORATORS.API_RESPONSE, context.getHandler()); const responseType = (responseMeta['200'] || responseMeta['201'] || {})['type']; if (!responseType) return data; const dataArray = Array.isArray(data) ? data : [ data ]; return dataArray.map((dataOrModel)=>{ const data = dataOrModel instanceof TypeboxModel ? dataOrModel.data : dataOrModel; if (responseType.validate) { responseType.validate(data); } return responseType.transform ? responseType.transform(data) : data; }); })); } }; TypeboxTransformInterceptor = __decorate$1([ Injectable(), __metadata("design:type", Function), __metadata("design:paramtypes", [ typeof Reflector === "undefined" ? Object : Reflector ]) ], TypeboxTransformInterceptor); var __decorate = globalThis && globalThis.__decorate || function(decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; let TypeboxValidationPipe = class TypeboxValidationPipe { transform(value, metadata) { const { metatype } = metadata; if (!isTypeboxDto(metatype)) { return value; } metatype.validate(metatype.beforeValidate(value)); return value; } }; TypeboxValidationPipe = __decorate([ Injectable() ], TypeboxValidationPipe); function patchNestJsSwagger() { // eslint-disable-next-line @typescript-eslint/no-explicit-any if (SchemaObjectFactory.prototype.__primatePatched) return; const defaultExplore = SchemaObjectFactory.prototype.exploreModelSchema; const extendedExplore = function exploreModelSchema(type, schemas, schemaRefsStack) { if (this['isLazyTypeFunc'](type)) { const factory = type; type = factory(); } if (!isTypeboxDto(type)) { return defaultExplore.apply(this, [ type, schemas, schemaRefsStack ]); } schemas[type.name] = type.toJsonSchema(); return type.name; }; SchemaObjectFactory.prototype.exploreModelSchema = extendedExplore; // eslint-disable-next-line @typescript-eslint/no-explicit-any SchemaObjectFactory.prototype.__primatePatched = true; } export { Params, TypeboxModel, TypeboxTransformInterceptor, TypeboxValidationException, TypeboxValidationPipe, applyFormats, createTypeboxDto, emailFormat, isTypeboxDto, patchNestJsSwagger, tryCoerceToNumber }; //# sourceMappingURL=nestjs-typebox.es.js.map