website refactor
This commit is contained in:
@@ -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>>;
|
||||
}
|
||||
45
apps/website/lib/contracts/mutations/MutationError.ts
Normal file
45
apps/website/lib/contracts/mutations/MutationError.ts
Normal 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';
|
||||
}
|
||||
}
|
||||
@@ -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>>;
|
||||
}
|
||||
|
||||
42
apps/website/lib/contracts/page-queries/PresentationError.ts
Normal file
42
apps/website/lib/contracts/page-queries/PresentationError.ts
Normal 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';
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
/**
|
||||
* Presenter contract
|
||||
*
|
||||
* @deprecated Must use builders instead.
|
||||
*
|
||||
* Pure, deterministic transformation between presentation models.
|
||||
*
|
||||
* Based on PRESENTERS.md:
|
||||
|
||||
@@ -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>>;
|
||||
}
|
||||
Reference in New Issue
Block a user