website refactor

This commit is contained in:
2026-01-13 02:38:49 +01:00
parent e981ebd9e9
commit 38b25bafe1
20 changed files with 1138 additions and 80 deletions

View File

@@ -5,8 +5,13 @@ import { Result } from "../Result";
*
* Purpose: Framework-agnostic write operations
*
* Rules:
* - Orchestrates services for writes
* Architecture:
* - Server Action constructs Mutation
* - Mutation constructs Service
* - Service creates own dependencies (API Client, Logger, ErrorReporter)
* - Service returns Result<void, DomainError>
* - Mutation maps DomainError → MutationError
* - Mutation returns Result<void, MutationError>
* - No HTTP/API calls directly
* - No 'use client' directive
* - No 'use server' directive
@@ -18,18 +23,48 @@ import { Result } from "../Result";
* Pattern:
* Server Action → Mutation → Service → API Client
*
* Example:
* ```typescript
* export class UpdateUserStatusMutation implements Mutation<UpdateUserStatusInput, void> {
* async execute(input: UpdateUserStatusInput): Promise<Result<void, MutationError>> {
* const service = new UserService();
* const result = await service.updateUserStatus(input.userId, input.status);
*
* if (result.isErr()) {
* return Result.err(mapToMutationError(result.error));
* }
*
* return Result.ok(undefined);
* }
* }
* ```
*
* Design Principle:
* Each mutation does ONE thing. If you need multiple operations,
* create multiple mutation classes (e.g., UpdateUserStatusMutation, DeleteUserMutation).
* This follows the same pattern as Page Queries.
*
* @template TInput - The input type for the mutation
* @template TOutput - The output type on success
* @template TError - The error type (default: string for backward compatibility)
*/
export interface Mutation<TInput = void, TOutput = void> {
export interface Mutation<TInput = void, TOutput = void, TError = string> {
/**
* Execute the mutation
*
* Manual construction pattern:
* ```typescript
* const service = new MyService();
* const result = await service.doWrite(input);
* if (result.isErr()) {
* return Result.err(mapToMutationError(result.error));
* }
* return Result.ok(undefined);
* ```
*
* @param input - Mutation input
* @returns Result indicating success or error
* @returns Promise<Result<TOutput, TError>>
*/
execute(input: TInput): Promise<Result<TOutput, string>>;
execute(input: TInput): Promise<Result<TOutput, TError>>;
}

View File

@@ -0,0 +1,45 @@
/**
* Mutation Error Type
*
* Errors that can be handled by the client after a mutation.
* These are mapped from DomainErrors by Mutations.
*/
export type MutationError =
| 'userNotFound' // User doesn't exist
| 'noPermission' // Insufficient permissions
| 'invalidData' // Validation failed
| 'updateFailed' // Update operation failed
| 'deleteFailed' // Delete operation failed
| 'createFailed' // Create operation failed
| 'networkError' // Network/communication error
| 'serverError' // Generic server error
| 'unknown'; // Unknown error
// Helper to map DomainError to MutationError
export function mapToMutationError(domainError: any): MutationError {
const errorType = domainError?.type || domainError?.name || 'unknown';
switch (errorType) {
case 'notFound':
case 'NotFoundError':
case 'userNotFound':
return 'userNotFound';
case 'unauthorized':
case 'UnauthorizedError':
case 'ForbiddenError':
return 'noPermission';
case 'validationError':
case 'ValidationError':
return 'invalidData';
case 'serverError':
case 'ServerError':
case 'HttpServerError':
return 'serverError';
case 'networkError':
case 'NetworkError':
return 'networkError';
default:
return 'unknown';
}
}

View File

@@ -2,25 +2,39 @@ import { Result } from "../Result";
/**
* PageQuery contract interface
*
*
* Defines the canonical contract for all server-side page queries.
*
* Based on WEBSITE_PAGE_QUERIES.md:
*
* Architecture:
* - Server-side composition classes
* - Call services that call apps/api
* - Assemble a Page DTO
* - Explicit result describing route outcome
* - Construct Services manually (no DI container)
* - Services create their own dependencies (API Client, Logger, ErrorReporter)
* - Services return Result<ApiDto, DomainError>
* - PageQuery maps DomainError → PresentationError
* - PageQuery returns Result<ViewData, PresentationError>
* - Do not implement business rules
*
* @template TApiDto - The API DTO type this query produces
*
* @template TViewData - The ViewData type this query produces for templates
* @template TParams - The parameters required to execute this query
* @template TError - The error type (default: string for backward compatibility)
*/
export interface PageQuery<TApiDto, TParams = void> {
export interface PageQuery<TViewData, TParams = void, TError = string> {
/**
* Execute the page query
*
*
* Manual construction pattern:
* ```typescript
* const service = new MyService();
* const result = await service.getData();
* if (result.isErr()) {
* return Result.err(mapToPresentationError(result.error));
* }
* const viewData = MyViewDataBuilder.build(result.value);
* return Result.ok(viewData);
* ```
*
* @param params - Parameters required for query execution
* @returns Promise resolving to a Result
* @returns Promise<Result<ViewData, TError>>
*/
execute(params: TParams): Promise<Result<TApiDto, string>>;
execute(params: TParams): Promise<Result<TViewData, TError>>;
}

View File

@@ -0,0 +1,42 @@
/**
* Presentation Error Type
*
* Errors that can be handled by the presentation layer (RSC pages, templates).
* These are mapped from DomainErrors by PageQueries.
*/
export type PresentationError =
| 'notFound' // Resource not found - show 404 page
| 'redirect' // Redirect to another page (e.g., login)
| 'unauthorized' // Not authorized - show access denied
| 'serverError' // Generic server error
| 'networkError' // Network/communication error
| 'validationError' // Invalid input data
| 'unknown'; // Unknown error
// Helper to map DomainError to PresentationError
export function mapToPresentationError(domainError: any): PresentationError {
const errorType = domainError?.type || domainError?.name || 'unknown';
switch (errorType) {
case 'notFound':
case 'NotFoundError':
return 'notFound';
case 'unauthorized':
case 'UnauthorizedError':
case 'ForbiddenError':
return 'unauthorized';
case 'serverError':
case 'ServerError':
case 'HttpServerError':
return 'serverError';
case 'networkError':
case 'NetworkError':
return 'networkError';
case 'validationError':
case 'ValidationError':
return 'validationError';
default:
return 'unknown';
}
}

View File

@@ -1,6 +1,8 @@
/**
* Presenter contract
*
* @deprecated Must use builders instead.
*
* Pure, deterministic transformation between presentation models.
*
* Based on PRESENTERS.md:

View File

@@ -1,37 +1,50 @@
/**
* Service contract
*
* Service Contract
*
* Orchestration boundary for server-side operations.
* Returns API DTOs or Page DTOs only.
* Services are self-contained and create their own dependencies.
* Returns Result<ApiDto, DomainError> for type-safe error handling.
* Must be stateless.
*
*
* Architecture:
* - Services are self-contained (no constructor parameters)
* - Services create their own dependencies (API Client, Logger, ErrorReporter)
* - Services return Result<ApiDto, DomainError>
* - Services convert HTTP errors to Domain errors
*
* Based on WEBSITE_CONTRACT.md:
* - Services orchestrate IO and composition
* - They do not prepare UI
* - They return ApiDto or PageDto only
* - They return Result<ApiDto, DomainError>
*/
import { Result } from '@/lib/contracts/Result';
/**
* Base service interface for orchestration operations
* Domain error type for services
* Services should define specific error types based on their domain
*/
export interface Service<TApiDto = unknown, TPageDto = unknown> {
export type DomainError =
| { type: 'notFound'; message: string }
| { type: 'unauthorized'; message: string }
| { type: 'forbidden'; message: string }
| { type: 'validation'; message: string }
| { type: 'serverError'; message: string }
| { type: 'networkError'; message: string }
| { type: 'unknown'; message: string };
/**
* Service interface for orchestration operations
* All service methods must return Result with domain errors
*
* Type Parameters:
* - TApiDto: The API Transport DTO type returned on success
* - TError: The domain error type (defaults to DomainError)
*/
export interface Service<TApiDto = unknown, TError extends DomainError = DomainError> {
/**
* Execute a service operation
* Returns either API Transport DTO or Page DTO
* Returns Result with API DTO or Domain Error
*/
execute(...args: unknown[]): Promise<TApiDto | TPageDto>;
}
/**
* Service that returns API Transport DTOs
*/
export interface ApiService<TApiDto = unknown> extends Service<TApiDto, never> {
execute(...args: unknown[]): Promise<TApiDto>;
}
/**
* Service that returns Page DTOs
*/
export interface PageService<TPageDto = unknown> extends Service<never, TPageDto> {
execute(...args: unknown[]): Promise<TPageDto>;
execute(...args: unknown[]): Promise<Result<TApiDto, TError>>;
}