170 lines
4.3 KiB
Markdown
170 lines
4.3 KiB
Markdown
# Mutations (Strict)
|
|
|
|
This document defines the **Mutation** pattern for `apps/website`.
|
|
|
|
Mutations exist to provide framework-agnostic write operations.
|
|
|
|
## 1) Definition
|
|
|
|
A **Mutation** is a framework-agnostic operation that orchestrates writes.
|
|
|
|
Mutations are the write equivalent of PageQueries.
|
|
|
|
## 2) Relationship to Next.js Server Actions
|
|
|
|
**Server Actions are the entry point**, but they should be thin wrappers:
|
|
|
|
```typescript
|
|
// app/admin/actions.ts (Next.js framework code)
|
|
'use server';
|
|
|
|
import { AdminUserMutation } from '@/lib/mutations/admin/AdminUserMutation';
|
|
import { revalidatePath } from 'next/cache';
|
|
|
|
export async function updateUserStatus(userId: string, status: string) {
|
|
const mutation = new AdminUserMutation();
|
|
const result = await mutation.updateUserStatus(userId, status);
|
|
|
|
if (result.isErr()) {
|
|
console.error('updateUserStatus failed:', result.getError());
|
|
throw new Error('Failed to update user status');
|
|
}
|
|
|
|
revalidatePath('/admin/users');
|
|
}
|
|
```
|
|
|
|
## 3) Mutation Structure
|
|
|
|
```typescript
|
|
// lib/mutations/admin/AdminUserMutation.ts
|
|
import { Result, ResultFactory } from '@/lib/contracts/Result';
|
|
|
|
export class AdminUserMutation {
|
|
private service: AdminService;
|
|
|
|
constructor() {
|
|
// Manual DI for serverless
|
|
const logger = new ConsoleLogger();
|
|
const errorReporter = new EnhancedErrorReporter(logger, {
|
|
showUserNotifications: true,
|
|
logToConsole: true,
|
|
reportToExternal: process.env.NODE_ENV === 'production',
|
|
});
|
|
const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001';
|
|
const apiClient = new AdminApiClient(baseUrl, errorReporter, logger);
|
|
this.service = new AdminService(apiClient);
|
|
}
|
|
|
|
async updateUserStatus(userId: string, status: string): Promise<Result<void, string>> {
|
|
try {
|
|
await this.service.updateUserStatus(userId, status);
|
|
return ResultFactory.ok(undefined);
|
|
} catch (error) {
|
|
return ResultFactory.error('UPDATE_USER_STATUS_FAILED');
|
|
}
|
|
}
|
|
|
|
async deleteUser(userId: string): Promise<Result<void, string>> {
|
|
try {
|
|
await this.service.deleteUser(userId);
|
|
return ResultFactory.ok(undefined);
|
|
} catch (error) {
|
|
return ResultFactory.error('DELETE_USER_FAILED');
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## 4) Why This Pattern?
|
|
|
|
**Benefits:**
|
|
|
|
1. **Framework independence** - Mutations can be tested without Next.js
|
|
2. **Consistent pattern** - Mirrors PageQueries for reads/writes
|
|
3. **Easy migration** - Can switch frameworks without rewriting business logic
|
|
4. **Testable** - Can unit test mutations in isolation
|
|
5. **Reusable** - Can be called from other contexts (cron jobs, etc.)
|
|
|
|
## 5) Naming Convention
|
|
|
|
- Mutations: `*Mutation.ts`
|
|
- Server Actions: `actions.ts` (thin wrappers)
|
|
|
|
## 6) File Structure
|
|
|
|
```
|
|
lib/
|
|
mutations/
|
|
admin/
|
|
AdminUserMutation.ts
|
|
AdminLeagueMutation.ts
|
|
league/
|
|
LeagueJoinMutation.ts
|
|
team/
|
|
TeamUpdateMutation.ts
|
|
```
|
|
|
|
## 7) Non-negotiable Rules
|
|
|
|
1. **Server Actions are thin wrappers** - They only handle framework concerns (revalidation, redirects)
|
|
2. **Mutations handle infrastructure** - They create services, handle errors
|
|
3. **Services handle business logic** - They orchestrate API calls
|
|
4. **Mutations are framework-agnostic** - No Next.js imports except in tests
|
|
5. **Mutations must be deterministic** - Same inputs = same outputs
|
|
|
|
## 8) Comparison with PageQueries
|
|
|
|
| Aspect | PageQuery | Mutation |
|
|
|--------|-----------|----------|
|
|
| Purpose | Read data | Write data |
|
|
| Location | `lib/page-queries/` | `lib/mutations/` |
|
|
| Framework | Called from RSC | Called from Server Actions |
|
|
| Infrastructure | Manual DI | Manual DI |
|
|
| Returns | `Result<ApiDto, string>` | `Result<void, string>` |
|
|
| Revalidation | N/A | Server Action handles it |
|
|
|
|
## 9) Example Flow
|
|
|
|
**Read (RSC):**
|
|
```
|
|
RSC page.tsx
|
|
↓
|
|
PageQuery.execute()
|
|
↓
|
|
Service
|
|
↓
|
|
API Client
|
|
↓
|
|
Result<ApiDto, string>
|
|
↓
|
|
ViewDataBuilder
|
|
↓
|
|
Template
|
|
```
|
|
|
|
**Write (Server Action):**
|
|
```
|
|
Client Component
|
|
↓
|
|
Server Action
|
|
↓
|
|
Mutation.execute()
|
|
↓
|
|
Service
|
|
↓
|
|
API Client
|
|
↓
|
|
Result<void, string>
|
|
↓
|
|
Revalidation
|
|
```
|
|
|
|
## 10) Enforcement
|
|
|
|
ESLint rules should enforce:
|
|
- Server Actions must call Mutations (not Services directly)
|
|
- Mutations must not import Next.js (except in tests)
|
|
- Mutations must use services
|
|
|
|
See `docs/architecture/website/WEBSITE_GUARDRAILS.md` for details. |