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