Using 'class-validator' to Validate Arguments

By default Discapp does basic command validation. For example:

  • Ensures that mention arguments are mention
  • Ensure that numeric arguments are numbers
  • Ensure that required arguments are given

But doing more complex validations are not the Discapp scope, since already exists lots of excelent libaries for validating data.

Discapp recommend you to use class-validator library for validating your arguments, as the syntax fits pretty well.

In this chapter you will learn how to integrate Discapp and class-validator.

Installing class-validator#

class-validator is not insalled by default in Discapp. You can install it by executing:

yarn add class-validator

Supercharging the BaseCommand#

Every command has to extends from BaseCommand, but not directly from it.

In this chapter we will write the abstract class EnhancedCommand that will extend from BaseCommand and add extra functionality to it.

The code bellow does the following:

  • Validates the arguments.
  • If they're not correct throw, so the execute method is not invoked.
  • Re-throws the error as a BadInputException so Discapp can warn the user about his error.
path/to/your/app/src/Commands/EnhancedCommand.ts
import { BaseCommand, BadInputException, StaticCommandContract } from "discapp";
import { validateOrReject, ValidationError } from "class-validator";
export abstract class EnhancedCommand extends BaseCommand {
/**
* Gets the correct argument name from the property name.
*
* This is necessary because in documentation the name of
* a argument can be different of the property name.
*
* @param property The name of the property
*/
private getCorrectArgumentName(property: string) {
const ThisCommand = this.constructor as StaticCommandContract;
const assocs = Array.from(ThisCommand.$assocs.entries());
for (const [propertyName, argumentName] of assocs) {
if (propertyName === property) {
return argumentName;
}
}
return property;
}
/**
* This function is executed BEFORE the `execute` method.
*
* It'll validate and catch input errors, rethrowing them as
* `BadInputException` so Discapp can warn the user.
*/
public async beforeExecute() {
try {
await validateOrReject(this);
} catch (error) {
const ThisCommand = this.constructor as StaticCommandContract;
const errors: ValidationError[] = error;
for (const error of errors) {
const correctArgumentName = this.getCorrectArgumentName(error.property);
throw new BadInputException(
"INVALID_ARGUMENT_VALUE",
ThisCommand.code,
correctArgumentName,
Object.values(error.constraints).join("\n")
);
}
}
}
}

Validating Your Classes#

Let's rewrite the GreetCommand.ts so it only accept names with 4 characters or more:

path/to/your/app/src/Commands/GreetCommand.ts
import { MinLength } from "class-validator";
import { Command, Argument } from "discapp";
import { EnhancedCommand } from "./EnhancedCommand";
@Command()
export class GreetCommand extends EnhancedCommand {
@Argument()
@MinLength(4)
public name: string;
public async execute() {
return `Hello, ${this.name}`;
}
}
note

The MinLength decorator is coming from class-validator.

Now if you try sending:

!greet Ava

You should see something like this, as Ava is only 3 characters long:

Can't greet Ava, because it's only 3 characters long

You can also compose multiple validations.

Let's you don't allow your command to greet persons with the name 'Adam':

path/to/your/app/src/Commands/GreetCommand.ts
import { Command, Argument } from "discapp";
import { MinLength, IsNotIn } from "class-validator";
import { EnhancedCommand } from "./EnhancedCommand";
@Command()
export class GreetCommand extends EnhancedCommand {
@Argument()
@MinLength(4)
@IsNotIn(["Adam"])
public name: string;
public async execute() {
return `Hello, ${this.name}`;
}
}

Now if you try to greet Adam by sending:

!greet Adam

You should see:

Can't greet Adam