@sdk-it/typescript 0.46.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
@@ -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
  }
@@ -621,6 +624,7 @@ export async function prepare<const E extends keyof typeof schemas>(
621
624
  // packages/typescript/src/lib/emitters/interface.ts
622
625
  import {
623
626
  followRef as followRef2,
627
+ isEmpty as isEmpty2,
624
628
  isRef as isRef2,
625
629
  parseRef as parseRef2,
626
630
  pascalcase as pascalcase2,
@@ -765,6 +769,9 @@ var TypeScriptEmitter = class {
765
769
  if (isRef2(schema)) {
766
770
  return this.#ref(schema.$ref, required);
767
771
  }
772
+ if (schema.not && isEmpty2(schema.not)) {
773
+ return appendOptional2("never", required);
774
+ }
768
775
  if (schema.allOf && Array.isArray(schema.allOf)) {
769
776
  return this.allOf(schema.allOf);
770
777
  }
@@ -807,14 +814,14 @@ function appendOptional2(type, isRequired) {
807
814
  import { merge, template } from "lodash-es";
808
815
  import { join } from "node:path";
809
816
  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";
817
+ import { followRef as followRef3, isEmpty as isEmpty4, isRef as isRef3, resolveRef as resolveRef2, sortArray } from "@sdk-it/core";
811
818
  import {
812
819
  forEachOperation as forEachOperation3
813
820
  } from "@sdk-it/spec";
814
821
 
815
822
  // packages/typescript/src/lib/sdk.ts
816
823
  import { camelcase as camelcase3 } from "stringcase";
817
- import { isEmpty, pascalcase as pascalcase3 } from "@sdk-it/core";
824
+ import { isEmpty as isEmpty3, pascalcase as pascalcase3 } from "@sdk-it/core";
818
825
  import {
819
826
  isBinaryContentType,
820
827
  isSseContentType,
@@ -967,7 +974,7 @@ function normalOperation() {
967
974
  function toHttpOutput(spec, operationName, status, response, withGenerics = true) {
968
975
  const typeScriptDeserialzer = new TypeScriptEmitter(spec);
969
976
  const interfaceName = pascalcase3(sanitizeTag3(response["x-response-name"]));
970
- if (!isEmpty(response.content)) {
977
+ if (!isEmpty3(response.content)) {
971
978
  const contentTypeResult = fromContentType(typeScriptDeserialzer, response);
972
979
  if (!contentTypeResult) {
973
980
  throw new Error(
@@ -1090,7 +1097,7 @@ function coearceRequestInput(spec, operation, type) {
1090
1097
  if (type === "application/empty") {
1091
1098
  objectSchema = {
1092
1099
  type: "object",
1093
- additionalProperties: isEmpty2(xProperties)
1100
+ additionalProperties: isEmpty4(xProperties)
1094
1101
  };
1095
1102
  } else {
1096
1103
  if (objectSchema.type !== "object") {
@@ -1360,7 +1367,7 @@ var offset_pagination_default = "type OffsetPaginationParams = {\n offset: numb
1360
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";
1361
1368
 
1362
1369
  // packages/typescript/src/lib/readme/readme.ts
1363
- import { isEmpty as isEmpty3 } from "@sdk-it/core";
1370
+ import { isEmpty as isEmpty5 } from "@sdk-it/core";
1364
1371
  import { forEachOperation as forEachOperation4 } from "@sdk-it/spec";
1365
1372
 
1366
1373
  // packages/typescript/src/lib/readme/prop.emitter.ts
@@ -1688,7 +1695,7 @@ function toReadme(spec, generator) {
1688
1695
  markdown.push(`#### Output`);
1689
1696
  for (const status in operation.responses) {
1690
1697
  const response = operation.responses[status];
1691
- if (!isEmpty3(response.content)) {
1698
+ if (!isEmpty5(response.content)) {
1692
1699
  const contentEntries = Object.entries(response.content);
1693
1700
  if (contentEntries.length === 1) {
1694
1701
  const [contentType, mediaType] = contentEntries[0];
@@ -1772,7 +1779,7 @@ function expandServerUrls(servers) {
1772
1779
 
1773
1780
  // packages/typescript/src/lib/typescript-snippet.ts
1774
1781
  import { camelcase as camelcase5, spinalcase as spinalcase3 } from "stringcase";
1775
- 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";
1776
1783
  import "@sdk-it/readme";
1777
1784
  import {
1778
1785
  forEachOperation as forEachOperation5,
@@ -1983,7 +1990,7 @@ var TypeScriptSnippet = class {
1983
1990
  }
1984
1991
  succinct(entry, operation, values) {
1985
1992
  let payload = "{}";
1986
- if (!isEmpty4(operation.requestBody)) {
1993
+ if (!isEmpty6(operation.requestBody)) {
1987
1994
  const contentTypes = Object.keys(operation.requestBody.content || {});
1988
1995
  const schema = resolveRef4(
1989
1996
  this.#spec,
@@ -2045,7 +2052,7 @@ var TypeScriptSnippet = class {
2045
2052
  return this.#streamDownload(entry, payload);
2046
2053
  }
2047
2054
  }
2048
- if (!isEmpty4(operation["x-pagination"])) {
2055
+ if (!isEmpty6(operation["x-pagination"])) {
2049
2056
  return this.#pagination(operation, entry, payload);
2050
2057
  }
2051
2058
  return this.#normal(entry, payload);
@@ -2129,7 +2136,7 @@ const result = await ${camelcase5(this.#clientName)}.request('${entry.method.toU
2129
2136
  baseUrl: expandServerUrls(this.#spec.servers ?? [])[0] ?? "http://localhost:3000"
2130
2137
  };
2131
2138
  const authOptions = this.#authentication();
2132
- if (!isEmpty4(authOptions)) {
2139
+ if (!isEmpty6(authOptions)) {
2133
2140
  const [firstAuth] = authOptions;
2134
2141
  const optionName = firstAuth["x-optionName"] ?? firstAuth.name;
2135
2142
  options[optionName] = firstAuth.example;
@@ -2169,7 +2176,7 @@ ${client.use}`;
2169
2176
  );
2170
2177
  const baseUrl = expandServerUrls(this.#spec.servers ?? [])[0] || "https://api.example.com";
2171
2178
  const authOptions = this.#authentication();
2172
- const hasApiKey = !isEmpty4(authOptions);
2179
+ const hasApiKey = !isEmpty6(authOptions);
2173
2180
  sections.push("### Configuration Options");
2174
2181
  sections.push("");
2175
2182
  sections.push("| Option | Type | Required | Description |");
@@ -2554,7 +2561,7 @@ ${client.use}`;
2554
2561
  }
2555
2562
  authenticationDocs() {
2556
2563
  const authOptions = this.#authentication();
2557
- if (isEmpty4(authOptions)) {
2564
+ if (isEmpty6(authOptions)) {
2558
2565
  return "";
2559
2566
  }
2560
2567
  const sections = [];
@@ -2636,7 +2643,7 @@ ${client.use}`;
2636
2643
  const initialClientOptions = {
2637
2644
  baseUrl: "https://api.production-service.com"
2638
2645
  };
2639
- if (!isEmpty4(authOptions)) {
2646
+ if (!isEmpty6(authOptions)) {
2640
2647
  const [primaryAuth] = authOptions;
2641
2648
  const authOptionName = primaryAuth["x-optionName"] ?? primaryAuth.name;
2642
2649
  initialClientOptions[authOptionName] = "YOUR_PRODUCTION_TOKEN";
@@ -2650,7 +2657,7 @@ ${client.use}`;
2650
2657
  "client.setOptions({",
2651
2658
  " baseUrl: 'https://api.staging-service.com',"
2652
2659
  ];
2653
- if (!isEmpty4(authOptions)) {
2660
+ if (!isEmpty6(authOptions)) {
2654
2661
  const [primaryAuth] = authOptions;
2655
2662
  const authOptionName = primaryAuth["x-optionName"] ?? primaryAuth.name;
2656
2663
  configurationUpdateCode.push(` ${authOptionName}: 'YOUR_STAGING_TOKEN'`);