refactor: now using Result object for Domain Errors
This commit is contained in:
107
README.md
107
README.md
@@ -9,6 +9,7 @@ Added more code examples:
|
||||
- Added Wallet module to show an example of using a UnitOfWork together with Domain Events
|
||||
- Added BDD tests example
|
||||
- Added GraphQL examples
|
||||
- Added Domain Events
|
||||
|
||||
Refactoring:
|
||||
|
||||
@@ -16,7 +17,7 @@ Refactoring:
|
||||
- Commands are now plain objects
|
||||
- Moved generic files to /libs directory
|
||||
- Refactored Entity/Aggregate creation
|
||||
- More small changes
|
||||
- And more
|
||||
|
||||
Updates in readme and code:
|
||||
|
||||
@@ -54,6 +55,7 @@ Though patterns and principles presented here are **framework/language agnostic*
|
||||
- [Domain Services](#Domain-Services)
|
||||
- [Value Objects](#Value-Objects)
|
||||
- [Enforcing invariants of Domain Objects](#Enforcing-invariants-of-Domain-Objects)
|
||||
- [Domain Errors](#Domain-Errors)
|
||||
- [Using libraries inside application's core](#Using-libraries-inside-applications-core)
|
||||
- [Interface Adapters](#Interface-Adapters)
|
||||
- [Controllers](#Controllers)
|
||||
@@ -68,7 +70,7 @@ Though patterns and principles presented here are **framework/language agnostic*
|
||||
|
||||
- [Other recommendations and best practices](#Other-recommendations-and-best-practices)
|
||||
|
||||
- [Error Handling](#Error-Handling)
|
||||
- [Exceptions Handling](#Exceptions-Handling)
|
||||
- [Testing](#Testing)
|
||||
- [Load Testing](#Load-Testing)
|
||||
- [Fuzz Testing](#Fuzz-Testing)
|
||||
@@ -177,6 +179,7 @@ This is the core of the system which is built using [DDD building blocks](https:
|
||||
- Aggregates
|
||||
- Domain Services
|
||||
- Value Objects
|
||||
- Domain Errors
|
||||
|
||||
### Application layer:
|
||||
|
||||
@@ -675,23 +678,61 @@ Read more about validation types described above:
|
||||
|
||||
- ["Secure by Design" Chapter 4.3: Validation](https://livebook.manning.com/book/secure-by-design/chapter-4/109).
|
||||
|
||||
## Domain Errors
|
||||
|
||||
Exceptions are for exceptional situations. Complex domains usually have a lot of errors that are not exceptional, but a part of a business logic (like seat already booked, choose another one). Those errors may need special handling. In those cases returning explicit error types can be a better approach than throwing.
|
||||
|
||||
Returning an error instead of throwing explicitly shows a type of each exception that a method can return so you can handle it accordingly. It can make an error handling and tracing easier.
|
||||
|
||||
To help with that use some kind of a Result object type with a Success or a Failure (an `Either` [monad](<https://en.wikipedia.org/wiki/Monad_(functional_programming)>) from functional languages like Haskell). Unlike throwing exceptions, this approach allows to define types for every error and will force you to handle those cases explicitly instead of using `try/catch`. For example:
|
||||
|
||||
```typescript
|
||||
if (await userRepo.exists(command.email)) {
|
||||
return Result.err(new UserAlreadyExistsError()); // <- returning an Error
|
||||
}
|
||||
// else
|
||||
const user = await this.userRepo.create(user);
|
||||
return Result.ok(user);
|
||||
```
|
||||
|
||||
[@badrap/result](https://www.npmjs.com/package/@badrap/result) - this is a nice npm package if you want to use a Result object.
|
||||
|
||||
Returning errors instead of throwing them adds a bit of extra boilerplate code, but makes your application more robust and secure.
|
||||
|
||||
**Note**: Distinguish between Domain Errors and Exceptions. Exceptions are usually thrown and not returned. If you return technical Exceptions (like connection failed, process out of memory etc), It may cause some security issues and goes against [Fail-fast](https://en.wikipedia.org/wiki/Fail-fast) principle. Instead of terminating a program flow, returning an exception continues program execution and allows it to run in an incorrect state, which may lead to more unexpected errors, so it's generally better to throw an Exception in those cases rather then returning it.
|
||||
|
||||
Example files:
|
||||
|
||||
- [user.errors.ts](src/modules/user/errors/user.errors.ts) - user errors
|
||||
- [create-user.service.ts](src/modules/user/commands/create-user/create-user.service.ts) - notice how `Result.err(new UserAlreadyExistsError())` is returned instead of throwing it.
|
||||
- [create-user.http.controller.ts](src/modules/user/commands/create-user/create-user.http.controller.ts) - in a user http controller we unwrap an error and decide what to do with it. If an error is `UserAlreadyExistsError` we throw a `Conflict Exception` which a user will receive as `409 - Conflict`. If an error is unknown we just throw it and NestJS will return it to the user as `500 - Internal Server Error`.
|
||||
- [create-user.cli.controller.ts](src/modules/user/commands/create-user/create-user.cli.controller.ts) - in a CLI controller we do not care about returning a correct status code so we just `.unwrap()` a result, which will just throw in case of an error.
|
||||
|
||||
Read more:
|
||||
|
||||
- ["Secure by Design" Chapter 9.2: Handling failures without exceptions](https://livebook.manning.com/book/secure-by-design/chapter-9/51)
|
||||
- [Flexible Error Handling w/ the Result Class](https://khalilstemmler.com/articles/enterprise-typescript-nodejs/handling-errors-result-class/)
|
||||
|
||||
## Using libraries inside application's core
|
||||
|
||||
Whether or not to use libraries in application layer and especially domain layer is a subject of a lot of debates. In real world, injecting every library instead of importing it directly is not always practical, so exceptions can be made for some single responsibility libraries that help to implement domain logic (like working with numbers).
|
||||
Whether or not to use libraries in application core and especially domain layer is a subject of a lot of debates. In real world, injecting every library instead of importing it directly is not always practical, so exceptions can be made for some single responsibility libraries that help to implement domain logic (like working with numbers).
|
||||
|
||||
Main recommendations to keep in mind is that libraries imported in application's core **shouldn't** expose:
|
||||
|
||||
- Functionality to access any out-of-process resources (http calls, database access etc);
|
||||
- Functionality not relevant to domain (frameworks, technology details like ORMs, Logger etc).
|
||||
- Functionality that brings randomness (generating random IDs, timestamps etc) since this makes tests unpredictable (though in TypeScript world it is not that big of a deal since this can be mocked by a test library without using DI);
|
||||
- Frameworks can be a real nuisance because by definition they want to be in control. Isolate them within the adapters and keep our domain model clean of them.
|
||||
- If a library changes often or has a lot of dependencies of its own it most likely shouldn't be used in domain layer.
|
||||
|
||||
To use such libraries consider creating an `anti-corruption` layer by using [adapter](https://refactoring.guru/design-patterns/adapter) or [facade](https://refactoring.guru/design-patterns/facade) patterns.
|
||||
|
||||
We sometimes tolerate libraries in the center: libraries are not in control so they are less intrusive. But be careful with general purpose libraries that may scatter across many domain objects. It will be hard to replace those libraries if needed. Tying only one or just few domain objects to some single-responsibility library should be fine. It is way easier to replace a specific library that is tied to one or few objects than a general purpose library that is everywhere.
|
||||
We sometimes tolerate libraries in the center, but be careful with general purpose libraries that may scatter across many domain objects. It will be hard to replace those libraries if needed. Tying only one or just few domain objects to some single-responsibility library should be fine. It is way easier to replace a specific library that is tied to one or few objects than a general purpose library that is everywhere.
|
||||
|
||||
Offload as much of irrelevant responsibilities as possible from the core, especially from domain layer. In addition, try to minimize usage of dependencies in general. More dependencies your software has means more potential errors and security holes. One technique for making software more robust is to minimize what your software depends on - the less that can go wrong, the less that will go wrong. On the other hand, removing all dependencies would be counterproductive as replicating that functionality would have been a huge amount of work and less reliable than just using a widely-used dependency. Finding a good balance is important, this skill requires experience.
|
||||
In addition to different libraries there are Frameworks. Frameworks can be a real nuisance because by definition they want to be in control and it's hard to replace a Framework later when your entire application is glued to it. Its fine to use Frameworks in outside layers (like infrastructure), but keep your domain clean of them when possible. You should be able to extract your domain layer and build a new infrastructure around it using any other framework without breaking your business logic.
|
||||
|
||||
NestJS makes a good job as it uses decorators which are not very intrusive, so you could use decorators like `@Inject()` without affecting your business logic at all and it's relatively easy to remove or replace it when needed. Don't give up on frameworks completely, but keep them in boundaries and don't let them affect your business logic.
|
||||
|
||||
Offload as much of irrelevant responsibilities as possible from the core, especially from domain layer. In addition, try to minimize usage of dependencies in general. More dependencies your software has means more potential errors and security holes. One technique for making software more robust is to minimize what your software depends on - the less that can go wrong, the less will go wrong. On the other hand, removing all dependencies would be counterproductive as replicating that functionality would have been a huge amount of work and less reliable than just using a widely-used dependency. Finding a good balance is important, this skill requires experience.
|
||||
|
||||
Read more:
|
||||
|
||||
@@ -902,21 +943,23 @@ Read more:
|
||||
|
||||
# Other recommendations and best practices
|
||||
|
||||
## Error Handling
|
||||
## Exceptions Handling
|
||||
|
||||
Unlike Domain Errors, exceptions should be thrown when something unexpected happens. Like when a process is out of memory or a database connection lost. In our case we also throw an Exception in Domain Objects constructor when validation fails, since we know our input is validated before it even reaches Domain so when validation of a domain object constructor fails it is an exceptional situation.
|
||||
|
||||
### Exception types
|
||||
|
||||
Consider extending `Error` object to make custom generic exception types for different situations. For example: `DomainException`, `ValidationException` etc. This is especially relevant in NodeJS world since there is no exceptions for different situations by default. Also, you can create domain-specific exceptions for different use-cases, like `NotEnoughFundsError` or `SeatIsAlreadyBookedError`.
|
||||
Consider extending `Error` object to make custom generic exception types for different situations. For example: `ArgumentInvalidException`, `ValidationException` etc. This is especially relevant in NodeJS world since there is no exceptions for different situations by default.
|
||||
|
||||
Keep in mind that application's `core` shouldn't throw HTTP exceptions or statuses since it shouldn't know in what context it is used, since it can be used by anything: HTTP controller, Microservice event handler, Command Line Interface etc. A better approach is to create custom error codes for your app, like `code: 'WALLET.NOT_ENOUGH_FUNDS'` etc.
|
||||
Keep in mind that application's `core` shouldn't throw HTTP exceptions or statuses since it shouldn't know in what context it is used, since it can be used by anything: HTTP controller, Microservice event handler, Command Line Interface etc. A better approach is to create custom error classes with appropriate error codes.
|
||||
|
||||
When used in HTTP context, for returning proper status code back to user an `instanceof` or a `switch/case` check against the custom code can be performed in exception interceptor or in a controller and appropriate HTTP exception can be returned depending on exception type/code.
|
||||
|
||||
Exception interceptor example: [exception.interceptor.ts](src/infrastructure/interceptors/exception.interceptor.ts) - notice how custom exceptions are converted to nest.js exceptions.
|
||||
|
||||
Adding a `name` or `code` string with type name or a custom status code for every exception is a good practice, since when that exception is transferred to another process `instanceof` check cannot be performed anymore so a `name`/`code` string is used instead. `name` or `code` enum types can be stored in a separate file so they can be shared and reused on a receiving side: [exception.types.ts](src/libs/exceptions/exception.types.ts).
|
||||
Adding a `code` string with a custom status code for every exception is a good practice, since when that exception is transferred to another process `instanceof` check cannot be performed anymore so a `code` string is used instead. `code` enum types can be stored in a separate file so they can be shared and reused on a receiving side: [exception.codes.ts](src/libs/exceptions/exception.codes.ts).
|
||||
|
||||
When using microservices, exception types/enums/codes can be packed into a library and reused in each microservice for consistency.
|
||||
When using microservices, exception codes can be packed into a library or a sub-module and reused in each microservice for consistency.
|
||||
|
||||
### Differentiate between programmer errors and operational errors
|
||||
|
||||
@@ -929,7 +972,7 @@ For example:
|
||||
|
||||
### Error metadata
|
||||
|
||||
Consider adding optional `metadata` object to exceptions (if language doesn't support anything similar by default) and pass some useful technical information about the error when throwing. This will make debugging easier.
|
||||
Consider adding optional `metadata` object to exceptions (if language doesn't support anything similar by default) and pass some useful technical information about the exception when throwing. This will make debugging easier.
|
||||
|
||||
**Important to keep in mind**: never log or add to `metadata` any sensitive information (like passwords, emails, phone or credit card numbers etc) since this information may leak into log files, and if log files are not protected properly this information can leak or be seen by developers who have access to log files. Aim adding only technical information to your logs.
|
||||
|
||||
@@ -942,7 +985,7 @@ Consider adding optional `metadata` object to exceptions (if language doesn't su
|
||||
Example files:
|
||||
|
||||
- [exception.base.ts](src/libs/exceptions/exception.base.ts) - Exception abstract base class
|
||||
- [domain.exception.ts](src/libs/exceptions/domain.exception.ts) - Domain Exception class example
|
||||
- [argument-invalid.exception.ts](src/libs/exceptions/argument-invalid.exception.ts) - Generic exception class example
|
||||
- Check [exceptions](src/libs/exceptions) folder to see more examples (some of them are exceptions from other languages like C# or Java)
|
||||
|
||||
Read more:
|
||||
@@ -950,44 +993,6 @@ Read more:
|
||||
- [Better error handling in JavaScript](https://iaincollins.medium.com/error-handling-in-javascript-a6172ccdf9af)
|
||||
- ["Secure by design" Chapter 9: Handling failures securely](https://livebook.manning.com/book/secure-by-design/chapter-9/)
|
||||
|
||||
### Alternatives to exceptions
|
||||
|
||||
There is an alternative approach of not throwing exceptions, but returning some kind of Result object type with a Success or a Failure (an `Either` [monad](<https://en.wikipedia.org/wiki/Monad_(functional_programming)>) from functional languages like Haskell). Unlike throwing exceptions, this approach allows to define types for exceptional outcomes and will force us to handle those cases explicitly instead of using `try/catch`. For example:
|
||||
|
||||
```typescript
|
||||
class User {
|
||||
// ...
|
||||
public createUser(): Result<User, EmailInvalidException> {
|
||||
// ...code for creating a user
|
||||
if (invalidEmail) {
|
||||
return Result.err(EmailInvalidException); // <- returning instead of throwing
|
||||
}
|
||||
return Result.ok(User);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This approach has its advantages each and may work nicely in some languages, especially in functional languages which support `Either` type natively, but is not widely used in TypeScript/Javascript world.
|
||||
|
||||
Advantages:
|
||||
|
||||
- Explicitly shows type of each exception that a method can return so you can handle it accordingly.
|
||||
- Complex domains may have a lot of exceptions that need special handling and are part of a business logic (like seat already booked, choose another one). In those cases explicit error types may be useful.
|
||||
- Makes error tracing easier.
|
||||
|
||||
Downsides:
|
||||
|
||||
- If used incorrectly, i.e for technical (connection failed) errors, It may cause some security issues and goes against [Fail-fast](https://en.wikipedia.org/wiki/Fail-fast) principle. Instead of terminating a program flow, returning exception continues program execution and allows it to run in an incorrect state, which may lead to more unexpected errors, so it's generally better to throw in those cases rather then returning an error.
|
||||
- It adds extra complexity. Exception cases returned somewhere deep inside application have to be handled by functions in upper layers until it reaches controllers which may add a lot of extra `if` statements.
|
||||
- More boilerplate code.
|
||||
|
||||
In most applications it makes more sense to just throw an exception and notify a user immediately. Use `Result`/`Either` error types carefully and if you really need it and know what you are doing (unless you're using a language like [Rust](<https://en.wikipedia.org/wiki/Rust_(programming_language)>) which has this functionality built-in).
|
||||
|
||||
Read more:
|
||||
|
||||
- ["Secure by Design" Chapter 9.2: Handling failures without exceptions](https://livebook.manning.com/book/secure-by-design/chapter-9/51)
|
||||
- [Flexible Error Handling w/ the Result Class](https://khalilstemmler.com/articles/enterprise-typescript-nodejs/handling-errors-result-class/)
|
||||
|
||||
## Testing
|
||||
|
||||
Software Testing helps catching bugs early. Properly tested software product ensures reliability, security and high performance which further results in time saving, cost effectiveness and customer satisfaction.
|
||||
|
||||
5
package-lock.json
generated
5
package-lock.json
generated
@@ -835,6 +835,11 @@
|
||||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"@badrap/result": {
|
||||
"version": "0.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@badrap/result/-/result-0.2.8.tgz",
|
||||
"integrity": "sha512-zC6LgWe8QWSTwxfQVoRyieFaTbCJ2WYvpJdZ0fONoUJT1ln/oWNb3zBPlPP5J9ehLAaFIZJ7aB8oISzYFs6grQ=="
|
||||
},
|
||||
"@bcoe/v8-coverage": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz",
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/gateway": "^0.41.0",
|
||||
"@badrap/result": "^0.2.8",
|
||||
"@nestjs/common": "^7.0.0",
|
||||
"@nestjs/core": "^7.0.0",
|
||||
"@nestjs/graphql": "^9.0.4",
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
// To avoid confusion between internal exceptions and NestJS exceptions
|
||||
ConflictException as NestConflictException,
|
||||
NotFoundException as NestNotFoundException,
|
||||
ForbiddenException as NestForbiddenException,
|
||||
} from '@nestjs/common';
|
||||
import { Observable, throwError } from 'rxjs';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
@@ -13,7 +12,6 @@ import {
|
||||
ExceptionBase,
|
||||
ConflictException,
|
||||
NotFoundException,
|
||||
DomainException,
|
||||
} from '@libs/exceptions';
|
||||
|
||||
export class ExceptionInterceptor implements NestInterceptor {
|
||||
@@ -23,9 +21,6 @@ export class ExceptionInterceptor implements NestInterceptor {
|
||||
): Observable<ExceptionBase> {
|
||||
return next.handle().pipe(
|
||||
catchError(err => {
|
||||
if (err instanceof DomainException) {
|
||||
throw new NestForbiddenException(err.message);
|
||||
}
|
||||
if (err instanceof NotFoundException) {
|
||||
throw new NestNotFoundException(err.message);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Result } from '../utils/result.util';
|
||||
|
||||
export interface UnitOfWorkPort {
|
||||
execute<T>(
|
||||
correlationId: string,
|
||||
callback: () => Promise<T>,
|
||||
options?: unknown,
|
||||
): Promise<T>;
|
||||
): Promise<T | Result<T>>;
|
||||
}
|
||||
|
||||
1
src/libs/ddd/domain/utils/result.util.ts
Normal file
1
src/libs/ddd/domain/utils/result.util.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { Result } from '@badrap/result';
|
||||
@@ -2,6 +2,7 @@ import { UnitOfWorkPort } from '@src/libs/ddd/domain/ports/unit-of-work.port';
|
||||
import { EntityTarget, getConnection, QueryRunner, Repository } from 'typeorm';
|
||||
import { IsolationLevel } from 'typeorm/driver/types/IsolationLevel';
|
||||
import { Logger } from 'src/libs/ddd/domain/ports/logger.port';
|
||||
import { Result } from '@src/libs/ddd/domain/utils/result.util';
|
||||
|
||||
export class TypeormUnitOfWork implements UnitOfWorkPort {
|
||||
constructor(private readonly logger: Logger) {}
|
||||
@@ -12,7 +13,7 @@ export class TypeormUnitOfWork implements UnitOfWorkPort {
|
||||
const queryRunner = this.queryRunners.get(correlationId);
|
||||
if (!queryRunner) {
|
||||
throw new Error(
|
||||
'Query runner not found. Incorrect correlationId or transaction is not started. To start a transaction wrap operations in a `execute` method.',
|
||||
'Query runner not found. Incorrect correlationId or transaction is not started. To start a transaction wrap operations in a "execute" method.',
|
||||
);
|
||||
}
|
||||
return queryRunner;
|
||||
@@ -46,18 +47,18 @@ export class TypeormUnitOfWork implements UnitOfWorkPort {
|
||||
this.logger.debug(`[Starting transaction]`);
|
||||
await queryRunner.startTransaction(options?.isolationLevel);
|
||||
// const queryRunner = this.getQueryRunner(correlationId);
|
||||
let result: T;
|
||||
let result: T | Result<T>;
|
||||
try {
|
||||
result = await callback();
|
||||
} catch (error) {
|
||||
try {
|
||||
await queryRunner.rollbackTransaction();
|
||||
this.logger.debug(
|
||||
`[Transaction rolled back] ${(error as Error).message}`,
|
||||
if (((result as unknown) as Result<T>)?.isErr) {
|
||||
await this.rollbackTransaction<T>(
|
||||
correlationId,
|
||||
((result as unknown) as Result.Err<T, Error>).error,
|
||||
);
|
||||
} finally {
|
||||
await this.finish(correlationId);
|
||||
return result;
|
||||
}
|
||||
} catch (error) {
|
||||
await this.rollbackTransaction<T>(correlationId, error as Error);
|
||||
throw error;
|
||||
}
|
||||
try {
|
||||
@@ -71,6 +72,18 @@ export class TypeormUnitOfWork implements UnitOfWorkPort {
|
||||
return result;
|
||||
}
|
||||
|
||||
private async rollbackTransaction<T>(correlationId: string, error: Error) {
|
||||
const queryRunner = this.getQueryRunner(correlationId);
|
||||
try {
|
||||
await queryRunner.rollbackTransaction();
|
||||
this.logger.debug(
|
||||
`[Transaction rolled back] ${(error as Error).message}`,
|
||||
);
|
||||
} finally {
|
||||
await this.finish(correlationId);
|
||||
}
|
||||
}
|
||||
|
||||
private async finish(correlationId: string): Promise<void> {
|
||||
const queryRunner = this.getQueryRunner(correlationId);
|
||||
try {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ExceptionBase } from './exception.base';
|
||||
import { Exceptions } from './exception.types';
|
||||
import { ExceptionCodes } from './exception.codes';
|
||||
|
||||
/**
|
||||
* Used to indicate that an incorrect argument was provided to a method/function/class constructor
|
||||
@@ -8,5 +8,5 @@ import { Exceptions } from './exception.types';
|
||||
* @extends {ExceptionBase}
|
||||
*/
|
||||
export class ArgumentInvalidException extends ExceptionBase {
|
||||
readonly name = Exceptions.argumentInvalid;
|
||||
readonly code = ExceptionCodes.argumentInvalid;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ExceptionBase } from './exception.base';
|
||||
import { Exceptions } from './exception.types';
|
||||
import { ExceptionCodes } from './exception.codes';
|
||||
|
||||
/**
|
||||
* Used to indicate that an argument was not provided (is empty object/array, null of undefined).
|
||||
@@ -8,5 +8,5 @@ import { Exceptions } from './exception.types';
|
||||
* @extends {ExceptionBase}
|
||||
*/
|
||||
export class ArgumentNotProvidedException extends ExceptionBase {
|
||||
readonly name = Exceptions.argumentNotProvided;
|
||||
readonly code = ExceptionCodes.argumentNotProvided;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ExceptionBase } from './exception.base';
|
||||
import { Exceptions } from './exception.types';
|
||||
import { ExceptionCodes } from './exception.codes';
|
||||
|
||||
/**
|
||||
* Used to indicate that an argument is out of allowed range
|
||||
@@ -9,5 +9,5 @@ import { Exceptions } from './exception.types';
|
||||
* @extends {ExceptionBase}
|
||||
*/
|
||||
export class ArgumentOutOfRangeException extends ExceptionBase {
|
||||
readonly name = Exceptions.argumentOutOfRange;
|
||||
readonly code = ExceptionCodes.argumentOutOfRange;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ExceptionBase } from './exception.base';
|
||||
import { Exceptions } from './exception.types';
|
||||
import { ExceptionCodes } from './exception.codes';
|
||||
|
||||
export class ConflictException extends ExceptionBase {
|
||||
readonly name = Exceptions.conflict;
|
||||
readonly code = ExceptionCodes.conflict;
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { ExceptionBase } from './exception.base';
|
||||
import { Exceptions } from './exception.types';
|
||||
|
||||
/**
|
||||
* Indicates violation of some domain rule.
|
||||
*
|
||||
* @class DomainException
|
||||
* @extends {ExceptionBase}
|
||||
*/
|
||||
export class DomainException extends ExceptionBase {
|
||||
readonly name = Exceptions.domainException;
|
||||
}
|
||||
@@ -1,11 +1,8 @@
|
||||
import { ObjectLiteral } from '../types';
|
||||
import { Exceptions } from './exception.types';
|
||||
|
||||
export interface SerializedException {
|
||||
name: string;
|
||||
message: string;
|
||||
code: string;
|
||||
stack?: string;
|
||||
metadata?: ObjectLiteral;
|
||||
metadata?: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -19,21 +16,22 @@ export abstract class ExceptionBase extends Error {
|
||||
/**
|
||||
* @param {string} message
|
||||
* @param {ObjectLiteral} [metadata={}]
|
||||
* **BE CAREFUL** not to include sensitive info in 'metadata' to prevent leaks since
|
||||
* all exception's data will end up in application's log files. Only include non-sensitive
|
||||
* **BE CAREFUL** not to include sensitive info in 'metadata'
|
||||
* to prevent leaks since all exception's data will end up
|
||||
* in application's log files. Only include non-sensitive
|
||||
* info that may help with debugging.
|
||||
*/
|
||||
constructor(readonly message: string, readonly metadata?: ObjectLiteral) {
|
||||
constructor(readonly message: string, readonly metadata?: unknown) {
|
||||
super(message);
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
}
|
||||
|
||||
abstract name: Exceptions;
|
||||
abstract code: string;
|
||||
|
||||
toJSON(): SerializedException {
|
||||
return {
|
||||
name: this.name,
|
||||
message: this.message,
|
||||
code: this.code,
|
||||
stack: this.stack,
|
||||
metadata: this.metadata,
|
||||
};
|
||||
|
||||
7
src/libs/exceptions/exception.codes.ts
Normal file
7
src/libs/exceptions/exception.codes.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export enum ExceptionCodes {
|
||||
argumentInvalid = 'GENERIC.ARGUMENT_INVALID',
|
||||
argumentOutOfRange = 'GENERIC.ARGUMENT_OUT_OF_RANGE',
|
||||
argumentNotProvided = 'GENERIC.ARGUMENT_NOT_PROVIDED',
|
||||
notFound = 'GENERIC.NOT_FOUND',
|
||||
conflict = 'GENERIC.CONFLICT',
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
export enum Exceptions {
|
||||
argumentInvalid = 'ArgumentInvalidException',
|
||||
argumentOutOfRange = 'ArgumentOutOfRangeException',
|
||||
argumentNotProvided = 'ArgumentNotProvidedException',
|
||||
notFound = 'NotFoundException',
|
||||
domainException = 'DomainException',
|
||||
conflict = 'ConflictException',
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
export * from './exception.base';
|
||||
export * from './argument-out-of-range.exception';
|
||||
export * from './domain.exception';
|
||||
export * from './conflict.exception';
|
||||
export * from './argument-invalid.exception';
|
||||
export * from './exception.types';
|
||||
export * from './exception.codes';
|
||||
export * from './not-found.exception';
|
||||
export * from './argument-not-provided.exception';
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { ExceptionBase } from './exception.base';
|
||||
import { Exceptions } from './exception.types';
|
||||
import { ExceptionCodes } from './exception.codes';
|
||||
|
||||
export class NotFoundException extends ExceptionBase {
|
||||
constructor(readonly message: string = 'Not found') {
|
||||
constructor(message = 'Not found') {
|
||||
super(message);
|
||||
}
|
||||
|
||||
readonly name = Exceptions.notFound;
|
||||
readonly code = ExceptionCodes.notFound;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Logger } from '@libs/ddd/domain/ports/logger.port';
|
||||
import { CreateUserCommand } from './create-user.command';
|
||||
import { CreateUserService } from './create-user.service';
|
||||
|
||||
// Allows creating a user using CLI
|
||||
// Allows creating a user using CLI (Command Line Interface)
|
||||
@Console({
|
||||
command: 'new',
|
||||
description: 'A command to create a user',
|
||||
@@ -40,6 +40,6 @@ export class CreateUserCliController {
|
||||
|
||||
const id = await this.service.execute(command);
|
||||
|
||||
this.logger.log('User created:', id.value);
|
||||
this.logger.log('User created:', id.unwrap().value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,6 @@ export class CreateUserGraphqlResolver {
|
||||
|
||||
const id = await this.service.execute(command);
|
||||
|
||||
return new IdResponse(id.value);
|
||||
return new IdResponse(id.unwrap().value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { Body, Controller, HttpStatus, Inject, Post } from '@nestjs/common';
|
||||
import {
|
||||
Body,
|
||||
ConflictException,
|
||||
Controller,
|
||||
HttpStatus,
|
||||
Inject,
|
||||
Post,
|
||||
} from '@nestjs/common';
|
||||
import { IdResponse } from '@libs/ddd/interface-adapters/dtos/id.response.dto';
|
||||
import { routesV1 } from '@config/app.routes';
|
||||
import { createUserSymbol } from '@modules/user/user.providers';
|
||||
@@ -6,6 +13,7 @@ import { ApiOperation, ApiResponse } from '@nestjs/swagger';
|
||||
import { CreateUserCommand } from './create-user.command';
|
||||
import { CreateUserService } from './create-user.service';
|
||||
import { CreateUserHttpRequest } from './create-user.request.dto';
|
||||
import { UserAlreadyExistsError } from '../../errors/user.errors';
|
||||
|
||||
@Controller(routesV1.version)
|
||||
export class CreateUserHttpController {
|
||||
@@ -30,8 +38,19 @@ export class CreateUserHttpController {
|
||||
async create(@Body() body: CreateUserHttpRequest): Promise<IdResponse> {
|
||||
const command = new CreateUserCommand(body);
|
||||
|
||||
const id = await this.service.execute(command);
|
||||
/* Unlike with throwing errors, by returning them
|
||||
you can explicitly see what errors this method returns.
|
||||
Just hover your cursor over a 'result' variable. */
|
||||
const result = await this.service.execute(command);
|
||||
|
||||
return new IdResponse(id.value);
|
||||
return result.unwrap(
|
||||
id => new IdResponse(id.value), // if ok return an id
|
||||
error => {
|
||||
// if error decide what to do with it
|
||||
if (error instanceof UserAlreadyExistsError)
|
||||
throw new ConflictException(error.message);
|
||||
throw error;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,6 @@ export class CreateUserMessageController {
|
||||
|
||||
const id = await this.service.execute(command);
|
||||
|
||||
return new IdResponse(id.value);
|
||||
return new IdResponse(id.unwrap().value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import { ID } from '@libs/ddd/domain/value-objects/id.value-object';
|
||||
import { UserRepositoryPort } from '@modules/user/database/user.repository.port';
|
||||
import { ConflictException } from '@libs/exceptions';
|
||||
import { Address } from '@modules/user/domain/value-objects/address.value-object';
|
||||
import { Email } from '@modules/user/domain/value-objects/email.value-object';
|
||||
import { UnitOfWork } from '@src/infrastructure/database/unit-of-work/unit-of-work';
|
||||
import { Result } from '@libs/ddd/domain/utils/result.util';
|
||||
import { CreateUserCommand } from './create-user.command';
|
||||
import { UserEntity } from '../../domain/entities/user.entity';
|
||||
import { UserAlreadyExistsError } from '../../errors/user.errors';
|
||||
|
||||
export class CreateUserService {
|
||||
constructor(protected readonly unitOfWork: UnitOfWork) {}
|
||||
|
||||
async execute(command: CreateUserCommand): Promise<ID> {
|
||||
async execute(
|
||||
command: CreateUserCommand,
|
||||
): Promise<Result<ID, UserAlreadyExistsError>> {
|
||||
/* Use a repository provided by UnitOfWork to include everything
|
||||
(including changes caused by Domain Events) into one
|
||||
atomic database transaction */
|
||||
@@ -20,7 +23,7 @@ export class CreateUserService {
|
||||
);
|
||||
// user uniqueness guard
|
||||
if (await userRepo.exists(command.email)) {
|
||||
throw new ConflictException('User already exists');
|
||||
return Result.err(new UserAlreadyExistsError());
|
||||
}
|
||||
|
||||
const user = UserEntity.create({
|
||||
@@ -35,7 +38,7 @@ export class CreateUserService {
|
||||
user.someBusinessLogic();
|
||||
|
||||
const created = await userRepo.save(user);
|
||||
return created.id;
|
||||
return Result.ok(created.id);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
9
src/modules/user/errors/user.errors.ts
Normal file
9
src/modules/user/errors/user.errors.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { ExceptionBase } from '@src/libs/exceptions';
|
||||
|
||||
export class UserAlreadyExistsError extends ExceptionBase {
|
||||
constructor(metadata?: unknown) {
|
||||
super('User already exists', metadata);
|
||||
}
|
||||
|
||||
public readonly code = 'USER.ALREADY_EXISTS';
|
||||
}
|
||||
Reference in New Issue
Block a user