@sdk-it/typescript 0.45.0 → 0.46.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,103 +1,121 @@
1
1
  # @sdk-it/typescript
2
2
 
3
- <p align="center">A type-safe SDK generator that converts OpenAPI specifications into TypeScript client.</p>
3
+ Generate a fully typed TypeScript client from an OpenAPI document. Generated
4
+ clients work in Node.js, browsers, and other JavaScript runtimes with `fetch`.
4
5
 
5
- ## Description
6
-
7
- This package transforms OpenAPI specifications into
8
-
9
- - a fully-typed TypeScript client
10
- - that works in Node.js, browsers, and any JavaScript runtime
11
- - with the ability to control the structure, style, and formatting of generated code
12
-
13
- ## Installation
6
+ ## Install
14
7
 
15
8
  ```bash
16
- npm install @sdk-it/typescript
9
+ npm install --save-dev @sdk-it/typescript
10
+ npm install zod fast-content-type-parse
17
11
  ```
18
12
 
19
- ## Usage
13
+ `zod` and `fast-content-type-parse` are runtime dependencies of clients
14
+ generated in `minimal` mode.
20
15
 
21
- ### Basic SDK Generation
16
+ ## Generate from an OpenAPI document
22
17
 
23
18
  ```typescript
24
19
  import { generate } from '@sdk-it/typescript';
25
20
 
26
- import spec from './openapi.json';
21
+ const spec = await fetch('https://api.openstatus.dev/v1/openapi').then(
22
+ (response) => response.json(),
23
+ );
27
24
 
28
25
  await generate(spec, {
29
- output: './client',
30
- name: 'MyAPI',
26
+ output: './src/generated/openstatus',
27
+ mode: 'minimal',
28
+ name: 'OpenStatus',
31
29
  });
32
30
  ```
33
31
 
34
- ### Remote Spec Example
32
+ `name` controls the generated client class name. `minimal` mode writes client
33
+ source files directly to `output`.
34
+
35
+ ## Use the generated client
35
36
 
36
37
  ```typescript
37
- import { generate } from '@sdk-it/typescript';
38
+ import { OpenStatus } from './src/generated/openstatus/index.ts';
38
39
 
39
- // Fetch remote OpenAPI specification
40
- const spec = await fetch('https://api.openstatus.dev/v1/openapi').then((res) =>
41
- res.json(),
42
- );
40
+ const client = new OpenStatus({
41
+ baseUrl: 'https://api.openstatus.dev/v1',
42
+ 'x-openstatus-key': process.env.OPENSTATUS_API_KEY,
43
+ });
43
44
 
44
- // Generate client SDK
45
- await generate(spec, {
46
- output: './client',
47
- name: 'OpenStatus',
45
+ const reports = await client.request('GET /status_report', {});
46
+ console.log(reports);
47
+ ```
48
+
49
+ `request` returns unwrapped response data. It throws `ParseError` when input
50
+ validation fails and an `APIError` subclass when the server returns a
51
+ non-successful response:
52
+
53
+ ```typescript
54
+ import {
55
+ APIError,
56
+ OpenStatus,
57
+ ParseError,
58
+ } from './src/generated/openstatus/index.ts';
59
+
60
+ const client = new OpenStatus({
61
+ baseUrl: 'https://api.openstatus.dev/v1',
62
+ 'x-openstatus-key': process.env.OPENSTATUS_API_KEY,
48
63
  });
64
+
65
+ try {
66
+ const report = await client.request('GET /status_report/{id}', { id: '42' });
67
+ console.log(report);
68
+ } catch (error) {
69
+ if (error instanceof ParseError) {
70
+ console.error(error.data);
71
+ } else if (error instanceof APIError) {
72
+ console.error(error.status, error.data);
73
+ } else {
74
+ throw error;
75
+ }
76
+ }
49
77
  ```
50
78
 
51
- ### Format Generated Code
79
+ ## Format generated code
52
80
 
53
- You can format the generated code using the `formatCode` option. Useful when committing generated code to source control.
81
+ `formatCode` runs after generation and receives the actual source directory plus
82
+ an environment with local package executables on `PATH`:
54
83
 
55
84
  ```typescript
85
+ import { execFile } from 'node:child_process';
86
+ import { promisify } from 'node:util';
87
+
56
88
  import { generate } from '@sdk-it/typescript';
57
89
 
90
+ const execFileAsync = promisify(execFile);
58
91
  const spec = await fetch('https://petstore.swagger.io/v2/swagger.json').then(
59
- (res) => res.json(),
92
+ (response) => response.json(),
60
93
  );
61
94
 
62
- // Format generated code using Prettier
63
95
  await generate(spec, {
64
- output: join(process.cwd(), 'node_modules/.sdk-it/client'),
65
- formatCode: ({ output, env }) => {
66
- execFile('prettier', [output, '--write'], { env: env });
96
+ output: './src/generated/petstore',
97
+ name: 'PetStore',
98
+ formatCode: async ({ output, env }) => {
99
+ await execFileAsync('prettier', [output, '--write'], { env });
67
100
  },
68
101
  });
69
102
  ```
70
103
 
71
- ### Run the script
104
+ ## Generate a standalone project
72
105
 
73
- ```bash
74
- # using recent versions of node
75
- node ./openapi.ts
76
-
77
- # using node < 22
78
- npx tsx ./openapi.ts
79
-
80
- # using bun
81
- bun ./openapi.ts
82
- ```
83
-
84
- - Use the generated SDK
106
+ `full` mode adds `package.json` and `tsconfig.json`, places generated source
107
+ under `<output>/src`, and records the generated client's runtime dependencies:
85
108
 
86
109
  ```typescript
87
- import { OpenStatus } from './client';
88
-
89
- const client = new OpenStatus({
90
- baseUrl: 'https://api.openstatus.dev/v1/',
110
+ await generate(spec, {
111
+ output: './generated/petstore',
112
+ mode: 'full',
113
+ name: 'PetStore',
114
+ packageName: '@acme/petstore',
91
115
  });
92
-
93
- const [result, error] = await client.request('GET /status_report', {});
94
116
  ```
95
117
 
96
- ## Using with Your Favorite Frameworks
97
-
98
- The SDK works on its own, but you might want native integration with your frameworks:
118
+ ## Framework integrations
99
119
 
100
120
  - [React Query](../../docs/react-query.md)
101
121
  - [Angular](../../docs/angular.md)
102
-
103
- Let us know what you're using, and we'll help you integrate it.
package/dist/index.js CHANGED
@@ -66,7 +66,7 @@ function createTool(entry, operation) {
66
66
  inputSchema: schemas.${schemaName},
67
67
  execute: async (input, options) => {
68
68
  console.log('Executing ${operation.operationId} tool with input:', input);
69
- const context = coerceContext(options.experimental_context);
69
+ const context = coerceContext(options.context);
70
70
  const response = await context.client.request(
71
71
  '${entry.method.toUpperCase()} ${entry.path}' ,
72
72
  input,
@@ -117,13 +117,13 @@ function createTool2(entry, operation) {
117
117
  }
118
118
 
119
119
  // packages/typescript/src/lib/agent/utils.txt
120
- var utils_default = "function coerceContext(context?: any) {\n if (!context) {\n throw new Error('Context is required');\n }\n return context as {\n client: any\n };\n}\n/**\n * Takes a Zod object schema and makes all optional properties nullable as well.\n * This is useful for APIs where optional fields can be explicitly set to null.\n *\n * @param schema - The Zod object schema to transform\n * @returns A new Zod schema with optional properties made nullable\n */\nfunction makeOptionalPropsNullable<T extends z.ZodRawShape>(\n schema: z.ZodObject<T>,\n) {\n const shape = schema.shape;\n const newShape = {} as Record<string, z.ZodType>;\n\n for (const [key, value] of Object.entries(shape)) {\n if (value instanceof z.ZodOptional) {\n // Make optional properties also nullable\n newShape[key] = value.unwrap().nullable().optional();\n } else {\n // Keep non-optional properties as they are\n newShape[key] = value;\n }\n }\n\n return z.object(newShape);\n}";
120
+ var utils_default = "function coerceContext(context?: any) {\n if (!context) {\n throw new Error('Context is required');\n }\n return context as {\n client: any\n };\n}\n/**\n * Takes a Zod object schema and makes all optional properties nullable as well.\n * This is useful for APIs where optional fields can be explicitly set to null.\n *\n * @param schema - The Zod object schema to transform\n * @returns A new Zod schema with optional properties made nullable\n */\nfunction makeOptionalPropsNullable<T extends z.ZodRawShape>(\n schema: z.ZodObject<T>,\n) {\n const shape = schema.shape;\n const newShape = {} as Record<string, z.ZodType>;\n\n for (const [key, value] of Object.entries(shape) as [string, z.ZodType][]) {\n if (value instanceof z.ZodOptional) {\n // Make optional properties also nullable\n newShape[key] = value.nullable();\n } else {\n // Keep non-optional properties as they are\n newShape[key] = value;\n }\n }\n\n return z.object(newShape);\n}\n";
121
121
 
122
122
  // packages/typescript/src/lib/client.ts
123
123
  import { toLitObject } from "@sdk-it/core";
124
124
 
125
125
  // packages/typescript/src/lib/emitters/zod.ts
126
- import { followRef, isRef, parseRef, pascalcase } from "@sdk-it/core";
126
+ import { followRef, isEmpty, isRef, parseRef, pascalcase } from "@sdk-it/core";
127
127
  import { isPrimitiveSchema, sanitizeTag } from "@sdk-it/spec";
128
128
  var ZodEmitter = class {
129
129
  #generatedRefs = /* @__PURE__ */ new Set();
@@ -344,6 +344,9 @@ var ZodEmitter = class {
344
344
  if (isRef(schema)) {
345
345
  return `${this.#ref(schema.$ref, true)}${appendOptional(required)}`;
346
346
  }
347
+ if (schema.not && isEmpty(schema.not)) {
348
+ return `z.never()${appendOptional(required)}`;
349
+ }
347
350
  if (schema.allOf && Array.isArray(schema.allOf)) {
348
351
  return this.allOf(schema.allOf ?? [], required);
349
352
  }
@@ -619,7 +622,14 @@ export async function prepare<const E extends keyof typeof schemas>(
619
622
  };
620
623
 
621
624
  // packages/typescript/src/lib/emitters/interface.ts
622
- import { followRef as followRef2, isRef as isRef2, parseRef as parseRef2, pascalcase as pascalcase2, resolveRef } from "@sdk-it/core";
625
+ import {
626
+ followRef as followRef2,
627
+ isEmpty as isEmpty2,
628
+ isRef as isRef2,
629
+ parseRef as parseRef2,
630
+ pascalcase as pascalcase2,
631
+ resolveRef
632
+ } from "@sdk-it/core";
623
633
  import { isPrimitiveSchema as isPrimitiveSchema2, sanitizeTag as sanitizeTag2 } from "@sdk-it/spec";
624
634
  var TypeScriptEmitter = class {
625
635
  #spec;
@@ -629,7 +639,7 @@ var TypeScriptEmitter = class {
629
639
  #stringifyKey = (value) => {
630
640
  return `'${value}'`;
631
641
  };
632
- object(schema, required = false) {
642
+ object(schema, _required = false) {
633
643
  const properties = schema.properties || {};
634
644
  const propEntries = Object.entries(properties).map(([key, propSchema]) => {
635
645
  const isRequired = (schema.required ?? []).includes(key);
@@ -649,7 +659,7 @@ var TypeScriptEmitter = class {
649
659
  /**
650
660
  * Handle arrays (items could be a single schema or a tuple)
651
661
  */
652
- #array(schema, required = false) {
662
+ #array(schema, _required = false) {
653
663
  const { items } = schema;
654
664
  if (!items) {
655
665
  return "any[]";
@@ -759,6 +769,9 @@ var TypeScriptEmitter = class {
759
769
  if (isRef2(schema)) {
760
770
  return this.#ref(schema.$ref, required);
761
771
  }
772
+ if (schema.not && isEmpty2(schema.not)) {
773
+ return appendOptional2("never", required);
774
+ }
762
775
  if (schema.allOf && Array.isArray(schema.allOf)) {
763
776
  return this.allOf(schema.allOf);
764
777
  }
@@ -801,14 +814,14 @@ function appendOptional2(type, isRequired) {
801
814
  import { merge, template } from "lodash-es";
802
815
  import { join } from "node:path";
803
816
  import { camelcase as camelcase4, spinalcase as spinalcase2 } from "stringcase";
804
- import { followRef as followRef3, isEmpty as isEmpty2, isRef as isRef3, resolveRef as resolveRef2, sortArray } from "@sdk-it/core";
817
+ import { followRef as followRef3, isEmpty as isEmpty4, isRef as isRef3, resolveRef as resolveRef2, sortArray } from "@sdk-it/core";
805
818
  import {
806
819
  forEachOperation as forEachOperation3
807
820
  } from "@sdk-it/spec";
808
821
 
809
822
  // packages/typescript/src/lib/sdk.ts
810
823
  import { camelcase as camelcase3 } from "stringcase";
811
- import { isEmpty, pascalcase as pascalcase3 } from "@sdk-it/core";
824
+ import { isEmpty as isEmpty3, pascalcase as pascalcase3 } from "@sdk-it/core";
812
825
  import {
813
826
  isBinaryContentType,
814
827
  isSseContentType,
@@ -855,9 +868,7 @@ function describe(p) {
855
868
  nextPageMapping: `${p.pageNumberParamName}: nextPageParams.page, ${p.pageSizeParamName}: nextPageParams.pageSize`
856
869
  };
857
870
  }
858
- throw new Error(
859
- `Unknown pagination type: ${p.type}`
860
- );
871
+ throw new Error(`Unknown pagination type: ${p.type}`);
861
872
  }
862
873
  function paginationOperation(pagination) {
863
874
  const shape = describe(pagination);
@@ -963,7 +974,7 @@ function normalOperation() {
963
974
  function toHttpOutput(spec, operationName, status, response, withGenerics = true) {
964
975
  const typeScriptDeserialzer = new TypeScriptEmitter(spec);
965
976
  const interfaceName = pascalcase3(sanitizeTag3(response["x-response-name"]));
966
- if (!isEmpty(response.content)) {
977
+ if (!isEmpty3(response.content)) {
967
978
  const contentTypeResult = fromContentType(typeScriptDeserialzer, response);
968
979
  if (!contentTypeResult) {
969
980
  throw new Error(
@@ -1086,7 +1097,7 @@ function coearceRequestInput(spec, operation, type) {
1086
1097
  if (type === "application/empty") {
1087
1098
  objectSchema = {
1088
1099
  type: "object",
1089
- additionalProperties: isEmpty2(xProperties)
1100
+ additionalProperties: isEmpty4(xProperties)
1090
1101
  };
1091
1102
  } else {
1092
1103
  if (objectSchema.type !== "object") {
@@ -1356,7 +1367,7 @@ var offset_pagination_default = "type OffsetPaginationParams = {\n offset: numb
1356
1367
  var page_pagination_default = "type InferPage<T> = T extends Page<infer U> ? U : never;\ntype PaginationParams<P extends number | bigint, S extends number | bigint> = {\n page?: P;\n pageSize?: S;\n};\n\ninterface Metadata {\n hasMore?: boolean;\n}\n\ntype PaginationRequestOptions = {\n signal?: AbortSignal;\n};\n\ntype PaginationResult<T, M extends Metadata> = {\n data: T[];\n meta: M;\n};\n\ntype FetchFn<\n T,\n M extends Metadata,\n P extends number | bigint,\n S extends number | bigint,\n> = (\n input: Partial<PaginationParams<P, S>>,\n requestOptions?: PaginationRequestOptions,\n) => Promise<PaginationResult<T, M>>;\n\n/**\n * @experimental\n */\nexport class Pagination<\n T,\n M extends Metadata,\n P extends number | bigint,\n S extends number | bigint,\n> {\n #meta: PaginationResult<T, M>['meta'] | null = null;\n #params: PaginationParams<P, S>;\n #currentPage: Page<T> | null = null;\n readonly #fetchFn: FetchFn<T, M, P, S>;\n readonly #requestOptions: PaginationRequestOptions;\n\n constructor(\n initialParams: Partial<PaginationParams<P, S>>,\n fetchFn: FetchFn<T, M, P, S>,\n requestOptions: PaginationRequestOptions = {},\n ) {\n this.#fetchFn = fetchFn;\n this.#requestOptions = requestOptions;\n this.#params = { ...initialParams, page: initialParams.page };\n }\n\n async getNextPage(requestOptions?: PaginationRequestOptions) {\n const result = await this.#fetchFn(this.#params, {\n ...this.#requestOptions,\n ...requestOptions,\n });\n this.#currentPage = new Page(result.data);\n this.#meta = result.meta;\n this.#params = {\n ...this.#params,\n page: (((this.#params.page as number) || 0) + 1) as never,\n };\n return this;\n }\n\n getCurrentPage() {\n if (!this.#currentPage) {\n throw new Error(\n 'No page data available. Please call getNextPage() first.',\n );\n }\n return this.#currentPage;\n }\n\n get hasMore() {\n if (!this.#meta) {\n throw new Error(\n 'No meta data available. Please call getNextPage() first.',\n );\n }\n return this.#meta.hasMore;\n }\n\n async *[Symbol.asyncIterator]() {\n for await (const page of this.iter()) {\n yield page.getCurrentPage();\n }\n }\n\n async *iter(requestOptions?: PaginationRequestOptions) {\n if (!this.#currentPage) {\n yield await this.getNextPage(requestOptions);\n }\n\n while (this.hasMore) {\n yield await this.getNextPage(requestOptions);\n }\n }\n\n get metadata() {\n if (!this.#meta) {\n throw new Error(\n 'No meta data available. Please call getNextPage() first.',\n );\n }\n return this.#meta;\n }\n}\n\nclass Page<T> {\n data: T[];\n constructor(data: T[]) {\n this.data = data;\n }\n}\n";
1357
1368
 
1358
1369
  // packages/typescript/src/lib/readme/readme.ts
1359
- import { isEmpty as isEmpty3 } from "@sdk-it/core";
1370
+ import { isEmpty as isEmpty5 } from "@sdk-it/core";
1360
1371
  import { forEachOperation as forEachOperation4 } from "@sdk-it/spec";
1361
1372
 
1362
1373
  // packages/typescript/src/lib/readme/prop.emitter.ts
@@ -1684,7 +1695,7 @@ function toReadme(spec, generator) {
1684
1695
  markdown.push(`#### Output`);
1685
1696
  for (const status in operation.responses) {
1686
1697
  const response = operation.responses[status];
1687
- if (!isEmpty3(response.content)) {
1698
+ if (!isEmpty5(response.content)) {
1688
1699
  const contentEntries = Object.entries(response.content);
1689
1700
  if (contentEntries.length === 1) {
1690
1701
  const [contentType, mediaType] = contentEntries[0];
@@ -1768,7 +1779,7 @@ function expandServerUrls(servers) {
1768
1779
 
1769
1780
  // packages/typescript/src/lib/typescript-snippet.ts
1770
1781
  import { camelcase as camelcase5, spinalcase as spinalcase3 } from "stringcase";
1771
- import { isEmpty as isEmpty4, pascalcase as pascalcase4, resolveRef as resolveRef4 } from "@sdk-it/core";
1782
+ import { isEmpty as isEmpty6, pascalcase as pascalcase4, resolveRef as resolveRef4 } from "@sdk-it/core";
1772
1783
  import "@sdk-it/readme";
1773
1784
  import {
1774
1785
  forEachOperation as forEachOperation5,
@@ -1979,7 +1990,7 @@ var TypeScriptSnippet = class {
1979
1990
  }
1980
1991
  succinct(entry, operation, values) {
1981
1992
  let payload = "{}";
1982
- if (!isEmpty4(operation.requestBody)) {
1993
+ if (!isEmpty6(operation.requestBody)) {
1983
1994
  const contentTypes = Object.keys(operation.requestBody.content || {});
1984
1995
  const schema = resolveRef4(
1985
1996
  this.#spec,
@@ -2041,7 +2052,7 @@ var TypeScriptSnippet = class {
2041
2052
  return this.#streamDownload(entry, payload);
2042
2053
  }
2043
2054
  }
2044
- if (!isEmpty4(operation["x-pagination"])) {
2055
+ if (!isEmpty6(operation["x-pagination"])) {
2045
2056
  return this.#pagination(operation, entry, payload);
2046
2057
  }
2047
2058
  return this.#normal(entry, payload);
@@ -2125,7 +2136,7 @@ const result = await ${camelcase5(this.#clientName)}.request('${entry.method.toU
2125
2136
  baseUrl: expandServerUrls(this.#spec.servers ?? [])[0] ?? "http://localhost:3000"
2126
2137
  };
2127
2138
  const authOptions = this.#authentication();
2128
- if (!isEmpty4(authOptions)) {
2139
+ if (!isEmpty6(authOptions)) {
2129
2140
  const [firstAuth] = authOptions;
2130
2141
  const optionName = firstAuth["x-optionName"] ?? firstAuth.name;
2131
2142
  options[optionName] = firstAuth.example;
@@ -2165,7 +2176,7 @@ ${client.use}`;
2165
2176
  );
2166
2177
  const baseUrl = expandServerUrls(this.#spec.servers ?? [])[0] || "https://api.example.com";
2167
2178
  const authOptions = this.#authentication();
2168
- const hasApiKey = !isEmpty4(authOptions);
2179
+ const hasApiKey = !isEmpty6(authOptions);
2169
2180
  sections.push("### Configuration Options");
2170
2181
  sections.push("");
2171
2182
  sections.push("| Option | Type | Required | Description |");
@@ -2550,7 +2561,7 @@ ${client.use}`;
2550
2561
  }
2551
2562
  authenticationDocs() {
2552
2563
  const authOptions = this.#authentication();
2553
- if (isEmpty4(authOptions)) {
2564
+ if (isEmpty6(authOptions)) {
2554
2565
  return "";
2555
2566
  }
2556
2567
  const sections = [];
@@ -2632,7 +2643,7 @@ ${client.use}`;
2632
2643
  const initialClientOptions = {
2633
2644
  baseUrl: "https://api.production-service.com"
2634
2645
  };
2635
- if (!isEmpty4(authOptions)) {
2646
+ if (!isEmpty6(authOptions)) {
2636
2647
  const [primaryAuth] = authOptions;
2637
2648
  const authOptionName = primaryAuth["x-optionName"] ?? primaryAuth.name;
2638
2649
  initialClientOptions[authOptionName] = "YOUR_PRODUCTION_TOKEN";
@@ -2646,7 +2657,7 @@ ${client.use}`;
2646
2657
  "client.setOptions({",
2647
2658
  " baseUrl: 'https://api.staging-service.com',"
2648
2659
  ];
2649
- if (!isEmpty4(authOptions)) {
2660
+ if (!isEmpty6(authOptions)) {
2650
2661
  const [primaryAuth] = authOptions;
2651
2662
  const authOptionName = primaryAuth["x-optionName"] ?? primaryAuth.name;
2652
2663
  configurationUpdateCode.push(` ${authOptionName}: 'YOUR_STAGING_TOKEN'`);
@@ -2695,7 +2706,7 @@ function availablePaginationTypes(spec) {
2695
2706
 
2696
2707
  // packages/typescript/src/lib/generate.ts
2697
2708
  async function generate(openapi, settings) {
2698
- const spec = toIR(
2709
+ const spec = await toIR(
2699
2710
  {
2700
2711
  spec: openapi,
2701
2712
  responses: { flattenErrorResponses: true },
@@ -2860,6 +2871,7 @@ ${utils_default}`
2860
2871
  name: packageName,
2861
2872
  version: "0.0.1",
2862
2873
  type: "module",
2874
+ ...settings.agentTools === "ai-sdk" ? { engines: { node: ">=22" } } : {},
2863
2875
  main: "./src/index.ts",
2864
2876
  module: "./src/index.ts",
2865
2877
  types: "./src/index.ts",
@@ -2876,7 +2888,9 @@ ${utils_default}`
2876
2888
  },
2877
2889
  dependencies: {
2878
2890
  "fast-content-type-parse": "^3.0.0",
2879
- zod: "^4.3.0"
2891
+ zod: "^4.3.0",
2892
+ ...settings.agentTools === "ai-sdk" ? { ai: "^7.0.29" } : {},
2893
+ ...settings.agentTools === "openai-agents" ? { "@openai/agents": "^0.13.4" } : {}
2880
2894
  }
2881
2895
  },
2882
2896
  null,