nodejs class-validator validating array of objects - node.js

I have a array of objects that looks like this
[{
name: 'some name'
catId: 2,
}, {
name: 'another name'
catId: 3,
}]
How can I validate using class-validator so that the name field is required and minimum 2 chars long in each object?
Thanks

To validate an array of items, you need to use #ValidateNested({ each: true }).
A complete example:
import { validate, IsString, MinLength, ValidateNested } from 'class-validator';
class MySubClass {
#IsString()
#MinLength(2)
public name: string;
constructor(name: string ){
this.name = name;
}
}
class WrapperClass {
#ValidateNested({ each: true })
public list: MySubClass[];
constructor(list: MySubClass[]) {
this.list = list;
}
}
const subClasses = Array(4)
.fill(null)
.map(x => new MySubClass('Test'))
subClasses[2].name = null;
const wrapperClass = new WrapperClass(subClasses);
const validationErrors = await validate(wrapperClass);
This will log a validation error for subClasses[2] as expected.

Related

Mongoose with Typescript: Trying to split schema, methods and statics in seperate files, problem with this has type any and is hidden by container

i'm trying to split up my single-files mongoose schemas with statics and methods.
(I found this tutorial for splitting: https://medium.com/swlh/using-typescript-with-mongodb-393caf7adfef ) I'm new to typescript but love the benefits it gives while coding.
I've splitted my user.ts into:
user.schema.ts
user.model.ts
user.types.ts
user.statics.ts
user.methods.ts
When i change this lines in my schema file:
UserSchema.statics.findUserForSignIn = async function findUserForSignIn(
email: string
): Promise<IUserDocument | null> {
const user = await this.findOne({ email: email });
if (!user) {
return user;
} else {
return user;
}
}
to UserSchema.statics.findUserForSignIn = findUserForSignIn;
and copy the Function findUserForSignIn to user.statics.ts, Typescript says "'this' implicitly has type 'any' because it does not have a type annotation" and "An outer value of 'this' is shadowed by this container."
So, how to add this properly? If i add this to findUserForSignIn with IUserModel as Type, add null to Promise return type it would nearly work:
export async function findUserForSignIn(
this: IUserModel,
email: string
): Promise<IUserDocument | null> {
const user = await this.findOne({ "person.email": email });
return user;
}
And if i add this to receiving function parameters: users gets to type IUserDocument, before it was any. I think its nice to have typeclear, not just any.
But: in user.schema.ts the UserSchema.statics.findUserForSignIn gets a red line from typescript. Type can not be assigned to other type. The signature of this is not identical.
If i change the type of this to any, all is okay. But the return is not longer from type IUserDocument. Mabye its okay if i get over an aggregation pipeline and only set the Promise-Return-Type. But that this: any gets hinted in yellow by typescript.
And, another question: if i pass this as first and email as second parameter, why is only one parameter required?
Anyone has an "how to" for me? Or can explain what i've done wrong? Or what is the best way? Or is it not possible to split statics and methods in seperate files from schema?
Original files:
user.schema.ts
import { Schema } from "mongoose";
import { PersonSchema } from "./person.schema";
import { findUserForSignIn } from "./user.statics";
import { IUserDocument } from "./user.types";
const UserSchema = new Schema<IUserDocument>({
firstname: {
type: String,
required: true,
},
lastname: {
type: String,
required: true,
},
email: {
type: String,
required: true,
},
});
UserSchema.statics.findUserForSignIn = findUserForSignIn;
export default UserSchema;
user.types.ts
import { Document, Model } from "mongoose";
import { IPerson } from "./person.types";
export interface IUser {
firstname: string;
lastname: string;
email: string;
}
export interface IUserDocument extends IUser, Document {}
export interface IUserModel extends Model<IUserDocument> {
findUserForSignIn: (email: string) => Promise<IUserDocument>;
}
user.model.ts
import { model } from "mongoose";
import UserSchema from "./user.schema";
import { IUserDocument, IUserModel } from "./user.types";
const User = model<IUserDocument>("User", UserSchema) as IUserModel;
export default User;
user.statics.ts
import { IUserDocument } from "./user.types";
export async function findUserForSignIn(
email: string
): Promise<IUserDocument | null> {
const user = await this.findOne({ email: email });
if (!user) {
return user;
} else {
return user;
}
}
The only way seems to change the user.statics.ts
export async function findUserForSignIn(
this: Model<IUserDocument>,
email: string
): Promise<IUserDocument | null> {
console.log("E-Mail", email);
const user = await this.findOne({ email: email });
return user;
}
this has to be of type Model
Then code seems to be okay.

How to combine multiple class-validator constrains in Nestjs request class?

I have two validator classes NameMinLengthValidator and NameMaxLengthValidator
import { ValidatorConstraint, ValidatorConstraintInterface, ValidationArguments } from 'class-validator';
#ValidatorConstraint({ name: 'name', async: false })
export class NameMinLengthValidator implements ValidatorConstraintInterface {
validate(text: string, args: ValidationArguments) {
return !!text && 2 <= text.length;
}
defaultMessage(args: ValidationArguments) {
return 'Name must be at least 2 characters.';
}
}
#ValidatorConstraint({ name: 'name', async: false })
export class NameMaxLengthValidator implements ValidatorConstraintInterface {
validate(text: string, args: ValidationArguments) {
return !!text && text.length <= 12;
}
defaultMessage(args: ValidationArguments) {
return 'Name must be max 12 characters.';
}
}
I have to this in every class where I want to validate these constraints
export class MyRequest {
#Validate(NameMinLengthValidator)
#Validate(NameMaxLengthValidator)
name: string;
}
I want to achieve something similar to this, how can I combine both validators?
export class MyRequestCombined {
#Validate(NameLengthValidator)
name: string;
}
You can use NestJS built-in function to combine multiple decorators.
Example from documentation
import { applyDecorators } from '#nestjs/common';
export function Auth(...roles: Role[]) {
return applyDecorators(
SetMetadata('roles', roles),
UseGuards(AuthGuard, RolesGuard),
ApiBearerAuth(),
ApiUnauthorizedResponse({ description: 'Unauthorized"' }),
);
}
source: https://docs.nestjs.com/custom-decorators

How can I validate an array of enum values with Nestjs

I feel like a combination of this thread and this thread is what I need to implement, I'm having trouble drawing them together.
I have a DTO that contains an enum.
Using Postman, I am sending a PurchasableType of FOO and expecting to get an error of some sort. Reading through the above links, it seems like the process is quite involved; which makes me thing I'm completely missing the point.
How can I use the validation pipe(s) to make sure only the values in the purchasable-type.enum.ts are allowed?
Thank you for any suggestions!
// create-order.dto.ts
import { IsEmail, IsNotEmpty, IsEnum } from 'class-validator';
import { PurchasableType } from '../enum/purchaseable-type.enum';
export class CreateOrderDto {
#IsNotEmpty()
readonly userId: string;
#IsNotEmpty()
readonly locationId: string;
#IsNotEmpty()
#IsEnum(PurchasableType)
readonly purchasableType: PurchasableType;
#IsNotEmpty()
#IsEmail()
readonly email: string;
}
// purchasable-type.enum.ts
export enum PurchasableType {
CLINIC = 'CLINIC',
EVENT = 'EVENT',
LESSON = 'LESSON',
RESERVATION = 'RESERVATION',
TEAM = 'TEAM',
}
EDIT
It seems I was also not defining the entity correctly, and that may have been the main issue. I am still curious if my implementation good/bad.
// order.entity.ts
...
import { PurchasableType } from '../enum/purchaseable-type.enum';
#Entity()
export class Order extends BaseEntity {
#PrimaryGeneratedColumn()
id: number;
#Column({
type: 'enum',
enum: PurchasableType,
})
Now when I send a purchasableType of foo I am getting a 500 error. If I send any valid value that is within the enum I am getting a 200/201.
EDIT 2
Sure - here is a bit wider view of what I've got. Everything seems to be working properly, I'd just like to have a better grasp of what was really happening.
// event.controller.ts
#Post('/:id/orders')
async purchaseEventTickets(#Body() createOrderDto: CreateOrderDto):
Promise<Order> {
return await this.eventService.purchaseEventTickets(createOrderDto);
}
// create-order.dto.ts
export class CreateOrderDto {
#IsNotEmpty()
#IsEnum(PurchasableType)
readonly purchasableType: PurchasableType;
}
// event.service.ts
async purchaseEventTickets(createOrderDto: CreateOrderDto): Promise<Order> {
...
return await this.orderRepository.createOrder(createOrderDto);
}
// order.repository.ts
async createOrder(createOrderDto: CreateOrderDto): Promise<Order> {
const { purchasableType } = createOrderDto;
const order = this.create();
order.purchasableType = purchasableType;
try {
await order.save();
} catch (error) {
this.logger.error(`Failed to create the order: ${error.stack}`);
throw new InternalServerErrorException();
}
return order;
}
Using Postman, if I send an invalid value of "Foo" as a PurchasableType I get the expected error.
It took me a while to find a good solution.
#ApiProperty({
description: 'List of enums',
isArray: true,
enum: MyEnum
})
#IsEnum(MyEnum, { each: true })
prop: MyEnum[];
Here is what your create-dto looks like that contains an enum.
// create-order.dto.ts
import { IsEmail, IsNotEmpty, IsEnum } from 'class-validator';
import { PurchasableType } from '../enum/purchaseable-type.enum';
export class CreateOrderDto {
...
#IsNotEmpty()
#IsEnum(PurchasableType)
readonly purchasableType: PurchasableType;
}
Here is what that enum file looks like:
// purchasable-type.enum.ts
export enum PurchasableType {
CLINIC = 'CLINIC',
EVENT = 'EVENT',
LESSON = 'LESSON',
RESERVATION = 'RESERVATION',
TEAM = 'TEAM',
}
From there I can confidently expect the value of the enum to be one of the above values. If some other value comes through, nest will throw a validation error.
Additionally, If you are attempting to use a nested object (or something with multiple attributes or an array) you can do something like this in your DTO:
import { PurchasableType } from '../interface/purchasable-type.interface';
...
#ApiProperty()
#IsArray()
#ArrayMinSize(7)
#ArrayMaxSize(7)
#ValidateNested({ each: true })
#Type(() => PurchasableType)
#IsNotEmpty()
readonly PurchasableType: PurchasableType[];
...
#IsArray()
#IsEnum(enum, { each: true })
prop: enum[]
#IsEnum(myEnum, { each: true })
#Transform((value) => myEnum[value])
tags: myEnum[];
None of the above worked for me, I did it this way:
#Column({ type: 'enum', enum: MyEnum, array: true })
myProperty: MyEnum[];

nestjs ValidatorConstraint in order, reflect custom validator to swagger definition

In nestjs, in order validate query parameter of startDate and endDate
1, sartDate must before endDate, here is custom validator
#ValidatorConstraint({ name: 'isBefore', async: false })
export class IsBeforeConstraint implements ValidatorConstraintInterface {
validate(propertyValue: string, args: ValidationArguments) {
console.log('check is before ');
if (args.object[args.constraints[0]]) {
return moment(propertyValue) < moment(args.object[args.constraints[0]]);
} else {
return true;
}
}
defaultMessage(args: ValidationArguments) {
return `"${args.property}" must be before "${args.constraints[0]}"`;
}
}
2, startDate and endDate must pass in with format 'YYYY-MM-DD'
#ValidatorConstraint({ name: 'isRightDateFormat', async: false })
export class DateFormatConstraint implements ValidatorConstraintInterface {
validate(propertyValue: string) {
console.log('check format ' + propertyValue);
return moment(propertyValue, 'YYYY-MM-DD', true).isValid();
}
defaultMessage(args: ValidationArguments) {
return `"${args.property}" must be with format YYYY-MM-DD`;
}
}
So For startDate, i want first validate format, then validate isBefoer than endDate, I am not able to control validation order as my following code :
export class HistoricalQueryDTO {
#IsNotEmpty()
#ApiProperty()
#Validate(DateFormatConstraint)
#Validate(IsBeforeConstraint, ['endDate'])
startDate: string;
#IsNotEmpty()
#ApiProperty()
endDate: string;
}
Also, how to apply the costome validator to swagger definition through Annotation ?
Only required validation is added to swagger
You can set descriptions, and patterns or any other Swagger Schema Property, but there's no way to use class-validator decorators to update your swagger file. They set different metadata for different reasons

NodeJS, TypeScript, Mongoose unable to get document with FindById

Well, newbie with NodeJS and Mongoose, will try to get a document from a collection and use a class to manage results as i want to do.
I followed many tutos, but... i don't understand why, with the following code, i always get a null object with a findById() method on a model.
After hours spent, i decide to get help...
So, first i define a Model (simplified version) :
import { Schema, Model, model } from 'mongoose';
import { DocumentInterface } from './../interfaces/document-product-interface';
import { ProductClass } from './product-class';
var productSchema: Schema = new Schema(
{
_id: {
type: String,
required: 'Required _id'
},
id: {
type: String,
required: 'EAN required'
},
product_name: {
type: String
}
}
);
// Scheme methods
productSchema.method('title', ProductClass.prototype.title);
export const Products: Model<DocumentInterface> = model<DocumentInterface>('ProductClass', productSchema);
Next, create a class (for business purpose) :
import { ProductInterface } from './../interfaces/product-interface';
export class ProductClass implements ProductInterface{
public _id: String;
public id: String;
public product_name_fr?: string;
public product_name?: string;
public constructor() {}
public title(): string {
return 'something';
}
}
Next... The Document interface :
import { Document } from 'mongoose';
import { ProductInterface } from './product-interface';
import { ProductClass } from 'models/product-class';
export interface DocumentInterface extends ProductInterface, Document, ProductClass {}
Finally, just a controller, to get a product :
import { Products } from '../models/product-model';
import { SoappliProductInterface } from './../interfaces/soappli-product-interface';
import { Request, Response, NextFunction} from 'express';
export class ProductController {
public get(request: Request, response: Response, next: NextFunction) {
console.log('Search for a product : ' + request.params.ean);
Products.findById(request.params.ean, (error: any, product: DocumentInterface) => {
if (error) {
response.status(500).send( {message: error})
} else {
console.log('Product: ' + JSON.stringify(product));
if (product) {
console.log('Product title: ' + product.title);
response.status(200).send(product);
} else {
response.status(404).send({message: 'no matches for : ' + request.params.ean})
}
}
});
}
}
When i run with Postman and use a correct _id, got null for product...
If i remove all classes and interfaces, get the entire document...
What's wrong with this code ?
Thx

Resources