Node.JS: How do you validate QueryParams in routing-controllers? - node.js

lets say you have an interface like this:
import { Get, QueryParam } from 'routing-controllers';
// ...
#Get('/students')
async getStudents(
#QueryParam('count') count?: number,
): Promise<void> {
console.log(count);
}
How do you ensure count is an int and not a float, for example? Something like this is not valid:
#IsInt() #QueryParam('count') count?: number,
IsInt can only be used on a class property, eg for a body model, not for a single parameter value. But according to. this https://github.com/typestack/routing-controllers#auto-validating-action-params it is possible:
This technique works not only with #Body but also with #Param,
#QueryParam, #BodyParam and other decorators.

I had missed this in the docs: https://github.com/typestack/routing-controllers#inject-query-parameters By injecting all of the QueryParams instead of individual QueryParam, you can validate them as a class model:
enum Roles {
Admin = "admin",
User = "user",
Guest = "guest",
}
class GetUsersQuery {
#IsPositive()
limit: number;
#IsAlpha()
city: string;
#IsEnum(Roles)
role: Roles;
#IsBoolean()
isActive: boolean;
}
#Get("/users")
getUsers(#QueryParams() query: GetUsersQuery) {
// here you can access query.role, query.limit
// and others valid query parameters
}
Also, make sure you don't use barrel-imports to import Enums, or open-api-generator will produce an error that the enum is undefined and not and object; eg avoid this: import { Roles } from '../..'

Related

NestJS: How to convert Mongoose document to regular class?

I have followed NestJS mongo docs to create a schema (without ID) from a regular class. Since this class has other general uses (unrelated to mongoose), I would like to have regular methods on that class as well:
#Schema({ _id: false })
export class Location {
#Prop()
lat: number;
#Prop()
lon: number;
regularMethod() { return something based on this.lat, this.lon }
}
export const LocationSchema = SchemaFactory.createForClass(Location);
export type CatDocument = HydratedDocument<Cat>;
#Schema()
export class Cat {
#Prop({ type: [LocationSchema] })
locations: Location[];
}
export const CatSchema = SchemaFactory.createForClass(Cat);
The problem is that if I query such an object from the db, regularMethod doesn't exist since the queried object is actually a Document based on Location, rather than Location. The document only exposes methods that were defined by the schema.methods, which is not what I need.
MyService {
constructor(#InjectModel(Cat.name) catModel: Model<CatDocument>) {}
async findLocations(catId: string) {
const cat = await catModel.findOneById(catId);
cat.location.forEach(loc => loc.regularMethod()) // no such method
}
}
Is there some obvious way to "cast" Location to the original class to have access to those methods?

Is it possible to maintain flat input structure with nestjs/graphql Args?

I have a simple query:
#Query(() => ProfileOutput)
async profile(#Args('id') id: string) {
return await this.profileFacade.findProfileById(input.id);
}
The problem is that I want to apply #IsMongoId() from class-validator for the id here. I do not want to create new #InputType here, because I do not want to change API specification. Is there a way to apply validator like #IsMongoId here without the need to change query definition for frontend?
For anyone seeking an answer I found a feature called dedicated argument class. Instead of creating new input type as I thought like this:
#InputType()
export class MongoIdBaseInput {
#IsMongoId()
#Field()
id: string;
}
#Query(() => ProfileOutput)
async profile(#Args('data') input: MongoIdBaseInput) {
return await this.profileFacade.findProfileById(input.id);
}
We can define it almost the same, but annotate input with ArgsType it will maintain flat structure of args for us:
#ArgsType()
export class MongoIdBaseInput {
#IsMongoId()
#Field()
id: string;
}
#Query(() => ProfileOutput)
async profile(#Args() input: MongoIdBaseInput) {
return await this.profileFacade.findProfileById(input.id);
}

NestJs/Swagger: How to add `additionalProperties: false` on an existing DTO class

Hello I am new to Nestjs and trying to implement additionalProperties: false on a DTO class that already has properties on it. I see that the additionalProperties key can be added inside #ApiProperty({ schema: ... { additionalProperties : false} }) but I want to add it like this:
class SomeResponseDto {
#ApiResponseProperty()
text: string;
#ApiResponseProperty()
id: string;
// maybe a new Decorator like this?
#ApiAdditionalProperties(false)
}
...so that only text and id is allowed in the SomeResponseDto. I want to avoid having to define every class as a schema object inside the controllers.
I should note that I'm using express-openapi-validator with nestjs/swagger, and do not want to use the class-validator/class-transformer plugins, so that I can validate responses as well as requests by using just nestjs/swagger decorators.
I have also tried this:
#ApiResponse({
status: 200,
description: 'success',
schema: {
oneOf: [
{
$ref: getSchemaPath(SomeResponseDto),
// additionalProperties: false, <-- this gets ignored
},
],
// additionalProperties: false, <-- this throws OpenApi invalid response errors
},
Is there any easy way to add additionalProperties: false on an existing DTO class?
Here is a workaround: Post this code inside the bootstrap() method of the application
const schemas = document?.components?.schemas;
Object.keys(schemas).forEach((item) => {
if (schemas[item]['properties']?.allowAdditional) {
schemas[item]['additionalProperties'] = true;
} else {
schemas[item]['additionalProperties'] = false;
}
});
This code above will set additionalProperties to false by default.
If for some reason you have a DTO class that you want to allow additionalProperties: true, then inside your DTO Class, add the following decorator and property:
export class SomeResponseDTO {
#ApiPropertyOptional()
allowAdditional?: boolean;
#ApiResponseProperty()
text: string;
#ApiResponseProperty()
id: string;
}
This is a simple solution for true/false case, but can be modified as needed to handle other use cases.
I hope this helps someone!

Conditionally serialize the response attributes

I want to return a different response from a Game controller depending on whether or not a User owns the game or is simply invited to it. Essentially: filter out certain attributes from the response if the user is only invited to the game.
Here's a naive implementation of what I want using two different controllers:
#SerializeOptions({
groups: ['invited'],
})
#Get(':id')
async findOne(#User() user, #Param('id') id: string) {
const retGame = await this.gamesService.findOne(id);
const ability = this.caslAbilityFactory.createForUser(user);
Throw401(ability.can(Action.Read, retGame));
return retGame;
}
#SerializeOptions({
groups: ['owner'],
})
#Get('full/:id')
async findOneFull(#User() user, #Param('id') id: string) {
const retGame = await this.gamesService.findOne(id);
const ability = this.caslAbilityFactory.createForUser(user);
Throw401(ability.can(Action.FullRead, retGame));
// the main difference is ^^^^^^^^, using a different CASL rule for authorization
return retGame;
}
I'm using a different set of CASL rules to allow a "full read" or not, the full read being only allowed for the game owner. That way I can attach a different group through the SerializeOptions decorator, which allows me to conditionally expose an entity attribute:
#Column()
#Expose({ groups: ['owner'] })
inviteKey: string;
But it feels wrong to use different routes and methods to basically do the same thing, I'd like to pass a dynamic condition (user.id === ownerId) instead of a group to the Expose decorator, and I believe the best next thing would be to use an interceptor to filter certain fields from the response. I'm not sure how to proceed from there, would an interceptor be the right approach?
I have stumbled upon the same problem, where it makes sense to have one route but based on user rights give the result with some attributes filtered out from the response. Disclaimer: Since I'm fairly new to nestjs there are some gaps in my knowledge and it might be possible that it is not a proper way to achieve the intended result.
To achieve this you'd have to:
Use your already defined class with exposed/excluded attributes - using class-transformer and groups should add/remove attributes from the class when transforming to json object
Create an interceptor - this will intercept the response before it gets sent to clinet
Use the interceptor for desired routes
So in your case, it could look something like this:
db/models/game.model.ts
import { Exclude, Expose } from 'class-transformer';
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
#Entity()
export class Game {
#Exponse()
#PrimaryGeneratedColumn()
id: number;
#Column()
#Expose({ groups: ['owner'] })
inviteKey: string;
#Column()
#Expose({ groups: ['owner', 'invited']})
name: string;
}
transform.interceptor.ts
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '#nestjs/common';
import { classToPlain } from 'class-transformer';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Game } from 'db/models/game.model';
export interface Response<T> {
data: T;
}
#Injectable()
export class TransformInterceptor<T>
implements NestInterceptor<T, Response<T>>
{
intercept(
context: ExecutionContext,
next: CallHandler,
): Observable<Response<T>> {
// This is how you can get access to requesting user (asuming it is in the request)
const request = context.switchToHttp().getRequest();
const user = request.user;
/* get your group based on the CASL rules. */
const group = 'invited';
// Transform the data (filter out private fields)
return next.handle().pipe(map((data) => {
if (data instanceof Game) {
return classToPlain(game, { groups: [group] });
}
// in case there is Game[]
if (
Array.isArray(data)
&& data[0] instanceof Game
) {
return data.map(game => classToPlain(game, { groups: [group] }));
}
// in case response is something else, don't touch it
return data;
}));
}
}
games.controller.ts
// Use the interceptor here - you no longer need `/games/full/:id` route
#UseInterceptors(TransformInterceptor)
#Get(':id')
async findOne(#User() user, #Param('id') id: string) {
// Return the Game instance here, interceptor will take care of transforming it
return this.gamesService.findOne(id);
}
As a result to your GET /games/:id as user with Action.Read access the output should be
{
"id": 1,
"name": "Best game in the business"
}
As a result to your GET /games/:id as user with Action.FullRead access the output should be
{
"id": 1,
"inviteKey": "super-secret-invite-key",
"name": "Best game in the business"
}
Hopefully this helps.

Wrong data from client passes GraphQL validation

I've made simple CRUD app with React and Apollo client on NestJS server with GraphQL API.
I have this simple Mutations:
schema.gql:
type Mutation {
createUser(input: CreateUserInput!): User! // CreateUserInput type you can see in user.input.ts below
updateUser(id: ID!, input: UpdateUserInput!): User!
deleteUser(id: ID!): User!
}
user.input.ts:
import { InputType, Field } from "#nestjs/graphql";
import { EmailScalar } from "../email.scalar-type";
#InputType()
export class CreateUserInput {
// EmailScalar is a custom Scalar GraphQL Type that i took from the internet and it worked well
#Field(() => EmailScalar)
readonly email: string;
#Field()
readonly name: string;
}
"EmailScalar" type checks if "email" input has *#*.* format basically
And when i make createUser Query to GraphQL API like this:
It cannot pass validation
(because Email type works fine)
But when Query sent from client - it passes validation:
NestJS server log (from code below)
users.resolver.ts:
#Mutation(() => User)
async createUser(#Args('input') input: CreateUserInput) { // Type from user.input.ts
Logger.log(input); // log from screenshot, so if it's here it passed validation
return this.usersService.create(input); // usersService makes requests to MongoDB
}
And it gets into MongoDB
Here is client side part:
App.tsx:
...
// CreateUserInput class is not imported to App.tsx (it is at server part) but it seems to be fine with it
const ADD_USER = gql`
mutation AddMutation($input: CreateUserInput!) {
createUser(input: $input) {
id
name
email
}
}
`
function App(props: any) {
const { loading, error, data } = useQuery(GET_USERS);
const [addUser] = useMutation(
ADD_USER,
{
update: (cache: any, { data: { createUser } }: any) => {
const { users } = cache.readQuery({ query: GET_USERS });
cache.writeQuery({
query: GET_USERS,
data: {
users: [createUser, ...users],
},
})
}
}
);
...
if (loading) return <p>Loading...</p>;
if (error) return <p>Error :(</p>;
return <UserTable users={data.users} addUser={addUser} updateUser={updateUser} deleteUser={deleteUser} />;
}
Can someone please explain to me, how does client Query passes validation and what have i done wrong?
Even two empty strings can pass through.
Never worked with NestJS, Apollo, React or GraphQL before, so I'm kinda lost.
For full code:
https://github.com/N238635/nest-react-crud-test
This is how your custom scalar's methods are defined:
parseValue(value: string): string {
return value;
}
serialize(value: string): string {
return value;
}
parseLiteral(ast: ValueNode): string {
if (ast.kind !== Kind.STRING) {
throw new GraphQLError('Query error: Can only parse strings got a: ' + ast.kind, [ast]);
}
// Regex taken from: http://stackoverflow.com/a/46181/761555
var re = /^([\w-]+(?:\.[\w-]+)*)#((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$/i;
if (!re.test(ast.value)) {
throw new GraphQLError('Query error: Not a valid Email', [ast]);
}
return ast.value;
}
parseLiteral is called when parsing literal values inside the query (i.e. literal strings wrapped in double quotes). parseValue is called when parsing variable values. When your client sends the query, it sends the value as a variable, not as a literal value. So parseValue is used instead of parseLiteral. But your parseValue does not do any kind of validation -- you just return the value as-is. You need to implement the validation logic in both methods.
It would also be a good idea to implement the serialize method so that your scalar can be used for both input and response validation.

Resources