@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 +75 -57
- package/dist/index.js +25 -15
- package/dist/index.js.map +3 -3
- package/dist/lib/emitters/interface.d.ts.map +1 -1
- package/dist/lib/emitters/zod.d.ts.map +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,103 +1,121 @@
|
|
|
1
1
|
# @sdk-it/typescript
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
13
|
+
`zod` and `fast-content-type-parse` are runtime dependencies of clients
|
|
14
|
+
generated in `minimal` mode.
|
|
20
15
|
|
|
21
|
-
|
|
16
|
+
## Generate from an OpenAPI document
|
|
22
17
|
|
|
23
18
|
```typescript
|
|
24
19
|
import { generate } from '@sdk-it/typescript';
|
|
25
20
|
|
|
26
|
-
|
|
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: './
|
|
30
|
-
|
|
26
|
+
output: './src/generated/openstatus',
|
|
27
|
+
mode: 'minimal',
|
|
28
|
+
name: 'OpenStatus',
|
|
31
29
|
});
|
|
32
30
|
```
|
|
33
31
|
|
|
34
|
-
|
|
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 {
|
|
38
|
+
import { OpenStatus } from './src/generated/openstatus/index.ts';
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
79
|
+
## Format generated code
|
|
52
80
|
|
|
53
|
-
|
|
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
|
-
(
|
|
92
|
+
(response) => response.json(),
|
|
60
93
|
);
|
|
61
94
|
|
|
62
|
-
// Format generated code using Prettier
|
|
63
95
|
await generate(spec, {
|
|
64
|
-
output:
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
104
|
+
## Generate a standalone project
|
|
72
105
|
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
##
|
|
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
|
|
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 (!
|
|
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:
|
|
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
|
|
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 (!
|
|
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
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 = !
|
|
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 (
|
|
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 (!
|
|
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 (!
|
|
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'`);
|