305 lines
9.1 KiB
Markdown
305 lines
9.1 KiB
Markdown
|
|
# Type & Validation Consolidation - Complete ✅
|
||
|
|
|
||
|
|
**Date**: October 2025
|
||
|
|
**Status**: ✅ Completed
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🎯 Goal Achieved
|
||
|
|
|
||
|
|
Successfully consolidated all types, validation schemas, and query parameters into the `@customer-portal/domain` package as the **single source of truth**.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## ✅ What Was Done
|
||
|
|
|
||
|
|
### 1. **Enhanced `domain/common/` with Core Schemas**
|
||
|
|
|
||
|
|
#### Added to `domain/common/schema.ts`:
|
||
|
|
```typescript
|
||
|
|
// API Response schemas
|
||
|
|
✅ apiSuccessResponseSchema<T>(dataSchema)
|
||
|
|
✅ apiErrorResponseSchema
|
||
|
|
✅ apiResponseSchema<T>(dataSchema)
|
||
|
|
|
||
|
|
// Pagination schemas
|
||
|
|
✅ paginationParamsSchema
|
||
|
|
✅ paginatedResponseSchema<T>(itemSchema)
|
||
|
|
|
||
|
|
// Query parameter schemas
|
||
|
|
✅ filterParamsSchema (search, sortBy, sortOrder)
|
||
|
|
✅ queryParamsSchema (pagination + filters)
|
||
|
|
```
|
||
|
|
|
||
|
|
#### Added to `domain/common/types.ts`:
|
||
|
|
```typescript
|
||
|
|
✅ FilterParams interface
|
||
|
|
✅ QueryParams type (PaginationParams & FilterParams)
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2. **Added Domain-Specific Query Parameters**
|
||
|
|
|
||
|
|
Each domain now has its own query parameter schema:
|
||
|
|
|
||
|
|
#### `domain/billing/schema.ts`:
|
||
|
|
```typescript
|
||
|
|
✅ invoiceQueryParamsSchema
|
||
|
|
- page, limit (pagination)
|
||
|
|
- status (invoice-specific)
|
||
|
|
- dateFrom, dateTo (invoice-specific)
|
||
|
|
✅ InvoiceQueryParams type
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `domain/subscriptions/schema.ts`:
|
||
|
|
```typescript
|
||
|
|
✅ subscriptionQueryParamsSchema
|
||
|
|
- page, limit (pagination)
|
||
|
|
- status, type (subscription-specific)
|
||
|
|
✅ SubscriptionQueryParams type
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `domain/orders/schema.ts`:
|
||
|
|
```typescript
|
||
|
|
✅ orderQueryParamsSchema
|
||
|
|
- page, limit (pagination)
|
||
|
|
- status, orderType (order-specific)
|
||
|
|
✅ OrderQueryParams type
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3. **Added Missing Validation Schema**
|
||
|
|
|
||
|
|
#### `domain/sim/schema.ts`:
|
||
|
|
```typescript
|
||
|
|
✅ simOrderActivationMnpSchema
|
||
|
|
✅ simOrderActivationAddonsSchema
|
||
|
|
✅ simOrderActivationRequestSchema (with refinements)
|
||
|
|
✅ SimOrderActivationRequest type
|
||
|
|
✅ SimOrderActivationMnp type
|
||
|
|
✅ SimOrderActivationAddons type
|
||
|
|
```
|
||
|
|
|
||
|
|
This replaced the inline type definition in `sim-order-activation.service.ts`.
|
||
|
|
|
||
|
|
### 4. **Removed Duplicate Type Definitions**
|
||
|
|
|
||
|
|
#### Deleted Files:
|
||
|
|
- ❌ `apps/portal/src/lib/api/response-helpers.ts` (duplicate ApiResponse)
|
||
|
|
- ❌ `apps/portal/src/lib/api/types.ts` (duplicate query params)
|
||
|
|
|
||
|
|
#### Updated Imports:
|
||
|
|
- ✅ `apps/portal/src/features/billing/hooks/useBilling.ts` - now imports from domain
|
||
|
|
- ✅ `apps/bff/src/modules/subscriptions/sim-orders.controller.ts` - added validation
|
||
|
|
- ✅ `apps/bff/src/modules/subscriptions/sim-order-activation.service.ts` - uses domain types
|
||
|
|
|
||
|
|
### 5. **Added Comprehensive Documentation**
|
||
|
|
|
||
|
|
Created:
|
||
|
|
- ✅ `packages/domain/README.md` - Complete usage guide
|
||
|
|
- ✅ `docs/PACKAGE-ORGANIZATION.md` - Architecture decisions
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 📊 Before vs After
|
||
|
|
|
||
|
|
### Before (Issues):
|
||
|
|
```typescript
|
||
|
|
// ❌ Problem 1: Multiple ApiResponse definitions
|
||
|
|
packages/domain/common/types.ts → ApiResponse<T> (success: boolean)
|
||
|
|
apps/portal/src/lib/api/response-helpers.ts → ApiResponse<T> (different shape)
|
||
|
|
apps/bff/src/integrations/whmcs/types.ts → WhmcsApiResponse<T> (result field)
|
||
|
|
|
||
|
|
// ❌ Problem 2: Query params scattered
|
||
|
|
apps/portal/src/lib/api/types.ts → InvoiceQueryParams
|
||
|
|
// No validation schemas!
|
||
|
|
|
||
|
|
// ❌ Problem 3: Missing validation
|
||
|
|
apps/bff/src/modules/subscriptions/sim-orders.controller.ts
|
||
|
|
@Post("activate")
|
||
|
|
async activate(@Body() body: SimOrderActivationRequest) {
|
||
|
|
// ❌ No validation pipe!
|
||
|
|
}
|
||
|
|
|
||
|
|
// ❌ Problem 4: Local type definitions
|
||
|
|
apps/bff/src/modules/subscriptions/sim-order-activation.service.ts
|
||
|
|
export interface SimOrderActivationRequest { ... } // ❌ Not in domain
|
||
|
|
```
|
||
|
|
|
||
|
|
### After (Solved):
|
||
|
|
```typescript
|
||
|
|
// ✅ Solution 1: Single ApiResponse source
|
||
|
|
packages/domain/common/types.ts → ApiResponse<T>
|
||
|
|
packages/domain/common/schema.ts → apiResponseSchema<T>(dataSchema)
|
||
|
|
// All apps import from domain
|
||
|
|
|
||
|
|
// ✅ Solution 2: Domain-specific query params
|
||
|
|
packages/domain/billing/schema.ts → invoiceQueryParamsSchema
|
||
|
|
packages/domain/subscriptions/schema.ts → subscriptionQueryParamsSchema
|
||
|
|
packages/domain/orders/schema.ts → orderQueryParamsSchema
|
||
|
|
// With Zod validation!
|
||
|
|
|
||
|
|
// ✅ Solution 3: Validation in place
|
||
|
|
apps/bff/src/modules/subscriptions/sim-orders.controller.ts
|
||
|
|
@Post("activate")
|
||
|
|
@UsePipes(new ZodValidationPipe(simOrderActivationRequestSchema))
|
||
|
|
async activate(@Body() body: SimOrderActivationRequest) {
|
||
|
|
// ✅ Validated!
|
||
|
|
}
|
||
|
|
|
||
|
|
// ✅ Solution 4: Types in domain
|
||
|
|
packages/domain/sim/schema.ts
|
||
|
|
export const simOrderActivationRequestSchema = z.object({ ... });
|
||
|
|
export type SimOrderActivationRequest = z.infer<typeof ...>;
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🏗️ Architecture Summary
|
||
|
|
|
||
|
|
### What Goes Where:
|
||
|
|
|
||
|
|
| Item | Location | Example |
|
||
|
|
|------|----------|---------|
|
||
|
|
| **Domain Types** | `domain/*/contract.ts` | `Invoice`, `Order` |
|
||
|
|
| **Validation Schemas** | `domain/*/schema.ts` | `invoiceSchema` |
|
||
|
|
| **Generic Schemas** | `domain/common/schema.ts` | `paginationParamsSchema` |
|
||
|
|
| **Domain Query Params** | `domain/*/schema.ts` | `invoiceQueryParamsSchema` |
|
||
|
|
| **Provider Mappers** | `domain/*/providers/` | `transformWhmcsInvoice()` |
|
||
|
|
| **Pure Utilities** | `domain/toolkit/` | `formatCurrency()` |
|
||
|
|
| **Framework Code** | `apps/*/lib/` or `apps/*/core/` | React hooks, API clients |
|
||
|
|
|
||
|
|
### Key Principle:
|
||
|
|
|
||
|
|
**Domain Package = Pure TypeScript**
|
||
|
|
- ✅ No React
|
||
|
|
- ✅ No NestJS
|
||
|
|
- ✅ No Next.js
|
||
|
|
- ✅ No framework dependencies
|
||
|
|
- ✅ Reusable across all apps
|
||
|
|
|
||
|
|
**App Directories = Framework-Specific**
|
||
|
|
- Apps can have their own `lib/` or `core/` directories
|
||
|
|
- These contain framework-specific utilities
|
||
|
|
- They should import types from domain
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 📝 Usage Examples
|
||
|
|
|
||
|
|
### Backend (BFF) Controller
|
||
|
|
```typescript
|
||
|
|
import { ZodValidationPipe } from "@bff/core/validation";
|
||
|
|
import {
|
||
|
|
invoiceQueryParamsSchema,
|
||
|
|
type InvoiceQueryParams
|
||
|
|
} from "@customer-portal/domain/billing";
|
||
|
|
|
||
|
|
@Controller("invoices")
|
||
|
|
export class InvoicesController {
|
||
|
|
@Get()
|
||
|
|
@UsePipes(new ZodValidationPipe(invoiceQueryParamsSchema))
|
||
|
|
async list(@Query() query: InvoiceQueryParams) {
|
||
|
|
// query is validated by Zod
|
||
|
|
// query.page, query.limit, query.status are all typed
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Frontend Hook
|
||
|
|
```typescript
|
||
|
|
import { useQuery } from "@tanstack/react-query";
|
||
|
|
import {
|
||
|
|
invoiceSchema,
|
||
|
|
type Invoice,
|
||
|
|
type InvoiceQueryParams
|
||
|
|
} from "@customer-portal/domain/billing";
|
||
|
|
|
||
|
|
function useInvoices(params: InvoiceQueryParams) {
|
||
|
|
return useQuery({
|
||
|
|
queryKey: ["invoices", params],
|
||
|
|
queryFn: async () => {
|
||
|
|
const response = await apiClient.get("/invoices", { params });
|
||
|
|
return invoiceSchema.array().parse(response.data);
|
||
|
|
},
|
||
|
|
});
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🎯 What's Actually in "Common"
|
||
|
|
|
||
|
|
Only **truly generic** utilities that apply to ALL domains:
|
||
|
|
|
||
|
|
### Types (`domain/common/types.ts`):
|
||
|
|
- `ApiResponse<T>` - Generic API response wrapper
|
||
|
|
- `PaginationParams` - Generic page/limit/offset
|
||
|
|
- `FilterParams` - Generic search/sortBy/sortOrder
|
||
|
|
- `IsoDateTimeString`, `EmailAddress`, etc.
|
||
|
|
|
||
|
|
### Schemas (`domain/common/schema.ts`):
|
||
|
|
- `apiResponseSchema(dataSchema)` - Generic API response validation
|
||
|
|
- `paginationParamsSchema` - Generic pagination
|
||
|
|
- `filterParamsSchema` - Generic filters
|
||
|
|
- `emailSchema`, `passwordSchema`, `nameSchema` - Primitive validators
|
||
|
|
|
||
|
|
### NOT in Common:
|
||
|
|
- ❌ Invoice-specific query params → in `domain/billing/`
|
||
|
|
- ❌ Subscription-specific filters → in `domain/subscriptions/`
|
||
|
|
- ❌ Order-specific validations → in `domain/orders/`
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## ✅ Validation Coverage
|
||
|
|
|
||
|
|
All major endpoints now have validation:
|
||
|
|
|
||
|
|
| Endpoint | Schema | Status |
|
||
|
|
|----------|--------|--------|
|
||
|
|
| `POST /orders` | `createOrderRequestSchema` | ✅ |
|
||
|
|
| `GET /orders/:id` | `sfOrderIdParamSchema` | ✅ |
|
||
|
|
| `GET /invoices` | `invoiceListQuerySchema` | ✅ |
|
||
|
|
| `GET /subscriptions` | `subscriptionQuerySchema` | ✅ |
|
||
|
|
| `POST /subscriptions/sim/orders/activate` | `simOrderActivationRequestSchema` | ✅ (NEW) |
|
||
|
|
| `POST /auth/signup` | `signupRequestSchema` | ✅ |
|
||
|
|
| `POST /auth/login` | `loginRequestSchema` | ✅ |
|
||
|
|
| `PATCH /me` | `updateProfileRequestSchema` | ✅ |
|
||
|
|
| `PATCH /me/address` | `updateAddressRequestSchema` | ✅ |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🚀 Next Steps (Optional Improvements)
|
||
|
|
|
||
|
|
### Could Consider:
|
||
|
|
1. **Migrate more utilities from toolkit** - Review `domain/toolkit/` and ensure all utilities are framework-agnostic
|
||
|
|
2. **Branded types enforcement** - Decide if you want to fully adopt branded types (`UserId`, `OrderId`, etc.) or remove them
|
||
|
|
3. **Add more query param schemas** - If there are more GET endpoints without validation
|
||
|
|
4. **Provider response validation** - Add Zod schemas for WHMCS/Salesforce raw responses
|
||
|
|
|
||
|
|
### Should NOT Do:
|
||
|
|
- ❌ Move React hooks to domain (framework-specific)
|
||
|
|
- ❌ Move API clients to domain (infrastructure)
|
||
|
|
- ❌ Move error handlers to domain (framework-specific)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 📚 Documentation
|
||
|
|
|
||
|
|
Complete guides available:
|
||
|
|
- **Usage**: `packages/domain/README.md`
|
||
|
|
- **Architecture**: `docs/PACKAGE-ORGANIZATION.md`
|
||
|
|
- **Domain Structure**: `docs/DOMAIN-STRUCTURE.md`
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🎉 Result
|
||
|
|
|
||
|
|
You now have:
|
||
|
|
- ✅ **Single source of truth** for all types
|
||
|
|
- ✅ **Complete validation coverage** with Zod
|
||
|
|
- ✅ **No duplicate type definitions** across codebase
|
||
|
|
- ✅ **Clear architecture boundaries** (domain vs. app)
|
||
|
|
- ✅ **Comprehensive documentation** for future development
|
||
|
|
|
||
|
|
**The domain package is now your true source of types and validation! 🚀**
|
||
|
|
|