Assist_Design/docs/_archive/SCHEMA-FIRST-MIGRATION.md

5.6 KiB

Schema-First Migration Guide

Status: 🚧 In Progress
Date: October 2025
Objective: Standardize all domain types to be derived from Zod schemas


🎯 Migration Strategy

Chosen Approach: Schema-First

All TypeScript types will be derived from Zod schemas using z.infer<typeof schema>.

Benefits:

  • Single source of truth (schema defines structure)
  • Impossible for types to drift from validation
  • Less maintenance (update schema, type auto-updates)
  • Runtime + compile-time safety guaranteed

📋 File Structure Pattern

Before (Mixed Approach):

// contract.ts
export interface Invoice {
  id: number;
  status: InvoiceStatus;
  // ...
}

// schema.ts
export const invoiceSchema = z.object({
  id: z.number(),
  status: invoiceStatusSchema,
  // ...
});

After (Schema-First):

// schema.ts
export const invoiceSchema = z.object({
  id: z.number().int().positive(),
  status: invoiceStatusSchema,
  // ...
});

// Derive type from schema
export type Invoice = z.infer<typeof invoiceSchema>;

// contract.ts
// Only for:
// 1. Constants (e.g., INVOICE_STATUS)
// 2. Complex business types without schemas
// 3. Union types
// 4. Provider-specific types
export const INVOICE_STATUS = {
  PAID: "Paid",
  UNPAID: "Unpaid",
  // ...
} as const;

export type InvoiceStatus = (typeof INVOICE_STATUS)[keyof typeof INVOICE_STATUS];

// Re-export inferred type
export type { Invoice } from './schema';

🔄 Migration Checklist

Phase 1: Core Domains

  • billing - invoices, billing summary
  • orders - order creation, order queries
  • customer - customer profile, address
  • auth - login, signup, password reset

Phase 2: Secondary Domains

  • subscriptions - service subscriptions
  • payments - payment methods, gateways
  • sim - SIM details, usage, management
  • catalog - product catalog

Phase 3: Supporting Domains

  • common - shared schemas
  • mappings - ID mappings
  • dashboard - dashboard types

📝 Migration Steps (Per Domain)

For each domain, follow these steps:

1. Audit Current State

  • Identify all types in contract.ts
  • Identify all schemas in schema.ts
  • Find which types already have corresponding schemas

2. Convert Schema to Type

// Before
export interface Invoice { ... }

// After (in schema.ts)
export const invoiceSchema = z.object({ ... });
export type Invoice = z.infer<typeof invoiceSchema>;

3. Update contract.ts

  • Remove duplicate interfaces
  • Keep only constants and business logic types
  • Re-export types from schema.ts

4. Update index.ts

  • Export both schemas and types
  • Maintain backward compatibility

5. Verify Imports

  • Check no breaking changes for consumers
  • Types still importable from domain package

🛠️ Special Cases

Case 1: Constants + Inferred Types

// schema.ts
export const invoiceStatusSchema = z.enum(["Paid", "Unpaid", "Overdue"]);
export type InvoiceStatus = z.infer<typeof invoiceStatusSchema>;

// contract.ts
export const INVOICE_STATUS = {
  PAID: "Paid",
  UNPAID: "Unpaid",
  OVERDUE: "Overdue",
} as const;

// Keep both - they serve different purposes

Case 2: Schemas Referencing Contract Types

// ❌ Bad - circular dependency
import type { SimTopUpRequest } from "./contract";
export const simTopUpRequestSchema: z.ZodType<SimTopUpRequest> = z.object({ ... });

// ✅ Good - derive from schema
export const simTopUpRequestSchema = z.object({
  quotaMb: z.number().int().min(100).max(51200),
});
export type SimTopUpRequest = z.infer<typeof simTopUpRequestSchema>;

Case 3: Complex Business Types

// contract.ts - Keep types that have no validation schema
export interface OrderBusinessValidation extends CreateOrderRequest {
  userId: string;
  opportunityId?: string;
}

// These are internal domain types, not API contracts

Case 4: Provider-Specific Types

// providers/whmcs/raw.types.ts
// Keep as-is - these are provider-specific, not validated
export interface WhmcsInvoiceRaw {
  id: string;
  status: string;
  // Raw WHMCS response shape
}

Success Criteria

After migration, each domain should have:

  1. schema.ts

    • All Zod schemas
    • Exported types derived from schemas (z.infer)
  2. contract.ts

    • Constants (e.g., INVOICE_STATUS)
    • Business logic types (not validated)
    • Re-exports of types from schema.ts
  3. index.ts

    • Exports all schemas
    • Exports all types
    • Maintains backward compatibility
  4. No Type Drift

    • Every validated type has a corresponding schema
    • All types are derived from schemas

🧪 Testing Strategy

After Each Domain Migration:

  1. Type Check

    pnpm run type-check
    
  2. Build Domain Package

    cd packages/domain && pnpm build
    
  3. Test Imports in Apps

    // Verify exports still work
    import { Invoice, invoiceSchema } from '@customer-portal/domain/billing';
    
  4. Run Schema Tests (if added)

    pnpm test packages/domain
    

📚 References


🚀 Next Steps

  1. Start with billing domain (most straightforward)
  2. Apply pattern to orders domain
  3. Replicate across all domains
  4. Update documentation
  5. Create linting rules to enforce pattern