refactor
This commit is contained in:
@@ -1,13 +1,16 @@
|
||||
import type { Result } from './Result';
|
||||
import type { ApplicationErrorCode } from '../errors/ApplicationErrorCode';
|
||||
|
||||
/**
|
||||
* Async Use Case interface for queries.
|
||||
*
|
||||
* Queries do not change system state and return data directly.
|
||||
* The output is most often a Result<T, E> where T is the data and E is a domain error code,
|
||||
* The output must be a Result<T, ApplicationErrorCode<E>> where T is the data and E is an application error code,
|
||||
* to handle business rejections explicitly. Use cases do not throw errors; they use error codes in Result.
|
||||
*
|
||||
* Example:
|
||||
* ```typescript
|
||||
* export type YourUseCaseError =
|
||||
* export type YourUseCaseErrorCode =
|
||||
* | 'SPONSOR_NOT_FOUND'
|
||||
* | 'PRICING_NOT_CONFIGURED'
|
||||
* | 'APPLICATIONS_CLOSED'
|
||||
@@ -15,16 +18,17 @@
|
||||
* | 'DUPLICATE_PENDING_REQUEST'
|
||||
* | 'OFFER_BELOW_MINIMUM';
|
||||
*
|
||||
* export class ApplyForSponsorshipUseCase implements AsyncUseCase<Input, Result<SuccessDTO, YourUseCaseError>> {
|
||||
* async execute(input: Input): Promise<Result<SuccessDTO, YourUseCaseError>> {
|
||||
* export class ApplyForSponsorshipUseCase implements AsyncUseCase<Input, SuccessDTO, YourUseCaseErrorCode> {
|
||||
* async execute(input: Input): Promise<Result<SuccessDTO, ApplicationErrorCode<YourUseCaseErrorCode>>> {
|
||||
* // implementation
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @template Input - The input type for the use case
|
||||
* @template Output - The output type returned by the use case, often Result<T, DomainErrorCode>
|
||||
* @template Success - The success type returned in the Result
|
||||
* @template ErrorCode - The error code type for ApplicationErrorCode
|
||||
*/
|
||||
export interface AsyncUseCase<Input, Output> {
|
||||
execute(input: Input): Promise<Output>;
|
||||
export interface AsyncUseCase<Input, Success, ErrorCode extends string> {
|
||||
execute(input: Input): Promise<Result<Success, ApplicationErrorCode<ErrorCode>>>;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
export interface Logger {
|
||||
debug(message: string, context?: Record<string, any>): void;
|
||||
info(message: string, context?: Record<string, any>): void;
|
||||
warn(message: string, context?: Record<string, any>): void;
|
||||
error(message: string, error?: Error, context?: Record<string, any>): void;
|
||||
debug(message: string, context?: unknown): void;
|
||||
info(message: string, context?: unknown): void;
|
||||
warn(message: string, context?: unknown): void;
|
||||
error(message: string, error?: Error, context?: unknown): void;
|
||||
}
|
||||
89
core/shared/application/Result.ts
Normal file
89
core/shared/application/Result.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Result type for handling success and error cases in a type-safe way.
|
||||
*
|
||||
* This class MUST ONLY be used within use cases (AsyncUseCase and UseCase implementations).
|
||||
* It is NOT allowed to be used in domain entities, services, repositories, presenters, or any other layer.
|
||||
* Use cases are the only place where business logic decisions that can fail should be made,
|
||||
* and Result provides a way to handle those failures without throwing exceptions.
|
||||
*
|
||||
* @template T - The type of the success value
|
||||
* @template E - The type of the error value, typically ApplicationErrorCode<string>
|
||||
*/
|
||||
export class Result<T, E = Error> {
|
||||
private constructor(
|
||||
private readonly _value?: T,
|
||||
private readonly _error?: E,
|
||||
private readonly _isSuccess: boolean = true
|
||||
) {}
|
||||
|
||||
static ok<T, E = Error>(value: T): Result<T, E> {
|
||||
return new Result<T, E>(value, undefined, true);
|
||||
}
|
||||
|
||||
static err<T, E = Error>(error: E): Result<T, E> {
|
||||
return new Result<T, E>(undefined, error, false);
|
||||
}
|
||||
|
||||
isOk(): boolean {
|
||||
return this._isSuccess;
|
||||
}
|
||||
|
||||
isErr(): boolean {
|
||||
return !this._isSuccess;
|
||||
}
|
||||
|
||||
unwrap(): T {
|
||||
if (!this._isSuccess) {
|
||||
throw new Error('Called unwrap on an error result');
|
||||
}
|
||||
return this._value!;
|
||||
}
|
||||
|
||||
unwrapOr(defaultValue: T): T {
|
||||
return this._isSuccess ? this._value! : defaultValue;
|
||||
}
|
||||
|
||||
unwrapErr(): E {
|
||||
if (this._isSuccess) {
|
||||
throw new Error('Called unwrapErr on a success result');
|
||||
}
|
||||
return this._error!;
|
||||
}
|
||||
|
||||
map<U>(fn: (value: T) => U): Result<U, E> {
|
||||
if (this._isSuccess) {
|
||||
return Result.ok(fn(this._value!));
|
||||
}
|
||||
return Result.err(this._error!);
|
||||
}
|
||||
|
||||
mapErr<F>(fn: (error: E) => F): Result<T, F> {
|
||||
if (!this._isSuccess) {
|
||||
return Result.err(fn(this._error!));
|
||||
}
|
||||
return Result.ok(this._value!);
|
||||
}
|
||||
|
||||
andThen<U>(fn: (value: T) => Result<U, E>): Result<U, E> {
|
||||
if (this._isSuccess) {
|
||||
return fn(this._value!);
|
||||
}
|
||||
return Result.err(this._error!);
|
||||
}
|
||||
|
||||
/**
|
||||
* Direct access to the value (for testing convenience).
|
||||
* Prefer using unwrap() in production code.
|
||||
*/
|
||||
get value(): T | undefined {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Direct access to the error (for testing convenience).
|
||||
* Prefer using unwrapErr() in production code.
|
||||
*/
|
||||
get error(): E | undefined {
|
||||
return this._error;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Result } from '../result/Result';
|
||||
import type { IApplicationError } from '../errors/ApplicationError';
|
||||
import type { Result } from './Result';
|
||||
import type { ApplicationError } from '../errors/ApplicationError';
|
||||
|
||||
export interface IApplicationService {
|
||||
readonly serviceName?: string;
|
||||
@@ -12,7 +12,7 @@ export interface IAsyncApplicationService<Input, Output> extends IApplicationSer
|
||||
export interface IAsyncResultApplicationService<
|
||||
Input,
|
||||
Output,
|
||||
Error = IApplicationError
|
||||
Error = ApplicationError
|
||||
> extends IApplicationService {
|
||||
execute(input: Input): Promise<Result<Output, Error>>;
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { Presenter } from '../presentation';
|
||||
import type { Result } from './Result';
|
||||
import type { ApplicationErrorCode } from '../errors/ApplicationErrorCode';
|
||||
|
||||
/**
|
||||
* Use Case interface for commands.
|
||||
@@ -7,28 +9,29 @@ import type { Presenter } from '../presentation';
|
||||
* but contain no infrastructure or framework concerns.
|
||||
*
|
||||
* Commands change system state and return nothing on success. They use a presenter to handle the output.
|
||||
* If a business rejection is possible, the output may be a Result<void, E> handled by the presenter.
|
||||
* If a business rejection is possible, the output must be a Result<T, ApplicationErrorCode<E>> handled by the presenter.
|
||||
* Use cases do not throw errors; they use error codes in Result.
|
||||
*
|
||||
* Example:
|
||||
* ```typescript
|
||||
* export type CreateRaceError =
|
||||
* export type CreateRaceErrorCode =
|
||||
* | 'INSUFFICIENT_PERMISSIONS'
|
||||
* | 'RACE_ALREADY_EXISTS'
|
||||
* | 'INVALID_RACE_CONFIG';
|
||||
*
|
||||
* export class CreateRaceUseCase implements UseCase<CreateRaceInput, Result<void, CreateRaceError>, ViewModel, Presenter<Result<void, CreateRaceError>, ViewModel>> {
|
||||
* execute(input: CreateRaceInput, presenter: Presenter<Result<void, CreateRaceError>, ViewModel>): Promise<void> {
|
||||
* export class CreateRaceUseCase implements UseCase<CreateRaceInput, void, CreateRaceErrorCode, ViewModel, Presenter<Result<void, ApplicationErrorCode<CreateRaceErrorCode>>, ViewModel>> {
|
||||
* execute(input: CreateRaceInput, presenter: Presenter<Result<void, ApplicationErrorCode<CreateRaceErrorCode>>, ViewModel>): Promise<void> {
|
||||
* // implementation
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @template Input - The input type for the use case
|
||||
* @template OutputDTO - The output DTO type, often Result<void, DomainErrorCode>
|
||||
* @template Success - The success type in the Result passed to the presenter
|
||||
* @template ErrorCode - The error code type for ApplicationErrorCode
|
||||
* @template ViewModel - The view model type
|
||||
* @template P - The presenter type, extending Presenter<OutputDTO, ViewModel>
|
||||
* @template P - The presenter type, extending Presenter<Result<Success, ApplicationErrorCode<ErrorCode>>, ViewModel>
|
||||
*/
|
||||
export interface UseCase<Input, OutputDTO, ViewModel, P extends Presenter<OutputDTO, ViewModel>> {
|
||||
export interface UseCase<Input, Success, ErrorCode extends string, ViewModel, P extends Presenter<Result<Success, ApplicationErrorCode<ErrorCode>>, ViewModel>> {
|
||||
execute(input: Input, presenter: P): Promise<void> | void;
|
||||
}
|
||||
Reference in New Issue
Block a user