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
JavaScript
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