Files
cfw-autumn/shared/api/versionUtils/versionChangeUtils/VersionChange.ts
2025-10-06 14:16:51 +01:00

155 lines
4.1 KiB
TypeScript

import type { ZodType, z } from "zod/v4";
import type { ApiVersion } from "../ApiVersion.js";
/**
* Resources that can be affected by version changes
*/
export enum AffectedResource {
Customer = "customer",
CusProduct = "cus_product",
CusFeature = "cus_feature",
CusBalance = "cus_balance",
Invoice = "invoice",
Product = "product",
// Add more as needed
}
/**
* Abstract base class for bidirectional version changes with Zod schema validation
*
* Uses Zod schemas for runtime validation and type inference.
*
* @example
* // Features changed from array to object in V1_2
* const V1_2_FeaturesSchema = z.record(z.string(), ApiCusFeatureSchema);
* const V1_1_FeaturesSchema = z.array(ApiCusFeatureSchema);
*
* class V1_2_FeaturesArrayToObject extends VersionChange {
* version = ApiVersion.V1_2;
* description = "Features: array ↔ object";
* affectedResources = [AffectedResource.CusFeature];
*
* newSchema = V1_2_FeaturesSchema;
* oldSchema = V1_1_FeaturesSchema;
*
* transformResponse({ input }) {
* // input is validated against newSchema
* return Object.values(input); // Returns V1_1 format (array)
* }
* }
*/
export abstract class VersionChange<
TNewSchema extends ZodType = ZodType,
TOldSchema extends ZodType = ZodType,
TDataSchema extends ZodType = ZodType,
> {
/**
* The version this change was introduced in
*/
abstract readonly version: ApiVersion;
/**
* Human-readable description of the change
*/
abstract readonly description: string;
/**
* Resources affected by this change
*/
abstract readonly affectedResources: AffectedResource[];
/**
* Zod schema for the newer version format
*/
abstract readonly newSchema: TNewSchema;
/**
* Zod schema for the older version format
*/
abstract readonly oldSchema: TOldSchema;
/**
* Optional Zod schema for additional context data
*/
readonly dataSchema?: TDataSchema;
/**
* Whether this change affects request transformations
* Default: true
*/
readonly affectsRequest: boolean = true;
/**
* Whether this change affects response transformations
* Default: true
*/
readonly affectsResponse: boolean = true;
/**
* Whether this change has side effects beyond transformation
* If true, transforms become no-ops and you must handle logic elsewhere
*/
readonly hasSideEffects: boolean = false;
/**
* Transform request data forward (old → new format)
* Applied when user sends old version, we transform to latest
*
* @param input - Request data in previous version format (validated against oldSchema)
* @param data - Additional context data for transformation (validated against dataSchema if provided)
* @returns Data in current version format (should match newSchema)
*/
transformRequest({
input,
data: _data,
}: {
input: z.infer<TOldSchema>;
data?: TDataSchema extends ZodType ? z.infer<TDataSchema> : never;
}): z.infer<TNewSchema> {
// Default: no-op (override if change affects requests)
return input as unknown as z.infer<TNewSchema>;
}
/**
* Transform response data backward (new → old format)
* Applied when user expects old version, we transform from latest
*
* @param input - Response data in current version format (validated against newSchema)
* @param data - Additional context data for transformation (validated against dataSchema if provided)
* @returns Data in previous version format (should match oldSchema)
*/
transformResponse({
input,
data: _data,
}: {
input: z.infer<TNewSchema>;
data?: TDataSchema extends ZodType ? z.infer<TDataSchema> : never;
}): z.infer<TOldSchema> {
// Default: no-op (override if change affects responses)
return input as unknown as z.infer<TOldSchema>;
}
/**
* Check if this change affects a specific resource
*/
affects(resource: AffectedResource): boolean {
return this.affectedResources.includes(resource);
}
/**
* Get the name of this change class
*/
get name(): string {
return this.constructor.name;
}
}
/**
* Helper type for constructing version changes
*/
export type VersionChangeConstructor = new () => VersionChange<
ZodType,
ZodType,
ZodType
>;