Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions billing-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
"type-check": "tsc --noEmit",
"test": "jest",
"clean": "rimraf dist",
"gen:types": "openapi-typescript ../services/billing-service/openapi.yaml -o src/schema.ts",
"lint:contract": "spectral lint ../services/billing-service/openapi.yaml --ruleset ../services/billing-service/.spectral.yaml",
"prepublishOnly": "npm run clean && npm run build"
},
"publishConfig": {
Expand All @@ -29,9 +31,11 @@
"axios": "^1.7.0"
},
"devDependencies": {
"@stoplight/spectral-cli": "^6.11.0",
"@types/jest": "29.5.12",
"@types/node": "18.19.0",
"jest": "29.7.0",
"openapi-typescript": "^7.4.0",
"rimraf": "^5.0.1",
"ts-jest": "29.1.1",
"typescript": "5.1.6"
Expand Down
47 changes: 47 additions & 0 deletions billing-client/src/contract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* Contract alignment guard.
*
* The hand-authored public types in `./types` and the OpenAPI contract in
* `services/billing-service/openapi.yaml` (codegen'd into `./schema`) must stay
* in lockstep. This module statically asserts equivalence so that any drift —
* a field added to the spec, a type changed in either place — becomes a
* COMPILE ERROR in `tsc`, not a runtime integration surprise.
*
* It exports nothing at runtime; it is type-level only. Regenerate `schema.ts`
* with `npm run gen:types` after editing the contract.
*/
import type { components } from './schema';
import type {
BillingSubscription,
Plan,
CreateSubscriptionRequest,
CreateSubscriptionResponse,
UpdateSubscriptionRequest,
EntityType,
} from './types';

type Schemas = components['schemas'];

/**
* `Exact<A, B>` resolves to `A` only when A and B are mutually assignable,
* otherwise to `never` — turning a mismatch into an unusable type.
*/
type Exact<A, B> = [A] extends [B] ? ([B] extends [A] ? A : never) : never;

// Each line fails to compile if the generated schema and the public type diverge.
type _Sub = Exact<BillingSubscription, Schemas['BillingSubscription']>;
type _Plan = Exact<Plan, Schemas['Plan']>;
type _CreateReq = Exact<CreateSubscriptionRequest, Schemas['CreateSubscriptionRequest']>;
type _CreateRes = Exact<CreateSubscriptionResponse, Schemas['CreateSubscriptionResponse']>;
type _UpdateReq = Exact<UpdateSubscriptionRequest, Schemas['UpdateSubscriptionRequest']>;
type _Entity = Exact<EntityType, Schemas['EntityType']>;

// Reference the aliases so `noUnusedLocals`-style lints don't strip them.
export type ContractAlignment = {
subscription: _Sub;
plan: _Plan;
createRequest: _CreateReq;
createResponse: _CreateRes;
updateRequest: _UpdateReq;
entityType: _Entity;
};
3 changes: 3 additions & 0 deletions billing-client/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
export { BillingClient } from './client';
// Generated OpenAPI surface (source of truth = services/billing-service/openapi.yaml).
export type { paths, components, operations } from './schema';
export type { ContractAlignment } from './contract';
export type {
BillingClientConfig,
BillingSubscription,
Expand Down
Loading