251 lines
5.6 KiB
Markdown
251 lines
5.6 KiB
Markdown
# 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
|
|
|