@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 +75 -57
- package/dist/index.js +22 -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
|
}
|
|
@@ -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
|
|
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 (!
|
|
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:
|
|
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
|
|
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 (!
|
|
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
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 = !
|
|
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 (
|
|
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 (!
|
|
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 (!
|
|
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'`);
|