@sdk-it/typescript 0.46.0 → 0.46.2

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
@@ -123,7 +123,7 @@ var utils_default = "function coerceContext(context?: any) {\n if (!context) {\
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
  }
@@ -353,6 +356,9 @@ var ZodEmitter = class {
353
356
  if (schema.oneOf && Array.isArray(schema.oneOf) && schema.oneOf.length) {
354
357
  return this.oneOf(schema.oneOf ?? [], required);
355
358
  }
359
+ if (schema.const !== void 0) {
360
+ return `z.literal(${JSON.stringify(schema.const)})${this.#suffixes(JSON.stringify(schema.default), required, false)}`;
361
+ }
356
362
  if (schema.enum && Array.isArray(schema.enum)) {
357
363
  const enumVals = schema.enum.map((val) => JSON.stringify(val));
358
364
  const defaultValue = enumVals.includes(JSON.stringify(schema.default)) ? JSON.stringify(schema.default) : void 0;
@@ -621,6 +627,7 @@ export async function prepare<const E extends keyof typeof schemas>(
621
627
  // packages/typescript/src/lib/emitters/interface.ts
622
628
  import {
623
629
  followRef as followRef2,
630
+ isEmpty as isEmpty2,
624
631
  isRef as isRef2,
625
632
  parseRef as parseRef2,
626
633
  pascalcase as pascalcase2,
@@ -765,6 +772,9 @@ var TypeScriptEmitter = class {
765
772
  if (isRef2(schema)) {
766
773
  return this.#ref(schema.$ref, required);
767
774
  }
775
+ if (schema.not && isEmpty2(schema.not)) {
776
+ return appendOptional2("never", required);
777
+ }
768
778
  if (schema.allOf && Array.isArray(schema.allOf)) {
769
779
  return this.allOf(schema.allOf);
770
780
  }
@@ -807,14 +817,14 @@ function appendOptional2(type, isRequired) {
807
817
  import { merge, template } from "lodash-es";
808
818
  import { join } from "node:path";
809
819
  import { camelcase as camelcase4, spinalcase as spinalcase2 } from "stringcase";
810
- import { followRef as followRef3, isEmpty as isEmpty2, isRef as isRef3, resolveRef as resolveRef2, sortArray } from "@sdk-it/core";
820
+ import { followRef as followRef3, isEmpty as isEmpty4, isRef as isRef3, resolveRef as resolveRef2, sortArray } from "@sdk-it/core";
811
821
  import {
812
822
  forEachOperation as forEachOperation3
813
823
  } from "@sdk-it/spec";
814
824
 
815
825
  // packages/typescript/src/lib/sdk.ts
816
826
  import { camelcase as camelcase3 } from "stringcase";
817
- import { isEmpty, pascalcase as pascalcase3 } from "@sdk-it/core";
827
+ import { isEmpty as isEmpty3, pascalcase as pascalcase3 } from "@sdk-it/core";
818
828
  import {
819
829
  isBinaryContentType,
820
830
  isSseContentType,
@@ -967,7 +977,7 @@ function normalOperation() {
967
977
  function toHttpOutput(spec, operationName, status, response, withGenerics = true) {
968
978
  const typeScriptDeserialzer = new TypeScriptEmitter(spec);
969
979
  const interfaceName = pascalcase3(sanitizeTag3(response["x-response-name"]));
970
- if (!isEmpty(response.content)) {
980
+ if (!isEmpty3(response.content)) {
971
981
  const contentTypeResult = fromContentType(typeScriptDeserialzer, response);
972
982
  if (!contentTypeResult) {
973
983
  throw new Error(
@@ -1090,7 +1100,7 @@ function coearceRequestInput(spec, operation, type) {
1090
1100
  if (type === "application/empty") {
1091
1101
  objectSchema = {
1092
1102
  type: "object",
1093
- additionalProperties: isEmpty2(xProperties)
1103
+ additionalProperties: isEmpty4(xProperties)
1094
1104
  };
1095
1105
  } else {
1096
1106
  if (objectSchema.type !== "object") {
@@ -1360,7 +1370,7 @@ var offset_pagination_default = "type OffsetPaginationParams = {\n offset: numb
1360
1370
  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";
1361
1371
 
1362
1372
  // packages/typescript/src/lib/readme/readme.ts
1363
- import { isEmpty as isEmpty3 } from "@sdk-it/core";
1373
+ import { isEmpty as isEmpty5 } from "@sdk-it/core";
1364
1374
  import { forEachOperation as forEachOperation4 } from "@sdk-it/spec";
1365
1375
 
1366
1376
  // packages/typescript/src/lib/readme/prop.emitter.ts
@@ -1688,7 +1698,7 @@ function toReadme(spec, generator) {
1688
1698
  markdown.push(`#### Output`);
1689
1699
  for (const status in operation.responses) {
1690
1700
  const response = operation.responses[status];
1691
- if (!isEmpty3(response.content)) {
1701
+ if (!isEmpty5(response.content)) {
1692
1702
  const contentEntries = Object.entries(response.content);
1693
1703
  if (contentEntries.length === 1) {
1694
1704
  const [contentType, mediaType] = contentEntries[0];
@@ -1772,7 +1782,7 @@ function expandServerUrls(servers) {
1772
1782
 
1773
1783
  // packages/typescript/src/lib/typescript-snippet.ts
1774
1784
  import { camelcase as camelcase5, spinalcase as spinalcase3 } from "stringcase";
1775
- import { isEmpty as isEmpty4, pascalcase as pascalcase4, resolveRef as resolveRef4 } from "@sdk-it/core";
1785
+ import { isEmpty as isEmpty6, pascalcase as pascalcase4, resolveRef as resolveRef4 } from "@sdk-it/core";
1776
1786
  import "@sdk-it/readme";
1777
1787
  import {
1778
1788
  forEachOperation as forEachOperation5,
@@ -1983,7 +1993,7 @@ var TypeScriptSnippet = class {
1983
1993
  }
1984
1994
  succinct(entry, operation, values) {
1985
1995
  let payload = "{}";
1986
- if (!isEmpty4(operation.requestBody)) {
1996
+ if (!isEmpty6(operation.requestBody)) {
1987
1997
  const contentTypes = Object.keys(operation.requestBody.content || {});
1988
1998
  const schema = resolveRef4(
1989
1999
  this.#spec,
@@ -2045,7 +2055,7 @@ var TypeScriptSnippet = class {
2045
2055
  return this.#streamDownload(entry, payload);
2046
2056
  }
2047
2057
  }
2048
- if (!isEmpty4(operation["x-pagination"])) {
2058
+ if (!isEmpty6(operation["x-pagination"])) {
2049
2059
  return this.#pagination(operation, entry, payload);
2050
2060
  }
2051
2061
  return this.#normal(entry, payload);
@@ -2129,7 +2139,7 @@ const result = await ${camelcase5(this.#clientName)}.request('${entry.method.toU
2129
2139
  baseUrl: expandServerUrls(this.#spec.servers ?? [])[0] ?? "http://localhost:3000"
2130
2140
  };
2131
2141
  const authOptions = this.#authentication();
2132
- if (!isEmpty4(authOptions)) {
2142
+ if (!isEmpty6(authOptions)) {
2133
2143
  const [firstAuth] = authOptions;
2134
2144
  const optionName = firstAuth["x-optionName"] ?? firstAuth.name;
2135
2145
  options[optionName] = firstAuth.example;
@@ -2169,7 +2179,7 @@ ${client.use}`;
2169
2179
  );
2170
2180
  const baseUrl = expandServerUrls(this.#spec.servers ?? [])[0] || "https://api.example.com";
2171
2181
  const authOptions = this.#authentication();
2172
- const hasApiKey = !isEmpty4(authOptions);
2182
+ const hasApiKey = !isEmpty6(authOptions);
2173
2183
  sections.push("### Configuration Options");
2174
2184
  sections.push("");
2175
2185
  sections.push("| Option | Type | Required | Description |");
@@ -2554,7 +2564,7 @@ ${client.use}`;
2554
2564
  }
2555
2565
  authenticationDocs() {
2556
2566
  const authOptions = this.#authentication();
2557
- if (isEmpty4(authOptions)) {
2567
+ if (isEmpty6(authOptions)) {
2558
2568
  return "";
2559
2569
  }
2560
2570
  const sections = [];
@@ -2636,7 +2646,7 @@ ${client.use}`;
2636
2646
  const initialClientOptions = {
2637
2647
  baseUrl: "https://api.production-service.com"
2638
2648
  };
2639
- if (!isEmpty4(authOptions)) {
2649
+ if (!isEmpty6(authOptions)) {
2640
2650
  const [primaryAuth] = authOptions;
2641
2651
  const authOptionName = primaryAuth["x-optionName"] ?? primaryAuth.name;
2642
2652
  initialClientOptions[authOptionName] = "YOUR_PRODUCTION_TOKEN";
@@ -2650,7 +2660,7 @@ ${client.use}`;
2650
2660
  "client.setOptions({",
2651
2661
  " baseUrl: 'https://api.staging-service.com',"
2652
2662
  ];
2653
- if (!isEmpty4(authOptions)) {
2663
+ if (!isEmpty6(authOptions)) {
2654
2664
  const [primaryAuth] = authOptions;
2655
2665
  const authOptionName = primaryAuth["x-optionName"] ?? primaryAuth.name;
2656
2666
  configurationUpdateCode.push(` ${authOptionName}: 'YOUR_STAGING_TOKEN'`);