This commit is contained in:
2025-12-16 21:05:01 +01:00
parent f61e3a4e5a
commit 7532c7ed6d
207 changed files with 7861 additions and 2606 deletions

View File

@@ -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>>>;
}

View File

@@ -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;
}

View File

@@ -1,3 +1,14 @@
/**
* 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,

View File

@@ -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>>;
}

View File

@@ -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;
}

View File

@@ -0,0 +1,10 @@
export interface DomainEvent<T = unknown> {
readonly eventType: string;
readonly aggregateId: string;
readonly eventData: T;
readonly occurredAt: Date;
}
export interface DomainEventPublisher {
publish(event: DomainEvent): Promise<void>;
}

View File

@@ -1,10 +0,0 @@
export interface IDomainEvent<T = any> {
readonly eventType: string;
readonly aggregateId: string;
readonly eventData: T;
readonly occurredAt: Date;
}
export interface IDomainEventPublisher {
publish(event: IDomainEvent): Promise<void>;
}

View File

@@ -1,4 +1,4 @@
import type { Result } from '../result/Result';
import type { Result } from '../application/Result';
import type { IDomainError } from '../errors/DomainError';
export interface IDomainService {

View File

@@ -12,7 +12,7 @@ export type CommonApplicationErrorKind =
/**
* @deprecated Use ApplicationErrorCode in Result instead of throwing ApplicationError.
*/
export interface IApplicationError<K extends string = CommonApplicationErrorKind, D = unknown> extends Error {
export interface ApplicationError<K extends string = CommonApplicationErrorKind, D = unknown> extends Error {
readonly type: 'application';
readonly context: string;
readonly kind: K;

View File

@@ -1,6 +1,4 @@
export type ApplicationErrorCode<
Code extends string,
Details = undefined
> = Details extends undefined
? { code: Code }
: { code: Code; details: Details };
export type ApplicationErrorCode<Code extends string, Details = undefined> =
Details extends undefined
? { code: Code }
: { code: Code; details: Details };

View File

@@ -1,4 +1,4 @@
export * from './result/Result';
export * from './application/Result';
export * as application from './application';
export * as domain from './domain';
export * as errors from './errors';