diff --git a/src/decorators.ts b/src/decorators.ts index ac38cf7..b4e303d 100644 --- a/src/decorators.ts +++ b/src/decorators.ts @@ -34,19 +34,21 @@ const ajv = new Ajv({ allErrors: false, }); -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function isSchemaValidator(type: any): type is SchemaValidator { +export function isSchemaValidator( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + type: any +): type is SchemaValidator { return type && typeof type === 'object' && typeof type.validate === 'function'; } -export function buildSchemaValidator({ +export function buildSchemaValidator({ type, schema, coerceTypes, stripUnknownProps, name, required, -}: SchemaValidatorConfig): SchemaValidator { +}: SchemaValidatorConfig): SchemaValidator { if (!type) { throw new Error('Validator missing "type".'); } @@ -59,7 +61,7 @@ export function buildSchemaValidator({ throw new Error(`Validator "${name}" expects a TypeBox schema.`); } - const check = ajv.compile>(schema); + const check = ajv.compile>(schema); return { schema, @@ -119,20 +121,23 @@ export function buildSchemaValidator({ } if (check(processedDataOrArray)) return processedDataOrArray; - throw new AjvValidationException(type, check.errors); + throw new AjvValidationException(type, check.errors); }, }; } export function Validate< - T extends TSchema, - ResponseValidator extends ResponseValidatorConfig, - RequestValidators extends RequestValidatorConfig[], + TRequestSchema extends TSchema, + TResponseSchema extends TSchema, + ResponseValidator extends ResponseValidatorConfig, + RequestValidators extends RequestValidatorConfig[], MethodDecoratorType extends ( // eslint-disable-next-line @typescript-eslint/no-explicit-any - ...args: [...RequestConfigsToTypes, ...any[]] - ) => Promise> | Static, ->(validatorConfig: ValidatorConfig): MethodDecorator { + ...args: [...RequestConfigsToTypes, ...any[]] + ) => Promise>, +>( + validatorConfig: ValidatorConfig +): MethodDecorator { return (target, key, descriptor) => { let args = Reflect.getMetadata(ROUTE_ARGS_METADATA, target.constructor, key) ?? {}; @@ -144,7 +149,7 @@ export function Validate< const methodName = capitalize(String(key)); if (responseValidatorConfig) { - const validatorConfig: ResponseValidatorConfig = TypeGuard.IsSchema(responseValidatorConfig) + const validatorConfig: ResponseValidatorConfig = TypeGuard.IsSchema(responseValidatorConfig) ? { schema: responseValidatorConfig } : responseValidatorConfig; @@ -166,7 +171,10 @@ export function Validate< switch (validatorConfig.type) { case 'body': { const { required = true, name = `${methodName}Body`, pipes = [], ...config } = validatorConfig; - const validator = buildSchemaValidator({ ...config, name, required } as SchemaValidatorConfig); + const validator = buildSchemaValidator({ ...config, name, required } as SchemaValidatorConfig< + TRequestSchema, + TResponseSchema + >); const validatorPipe: PipeTransform = { transform: value => validator.validate(value) }; args = assignMetadata(args, RouteParamtypes.BODY, index, undefined, ...pipes, validatorPipe); @@ -180,7 +188,12 @@ export function Validate< case 'param': { const { required = true, coerceTypes = true, schema = Type.String(), pipes = [], ...config } = validatorConfig; - const validator = buildSchemaValidator({ ...config, coerceTypes, required, schema } as SchemaValidatorConfig); + const validator = buildSchemaValidator({ + ...config, + coerceTypes, + required, + schema, + } as SchemaValidatorConfig); const validatorPipe: PipeTransform = { transform: value => validator.validate(value) }; args = assignMetadata(args, RouteParamtypes.PARAM, index, validatorConfig.name, ...pipes, validatorPipe); @@ -192,7 +205,12 @@ export function Validate< case 'query': { const { required = false, coerceTypes = true, schema = Type.String(), pipes = [], ...config } = validatorConfig; - const validator = buildSchemaValidator({ ...config, coerceTypes, required, schema } as SchemaValidatorConfig); + const validator = buildSchemaValidator({ + ...config, + coerceTypes, + required, + schema, + } as SchemaValidatorConfig); const validatorPipe: PipeTransform = { transform: value => validator.validate(value) }; args = assignMetadata(args, RouteParamtypes.QUERY, index, validatorConfig.name, ...pipes, validatorPipe); @@ -215,15 +233,16 @@ const nestHttpDecoratorMap = { }; export const HttpEndpoint = < - S extends TSchema, - ResponseConfig extends Omit, 'responseCode'>, - RequestConfigs extends RequestValidatorConfig[], + TRequestSchema extends TSchema, + TResponseSchema extends TSchema, + ResponseConfig extends Omit, 'responseCode'>, + RequestConfigs extends RequestValidatorConfig[], MethodDecoratorType extends ( // eslint-disable-next-line @typescript-eslint/no-explicit-any - ...args: [...RequestConfigsToTypes, ...any[]] - ) => Promise> | Static, + ...args: [...RequestConfigsToTypes, ...any[]] + ) => Promise>, >( - config: HttpEndpointDecoratorConfig + config: HttpEndpointDecoratorConfig ): MethodDecorator => { const { method, responseCode = 200, path, validate, ...apiOperationOptions } = config; diff --git a/src/exceptions.ts b/src/exceptions.ts index 45537d6..0240bda 100644 --- a/src/exceptions.ts +++ b/src/exceptions.ts @@ -1,10 +1,11 @@ import { BadRequestException, HttpStatus } from '@nestjs/common'; +import { TSchema } from '@sinclair/typebox/type'; import { ErrorObject } from 'ajv'; import type { ValidatorType } from './types.js'; -export class AjvValidationException extends BadRequestException { - constructor(type: ValidatorType, errors: Array | null | undefined) { +export class AjvValidationException extends BadRequestException { + constructor(type: ValidatorType, errors: Array | null | undefined) { const topLevelErrors: ErrorObject[] = []; const unionPaths: string[] = []; diff --git a/src/types.ts b/src/types.ts index 537cb79..7406736 100644 --- a/src/types.ts +++ b/src/types.ts @@ -16,21 +16,22 @@ export type MethodDecorator = ( ) => TypedPropertyDescriptor | void; export interface HttpEndpointDecoratorConfig< - TTSchema extends TSchema, - ResponseConfig extends ResponseValidatorConfig = ResponseValidatorConfig, - RequestConfigs extends RequestValidatorConfig[] = RequestValidatorConfig[], + TRequestSchema extends TSchema, + TResponseSchema extends TSchema, + ResponseConfig extends ResponseValidatorConfig = ResponseValidatorConfig, + RequestConfigs extends RequestValidatorConfig[] = RequestValidatorConfig[], > extends Omit { method: 'GET' | 'POST' | 'PATCH' | 'DELETE' | 'PUT'; responseCode?: number; path?: string; - validate?: ValidatorConfig; + validate?: ValidatorConfig; } -export interface SchemaValidator { - schema: TTSchema; +export interface SchemaValidator { + schema: TRequestSchema | TResponseSchema; name: string; - check: ValidateFunction>; - validate(data: Obj | Obj[]): Static; + check: ValidateFunction>; + validate(data: Obj | Obj[]): Static; } export interface ValidatorConfigBase { schema?: TTSchema; @@ -40,51 +41,56 @@ export interface ValidatorConfigBase { required?: boolean; pipes?: (PipeTransform | Type)[]; } -export interface ResponseValidatorConfig extends ValidatorConfigBase { - schema: TTSchema; +export interface ResponseValidatorConfig extends ValidatorConfigBase { + schema: TResponseSchema; type?: 'response'; responseCode?: number; required?: true; pipes?: never; } -export interface ParamValidatorConfig extends ValidatorConfigBase { - schema?: TTSchema; +export interface ParamValidatorConfig extends ValidatorConfigBase { + schema?: TRequestSchema; type: 'param'; name: string; stripUnknownProps?: never; } -export interface QueryValidatorConfig extends ValidatorConfigBase { - schema?: TTSchema; +export interface QueryValidatorConfig extends ValidatorConfigBase { + schema?: TRequestSchema; type: 'query'; name: string; stripUnknownProps?: never; } -export interface BodyValidatorConfig extends ValidatorConfigBase { - schema: TTSchema; +export interface BodyValidatorConfig extends ValidatorConfigBase { + schema: TRequestSchema; type: 'body'; } -export type RequestValidatorConfig = - | ParamValidatorConfig - | QueryValidatorConfig - | BodyValidatorConfig; -export type SchemaValidatorConfig = RequestValidatorConfig | ResponseValidatorConfig; +export type RequestValidatorConfig = + | ParamValidatorConfig + | QueryValidatorConfig + | BodyValidatorConfig; +export type SchemaValidatorConfig = + | RequestValidatorConfig + | ResponseValidatorConfig; -export type ValidatorType = NonNullable; +export type ValidatorType = NonNullable< + SchemaValidatorConfig['type'] +>; export interface ValidatorConfig< - TTSchema extends TSchema, - ResponseConfig extends ResponseValidatorConfig, - RequestConfigs extends RequestValidatorConfig[], + TRequestSchema extends TSchema, + TResponseSchema extends TSchema, + ResponseConfig extends ResponseValidatorConfig, + RequestConfigs extends RequestValidatorConfig[], > { - response?: TTSchema | ResponseConfig; + response?: TResponseSchema | ResponseConfig; request?: [...RequestConfigs]; } -export type RequestConfigsToTypes = { +export type RequestConfigsToTypes[]> = { [K in keyof RequestConfigs]: RequestConfigs[K]['required'] extends false ? RequestConfigs[K]['schema'] extends TSchema ? Static | undefined