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

251 lines
5.6 KiB
Markdown
Raw Normal View History

# 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):**
```typescript
// contract.ts
export interface Invoice {
id: number;
status: InvoiceStatus;
// ...
}
// schema.ts
export const invoiceSchema = z.object({
id: z.number(),
status: invoiceStatusSchema,
// ...
});
```
### **After (Schema-First):**
```typescript
// 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**
```typescript
// 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**
```typescript
// 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**
```typescript
// ❌ 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**
```typescript
// 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**
```typescript
// 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**
```bash
pnpm run type-check
```
2. **Build Domain Package**
```bash
cd packages/domain && pnpm build
```
3. **Test Imports in Apps**
```typescript
// Verify exports still work
import { Invoice, invoiceSchema } from '@customer-portal/domain/billing';
```
4. **Run Schema Tests** (if added)
```bash
pnpm test packages/domain
```
---
## 📚 References
- **Zod Documentation**: https://zod.dev/
- **Type Inference**: https://zod.dev/?id=type-inference
- **Schema Composition**: https://zod.dev/?id=objects
---
## 🚀 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