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. Can be configured to patch @nestjs/swagger allowing OpenAPI gene

155 lines 8.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.HttpEndpoint = void 0; exports.isSchemaValidator = isSchemaValidator; exports.buildSchemaValidator = buildSchemaValidator; exports.Validate = Validate; const common_1 = require("@nestjs/common"); const constants_js_1 = require("@nestjs/common/constants.js"); const route_paramtypes_enum_js_1 = require("@nestjs/common/enums/route-paramtypes.enum.js"); const extend_metadata_util_js_1 = require("@nestjs/common/utils/extend-metadata.util.js"); const swagger_1 = require("@nestjs/swagger"); const constants_js_2 = require("@nestjs/swagger/dist/constants.js"); const typebox_1 = require("@sinclair/typebox"); const compiler_1 = require("@sinclair/typebox/compiler"); const value_1 = require("@sinclair/typebox/value"); const analyze_schema_js_1 = require("./analyze-schema.js"); const exceptions_js_1 = require("./exceptions.js"); const interceptors_js_1 = require("./interceptors.js"); const util_js_1 = require("./util.js"); // eslint-disable-next-line @typescript-eslint/no-explicit-any function isSchemaValidator(type) { return type && typeof type === 'object' && typeof type.validate === 'function'; } function buildSchemaValidator(config) { const { type, schema, coerceTypes, stripUnknownProps, name, required } = config; if (!type) { throw new Error('Validator missing "type".'); } if (!name) { throw new Error(`Validator of type "${type}" missing name.`); } if (!typebox_1.TypeGuard.IsSchema(schema)) { throw new Error(`Validator "${name}" expects a TypeBox schema.`); } const analysis = (0, analyze_schema_js_1.analyzeSchema)(schema); const references = [...analysis.references.values()]; const checker = compiler_1.TypeCompiler.Compile(schema, references); return { schema, name, check: checker.Check, analysis, validate(data) { if (analysis.hasDefault) { data = (0, value_1.Default)(schema, references, data); } if (data === undefined && !required) { return; } if (stripUnknownProps) { data = (0, value_1.Clean)(schema, references, data); } if (coerceTypes) { data = (0, value_1.Convert)(schema, references, data); } if (analysis.hasTransform && type === 'response') { data = (0, value_1.TransformEncode)(schema, references, data); } if (!checker.Check(data)) { throw new exceptions_js_1.TypeboxValidationException(type, checker.Errors(data)); } if (analysis.hasTransform && type !== 'response') { data = (0, value_1.TransformDecode)(schema, references, data); } return data; }, }; } function Validate(validatorConfig) { return (target, key, descriptor) => { let args = Reflect.getMetadata(constants_js_1.ROUTE_ARGS_METADATA, target.constructor, key) ?? {}; // eslint-disable-next-line @typescript-eslint/no-explicit-any (0, extend_metadata_util_js_1.extendArrayMetadata)(constants_js_1.INTERCEPTORS_METADATA, [interceptors_js_1.TypeboxTransformInterceptor], descriptor.value); const { response: responseValidatorConfig, request: requestValidatorConfigs } = validatorConfig; const methodName = (0, util_js_1.capitalize)(String(key)); if (responseValidatorConfig) { const validatorConfig = typebox_1.TypeGuard.IsSchema(responseValidatorConfig) ? { schema: responseValidatorConfig } : responseValidatorConfig; const { responseCode = 200, description, example, required = true, stripUnknownProps = true, name = `${methodName}Response`, ...config } = validatorConfig; const validator = buildSchemaValidator({ ...config, required, stripUnknownProps, name, type: 'response' }); // eslint-disable-next-line @typescript-eslint/no-explicit-any Reflect.defineMetadata(constants_js_2.DECORATORS.API_RESPONSE, { [responseCode]: { type: validator, description, example } }, target[key]); } requestValidatorConfigs?.forEach((validatorConfig, index) => { switch (validatorConfig.type) { case 'body': { const { required = true, name = `${methodName}Body`, pipes = [], ...config } = validatorConfig; const validator = buildSchemaValidator({ ...config, name, required }); const validatorPipe = { transform: value => validator.validate(value) }; args = (0, common_1.assignMetadata)(args, route_paramtypes_enum_js_1.RouteParamtypes.BODY, index, undefined, ...pipes, validatorPipe); Reflect.defineMetadata(constants_js_1.ROUTE_ARGS_METADATA, args, target.constructor, key); // eslint-disable-next-line @typescript-eslint/no-explicit-any (0, swagger_1.ApiBody)({ type: validator, required })(target, key, descriptor); break; } case 'param': { const { required = true, coerceTypes = true, schema = typebox_1.Type.String(), pipes = [], ...config } = validatorConfig; const validator = buildSchemaValidator({ ...config, coerceTypes, required, schema }); const validatorPipe = { transform: value => validator.validate(value) }; args = (0, common_1.assignMetadata)(args, route_paramtypes_enum_js_1.RouteParamtypes.PARAM, index, validatorConfig.name, ...pipes, validatorPipe); Reflect.defineMetadata(constants_js_1.ROUTE_ARGS_METADATA, args, target.constructor, key); (0, swagger_1.ApiParam)({ name: validatorConfig.name, schema: validatorConfig.schema, required })(target, key, descriptor); break; } case 'query': { const { required = false, coerceTypes = true, schema = typebox_1.Type.String(), pipes = [], ...config } = validatorConfig; const validator = buildSchemaValidator({ ...config, coerceTypes, required, schema }); const validatorPipe = { transform: value => validator.validate(value) }; args = (0, common_1.assignMetadata)(args, route_paramtypes_enum_js_1.RouteParamtypes.QUERY, index, validatorConfig.name, ...pipes, validatorPipe); Reflect.defineMetadata(constants_js_1.ROUTE_ARGS_METADATA, args, target.constructor, key); (0, swagger_1.ApiQuery)({ name: validatorConfig.name, schema: validatorConfig.schema, required })(target, key, descriptor); } } }); return descriptor; }; } const nestHttpDecoratorMap = { GET: common_1.Get, POST: common_1.Post, PATCH: common_1.Patch, DELETE: common_1.Delete, PUT: common_1.Put, }; const HttpEndpoint = (config) => { const { method, responseCode = 200, path, validate, ...apiOperationOptions } = config; const decorators = [nestHttpDecoratorMap[method](path), (0, common_1.HttpCode)(responseCode), (0, swagger_1.ApiOperation)(apiOperationOptions)]; if (path) { const pathParams = path .split('/') .filter(seg => seg.startsWith(':')) .map(seg => ({ name: seg.replace(/^:([^\?]+)\??$/, '$1'), required: !seg.endsWith('?') })); // TODO: handle optional path parameters for (const pathParam of pathParams) { const paramValidator = validate?.request?.find(v => v.name === pathParam.name); if (!paramValidator) { throw new Error(`Path param "${pathParam.name}" is missing a request validator.`); } if (paramValidator.required === false && pathParam.required === true) { throw new Error(`Optional path param "${pathParam.name}" is required in validator.`); } } const missingPathParam = validate?.request?.find(v => v.type === 'param' && !pathParams.some(p => p.name == v.name)); if (missingPathParam) { throw new Error(`Request validator references non-existent path parameter "${missingPathParam.name}".`); } } if (validate) { decorators.push(Validate(validate)); } return (0, common_1.applyDecorators)(...decorators); }; exports.HttpEndpoint = HttpEndpoint; //# sourceMappingURL=decorators.js.map