Files
gridpilot.gg/docs/architecture/website/MUTATIONS.md
2026-01-13 01:36:27 +01:00

4.3 KiB

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:

// 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

// 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.