Pipes
Pipes are a powerful feature of the engine system that allow you to transform and manipulate data when it is being injected into your engine steps. Pipes can be used to perform various operations on the data, such as validation, transformation, or formatting, before it is passed to the step.
A Pipe is a decorator that can be applied to a class, method, or property to specify a the transformations that will be applied to the data being injected. Pipes can be used in conjunction with parameters to create more complex and dynamic workflows. A pipe can include more than one transformation, and the transformations will be applied in the order they are defined. We will refer to these transformations as pipe-steps.
Already Available Pipe-Steps
AxiSpark.js comes with a set of built-in pipe-steps that you can use out of the box. These pipe-steps cover common use cases and can be easily integrated into your engine steps. Here are the available pipes.
| Pipe-Step | Description |
|---|---|
LowerCase() | Converts the input string to lowercase. |
UpperCase() | Converts the input string to uppercase. |
MaxLength(maxLength: number) | Checks if the input string exceeds a specified maximum length. |
MinLength(minLength: number) | Checks if the input string meets a specified minimum length. |
Min(minValue: number) | Checks if the input string meets a specified minimum value. |
Max(maxValue: number) | Checks if the input string exceeds a specified maximum value. |
NotEmpty() | Checks if the input string is not empty. |
ParseBoolean() | Transforms the input string to a boolean value. |
ParseFloat() | Transforms the input string to a float value. |
ParseInt() | Transforms the input string to an integer value. |
Range(min: number, max: number) | Checks if the input string is within a specified range. |
Regex(pattern: string) | Checks if the input string matches a specified regular expression pattern. |
Trim() | Removes leading and trailing whitespace from the input string. |
ValidateSchema(options?: ValidatorOptions) | Validates the input object against the class declared as the parameter type, using class-validator. See Schema Validation. |
Schema Validation
ValidateSchema() validates structured data, such as a request body, against a DTO class decorated with class-validator decorators. The class is not passed to the pipe-step: it is inferred from the type declared for the parameter, using the metadata TypeScript emits when emitDecoratorMetadata is enabled.
import { IsEmail, IsInt, Min } from 'class-validator';
import { Pipe, ValidateSchema } from '@axisparkjs/engine';
class CreateUserDto {
@IsEmail()
email!: string;
@IsInt()
@Min(18)
age!: number;
}
@Controller('/users')
export class UserController {
@Post()
create(@Body() @Pipe(ValidateSchema({ whitelist: true })) user: CreateUserDto) {
// `user` is a validated `CreateUserDto` instance
}
}
When validation succeeds, the value is passed to the next pipe-step as an instance of the declared class. When it fails, a PipeStepValidationError is thrown, and its message lists every failing constraint with its property path (for example email: email must be an email; age: age must not be less than 18). Over HTTP, this error is returned as a 400 Bad Request response.
ValidateSchema() accepts the following class-validator options, which are passed through unchanged:
export interface ValidatorOptions {
skipMissingProperties?: boolean;
whitelist?: boolean;
forbidNonWhitelisted?: boolean;
groups?: string[];
dismissDefaultMessages?: boolean;
validationError?: {
target?: boolean;
value?: boolean;
};
forbidUnknownValues?: boolean;
stopAtFirstError?: boolean;
}
:::warning Runtime type limitations
TypeScript does not preserve interfaces, generic types, union types or type literals at runtime, so they cannot be used as inferred schemas. Primitive types (string, number, ...), Object, Array and Date are not supported either. Using ValidateSchema() on such a parameter throws a PipeStepError explaining that schema validation requires a class-typed parameter.
The incoming value is converted to an instance of the declared class without running its constructor, and nested objects are not converted. To validate nested objects with @ValidateNested(), they must already be instances of their own classes.
:::
Creating Custom Pipe-Steps
You can create your own custom pipe-steps by implementing the PipeStep interface and defining the function that will be included in the Pipe decorator. A custom pipe-step can be used to perform any transformation or validation that is not covered by the built-in pipe-steps. Here is an example of how to create a custom pipe-step.
import { ParameterDefinition, PipeStep } from '@axisparkjs/engine';
import { Injectable } from '@axisparkjs/di';
// Transformation logic
@Injectable()
export class CustomCasePipeStep implements PipeStep<string, string> {
execute(parameter: ParameterDefinition): any {
...
}
}
// Function to include in the Pipe decorator
export const Custom = () => CustomCasePipeStep;
Pipe-steps can also include additional parameters to customize their behavior. For example, you can create a pipe-step that accepts a parameter to specify the number of decimal places to round a number to.
import { ParameterDefinition, PipeStep } from '@axisparkjs/engine';
import { Injectable } from '@axisparkjs/di';
// Pipe-step configuration interface
export interface CustomPipeStepConfig extends PipeStepConfig {
value: number;
}
// Transformation logic
@Injectable()
export class CustomPipeStep implements PipeStep<number, number> {
execute(parameter: ParameterDefinition, config: CustomPipeStepConfig): any {
...
}
}
// Function to include in the Pipe decorator
export const Custom = (value: number) => {
return {
pipeStep: CustomPipeStep,
PipeStepConfig: { value } as CustomPipeStepConfig
};
};
Using Pipes in Engine Steps
In any engine step, you can use the @Pipe() decorator to specify the pipe-steps that will be applied to the data being injected. The @Pipe() decorator accepts one or more pipe-step functions as arguments. Here is an example of how to use pipes in an engine step.
import { EngineStep, Pipe } from '@axisparkjs/engine';
@Pipe(NotEmpty(), Trim())
export class Example {
@Pipe(MaxLength(10))
async execute(@AnyParamterDecorator() @Pipe(LowerCase()) input: string): Promise<void> {
...
}
}
In this example, for parameter input, the 4 pipe-steps NotEmpty(), Trim(), MaxLength(10), LowerCase() and will be applied in the order they are defined. The data will first be checked to ensure it is not empty, then any leading or trailing whitespace will be removed, then it will be checked to ensure it does not exceed a maximum length of 10 characters, and finally it will be converted to lowercase before being passed to the execute method.
Pipe-Step Errors
Pipe-steps report invalid input by throwing a PipeStepParsingError (the value cannot be converted, e.g. ParseInt()) or a PipeStepValidationError (the value does not meet a constraint, e.g. Range() or ValidateSchema()). Both extend PipeStepError.
Over HTTP, the global error filter returns parsing and validation errors as 400 Bad Request, with the error class name as the cause:
{
"error": "Parsing failed for 'path' as integer: Value 'abc' is not a valid integer.",
"status": 400,
"cause": "PipeStepParsingError"
}
Any other PipeStepError signals a misconfiguration rather than invalid input (for example, ValidateSchema() on a parameter that is not typed with a class), so it is returned as 500 Internal Server Error.
NotEmpty()andTrim()are applied to the class, so they will be applied to all parameters of any method of the class.MaxLength(10)is applied to the method, so it will be applied to all parameters of the method.LowerCase()is applied to the parameter, so it will only be applied to that specific parameter.