@kilbergr/nest-cqrs-factory v0.1.0-Alpha.8
Nest CQRS Factory
The Nest CQRS Factory is a library that provides an opinionated way to declare and build CQRS commands and queries in a NestJS application. The library integrates with the NestJS Cqrs Module.
Commands and Queries can be declared as classes with class validators and class transformer decorators. The Command and Query factories provide a builder to create instances of the commands and queries alongside their validation, transformation and execution.
The library also provides a unified way to log the commands and queries' success or failure with their time of execution.
The library introduces also a convention to throw a typed exception when a command or query fails. The base exceptions are CommandFailedException and QueryFailedException respectively.
Installation
# with npm
$ npm install @kilbergr/nest-cqrs-factory
# with yarn
$ yarn add @kilbergr/nest-cqrs-factoryImport
import { CommandFactory, QueryFactory } from '@kilbergr/nest-cqrs-factory';Commands
The command declaration requires @Command decorator. The decorator fabricates
metadata which is attached to the command constructor function under a private symbol-based property. The command metadata contains:
description: a description of a command for future inspection purposes.throws: a reference to an exception class that the command may throw. the exception class should extend theCommandFailedExceptionclass and if not provided, the command will throw a genericCommandFailedException.
Example:
@Command()
export class CreateUser {
@IsString()
@IsNotEmpty()
name: string;
@IsEmail()
email: string;
}Command Failed Exception
The CommandFailedException describes the error by a cod and message. The exception
also keeps track of the command that failed and the original error that caused the failure. Both for retrieval, logging and debugging purposes.
The base CommandFailedException defines three basic factories:
HandlerNotFound: when the command handler is not found. (Forwards theCommandHandlerNotFoundExceptionfrom@nestjs/cqrspackage).InvalidOperation: when the command is invalid. TheorigErroris anAggregateErrorwithValidationErrorsinside.InternalHandlerError: when the command handler throws any other error than theCommandFailedExceptionor its child. The library presumes that if the programmer wants the command to fail, he will implement theCommandFailedExceptionchild class for a given command. So, if the handler throws any other error, it is considered an unhandled error and theInternalHandlerErroris thrown with the original error inside.
Example of custom Command Failed Exception:
export class CreateUserException extends CommandFailedException {
public static errorCodes = {
...CommandFailedException.errorCodes,
USER_ALREADY_EXISTS: 'USER_ALREADY_EXISTS',
};
public static UserAlreadyExists(command: CreateUser): CreateUserException {
return new CreateUserException(
CreateUserException.errorCodes.USER_ALREADY_EXISTS,
command,
`User "${command.name}" already exists`,
);
}
}To instruct the CommandFactory that the custom CreateUserException
should be thrown in case of this command failure (for example in case of
wrong validation or internal handler error), the throws property should be
set in the @Command decorator:
@Command({
throws: CreateUserException,
})
export class CreateUser {
...
}Building and Executing Commands
The CommandFactory provides a build method to create a self-executable
instance of a command. The build method validates the command and transforms
it to the DTO object. The execute method executes the command and logs the
success or failure of the command. (The build method can be skipped if the command
execution is called directly).
Example:
class UserController {
constructor(
private readonly commandFactory: CommandFactory,
): Promise<string> {}
public async create(dto: CreateUserDto): Promise<User> {
const result = await this.commandFactory
.create<CreateUser, string>(CreateUser)
.name(dto.name)
.email(dto.email)
.execute();
return result;
}
}Command logging
The CommandFactory logs the command execution with the Logger service. The
log message contains the command name, the time of execution, the success or
failure status and the error message in case of failure.
Success Log
{
"message": "Command 'CreateUser' succeeded in 50ms.",
"cqrs": {
"kind": "command",
"name": "CreateUser",
"status": "success",
"duration": 50
}
}Error log
{
"message": "Command 'CreateUser' failed after 50ms with code 'USER_ALREADY_EXISTS and a reason: 'User John Doe already exists'.",
"cqrs": {
"kind": "command",
"name": "CreateUser",
"status": "error",
"duration": 50,
"errorCode": "USER_ALREADY_EXISTS",
"errorMessage": "User 'John Doe' already exists"
}
}Queries
The query declaration requires @Query decorator. The decorator fabricates
metadata which is attached to the query constructor function under a private symbol-based property. The query metadata contains:
description: a description of a query for future inspection purposes.throws: a reference to an exception class that the query may throw. the exception class should extend theQueryFailedExceptionclass and if not provided, the query will throw a genericQueryFailedException.
Example:
@Query()
export class GetUser {
@IsString()
@IsNotEmpty()
id: string;
}Query Failed Exception
The QueryFailedException describes the error by a cod and message. The exception
also keeps track of the query that failed and the original error that caused the failure. Both for retrieval, logging and debugging purposes.
The base QueryFailedException defines three basic factories:
HandlerNotFound: when the query handler is not found. (Forwards theQueryHandlerNotFoundExceptionfrom@nestjs/cqrspackage).InvalidOperation: when the query is invalid. TheorigErroris anAggregateErrorwithValidationErrorsinside.InternalHandlerError: when the query handler throws any other error than theQueryFailedExceptionor its child. The library presumes that if the programmer wants the query to fail, he will implement theQueryFailedExceptionchild class for a given query. So, if the handler throws any other error, it is considered an unhandled error and theInternalHandlerErroris thrown with the original error inside.
Example of custom Query Failed Exception:
export class GetUserException extends QueryFailedException {
public static errorCodes = {
...QueryFailedException.errorCodes,
USER_NOT_FOUND: 'USER_NOT_FOUND',
};
public static UserNotFound(query: GetUser): GetUserException {
return new GetUserException(
GetUserException.errorCodes.USER_NOT_FOUND,
query,
`User with id "${query.id}" not found`,
);
}
}To instruct the QueryFactory that the custom GetUserException
should be thrown in case of this query failure (for example in case of
wrong validation or internal handler error), the throws property should be
set in the @Query decorator:
@Query({
throws: GetUserException,
})
export class GetUser {
...
}Building and Executing Queries
The QueryFactory provides a build method to create a self-executable
instance of a query. The build method validates the query and transforms
it to the DTO object. The execute method executes the query and logs the
success or failure of the query. (The build method can be skipped if the query
execution is called directly).
Example:
class UserController {
constructor(private readonly queryFactory: QueryFactory): Promise<User> {}
public async get(id: string): Promise<User> {
const result = await this.queryFactory
.create<GetUser, User>(GetUser)
.id(id)
.execute();
return result;
}
}Query logging
The QueryFactory logs the query execution with the Logger service. The
log message contains the query name, the time of execution, the success or
failure status and the error message in case of failure.
Success Log
{
"message": "Query 'GetUser' succeeded in 50ms.",
"cqrs": {
"kind": "query",
"name": "GetUser",
"status": "success",
"duration": 50
}
}Error log
{
"message": "Query 'GetUser' failed after 50ms with code 'USER_NOT_FOUND and a reason: 'User with id '123' not found'.",
"cqrs": {
"kind": "query",
"name": "GetUser",
"status": "error",
"duration": 50,
"errorCode": "USER_NOT_FOUND",
"errorMessage": "User with id '123' not found"
}
}Development
Running the tests
$ npm run testBuilding the package
$ npm run buildPublishing the package
$ npm publish1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago