Skip to content

Commit

Permalink
fixed return type
Browse files Browse the repository at this point in the history
  • Loading branch information
Omri-Levy committed Jun 3, 2024
1 parent a94f152 commit 080d409
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 51 deletions.
63 changes: 41 additions & 22 deletions src/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<TRequestSchema extends TSchema, TResponseSchema extends TSchema>(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type: any
): type is SchemaValidator<TRequestSchema, TResponseSchema> {
return type && typeof type === 'object' && typeof type.validate === 'function';
}

export function buildSchemaValidator<TTSchema extends TSchema>({
export function buildSchemaValidator<TRequestSchema extends TSchema, TResponseSchema extends TSchema>({
type,
schema,
coerceTypes,
stripUnknownProps,
name,
required,
}: SchemaValidatorConfig<TTSchema>): SchemaValidator<TTSchema> {
}: SchemaValidatorConfig<TRequestSchema, TResponseSchema>): SchemaValidator<TRequestSchema, TResponseSchema> {
if (!type) {
throw new Error('Validator missing "type".');
}
Expand All @@ -59,7 +61,7 @@ export function buildSchemaValidator<TTSchema extends TSchema>({
throw new Error(`Validator "${name}" expects a TypeBox schema.`);
}

const check = ajv.compile<Static<TTSchema>>(schema);
const check = ajv.compile<Static<TRequestSchema | TResponseSchema>>(schema);

return {
schema,
Expand Down Expand Up @@ -119,20 +121,23 @@ export function buildSchemaValidator<TTSchema extends TSchema>({
}

if (check(processedDataOrArray)) return processedDataOrArray;
throw new AjvValidationException(type, check.errors);
throw new AjvValidationException<TRequestSchema, TResponseSchema>(type, check.errors);
},
};
}

export function Validate<
T extends TSchema,
ResponseValidator extends ResponseValidatorConfig<T>,
RequestValidators extends RequestValidatorConfig[],
TRequestSchema extends TSchema,
TResponseSchema extends TSchema,
ResponseValidator extends ResponseValidatorConfig<TResponseSchema>,
RequestValidators extends RequestValidatorConfig<TRequestSchema>[],
MethodDecoratorType extends (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
...args: [...RequestConfigsToTypes<RequestValidators>, ...any[]]
) => Promise<Static<ResponseValidator['schema']>> | Static<ResponseValidator['schema']>,
>(validatorConfig: ValidatorConfig<T, ResponseValidator, RequestValidators>): MethodDecorator<MethodDecoratorType> {
...args: [...RequestConfigsToTypes<TRequestSchema, RequestValidators>, ...any[]]
) => Promise<Static<ResponseValidator['schema']>>,
>(
validatorConfig: ValidatorConfig<TRequestSchema, TResponseSchema, ResponseValidator, RequestValidators>
): MethodDecorator<MethodDecoratorType> {
return (target, key, descriptor) => {
let args = Reflect.getMetadata(ROUTE_ARGS_METADATA, target.constructor, key) ?? {};

Expand All @@ -144,7 +149,7 @@ export function Validate<
const methodName = capitalize(String(key));

if (responseValidatorConfig) {
const validatorConfig: ResponseValidatorConfig = TypeGuard.IsSchema(responseValidatorConfig)
const validatorConfig: ResponseValidatorConfig<TResponseSchema> = TypeGuard.IsSchema(responseValidatorConfig)
? { schema: responseValidatorConfig }
: responseValidatorConfig;

Expand All @@ -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);
Expand All @@ -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<TRequestSchema, TResponseSchema>);
const validatorPipe: PipeTransform = { transform: value => validator.validate(value) };

args = assignMetadata(args, RouteParamtypes.PARAM, index, validatorConfig.name, ...pipes, validatorPipe);
Expand All @@ -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<TRequestSchema, TResponseSchema>);
const validatorPipe: PipeTransform = { transform: value => validator.validate(value) };

args = assignMetadata(args, RouteParamtypes.QUERY, index, validatorConfig.name, ...pipes, validatorPipe);
Expand All @@ -215,15 +233,16 @@ const nestHttpDecoratorMap = {
};

export const HttpEndpoint = <
S extends TSchema,
ResponseConfig extends Omit<ResponseValidatorConfig<S>, 'responseCode'>,
RequestConfigs extends RequestValidatorConfig[],
TRequestSchema extends TSchema,
TResponseSchema extends TSchema,
ResponseConfig extends Omit<ResponseValidatorConfig<TResponseSchema>, 'responseCode'>,
RequestConfigs extends RequestValidatorConfig<TRequestSchema>[],
MethodDecoratorType extends (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
...args: [...RequestConfigsToTypes<RequestConfigs>, ...any[]]
) => Promise<Static<ResponseConfig['schema']>> | Static<ResponseConfig['schema']>,
...args: [...RequestConfigsToTypes<TRequestSchema, RequestConfigs>, ...any[]]
) => Promise<Static<ResponseConfig['schema']>>,
>(
config: HttpEndpointDecoratorConfig<S, ResponseConfig, RequestConfigs>
config: HttpEndpointDecoratorConfig<TRequestSchema, TResponseSchema, ResponseConfig, RequestConfigs>
): MethodDecorator<MethodDecoratorType> => {
const { method, responseCode = 200, path, validate, ...apiOperationOptions } = config;

Expand Down
5 changes: 3 additions & 2 deletions src/exceptions.ts
Original file line number Diff line number Diff line change
@@ -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<ErrorObject> | null | undefined) {
export class AjvValidationException<TRequestSchema extends TSchema, TResponseSchema extends TSchema> extends BadRequestException {
constructor(type: ValidatorType<TRequestSchema, TResponseSchema>, errors: Array<ErrorObject> | null | undefined) {
const topLevelErrors: ErrorObject[] = [];
const unionPaths: string[] = [];

Expand Down
60 changes: 33 additions & 27 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,22 @@ export type MethodDecorator<T extends Function = any> = (
) => TypedPropertyDescriptor<T> | void;

export interface HttpEndpointDecoratorConfig<
TTSchema extends TSchema,
ResponseConfig extends ResponseValidatorConfig<TTSchema> = ResponseValidatorConfig<TTSchema>,
RequestConfigs extends RequestValidatorConfig[] = RequestValidatorConfig[],
TRequestSchema extends TSchema,
TResponseSchema extends TSchema,
ResponseConfig extends ResponseValidatorConfig<TResponseSchema> = ResponseValidatorConfig<TResponseSchema>,
RequestConfigs extends RequestValidatorConfig<TRequestSchema>[] = RequestValidatorConfig<TRequestSchema>[],
> extends Omit<ApiOperationOptions, 'requestBody' | 'parameters'> {
method: 'GET' | 'POST' | 'PATCH' | 'DELETE' | 'PUT';
responseCode?: number;
path?: string;
validate?: ValidatorConfig<TTSchema, ResponseConfig, RequestConfigs>;
validate?: ValidatorConfig<TRequestSchema, TResponseSchema, ResponseConfig, RequestConfigs>;
}

export interface SchemaValidator<TTSchema extends TSchema> {
schema: TTSchema;
export interface SchemaValidator<TRequestSchema extends TSchema, TResponseSchema extends TSchema> {
schema: TRequestSchema | TResponseSchema;
name: string;
check: ValidateFunction<Static<TTSchema>>;
validate(data: Obj | Obj[]): Static<TTSchema>;
check: ValidateFunction<Static<TRequestSchema | TResponseSchema>>;
validate(data: Obj | Obj[]): Static<TRequestSchema | TResponseSchema>;
}
export interface ValidatorConfigBase<TTSchema extends TSchema> {
schema?: TTSchema;
Expand All @@ -40,51 +41,56 @@ export interface ValidatorConfigBase<TTSchema extends TSchema> {
required?: boolean;
pipes?: (PipeTransform | Type<PipeTransform>)[];
}
export interface ResponseValidatorConfig<TTSchema extends TSchema> extends ValidatorConfigBase<TTSchema> {
schema: TTSchema;
export interface ResponseValidatorConfig<TResponseSchema extends TSchema> extends ValidatorConfigBase<TResponseSchema> {
schema: TResponseSchema;
type?: 'response';
responseCode?: number;
required?: true;
pipes?: never;
}

export interface ParamValidatorConfig<TTSchema extends TSchema> extends ValidatorConfigBase<TTSchema> {
schema?: TTSchema;
export interface ParamValidatorConfig<TRequestSchema extends TSchema> extends ValidatorConfigBase<TRequestSchema> {
schema?: TRequestSchema;
type: 'param';
name: string;
stripUnknownProps?: never;
}

export interface QueryValidatorConfig<TTSchema extends TSchema> extends ValidatorConfigBase<TTSchema> {
schema?: TTSchema;
export interface QueryValidatorConfig<TRequestSchema extends TSchema> extends ValidatorConfigBase<TRequestSchema> {
schema?: TRequestSchema;
type: 'query';
name: string;
stripUnknownProps?: never;
}

export interface BodyValidatorConfig<TTSchema extends TSchema> extends ValidatorConfigBase<TTSchema> {
schema: TTSchema;
export interface BodyValidatorConfig<TRequestSchema extends TSchema> extends ValidatorConfigBase<TRequestSchema> {
schema: TRequestSchema;
type: 'body';
}

export type RequestValidatorConfig<TTSchema extends TSchema> =
| ParamValidatorConfig<TTSchema>
| QueryValidatorConfig<TTSchema>
| BodyValidatorConfig<TTSchema>;
export type SchemaValidatorConfig<TTSchema extends TSchema> = RequestValidatorConfig<TTSchema> | ResponseValidatorConfig<TTSchema>;
export type RequestValidatorConfig<TRequestSchema extends TSchema> =
| ParamValidatorConfig<TRequestSchema>
| QueryValidatorConfig<TRequestSchema>
| BodyValidatorConfig<TRequestSchema>;
export type SchemaValidatorConfig<TRequestSchema extends TSchema, TResponseSchema extends TSchema> =
| RequestValidatorConfig<TRequestSchema>
| ResponseValidatorConfig<TResponseSchema>;

export type ValidatorType = NonNullable<SchemaValidatorConfig['type']>;
export type ValidatorType<TRequestSchema extends TSchema, TResponseSchema extends TSchema> = NonNullable<
SchemaValidatorConfig<TRequestSchema, TResponseSchema>['type']
>;

export interface ValidatorConfig<
TTSchema extends TSchema,
ResponseConfig extends ResponseValidatorConfig<TTSchema>,
RequestConfigs extends RequestValidatorConfig[],
TRequestSchema extends TSchema,
TResponseSchema extends TSchema,
ResponseConfig extends ResponseValidatorConfig<TResponseSchema>,
RequestConfigs extends RequestValidatorConfig<TRequestSchema>[],
> {
response?: TTSchema | ResponseConfig;
response?: TResponseSchema | ResponseConfig;
request?: [...RequestConfigs];
}

export type RequestConfigsToTypes<RequestConfigs extends RequestValidatorConfig[]> = {
export type RequestConfigsToTypes<TRequestSchema extends TSchema, RequestConfigs extends RequestValidatorConfig<TRequestSchema>[]> = {
[K in keyof RequestConfigs]: RequestConfigs[K]['required'] extends false
? RequestConfigs[K]['schema'] extends TSchema
? Static<RequestConfigs[K]['schema']> | undefined
Expand Down

0 comments on commit 080d409

Please sign in to comment.