website refactor
This commit is contained in:
@@ -37,15 +37,38 @@ apps/website/lib/services/
|
||||
|
||||
### Service Definition
|
||||
|
||||
Services create their own dependencies:
|
||||
|
||||
```typescript
|
||||
import { AdminApiClient } from '@/lib/api/admin/AdminApiClient';
|
||||
import type { UserDto } from '@/lib/api/admin/AdminApiClient';
|
||||
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
|
||||
import { ConsoleErrorReporter } from '@/lib/infrastructure/logging/ConsoleErrorReporter';
|
||||
|
||||
export class AdminService {
|
||||
constructor(private readonly apiClient: AdminApiClient) {}
|
||||
private apiClient: AdminApiClient;
|
||||
|
||||
async updateUserStatus(userId: string, status: string): Promise<UserDto> {
|
||||
return this.apiClient.updateUserStatus(userId, status);
|
||||
constructor() {
|
||||
// Service creates its own dependencies
|
||||
const baseUrl = process.env.NEXT_PUBLIC_API_URL || '';
|
||||
this.apiClient = new AdminApiClient(
|
||||
baseUrl,
|
||||
new ConsoleErrorReporter(),
|
||||
new ConsoleLogger()
|
||||
);
|
||||
}
|
||||
|
||||
async updateUserStatus(userId: string, status: string): Promise<Result<UserDto, DomainError>> {
|
||||
try {
|
||||
const result = await this.apiClient.updateUserStatus(userId, status);
|
||||
return Result.ok(result);
|
||||
} catch (error) {
|
||||
// Convert HTTP errors to domain errors
|
||||
if (error instanceof HttpForbiddenError) {
|
||||
return Result.err(new ForbiddenError('Insufficient permissions'));
|
||||
}
|
||||
return Result.err(new UnknownError('Failed to update user'));
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -54,101 +77,113 @@ export class AdminService {
|
||||
|
||||
```typescript
|
||||
// apps/website/lib/page-queries/AdminDashboardPageQuery.ts
|
||||
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
|
||||
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
|
||||
import { AdminApiClient } from '@/lib/api/admin/AdminApiClient';
|
||||
import { AdminService } from '@/lib/services/admin/AdminService';
|
||||
import { AdminDashboardViewDataBuilder } from '@/lib/builders/view-data/AdminDashboardViewDataBuilder';
|
||||
|
||||
export class AdminDashboardPageQuery {
|
||||
async execute(): Promise<PageQueryResult<AdminDashboardPageDto>> {
|
||||
// Create infrastructure
|
||||
const logger = new ConsoleLogger();
|
||||
const errorReporter = new EnhancedErrorReporter(logger, {...});
|
||||
const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001';
|
||||
const apiClient = new AdminApiClient(baseUrl, errorReporter, logger);
|
||||
const service = new AdminService(apiClient);
|
||||
export class AdminDashboardPageQuery implements PageQuery<AdminDashboardViewData, void> {
|
||||
async execute(): Promise<Result<AdminDashboardViewData, DashboardPageError>> {
|
||||
// Manual construction: Service creates its own dependencies
|
||||
const service = new AdminService();
|
||||
|
||||
// Use service
|
||||
const stats = await service.getDashboardStats();
|
||||
const result = await service.getDashboardStats();
|
||||
|
||||
// Transform to Page DTO
|
||||
return { status: 'ok', dto: transformToPageDto(stats) };
|
||||
if (result.isErr()) {
|
||||
return Result.err(mapToPresentationError(result.error));
|
||||
}
|
||||
|
||||
// Transform to ViewData using Builder
|
||||
const viewData = AdminDashboardViewDataBuilder.build(result.value);
|
||||
return Result.ok(viewData);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Usage in Server Actions (Writes)
|
||||
### Usage in Mutations (Writes)
|
||||
|
||||
```typescript
|
||||
// apps/website/app/admin/actions.ts
|
||||
'use server';
|
||||
|
||||
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
|
||||
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
|
||||
import { AdminApiClient } from '@/lib/api/admin/AdminApiClient';
|
||||
// apps/website/lib/mutations/UpdateUserStatusMutation.ts
|
||||
import { AdminService } from '@/lib/services/admin/AdminService';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
|
||||
export async function updateUserStatus(userId: string, status: string): Promise<void> {
|
||||
try {
|
||||
// Create infrastructure
|
||||
const logger = new ConsoleLogger();
|
||||
const errorReporter = new EnhancedErrorReporter(logger, {
|
||||
showUserNotifications: true,
|
||||
logToConsole: true,
|
||||
reportToExternal: process.env.NODE_ENV === 'production',
|
||||
});
|
||||
export class UpdateUserStatusMutation implements Mutation<UpdateUserStatusInput, void> {
|
||||
async execute(input: UpdateUserStatusInput): Promise<Result<void, MutationError>> {
|
||||
// Manual construction: Service creates its own dependencies
|
||||
const service = new AdminService();
|
||||
|
||||
const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001';
|
||||
const apiClient = new AdminApiClient(baseUrl, errorReporter, logger);
|
||||
const service = new AdminService(apiClient);
|
||||
const result = await service.updateUserStatus(input.userId, input.status);
|
||||
|
||||
// Use service (NOT API client directly)
|
||||
await service.updateUserStatus(userId, status);
|
||||
if (result.isErr()) {
|
||||
return Result.err(mapToMutationError(result.error));
|
||||
}
|
||||
|
||||
// Revalidate
|
||||
revalidatePath('/admin/users');
|
||||
} catch (error) {
|
||||
console.error('updateUserStatus failed:', error);
|
||||
throw new Error('Failed to update user status');
|
||||
return Result.ok(undefined);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Infrastructure Concerns
|
||||
### Usage in Server Actions
|
||||
|
||||
**Where should logging/error reporting live?**
|
||||
```typescript
|
||||
// app/admin/actions.ts
|
||||
'use server';
|
||||
|
||||
In the current architecture, **server actions and PageQueries create infrastructure**. This is acceptable because:
|
||||
1. Next.js serverless functions are stateless
|
||||
2. Each request needs fresh infrastructure
|
||||
3. Manual DI is clearer than magic containers
|
||||
import { UpdateUserStatusMutation } from '@/lib/mutations/UpdateUserStatusMutation';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
|
||||
**Key principle**: Services orchestrate, they don't create infrastructure.
|
||||
export async function updateUserStatus(input: UpdateUserStatusInput) {
|
||||
// Manual construction: Mutation creates Service
|
||||
const mutation = new UpdateUserStatusMutation();
|
||||
|
||||
const result = await mutation.execute(input);
|
||||
|
||||
if (result.isErr()) {
|
||||
return { success: false, error: result.error };
|
||||
}
|
||||
|
||||
revalidatePath('/admin/users');
|
||||
return { success: true };
|
||||
}
|
||||
```
|
||||
|
||||
## Dependency Chain
|
||||
|
||||
```
|
||||
Server Action / PageQuery
|
||||
↓ (creates infrastructure)
|
||||
RSC Page / Server Action
|
||||
↓ (manual construction)
|
||||
PageQuery / Mutation
|
||||
↓ (manual construction)
|
||||
Service
|
||||
↓ (orchestrates)
|
||||
↓ (creates own dependencies)
|
||||
API Client
|
||||
↓ (makes HTTP calls)
|
||||
API
|
||||
```
|
||||
|
||||
**Key Principle**: Each layer manually constructs the next layer. Services create their own infrastructure (API Client, Logger, ErrorReporter).
|
||||
|
||||
## Why Manual Construction?
|
||||
|
||||
**See**: [DEPENDENCY_CONSTRUCTION.md](./DEPENDENCY_CONSTRUCTION.md)
|
||||
|
||||
**Summary**:
|
||||
- ✅ Explicit and clear
|
||||
- ✅ No singleton issues
|
||||
- ✅ No request-scoping problems
|
||||
- ✅ Easy to test (pass mocks to constructor)
|
||||
- ✅ Works perfectly with Next.js RSC
|
||||
- ❌ No DI container needed
|
||||
|
||||
## Naming
|
||||
|
||||
- Service classes: `*Service`
|
||||
- Service methods: Return DTOs (not ViewModels)
|
||||
- Variable names: `apiDto`, `pageDto` (never just `dto`)
|
||||
- Service methods: Return `Result<T, DomainError>`
|
||||
- Variable names: `apiDto`, `viewData` (never just `dto`)
|
||||
|
||||
## Comparison with Other Layers
|
||||
|
||||
| Layer | Purpose | Example |
|
||||
|-------|---------|---------|
|
||||
| **Website Service** | Orchestrate API calls | `AdminService` |
|
||||
| **Website Service** | Orchestrate API calls, handle errors | `AdminService` |
|
||||
| **API Client** | HTTP infrastructure | `AdminApiClient` |
|
||||
| **Core Use Case** | Business rules | `CreateLeagueUseCase` |
|
||||
| **Domain Service** | Cross-entity logic | `StrengthOfFieldCalculator` |
|
||||
@@ -170,8 +205,13 @@ class AdminService {
|
||||
```typescript
|
||||
// CORRECT
|
||||
class AdminService {
|
||||
async getUser(userId: string): Promise<UserDto> {
|
||||
return this.apiClient.getUser(userId); // ✅ DTOs are fine
|
||||
async getUser(userId: string): Promise<Result<UserDto, DomainError>> {
|
||||
try {
|
||||
const dto = await this.apiClient.getUser(userId);
|
||||
return Result.ok(dto); // ✅ DTOs are fine
|
||||
} catch (error) {
|
||||
return Result.err(new NotFoundError('User not found'));
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -191,8 +231,13 @@ class AdminService {
|
||||
```typescript
|
||||
// CORRECT
|
||||
class AdminService {
|
||||
async getUser(userId: string): Promise<UserDto> {
|
||||
return this.apiClient.getUser(userId);
|
||||
async getUser(userId: string): Promise<Result<UserDto, DomainError>> {
|
||||
try {
|
||||
const dto = await this.apiClient.getUser(userId);
|
||||
return Result.ok(dto);
|
||||
} catch (error) {
|
||||
return Result.err(new NotFoundError('User not found'));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Business logic in core use case or page query
|
||||
@@ -208,24 +253,48 @@ export async function updateUserStatus(userId: string, status: string) {
|
||||
}
|
||||
```
|
||||
|
||||
✅ **Correct**: Server action uses service
|
||||
✅ **Correct**: Server action uses Mutation
|
||||
```typescript
|
||||
// CORRECT
|
||||
'use server';
|
||||
export async function updateUserStatus(userId: string, status: string) {
|
||||
const apiClient = new AdminApiClient(...);
|
||||
const service = new AdminService(apiClient);
|
||||
await service.updateUserStatus(userId, status); // ✅ Uses service
|
||||
export async function updateUserStatus(input: UpdateUserStatusInput) {
|
||||
const mutation = new UpdateUserStatusMutation();
|
||||
const result = await mutation.execute(input);
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
❌ **Wrong**: PageQuery creates API Client
|
||||
```typescript
|
||||
// WRONG
|
||||
export class DashboardPageQuery {
|
||||
async execute() {
|
||||
const apiClient = new DashboardApiClient(...); // ❌ Should use Service
|
||||
return await apiClient.getOverview();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
✅ **Correct**: PageQuery uses Service
|
||||
```typescript
|
||||
// CORRECT
|
||||
export class DashboardPageQuery {
|
||||
async execute() {
|
||||
const service = new DashboardService(); // ✅ Service creates API Client
|
||||
return await service.getDashboardOverview();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
Website services are **thin orchestration wrappers** around API clients. They handle infrastructure concerns so that PageQueries and Server Actions can focus on composition and validation.
|
||||
Website services are **thin orchestration wrappers** that create their own dependencies and handle error conversion.
|
||||
|
||||
**Key principles**:
|
||||
1. Services orchestrate API calls
|
||||
2. Server actions/PageQueries create infrastructure
|
||||
3. Services don't create ViewModels
|
||||
4. Services don't contain business rules
|
||||
5. **Server actions MUST use services, not API clients directly**
|
||||
1. ✅ Services create their own dependencies (API Client, Logger, ErrorReporter)
|
||||
2. ✅ Services return `Result<T, DomainError>`
|
||||
3. ✅ Services convert HTTP errors to Domain errors
|
||||
4. ❌ Services don't create ViewModels
|
||||
5. ❌ Services don't contain business rules
|
||||
6. ✅ PageQueries/Mutations use Services, not API Clients directly
|
||||
7. ✅ Manual construction (no DI container in RSC)
|
||||
Reference in New Issue
Block a user