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
11 changes: 11 additions & 0 deletions .changeset/sdk-additional-types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@epilot/sdk": patch
---

Expose the clients' hand-written `additional-types.ts` through the SDK

`@epilot/sdk/<api>` re-exported only the types generated from an API's OpenAPI specification. Types a client declares by hand in `src/additional-types.ts` — types the API no longer returns but consumers still need — were silently dropped, so code migrating from `@epilot/<api>-client` to `@epilot/sdk/<api>` had to re-declare them.

`scripts/generate-sdk-v2.ts` now copies each client's `additional-types.ts` to `src/types/<api>-additional.d.ts` (rewriting its `./openapi` import to point at the copied generated types) and re-exports it from `src/apis/<api>.ts`.

For `@epilot/sdk/pricing` this restores `PriceTierEnhanced`, `Cart`, and `AvailabilityDate`, matching the surface of `@epilot/pricing-client`.
47 changes: 47 additions & 0 deletions packages/epilot-sdk-v2/__tests__/additional-types.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { existsSync, readdirSync, readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { describe, expect, it } from 'vitest';

import type { AvailabilityDate, Cart, PriceTierEnhanced } from '../src/apis/pricing';

const CLIENTS_DIR = resolve(__dirname, '../../../clients');
const TYPES_DIR = resolve(__dirname, '../src/types');
const APIS_DIR = resolve(__dirname, '../src/apis');

/**
* Types that are hand-written in a client's `additional-types.ts` (i.e. not part
* of the OpenAPI specification) must stay reachable through the SDK, otherwise
* consumers migrating from `@epilot/<x>-client` to `@epilot/sdk/<x>` have to
* re-declare them. See clients/pricing-client/src/additional-types.ts.
*/
describe('additional types are exposed by the SDK', () => {
const clientsWithAdditionalTypes = readdirSync(CLIENTS_DIR, { withFileTypes: true })
.filter((d) => d.isDirectory() && d.name.endsWith('-client'))
.filter((d) => existsSync(resolve(CLIENTS_DIR, d.name, 'src/additional-types.ts')))
.map((d) => ({ dirName: d.name, kebabName: d.name.replace(/-client$/, '') }));

it('finds at least one client with additional types', () => {
expect(clientsWithAdditionalTypes.length).toBeGreaterThan(0);
});

it.each(clientsWithAdditionalTypes)('$dirName additional types are copied and re-exported', ({ kebabName }) => {
const copied = resolve(TYPES_DIR, `${kebabName}-additional.d.ts`);
expect(existsSync(copied), `${copied} is missing — run pnpm generate-sdk`).toBe(true);

// The copied file must not keep pointing at the client's own './openapi'.
expect(readFileSync(copied, 'utf-8')).not.toMatch(/from '\.\/openapi'/);

const apiFile = readFileSync(resolve(APIS_DIR, `${kebabName}.ts`), 'utf-8');
expect(apiFile).toContain(`export type * from '../types/${kebabName}-additional'`);
});

it('exposes the pricing additional types through @epilot/sdk/pricing', () => {
const tier: PriceTierEnhanced = { unit_amount_gross: 119, unit_amount_gross_decimal: '119.00' };
const availability: AvailabilityDate = { available_start_date: '2017-07-21' };
const cart: Cart = { id: 'cart-1', status: 'draft' };

expect(tier.unit_amount_gross).toBe(119);
expect(availability.available_start_date).toBe('2017-07-21');
expect(cart.id).toBe('cart-1');
});
});
1 change: 1 addition & 0 deletions packages/epilot-sdk-v2/src/apis/pricing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export { authorize } from '../authorize';
export type { TokenArg } from '../authorize';
import type { Client } from '../types/pricing';
export type * from '../types/pricing';
export type * from '../types/pricing-additional';
export type { OpenAPIClient } from 'openapi-client-axios';

/* eslint-disable @typescript-eslint/no-require-imports */
Expand Down
59 changes: 59 additions & 0 deletions packages/epilot-sdk-v2/src/types/pricing-additional.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/* Auto-copied from pricing-client/src/additional-types.ts */
/**
* Additional legacy types that were removed from the OpenAPI specification
* because they are no longer used by the API.
*
* These types remain temporarily for backwards compatibility and will be
* removed once all dependent code has been migrated.
*/

import type { Address, Amounts, Customer, MetaData, OrderStatus, PriceItems, PriceTier, TotalDetails } from './pricing';

export interface AvailabilityDate {
/**
* The availability interval start date
* example:
* 2017-07-21
*/
available_start_date?: string; // date
/**
* The availability interval end date
* example:
* 2017-07-21
*/
available_end_date?: string; // date
}

export interface PriceTierEnhanced extends PriceTier {
unit_amount_gross?: number;
unit_amount_gross_decimal?: string;
flat_fee_amount_gross?: number;
flat_fee_amount_gross_decimal?: string;
}

export interface Cart extends Amounts {
/**
* The cart identifier
*/
id?: string;
/**
* The user's Organization Id the cart belongs to
*/
org_id?: string;
/**
* The status of the Cart:
* - open - the cart checkout is still in progress. Payment processing has not started
* - complete - the cart checkout is complete. Payment processing may still be in progress
* - expired - the cart checkout has expired. No further processing will occur
*
*/
status?: OrderStatus;
customer?: Customer;
billing_address?: Address;
delivery_address?: Address;
metadata?: /* A set of key-value pairs used to store meta data information about an entity. */ MetaData;
line_items?: /* Tracks a set of product prices, quantities, (discounts) and taxes. */ PriceItems;
total_details?: /* The total details with tax (and discount) aggregated totals. */ TotalDetails;
created_at?: string; // date-time
updated_at?: string; // date-time
}
32 changes: 32 additions & 0 deletions scripts/generate-sdk-v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* Generates the epilot-sdk v2 package files from existing clients:
* - Copies openapi-runtime.json definitions
* - Copies openapi.d.ts type files
* - Copies hand-written additional-types.ts type files
* - Generates per-API lazy loader files (apis/*.ts)
* - Generates the API registry (apis/_registry.ts)
* - Updates package.json subpath exports
Expand Down Expand Up @@ -33,6 +34,7 @@ type ClientInfo = {
kebabName: string; // e.g. "entity" or "entity-mapping" (kebab-case, used for file names & exports)
hasDefinition: boolean;
hasTypes: boolean;
hasAdditionalTypes: boolean;
};

const discoverClients = (): ClientInfo[] => {
Expand All @@ -51,6 +53,7 @@ const discoverClients = (): ClientInfo[] => {
kebabName: baseName, // baseName is already kebab-case (e.g. "entity-mapping")
hasDefinition: existsSync(resolve(clientDir, 'openapi-runtime.json')),
hasTypes: existsSync(resolve(clientDir, 'openapi.d.ts')),
hasAdditionalTypes: existsSync(resolve(clientDir, 'additional-types.ts')),
};
});
};
Expand Down Expand Up @@ -180,6 +183,30 @@ const copyTypes = (clients: ClientInfo[]) => {
}
};

/**
* Copies the hand-written `additional-types.ts` of a client (types that are not
* part of the OpenAPI specification but are still part of the client's public
* API surface) into the SDK types directory, so the SDK exposes the exact same
* types as the standalone client package.
*/
const copyAdditionalTypes = (clients: ClientInfo[]) => {
mkdirSync(TYPES_DIR, { recursive: true });

for (const client of clients) {
if (!client.hasAdditionalTypes) continue;
const src = resolve(CLIENTS_DIR, client.dirName, 'src/additional-types.ts');
let content = readFileSync(src, 'utf-8');

// The client resolves generated types via './openapi', in the SDK they live
// in a sibling file named after the API.
content = content.replace(/(\bfrom\s+')\.\/openapi(')/g, `$1./${client.kebabName}$2`);
content = `/* Auto-copied from ${client.dirName}/src/additional-types.ts */\n${content}`;

const dest = resolve(TYPES_DIR, `${client.kebabName}-additional.d.ts`);
writeFileSync(dest, content);
}
};

type SchemaDoc = {
name: string;
description: string;
Expand Down Expand Up @@ -333,6 +360,9 @@ const generateApiFile = (client: ClientInfo): string => {
lines.push(`import type { Client } from '../types/${client.kebabName}'`);

lines.push(`export type * from '../types/${client.kebabName}'`);
if (client.hasAdditionalTypes) {
lines.push(`export type * from '../types/${client.kebabName}-additional'`);
}
lines.push(`export type { OpenAPIClient } from 'openapi-client-axios'`);
} else {
lines.push(`import type { AxiosInstance } from 'axios'`);
Expand Down Expand Up @@ -1039,6 +1069,7 @@ const main = () => {

console.log('Copying types...');
copyTypes(clients);
copyAdditionalTypes(clients);

console.log('Generating per-API files...');
mkdirSync(APIS_DIR, { recursive: true });
Expand Down Expand Up @@ -1076,6 +1107,7 @@ const main = () => {
console.log(`\nGenerated:`);
console.log(` - ${validClients.length} definition files`);
console.log(` - ${clients.filter((c) => c.hasTypes).length} type files`);
console.log(` - ${clients.filter((c) => c.hasAdditionalTypes).length} additional type files`);
console.log(` - ${validClients.length} API entry files`);
console.log(` - 1 registry file`);
console.log(` - ${docCount} API docs + index`);
Expand Down
Loading