Create a Header Custom Validation with NestJS and class-validator - nestjs

I have been working to validate a request using the class-validator, and NestJS validation plus trying to validate the header contents. My basic interfaces are all working, but now I am trying to compare some header field data the same way.
I had this question about the custom decorator to try to handle the headers, but the solution to that question, will return the one header. I want to be able to handle them all, similar to how all the body() data is processed.
I need to be able to create a custom decorator for extracting the header fields, and being able to pass them into the class-validator DTO.
For Instance, I want to validate three header fields, such as:
User-Agent = 'Our Client Apps'
Content-Type = 'application/json'
traceabilityId = uuid
There are more fields, but if I can get this going, then I can extrapolate out the rest. I have a simple controller example:
#Controller(/rest/package)
export class PackageController {
constructor(
private PackageData_:PackageService
)
{ }
...
#Post('inquiry')
#HttpCode(HttpStatus.OK) // Not creating data, but need body, so return 200 OK
async StatusInquiry(
#RequestHeader() HeaderInfo:HeadersDTO, // This should be the Headers validation using the decorator from the question above.
I am trying to validate that the headers of the request contain some specific data, and I am using NestJS. I found this information. While this is what I want to do, and it looks proper, the ClassType reference does not exist, and I am not sure what to use instead.
From the example, the decorator is referring to.
request-header.decorator.ts
export interface iError {
statusCode:number;
messages:string[];
error:string;
}
export const RequestHeader = createParamDecorator(
async (value: any, ctx: ExecutionContext) => {
// extract headers
const headers = ctx.switchToHttp().getRequest().headers;
// Convert headers to DTO object
const dto = plainToClass(value, headers, { excludeExtraneousValues: true });
// Validate
const errors: ValidationError[] = await validate(dto);
if (errors.length > 0) {
let ErrorInfo:IError = {
statusCode: HttpStatus.BAD_REQUEST,
error: 'Bad Request',
message: new Array<string>()
};
errors.map(obj => {
AllErrors = Object.values(obj.constraints);
AllErrors.forEach( (OneError) => {
OneError.forEach( (Key) => {
ErrorInfo.message.push(Key);
});
});
// Your example, but wanted to return closer to how the body looks, for common error parsing
//Get the errors and push to custom array
// let validationErrors = errors.map(obj => Object.values(obj.constraints));
throw new HttpException(`${ErrorInfo}`, HttpStatus.BAD_REQUEST);
}
// return header dto object
return dto;
},
I am having trouble generically mapping the constraints into a string array.
My HeadersDTO.ts:
import { Expose } from 'class-transformer';
import { Equals, IsIn, IsString } from 'class-validator';
export class HeadersDTO {
#IsString()
#Equals('OurApp')
#Expose({ name: 'user-agent' })
public readonly 'user-agent':string;
#IsString()
#IsIn(['PRODUCTION', 'TEST'])
public readonly operationMode:string;
}
Headers being sent via Postman for the request:
Content-Type:application/json
operationMode:PRODUCTION
Accept-Language:en

I have just tested the following code and this is working. I think you are missing appropriate type over here,
async StatusInquiry(
#RequestHeader() HeaderInfo:HeadersDTO,
You should have HeadersDTO passed in as param in RequestHeader Decorator this #RequestHeader(HeadersDTO) HeaderInfo:HeadersDTO,
Then I have created customDecorator.ts like this,
export const RequestHeader = createParamDecorator(
//Removed ClassType<unknown>,, I don't think you need this here
async (value: any, ctx: ExecutionContext) => {
// extract headers
const headers = ctx.switchToHttp().getRequest().headers;
// Convert headers to DTO object
const dto = plainToClass(value, headers, { excludeExtraneousValues: true });
// Validate
const errors: ValidationError[] = await validate(dto);
if (errors.length > 0) {
//Get the errors and push to custom array
let validationErrors = errors.map(obj => Object.values(obj.constraints));
throw new HttpException(`Validation failed with following Errors: ${validationErrors}`, HttpStatus.BAD_REQUEST);
}
// return header dto object
return dto;
},
);
My HeadersDTO.ts file
export class HeadersDTO
{
#IsDefined()
#Expose({ name: 'custom-header' })
"custom-header": string; // note the param here is in double quotes
}
The reason i found this when I looked compiled TS file, it looks like this,
class HeadersDTO {
}
tslib_1.__decorate([
class_validator_1.IsDefined(),
class_transformer_1.Expose({ name: 'custom-header' }),
tslib_1.__metadata("design:type", String)
], HeadersDTO.prototype, "custom-header", void 0);
exports.HeadersDTO = HeadersDTO;
I get following error when i do not pass the header ,
[
ValidationError {
target: HeadersDTO { 'custom-header': undefined },
value: undefined,
property: 'custom-header',
children: [],
constraints: { isDefined: 'custom-header should not be null or undefined' }
}
]

As mentioned in the issue you referenced, you need to create a DTO class and pass it to RequestHeader decorator.
e.g
export class MyHeaderDTO {
#IsString()
#IsDefined()
#Expose({ name: 'myheader1' }) // required as headers are case insensitive
myHeader1: string;
}
...
#Get('/hello')
getHello(#RequestHeader(MyHeaderDTO) headers: MyHeaderDTO) {
console.log(headers);
}

Related

Assign route dynamically Node/Express

I need dynamically assign a new route but it for some reason refuses to work.
When I send a request in the Postman it just keeps waiting for a response
The whole picture of what I am doing is the following:
I've got a controller with a decorator on one of its methods
#Controller()
export class Test {
#RESTful({
endpoint: '/product/test',
method: 'post',
})
async testMe() {
return {
type: 'hi'
}
}
}
export function RESTful({ endpoint, method, version }: { endpoint: string, version?: string, method: HTTPMethodTypes }) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor): void {
const originalMethod = descriptor.value
Reflect.defineMetadata(propertyKey, {
endpoint,
method,
propertyKey,
version
}, target)
return originalMethod
}
}
export function Controller() {
return function (constructor: any) {
const methods = Object.getOwnPropertyNames(constructor.prototype)
Container.set(constructor)
for (let action of methods) {
const route: RESTfulRoute = Reflect.getMetadata(action, constructor.prototype)
if (route) {
const version: string = route.version ? `/${route.version}` : '/v1'
Container.get(Express).injectRoute((instance: Application) => {
instance[route.method](`/api${version}${route.endpoint}`, async () => {
return await Reflect.getOwnPropertyDescriptor(constructor, route.propertyKey)
// return await constructor.prototype[route.propertyKey](req, res)
})
})
}
}
}
}
Is it possible to dynamically set the route in the way?
I mainly use GraphQL but sometimes I need RESTful API too. So, I want to solve this by that decorator
In order for the response to finish, there must be a res.end() or res.json(...) or similar. But I cannot see that anywhere in your code.

How can i test this decorator?

Thats basically the model of the code. I tried somethings but i`m new at testing, so the lines isnt been covered.
So, I don't have idea of how I can acess and testing the content from createParamDecorator(). Or of how I test the headers, dto, and everything.
I tried to mock de ExecutionContext value and the switchToHttp() and put everything to return null and the test pass. But the lines wasn't covered.
import { plainToClass } from 'class-transformer';
// The import below fails
import { ClassType } from 'class-transformer/ClassTransformer';
import { validateOrReject } from 'class-validator';
export const RequestHeader = createParamDecorator(
async (value: any, ctx: ExecutionContext) => {
// extract headers
const headers = ctx.switchToHttp().getRequest().headers;
// Convert headers to DTO object
const dto = plainToClass(value, headers, { excludeExtraneousValues: true });
// Validate
await validateOrReject(dto);
// return header dto object
return dto;
},
);

Nestjs - Use applyDecorators inside createParamDecorator

I'm using Nestjs decorators and am trying to make the most of custom decorators. I'm trying to write my own custom #Body param decorator that validates and applies multiple decorators at the same time.
Does anyone know if the below is possible? I'm having difficulty getting the second argument in the transform call of the pipes to have metadata: ArgumentMetadata.
export const MyParamDecorator = <T>(myDto: T) => {
return createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
applyDecorators( // also get SetMeta and Pipes to validate DTO
SetMetadata('thisWorks', true)
UsePipes(CustomValidationPipe, OtherPipe), // + add MyDTO - type T somehow..
);
return doAsyncWork()
},
)();
}
#Controller('users')
export class UsersController {
#Patch(':id')
update(#MyParamDecorator() asyncWork: Promise<any>) { // <------ Promise<any> is custom async opperation that will be handled. (So I can't type the DTO here..)
return reqBody;
}
}
I ran across this question because I needed a similar answer. Hopefully what I've found is helpful.
Part 1. You can do async processing in a decorator.
And, it would resolve for you, so your controller would look like:
update(#MyParamDecorator() asyncWork: any) {
Notice that Promise<any> is just any.
Part 2. You can get ArgumentMetadata using an enhancer.
Here is a quick example, let's assume METADATA__PARAM_TYPE is myType.
param-type.enhancer.ts
export const paramTypeEnhancer: ParamDecoratorEnhancer = (
target: Record<string, unknown>,
propertyKey: string,
parameterIndex: number,
): void => {
const paramTypes = Reflect.getOwnMetadata('design:paramtypes', target, propertyKey);
const metatype = paramTypes[parameterIndex];
Reflect.defineMetadata(METADATA__PARAM_TYPE, metatype, target[propertyKey]);
};
my-decorator.ts
import { paramTypeEnhancer } from './param-type.enhancer';
export const MyDecorator = createParamDecorator(
async (data: unknown, ctx: ExecutionContext): Promise<any> => {
const metatype = Reflect.getOwnMetadata(METADATA__PARAM_TYPE, ctx.getHandler());
const argument: ArgumentMetadata = {
type: 'custom',
data: undefined,
metatype: metatype,
};
// Do processing here... You can return a promise.
},
[paramTypeEnhancer],
);
See this gist for a full annotated version: https://gist.github.com/josephdpurcell/fc04cfd428a6ee9d7ffb64685e4fe3a6

NestJS Validate Headers using class-transform example

I am trying to validate that the headers of the request contain some specific data, and I am using NestJS. I found this information. While this is what I want to do, and it looks proper, the ClassType reference does not exist, and I am not sure what to use instead.
From the example, the decorator is referring to.
request-header.decorator.ts
import { createParamDecorator, ExecutionContext } from '#nestjs/commom'
import { plainToClass } from 'class-transformer';
// The import below fails
import { ClassType } from 'class-transformer/ClassTransformer';
import { validateOrReject } from 'class-validator';
export const RequestHeader = createParamDecorator(
async (value: ClassType<unknown>, ctx: ExecutionContext) => {
// extract headers
const headers = ctx.switchToHttp().getRequest().headers;
// Convert headers to DTO object
const dto = plainToClass(value, headers, { excludeExtraneousValues: true });
// Validate
await validateOrReject(dto);
// return header dto object
return dto;
},
);
Rather than passing the type through a decorator like this, I'd suggest making a custom decorator and setting the validateCustomDecorators option for the ValidationPipe to true. The decorator would look something like
const Header = createParamDecorator((data: unknown, context: ExecutionContext) => {
const req = context.switchToHttp().getRequest();
if (data) {
return req.headers[data];
}
return req.headers;
});
And now instead of #Header() from #nestjs/common you can you #Header() from this file and get the ValidationPipe to run after applying the appropriate type metadata

How to Type Fastify Reply payloads?

I'm just getting into Fastify with Typescript and really enjoying it.
However, I'm trying to figure out if I can type the response payload. I have the response schema for serialization working and that may be sufficient, but I have internally typed objects (such as IUser) that it would be nice to have Typescript check against.
The following works great, but I'd like to return an TUser for example and have typescript if I return something different. Using schema merely discludes fields.
interface IUser {
firstname: string,
lastname: string
} // Not in use in example
interface IUserRequest extends RequestGenericInterface {
Params: { username: string };
}
const getUserHandler = async (
req: FastifyRequest<IUserRequest, RawServerBase, IncomingMessage | Http2ServerRequest>
) => {
const { username } = req.params;
return { ... }; // Would like to return instance of IUser
};
app.get<IUserRequest>('/:username', { schema }, helloWorldHandler);
Is there an equivalent of RequestGenericInterface I can extend for the response?
Small Update: It seems that the reply.send() can be used to add the type, but it would be nice for self-documentation sake to provide T higher up.
From the documentation:
Using the two interfaces, define a new API route and pass them as generics. The shorthand route methods (i.e. .get) accept a generic object RouteGenericInterface containing five named properties: Body, Querystring, Params, Headers and Reply.
You can use the Reply type.
interface MiscIPAddressRes {
ipv4: string
}
server.get<{
Reply: MiscIPAddressRes
}>('/misc/ip-address', async (req, res) => {
res
.status(_200_OKAY)
.send({
ipv4: req.ip // this will be typechecked
})
})
After looking at the type definitions, I found out that there is also an alternative way to only type-check the handler (like in Julien TASSIN's answer), like this:
import { FastifyReply, FastifyRequest, RawReplyDefaultExpression, RawRequestDefaultExpression, RawServerDefault } from "fastify";
import { RouteGenericInterface } from "fastify/types/route";
interface IUser {
firstname: string;
lastname: string;
}
interface IUserRequest extends RouteGenericInterface {
Params: { username: string };
Reply: IUser; // put the response payload interface here
}
function getUserHandler(
request: FastifyRequest<IUserRequest>,
reply: FastifyReply<
RawServerDefault,
RawRequestDefaultExpression,
RawReplyDefaultExpression,
IUserRequest // put the request interface here
>
) {
const { username } = request.params;
// do something
// the send() parameter is now type-checked
return reply.send({
firstname: "James",
lastname: "Bond",
});
}
You can also create your own interface with generic to save writing repeating lines, like this:
import { FastifyReply, FastifyRequest, RawReplyDefaultExpression, RawRequestDefaultExpression, RawServerDefault } from "fastify";
import { RouteGenericInterface } from "fastify/types/route";
export interface FastifyReplyWithPayload<Payload extends RouteGenericInterface>
extends FastifyReply<
RawServerDefault,
RawRequestDefaultExpression,
RawReplyDefaultExpression,
Payload
> {}
then use the interface like this:
function getUserHandler(
request: FastifyRequest<IUserRequest>,
reply: FastifyReplyWithPayload<IUserRequest>
) {
const { username } = request.params;
// do something
// the send() parameter is also type-checked like above
return reply.send({
firstname: "James",
lastname: "Bond",
});
}
If you want to type the handler only, you can perform it this way
import { RawReplyDefaultExpression, RawRequestDefaultExpression, RawServerDefault, RouteHandler, RouteHandlerMethod } from "fastify";
const getUserHandler: RouteHandlerMethod<
RawServerDefault,
RawRequestDefaultExpression,
RawReplyDefaultExpression,
{ Reply: IUser; Params: { username: string } }
> = async (
req: FastifyRequest<IUserRequest, RawServerBase, IncomingMessage | Http2ServerRequest>
) => {
const { username } = req.params;
return { ... }; // Would like to return instance of IUser
};
Trying to type these was a truely awful experience. Thanks to the other answers, this is where I ended up. Bit of a code dump to make life easier for others.
request-types.ts
With this I am standardising my response to optionally have data and message.
import {
FastifyReply,
FastifyRequest,
RawReplyDefaultExpression,
RawRequestDefaultExpression,
RawServerDefault,
} from 'fastify';
type ById = {
id: string;
};
type ApiRequest<Body = void, Params = void, Reply = void> = {
Body: Body;
Params: Params;
Reply: { data?: Reply & ById; message?: string };
};
type ApiResponse<Body = void, Params = void, Reply = {}> = FastifyReply<
RawServerDefault,
RawRequestDefaultExpression,
RawReplyDefaultExpression,
ApiRequest<Body, Params, Reply>
>;
type RouteHandlerMethod<Body = void, Params = void, Reply = void> = (
request: FastifyRequest<ApiRequest<Body, Params, Reply>>,
response: ApiResponse<Body, Params, Reply>
) => void;
export type DeleteRequestHandler<ReplyPayload = ById> = RouteHandlerMethod<void, ById, ReplyPayload>;
export type GetRequestHandler<ReplyPayload> = RouteHandlerMethod<void, ById, ReplyPayload>;
export type PostRequestHandler<Payload, ReplyPayload> = RouteHandlerMethod<Payload, void, ReplyPayload>;
export type PatchRequestHandler<Payload, ReplyPayload> = RouteHandlerMethod<Payload, ById, ReplyPayload>;
export type PutRequestHandler<Payload, ReplyPayload> = RouteHandlerMethod<Payload, ById, ReplyPayload>;
Usage
get-account.ts - GetRequestHandler
export const getAccount: GetRequestHandler<AccountResponseDto> = async (request, reply) => {
const { id } = request.params;
...
const account = await Account.findOne....
...
if (account) {
return reply.status(200).send({ data: account });
}
return reply.status(404).send({ message: 'Account not found' });
};
delete-entity.ts - DeleteRequestHandler
export const deleteEntity: DeleteRequestHandler = async (request, reply) => {
const { id } = request.params;
...
// Indicate success by 200 and returning the id of the deleted entity
return reply.status(200).send({ data: { id } });
};
update-account.ts - PatchRequestHandler
export const updateAccount: PatchRequestHandler<
UpdateAccountRequestDto,
AccountResponseDto
> = async (request, reply) => {
const { id } = request.params;
...
return reply.status(200).send({ data: account });
};
register-account-routes.ts - No errors with provided handler.
export const registerAccountRoutes = (app: FastifyInstance) => {
app.get(EndPoints.ACCOUNT_BY_ID, getAccount);
app.patch(EndPoints.ACCOUNT_BY_ID, updateAccount);
app.post(EndPoints.ACCOUNTS_AUTHENTICATE, authenticate);
app.put(EndPoints.ACCOUNTS, createAccount);
};

Resources