Files
cfw-autumn/shared/api/versionUtils/README.md
2025-10-06 10:03:12 +01:00

222 lines
5.5 KiB
Markdown

# API Versioning System
Stripe-inspired versioning with CalVer (external) and SemVer (internal).
## Core Concept
**Always build latest format → Transform backwards automatically**
```typescript
// Your handler
const latestData = { features: { f1: {...} } }; // V1_2 format
return applyVersionChanges({
data: latestData,
currentVersion: new ApiVersionClass(LATEST_VERSION),
targetVersion: ctx.apiVersion, // From middleware
resource: AffectedResource.Customer
});
```
## Quick Reference
### Version Comparison
```typescript
// ctx.apiVersion ready in middleware
if (ctx.apiVersion.gte(ApiVersion.V1_1)) { ... }
if (ctx.apiVersion.lt(ApiVersion.V1_2)) { ... }
```
### Version Mapping
| SemVer | CalVer | Legacy v1 | Legacy v2 |
|--------|--------|-----------|-----------|
| V1_4 | 2025-06-01 | - | 1.4 (beta) |
| V1_2 | 2025-05-05 | - | 1.2 |
| V1_1 | 2025-04-17 | - | 1.1 |
| V0_2 | 2025-04-01 | 0.2 | 1.0 |
| V0_1 | 2025-02-01 | 0.1 | - |
### Side Effects
```typescript
if (ctx.apiVersion.lt(ApiVersion.V1_1)) {
expandArray.push(CusExpand.Invoices);
}
```
## How Transforms Work
1. **User requests V1_1**, your data is V1_2:
```
V1_2 { features: { f1: {...} } }
↓ FeaturesArrayToObject.transform()
V1_1 { features: [{ feature_id: 'f1', ...}] }
```
2. **Multiple versions back** (V1_2 → V0_2):
```
V1_2 → V1_1 → V0_2
(Each transform applied in sequence)
```
## File Structure
```
shared/api/
├── versionUtils/
│ ├── ApiVersion.ts # Version enum
│ ├── ApiVersionClass.ts # Comparison methods
│ ├── versionRegistry.ts # SemVer ↔ CalVer mappings
│ ├── versionRegistryUtils.ts # Helper functions
│ ├── convertVersionUtils.ts # Conversion utils
│ ├── versionBranchUtils.ts # Branching helpers
│ └── versionChangeUtils/
│ ├── VersionChange.ts # Abstract base
│ ├── VersionChangeRegistryClass.ts # Registry class
│ ├── versionChangeRegistry.ts # Register all changes
│ └── applyVersionChanges.ts # Transform engine
└── customers/
└── changes/ # Customer-specific changes
├── V1_2_FeaturesArrayToObject.ts
├── V1_1_MergedResponse.ts
├── V1_1_LegacyExpandInvoices.ts # Side effect
└── V0_2_ProductItems.ts
```
### Change Organization
**Version changes live with the resource they affect:**
- Customer changes → `shared/api/customers/changes/`
- Product changes → `shared/api/products/changes/`
- Invoice changes → `shared/api/invoices/changes/`
**Naming convention:** `V{version}_{Description}.ts`
- `V1_2_FeaturesArrayToObject.ts`
- `V1_1_MergedResponse.ts`
## Creating Version Changes
### 1. Create Change Class
```typescript
// shared/api/customers/changes/V1_3_MyChange.ts
import { ApiVersion, VersionChange, AffectedResource } from "@autumn/shared";
export class V1_3_MyChange extends VersionChange {
readonly version = ApiVersion.V1_3;
readonly description = "Brief description";
readonly affectedResources = [AffectedResource.Customer];
transform({ data }: { data: any }): any {
// Transform FROM V1_3 TO V1_2
return { ...data, oldField: data.newField };
}
}
```
### 2. Register in Registry
```typescript
// versionChangeUtils/versionChangeRegistry.ts
export const V1_3_CHANGES = [
V1_3_MyChange,
V1_3_AnotherChange,
];
export function registerAllVersionChanges() {
VersionChangeRegistryClass.register({
version: ApiVersion.V1_3,
changes: V1_3_CHANGES
});
// ... other versions
}
```
### Side Effect Changes
```typescript
export class V1_3_MySideEffect extends VersionChange {
readonly hasSideEffects = true; // Mark as side effect
// ... rest
}
// In handler:
if (ctx.apiVersion.lt(ApiVersion.V1_3)) {
// Handle side effect logic
}
```
## CalVer with .clover Support
System supports `.clover` suffix for non-breaking changes:
- `2025-04-17` → Breaking change
- `2025-04-17.clover` → Non-breaking update (future use)
Both map to same SemVer internally.
## Usage Patterns
### Standard Handler
```typescript
export const handleGet = createRoute({
handler: async (c) => {
const ctx = c.get("ctx");
// Build latest
const data = buildLatest();
// Transform
return c.json(applyVersionChanges({
data,
currentVersion: new ApiVersionClass(LATEST_VERSION),
targetVersion: ctx.apiVersion,
resource: AffectedResource.Customer
}));
}
});
```
### With Version Logic
```typescript
// Check version
if (ctx.apiVersion.lt(ApiVersion.V1_1)) {
expandArray.push(CusExpand.Invoices);
}
// Use helper
const withItems = ctx.apiVersion.gte(ApiVersion.V0_2);
```
## Middleware
`apiVersionMiddleware` resolves version from:
1. `x-api-version` header (CalVer: "2025-04-17")
2. `org.api_version` (legacy: 1.1)
3. `org.config.api_version` (legacy: 0.2)
4. Default: V0_2
Result stored in `ctx.apiVersion` (ApiVersionClass).
## Migration from Old System
### Before
```typescript
const apiVersion = orgToVersion({ org, reqApiVersion });
if (apiVersion >= LegacyVersion.v1_1) { ... }
```
### After
```typescript
if (ctx.apiVersion.gte(ApiVersion.V1_1)) { ... }
```
## Key Principles
1. **Always build latest** - Let transforms handle old versions
2. **Transforms go backwards** - New → Old, never Old → New
3. **Object parameters** - All functions use `{ param }` signature
4. **Descending order** - Version lists newest first
5. **Resource organization** - Changes live with affected resources