api 5.0.0-beta.2 → 5.0.0
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 +7 -8
- package/dist/bin.js +1 -1
- package/dist/cache.d.ts +38 -3
- package/dist/cache.js +7 -26
- package/dist/cli/codegen/index.d.ts +1 -1
- package/dist/cli/codegen/language.d.ts +1 -1
- package/dist/cli/codegen/language.js +13 -0
- package/dist/cli/codegen/languages/typescript/util.d.ts +21 -0
- package/dist/cli/codegen/languages/typescript/util.js +185 -0
- package/dist/cli/codegen/languages/typescript.d.ts +36 -41
- package/dist/cli/codegen/languages/typescript.js +394 -414
- package/dist/cli/commands/install.js +6 -6
- package/dist/cli/storage.d.ts +1 -1
- package/dist/cli/storage.js +2 -2
- package/dist/core/errors/fetchError.d.ts +12 -0
- package/dist/core/errors/fetchError.js +36 -0
- package/dist/core/getJSONSchemaDefaults.d.ts +1 -1
- package/dist/core/index.d.ts +12 -4
- package/dist/core/index.js +36 -11
- package/dist/core/parseResponse.d.ts +6 -1
- package/dist/core/parseResponse.js +9 -3
- package/dist/core/prepareAuth.js +47 -18
- package/dist/core/prepareParams.d.ts +0 -3
- package/dist/core/prepareParams.js +102 -41
- package/dist/fetcher.d.ts +1 -1
- package/dist/fetcher.js +3 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +24 -40
- package/dist/packageInfo.d.ts +1 -1
- package/dist/packageInfo.js +1 -1
- package/package.json +31 -17
- package/src/bin.ts +2 -1
- package/src/cache.ts +9 -31
- package/src/cli/codegen/index.ts +1 -1
- package/src/cli/codegen/language.ts +18 -1
- package/src/cli/codegen/languages/typescript/util.ts +183 -0
- package/src/cli/codegen/languages/typescript.ts +348 -340
- package/src/cli/commands/install.ts +6 -8
- package/src/cli/storage.ts +4 -4
- package/src/core/errors/fetchError.ts +31 -0
- package/src/core/getJSONSchemaDefaults.ts +3 -2
- package/src/core/index.ts +53 -18
- package/src/core/parseResponse.ts +8 -2
- package/src/core/prepareAuth.ts +55 -31
- package/src/core/prepareParams.ts +112 -41
- package/src/fetcher.ts +5 -4
- package/src/index.ts +24 -32
- package/src/packageInfo.ts +1 -1
- package/src/typings.d.ts +0 -1
- package/tsconfig.json +1 -1
|
@@ -1,27 +1,26 @@
|
|
|
1
|
-
import type Oas from 'oas';
|
|
2
|
-
import type { Operation } from 'oas';
|
|
3
|
-
import type { HttpMethods, JSONSchema, SchemaObject } from 'oas/@types/rmoas.types';
|
|
4
|
-
import type {
|
|
5
|
-
ClassDeclaration,
|
|
6
|
-
JSDocStructure,
|
|
7
|
-
MethodDeclaration,
|
|
8
|
-
OptionalKind,
|
|
9
|
-
ParameterDeclarationStructure,
|
|
10
|
-
TypeParameterDeclarationStructure,
|
|
11
|
-
} from 'ts-morph';
|
|
12
|
-
import type { Options as JSONSchemaToTypescriptOptions } from 'json-schema-to-typescript';
|
|
13
1
|
import type Storage from '../../storage';
|
|
14
2
|
import type { InstallerOptions } from '../language';
|
|
3
|
+
import type Oas from 'oas';
|
|
4
|
+
import type { Operation } from 'oas';
|
|
5
|
+
import type { HttpMethods, SchemaObject } from 'oas/dist/rmoas.types';
|
|
6
|
+
import type { ClassDeclaration, JSDocStructure, OptionalKind, ParameterDeclarationStructure } from 'ts-morph';
|
|
15
7
|
|
|
16
8
|
import fs from 'fs';
|
|
17
9
|
import path from 'path';
|
|
18
|
-
|
|
19
|
-
import logger from '../../logger';
|
|
20
|
-
import objectHash from 'object-hash';
|
|
21
|
-
import { IndentationText, Project, QuoteKind, ScriptTarget } from 'ts-morph';
|
|
22
|
-
import { compile } from 'json-schema-to-typescript';
|
|
23
|
-
import { format as prettier } from 'json-schema-to-typescript/dist/src/formatter';
|
|
10
|
+
|
|
24
11
|
import execa from 'execa';
|
|
12
|
+
import setWith from 'lodash.setwith';
|
|
13
|
+
import { IndentationText, Project, QuoteKind, ScriptTarget, VariableDeclarationKind } from 'ts-morph';
|
|
14
|
+
|
|
15
|
+
import logger from '../../logger';
|
|
16
|
+
import CodeGeneratorLanguage from '../language';
|
|
17
|
+
|
|
18
|
+
import { docblockEscape, formatter, generateTypeName, wordWrap } from './typescript/util';
|
|
19
|
+
|
|
20
|
+
export type TSGeneratorOptions = {
|
|
21
|
+
outputJS?: boolean;
|
|
22
|
+
compilerTarget?: 'cjs' | 'esm';
|
|
23
|
+
};
|
|
25
24
|
|
|
26
25
|
type OperationTypeHousing = {
|
|
27
26
|
types: {
|
|
@@ -31,11 +30,6 @@ type OperationTypeHousing = {
|
|
|
31
30
|
operation: Operation;
|
|
32
31
|
};
|
|
33
32
|
|
|
34
|
-
// https://www.30secondsofcode.org/js/s/word-wrap
|
|
35
|
-
function wordWrap(str: string, max = 88) {
|
|
36
|
-
return str.replace(new RegExp(`(?![^\\n]{1,${max}}$)([^\\n]{1,${max}})\\s`, 'g'), '$1\n');
|
|
37
|
-
}
|
|
38
|
-
|
|
39
33
|
export default class TSGenerator extends CodeGeneratorLanguage {
|
|
40
34
|
project: Project;
|
|
41
35
|
|
|
@@ -47,28 +41,23 @@ export default class TSGenerator extends CodeGeneratorLanguage {
|
|
|
47
41
|
|
|
48
42
|
files: Record<string, string>;
|
|
49
43
|
|
|
50
|
-
methodGenerics: Map<string, MethodDeclaration>;
|
|
51
|
-
|
|
52
44
|
sdk: ClassDeclaration;
|
|
53
45
|
|
|
54
|
-
schemas:
|
|
46
|
+
schemas: Record<
|
|
55
47
|
string,
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
48
|
+
// Operation-level type
|
|
49
|
+
| {
|
|
50
|
+
body?: any;
|
|
51
|
+
metadata?: any;
|
|
52
|
+
response?: Record<string, any>;
|
|
53
|
+
}
|
|
54
|
+
// Wholesale collection of `$ref` pointer types
|
|
55
|
+
| Record<string, any>
|
|
61
56
|
>;
|
|
62
57
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
identifier: string,
|
|
67
|
-
opts: {
|
|
68
|
-
outputJS?: boolean;
|
|
69
|
-
compilerTarget?: 'cjs' | 'esm';
|
|
70
|
-
} = {}
|
|
71
|
-
) {
|
|
58
|
+
usesHTTPMethodRangeInterface = false;
|
|
59
|
+
|
|
60
|
+
constructor(spec: Oas, specPath: string, identifier: string, opts: TSGeneratorOptions = {}) {
|
|
72
61
|
const options: { outputJS: boolean; compilerTarget: 'cjs' | 'esm' } = {
|
|
73
62
|
outputJS: false,
|
|
74
63
|
compilerTarget: 'cjs',
|
|
@@ -83,10 +72,14 @@ export default class TSGenerator extends CodeGeneratorLanguage {
|
|
|
83
72
|
super(spec, specPath, identifier);
|
|
84
73
|
|
|
85
74
|
this.requiredPackages = {
|
|
86
|
-
|
|
75
|
+
api: {
|
|
87
76
|
reason: "Required for the `api/dist/core` library that the codegen'd SDK uses for making requests.",
|
|
88
77
|
url: 'https://npm.im/api',
|
|
89
78
|
},
|
|
79
|
+
'json-schema-to-ts': {
|
|
80
|
+
reason: 'Required for TypeScript type handling.',
|
|
81
|
+
url: 'https://npm.im/json-schema-to-ts',
|
|
82
|
+
},
|
|
90
83
|
oas: {
|
|
91
84
|
reason: 'Used within `api/dist/core` and is also loaded for TypeScript types.',
|
|
92
85
|
url: 'https://npm.im/oas',
|
|
@@ -99,10 +92,12 @@ export default class TSGenerator extends CodeGeneratorLanguage {
|
|
|
99
92
|
quoteKind: QuoteKind.Single,
|
|
100
93
|
},
|
|
101
94
|
compilerOptions: {
|
|
102
|
-
|
|
95
|
+
// If we're exporting a TypeScript SDK then we don't need to pollute the codegen directory
|
|
96
|
+
// with unnecessary declaration `.d.ts` files.
|
|
97
|
+
declaration: options.outputJS,
|
|
98
|
+
outDir: 'dist',
|
|
103
99
|
resolveJsonModule: true,
|
|
104
100
|
target: options.compilerTarget === 'cjs' ? ScriptTarget.ES5 : ScriptTarget.ES2020,
|
|
105
|
-
outDir: 'dist',
|
|
106
101
|
|
|
107
102
|
// If we're compiling to a CJS target then we need to include this compiler option
|
|
108
103
|
// otherwise TS will attempt to load our `openapi.json` import with a `.default` property
|
|
@@ -118,18 +113,7 @@ export default class TSGenerator extends CodeGeneratorLanguage {
|
|
|
118
113
|
this.outputJS = options.outputJS;
|
|
119
114
|
|
|
120
115
|
this.types = new Map();
|
|
121
|
-
this.
|
|
122
|
-
this.schemas = new Map();
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
static formatter(content: string) {
|
|
126
|
-
return prettier(content, {
|
|
127
|
-
format: true,
|
|
128
|
-
style: {
|
|
129
|
-
printWidth: 120,
|
|
130
|
-
singleQuote: true,
|
|
131
|
-
},
|
|
132
|
-
} as JSONSchemaToTypescriptOptions);
|
|
116
|
+
this.schemas = {};
|
|
133
117
|
}
|
|
134
118
|
|
|
135
119
|
async installer(storage: Storage, opts: InstallerOptions = {}): Promise<void> {
|
|
@@ -158,7 +142,7 @@ export default class TSGenerator extends CodeGeneratorLanguage {
|
|
|
158
142
|
// This will install the installed SDK as a dependency within the current working directory,
|
|
159
143
|
// adding `@api/<sdk identifier>` as a dependency there so you can load it with
|
|
160
144
|
// `require('@api/<sdk identifier>)`.
|
|
161
|
-
return execa('npm', [...npmInstall, storage.getIdentifierStorageDir()
|
|
145
|
+
return execa('npm', [...npmInstall].filter(Boolean), { cwd: storage.getIdentifierStorageDir() }).then(res => {
|
|
162
146
|
if (opts.dryRun) {
|
|
163
147
|
(opts.logger ? opts.logger : logger)(res.command);
|
|
164
148
|
(opts.logger ? opts.logger : logger)(res.stdout);
|
|
@@ -171,46 +155,129 @@ export default class TSGenerator extends CodeGeneratorLanguage {
|
|
|
171
155
|
*
|
|
172
156
|
*/
|
|
173
157
|
async generator() {
|
|
174
|
-
const
|
|
158
|
+
const sdkSource = this.createSourceFile();
|
|
159
|
+
|
|
160
|
+
if (Object.keys(this.schemas).length) {
|
|
161
|
+
this.createSchemasFile();
|
|
162
|
+
this.createTypesFile();
|
|
175
163
|
|
|
176
|
-
|
|
164
|
+
// Export all of our available types so they can be used in SDK implementations.
|
|
165
|
+
//
|
|
166
|
+
// We're exporting all of the types individually because TS has no way right now of allowing
|
|
167
|
+
// us to do `export type * from './types'` on a non-named entry.
|
|
168
|
+
//
|
|
169
|
+
// https://github.com/microsoft/TypeScript/issues/37238
|
|
170
|
+
const types = Array.from(this.types.keys());
|
|
171
|
+
types.sort();
|
|
177
172
|
|
|
178
|
-
|
|
173
|
+
sdkSource.addExportDeclarations([
|
|
174
|
+
{
|
|
175
|
+
isTypeOnly: true,
|
|
176
|
+
namedExports: types,
|
|
177
|
+
moduleSpecifier: './types',
|
|
178
|
+
},
|
|
179
|
+
]);
|
|
180
|
+
} else {
|
|
181
|
+
// If we don't have any schemas then we shouldn't import a `types` file that doesn't exist.
|
|
182
|
+
sdkSource
|
|
183
|
+
.getImportDeclarations()
|
|
184
|
+
.find(id => id.getText() === "import type * as types from './types';")
|
|
185
|
+
.remove();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// If this SDK doesn't use the `HTTPMethodRange` interface for handling `2XX` response status
|
|
189
|
+
// codes then we should remove it from being imported.
|
|
190
|
+
if (!this.usesHTTPMethodRangeInterface) {
|
|
191
|
+
sdkSource
|
|
192
|
+
.getImportDeclarations()
|
|
193
|
+
.find(id => id.getText().includes('HTTPMethodRange'))
|
|
194
|
+
.replaceWithText("import type { ConfigOptions, FetchResponse } from 'api/dist/core'");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (this.outputJS) {
|
|
198
|
+
return this.project
|
|
199
|
+
.emitToMemory()
|
|
200
|
+
.getFiles()
|
|
201
|
+
.map(sourceFile => {
|
|
202
|
+
const file = path.basename(sourceFile.filePath);
|
|
203
|
+
if (file === 'schemas.js' || file === 'types.js') {
|
|
204
|
+
// If we're generating a JS SDK then we don't need to generate these two files as the
|
|
205
|
+
// user will have `.d.ts` files for them instead.
|
|
206
|
+
return {};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
let code = formatter(sourceFile.text);
|
|
210
|
+
if (file === 'index.js' && this.compilerTarget === 'cjs') {
|
|
211
|
+
/**
|
|
212
|
+
* There's an annoying quirk with `ts-morph` where if we're exporting a default export
|
|
213
|
+
* to a CJS environment, it'll export it as `exports.default`. Because we don't want
|
|
214
|
+
* folks in these environments to have to load their SDKs with
|
|
215
|
+
* `require('@api/sdk').default` we're overriding that here to change it to being the
|
|
216
|
+
* module exports.
|
|
217
|
+
*
|
|
218
|
+
* `ts-morph` unfortunately doesn't give us any options for programatically doing this
|
|
219
|
+
* so we need to resort to modifying the emitted JS code.
|
|
220
|
+
*/
|
|
221
|
+
code = code
|
|
222
|
+
.replace(/Object\.defineProperty\(exports, '__esModule', { value: true }\);\n/, '')
|
|
223
|
+
.replace('exports.default = createSDK;', 'module.exports = createSDK;');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
[file]: code,
|
|
228
|
+
};
|
|
229
|
+
})
|
|
230
|
+
.reduce((prev, next) => Object.assign(prev, next));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return [
|
|
234
|
+
...this.project.getSourceFiles().map(sourceFile => ({
|
|
235
|
+
[sourceFile.getBaseName()]: formatter(sourceFile.getFullText()),
|
|
236
|
+
})),
|
|
237
|
+
|
|
238
|
+
// Because we're returning the raw source files for TS generation we also need to separately
|
|
239
|
+
// emit out our declaration files so we can put those into a separate file in the installed
|
|
240
|
+
// SDK directory.
|
|
241
|
+
...this.project
|
|
242
|
+
.emitToMemory({ emitOnlyDtsFiles: true })
|
|
243
|
+
.getFiles()
|
|
244
|
+
.map(sourceFile => ({
|
|
245
|
+
[path.basename(sourceFile.filePath)]: formatter(sourceFile.text),
|
|
246
|
+
})),
|
|
247
|
+
].reduce((prev, next) => Object.assign(prev, next));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Create our main SDK source file.
|
|
252
|
+
*
|
|
253
|
+
*/
|
|
254
|
+
createSourceFile() {
|
|
255
|
+
const { operations } = this.loadOperationsAndMethods();
|
|
256
|
+
|
|
257
|
+
const sourceFile = this.project.createSourceFile('index.ts', '');
|
|
258
|
+
|
|
259
|
+
sourceFile.addImportDeclarations([
|
|
260
|
+
// This import will be automatically removed later if the SDK ends up not having any types.
|
|
261
|
+
{ defaultImport: 'type * as types', moduleSpecifier: './types' },
|
|
262
|
+
{
|
|
263
|
+
// `HTTPMethodRange` will be conditionally removed later if it ends up not being used.
|
|
264
|
+
defaultImport: 'type { ConfigOptions, FetchResponse, HTTPMethodRange }',
|
|
265
|
+
moduleSpecifier: 'api/dist/core',
|
|
266
|
+
},
|
|
179
267
|
{ defaultImport: 'Oas', moduleSpecifier: 'oas' },
|
|
180
268
|
{ defaultImport: 'APICore', moduleSpecifier: 'api/dist/core' },
|
|
181
269
|
{ defaultImport: 'definition', moduleSpecifier: this.specPath },
|
|
182
270
|
]);
|
|
183
271
|
|
|
184
272
|
// @todo add TOS, License, info.* to a docblock at the top of the SDK.
|
|
185
|
-
this.sdk =
|
|
273
|
+
this.sdk = sourceFile.addClass({
|
|
186
274
|
name: 'SDK',
|
|
275
|
+
properties: [
|
|
276
|
+
{ name: 'spec', type: 'Oas' },
|
|
277
|
+
{ name: 'core', type: 'APICore' },
|
|
278
|
+
],
|
|
187
279
|
});
|
|
188
280
|
|
|
189
|
-
// There's an annoying quirk with `ts-morph` where if we set the SDK class to be the default
|
|
190
|
-
// export with `isDefaultExport` then when we compile it to an ES5 target for CJS environments
|
|
191
|
-
// it'll be exported as `export.default = SDK`, which when you try to load it you'll need to
|
|
192
|
-
// run `require('@api/sdk').default`.
|
|
193
|
-
//
|
|
194
|
-
// Instead here by plainly creating the SDK class in the source file and then setting this
|
|
195
|
-
// export assignment it'll export the SDK class as `module.exports = SDK` so people can cleanly
|
|
196
|
-
// load the SDK with `require('@api/sdk)`.
|
|
197
|
-
//
|
|
198
|
-
// A whole lot of debugging went into here to let people not have to worry about `.default`
|
|
199
|
-
// messes. I hope it's worth it!
|
|
200
|
-
if (this.compilerTarget === 'cjs') {
|
|
201
|
-
sdkSource.addExportAssignment({
|
|
202
|
-
expression: 'SDK',
|
|
203
|
-
});
|
|
204
|
-
} else {
|
|
205
|
-
this.sdk.setIsDefaultExport(true);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
this.sdk.addProperties([
|
|
209
|
-
{ name: 'spec', type: 'Oas' },
|
|
210
|
-
{ name: 'core', type: 'APICore' },
|
|
211
|
-
{ name: 'authKeys', type: '(number | string)[][]', initializer: '[]' },
|
|
212
|
-
]);
|
|
213
|
-
|
|
214
281
|
this.sdk.addConstructor({
|
|
215
282
|
statements: writer => {
|
|
216
283
|
writer.writeLine('this.spec = Oas.init(definition);');
|
|
@@ -220,21 +287,6 @@ export default class TSGenerator extends CodeGeneratorLanguage {
|
|
|
220
287
|
});
|
|
221
288
|
|
|
222
289
|
// Add our core API methods for controlling auth, servers, and various configurable abilities.
|
|
223
|
-
sdkSource.addInterface({
|
|
224
|
-
name: 'ConfigOptions',
|
|
225
|
-
properties: [
|
|
226
|
-
{
|
|
227
|
-
name: 'parseResponse',
|
|
228
|
-
type: 'boolean',
|
|
229
|
-
docs: [
|
|
230
|
-
wordWrap(
|
|
231
|
-
'By default we parse the response based on the `Content-Type` header of the request. You can disable this functionality by negating this option.'
|
|
232
|
-
),
|
|
233
|
-
],
|
|
234
|
-
},
|
|
235
|
-
],
|
|
236
|
-
});
|
|
237
|
-
|
|
238
290
|
this.sdk.addMethods([
|
|
239
291
|
{
|
|
240
292
|
name: 'config',
|
|
@@ -243,14 +295,14 @@ export default class TSGenerator extends CodeGeneratorLanguage {
|
|
|
243
295
|
docs: [
|
|
244
296
|
{
|
|
245
297
|
description: writer =>
|
|
246
|
-
writer.writeLine(
|
|
247
|
-
wordWrap('Optionally configure various options, such as response parsing, that the SDK allows.')
|
|
248
|
-
),
|
|
298
|
+
writer.writeLine(wordWrap('Optionally configure various options that the SDK allows.')),
|
|
249
299
|
tags: [
|
|
250
300
|
{ tagName: 'param', text: 'config Object of supported SDK options and toggles.' },
|
|
251
301
|
{
|
|
252
302
|
tagName: 'param',
|
|
253
|
-
text:
|
|
303
|
+
text: wordWrap(
|
|
304
|
+
'config.timeout Override the default `fetch` request timeout of 30 seconds. This number should be represented in milliseconds.'
|
|
305
|
+
),
|
|
254
306
|
},
|
|
255
307
|
],
|
|
256
308
|
},
|
|
@@ -323,112 +375,102 @@ sdk.server('https://eu.api.example.com/v14');`)
|
|
|
323
375
|
},
|
|
324
376
|
]);
|
|
325
377
|
|
|
326
|
-
// Add all common method accessors into the SDK.
|
|
327
|
-
Array.from(methods).forEach((method: string) => this.createGenericMethodAccessor(method));
|
|
328
|
-
|
|
329
378
|
// Add all available operation ID accessors into the SDK.
|
|
330
379
|
Object.entries(operations).forEach(([operationId, data]: [string, OperationTypeHousing]) => {
|
|
331
380
|
this.createOperationAccessor(data.operation, operationId, data.types.params, data.types.responses);
|
|
332
381
|
});
|
|
333
382
|
|
|
334
|
-
//
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
383
|
+
// Export our SDK into the source file.
|
|
384
|
+
sourceFile.addVariableStatement({
|
|
385
|
+
declarationKind: VariableDeclarationKind.Const,
|
|
386
|
+
declarations: [
|
|
387
|
+
{
|
|
388
|
+
name: 'createSDK',
|
|
389
|
+
initializer: writer => {
|
|
390
|
+
// `ts-morph` doesn't have any way to cleanly create an IFEE.
|
|
391
|
+
writer.writeLine('(() => { return new SDK(); })()');
|
|
392
|
+
return writer;
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
],
|
|
338
396
|
});
|
|
339
397
|
|
|
340
|
-
|
|
341
|
-
return this.project
|
|
342
|
-
.emitToMemory()
|
|
343
|
-
.getFiles()
|
|
344
|
-
.map(sourceFile => ({
|
|
345
|
-
[path.basename(sourceFile.filePath)]: TSGenerator.formatter(sourceFile.text),
|
|
346
|
-
}))
|
|
347
|
-
.reduce((prev, next) => Object.assign(prev, next));
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
return [
|
|
351
|
-
...this.project.getSourceFiles().map(sourceFile => ({
|
|
352
|
-
[sourceFile.getBaseName()]: TSGenerator.formatter(sourceFile.getFullText()),
|
|
353
|
-
})),
|
|
398
|
+
sourceFile.addExportAssignment({ isExportEquals: false, expression: 'createSDK' });
|
|
354
399
|
|
|
355
|
-
|
|
356
|
-
// emit out our declaration files so we can put those into a separate file in the installed
|
|
357
|
-
// SDK directory.
|
|
358
|
-
...this.project
|
|
359
|
-
.emitToMemory({ emitOnlyDtsFiles: true })
|
|
360
|
-
.getFiles()
|
|
361
|
-
.map(sourceFile => ({
|
|
362
|
-
[path.basename(sourceFile.filePath)]: TSGenerator.formatter(sourceFile.text),
|
|
363
|
-
})),
|
|
364
|
-
].reduce((prev, next) => Object.assign(prev, next));
|
|
400
|
+
return sourceFile;
|
|
365
401
|
}
|
|
366
402
|
|
|
367
403
|
/**
|
|
368
|
-
* Create
|
|
404
|
+
* Create our main schemas file. This is where all of the JSON Schema that our TypeScript typing
|
|
405
|
+
* infrastructure sources its data from. Without this there are no types.
|
|
369
406
|
*
|
|
370
|
-
* @param method
|
|
371
407
|
*/
|
|
372
|
-
|
|
373
|
-
const
|
|
374
|
-
const docblock: OptionalKind<JSDocStructure> = {
|
|
375
|
-
description: writer => {
|
|
376
|
-
writer.writeLine(`Access any ${method.toUpperCase()} endpoint on your API.`);
|
|
377
|
-
return writer;
|
|
378
|
-
},
|
|
379
|
-
tags: [{ tagName: 'param', text: 'path API path to make a request against.' }],
|
|
380
|
-
};
|
|
408
|
+
createSchemasFile() {
|
|
409
|
+
const sourceFile = this.project.createSourceFile('schemas.ts', '');
|
|
381
410
|
|
|
382
|
-
|
|
383
|
-
if (method !== 'get') {
|
|
384
|
-
parameters.push({ name: 'body', type: 'unknown', hasQuestionToken: true });
|
|
385
|
-
docblock.tags.push({ tagName: 'param', text: 'body Request body payload data.' });
|
|
386
|
-
}
|
|
411
|
+
const sortedSchemas = new Map(Array.from(Object.entries(this.schemas)).sort());
|
|
387
412
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
413
|
+
Array.from(sortedSchemas).forEach(([schemaName, schema]) => {
|
|
414
|
+
sourceFile.addVariableStatement({
|
|
415
|
+
declarationKind: VariableDeclarationKind.Const,
|
|
416
|
+
declarations: [
|
|
417
|
+
{
|
|
418
|
+
name: schemaName,
|
|
419
|
+
initializer: writer => {
|
|
420
|
+
/**
|
|
421
|
+
* This is the conversion prefix that we add to all `$ref` pointers we find in
|
|
422
|
+
* generated JSON Schema.
|
|
423
|
+
*
|
|
424
|
+
* Because the pointer name is a string we want to have it reference the schema
|
|
425
|
+
* constant we're adding into the codegen'd schema file. As there's no way, not even
|
|
426
|
+
* using `eval()` in this case, to convert a string to a constant we're prefixing
|
|
427
|
+
* them with this so we can later remove it and rewrite the value to a literal.
|
|
428
|
+
* eg. `'Pet'` becomes `Pet`.
|
|
429
|
+
*
|
|
430
|
+
* And because our TypeScript type name generator properly ignores `:`, this is safe
|
|
431
|
+
* to prepend to all generated type names.
|
|
432
|
+
*/
|
|
433
|
+
let str = JSON.stringify(schema);
|
|
434
|
+
str = str.replace(/"::convert::([a-zA-Z_$\\d]*)"/g, '$1');
|
|
435
|
+
|
|
436
|
+
writer.writeLine(`${str} as const`);
|
|
437
|
+
return writer;
|
|
438
|
+
},
|
|
439
|
+
},
|
|
440
|
+
],
|
|
441
|
+
});
|
|
392
442
|
});
|
|
393
443
|
|
|
394
|
-
|
|
395
|
-
method,
|
|
396
|
-
this.sdk.addMethod({
|
|
397
|
-
name: method,
|
|
398
|
-
returnType: 'Promise<T>',
|
|
399
|
-
parameters,
|
|
400
|
-
typeParameters: ['T = unknown'],
|
|
401
|
-
docs: [docblock],
|
|
402
|
-
statements: writer => {
|
|
403
|
-
/**
|
|
404
|
-
* @example return this.core.fetch(path, 'get', body, metadata);
|
|
405
|
-
* @example return this.core.fetch(path, 'get', metadata);
|
|
406
|
-
*/
|
|
407
|
-
const fetchStmt = writer.write('return this.core.fetch(path, ').quote(method).write(', ');
|
|
408
|
-
|
|
409
|
-
const fetchArgs = parameters.slice(1).map(p => p.name);
|
|
410
|
-
fetchArgs.forEach((arg, i) => {
|
|
411
|
-
fetchStmt.write(arg);
|
|
412
|
-
if (fetchArgs.length > 1 && i !== fetchArgs.length) {
|
|
413
|
-
fetchStmt.write(', ');
|
|
414
|
-
}
|
|
415
|
-
});
|
|
444
|
+
sourceFile.addStatements(`export { ${Array.from(sortedSchemas.keys()).join(', ')} }`);
|
|
416
445
|
|
|
417
|
-
|
|
446
|
+
return sourceFile;
|
|
447
|
+
}
|
|
418
448
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
449
|
+
/**
|
|
450
|
+
* Create our main types file. This sources its data from the JSON Schema `schemas.ts` file and
|
|
451
|
+
* will re-export types to be used in TypeScript implementations and IDE intellisense. This
|
|
452
|
+
* typing work is functional with the `json-schema-to-ts` library.
|
|
453
|
+
*
|
|
454
|
+
* @see {@link https://npm.im/json-schema-to-ts}
|
|
455
|
+
*/
|
|
456
|
+
createTypesFile() {
|
|
457
|
+
const sourceFile = this.project.createSourceFile('types.ts', '');
|
|
458
|
+
|
|
459
|
+
sourceFile.addImportDeclarations([
|
|
460
|
+
{ defaultImport: 'type { FromSchema }', moduleSpecifier: 'json-schema-to-ts' },
|
|
461
|
+
{ defaultImport: '* as schemas', moduleSpecifier: './schemas' },
|
|
462
|
+
]);
|
|
463
|
+
|
|
464
|
+
Array.from(new Map(Array.from(this.types.entries()).sort())).forEach(([typeName, typeExpression]) => {
|
|
465
|
+
sourceFile.addTypeAlias({ isExported: true, name: typeName, type: typeExpression });
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
return sourceFile;
|
|
423
469
|
}
|
|
424
470
|
|
|
425
471
|
/**
|
|
426
472
|
* Create operation accessors on the SDK.
|
|
427
473
|
*
|
|
428
|
-
* @param operation
|
|
429
|
-
* @param operationId
|
|
430
|
-
* @param paramTypes
|
|
431
|
-
* @param responseTypes
|
|
432
474
|
*/
|
|
433
475
|
createOperationAccessor(
|
|
434
476
|
operation: Operation,
|
|
@@ -436,7 +478,7 @@ sdk.server('https://eu.api.example.com/v14');`)
|
|
|
436
478
|
paramTypes?: OperationTypeHousing['types']['params'],
|
|
437
479
|
responseTypes?: OperationTypeHousing['types']['responses']
|
|
438
480
|
) {
|
|
439
|
-
const docblock: OptionalKind<JSDocStructure> = {
|
|
481
|
+
const docblock: OptionalKind<JSDocStructure> = {};
|
|
440
482
|
const summary = operation.getSummary();
|
|
441
483
|
const description = operation.getDescription();
|
|
442
484
|
if (summary || description) {
|
|
@@ -445,9 +487,9 @@ sdk.server('https://eu.api.example.com/v14');`)
|
|
|
445
487
|
// what we surface the main docblock description.
|
|
446
488
|
docblock.description = writer => {
|
|
447
489
|
if (description) {
|
|
448
|
-
writer.writeLine(description);
|
|
490
|
+
writer.writeLine(docblockEscape(wordWrap(description)));
|
|
449
491
|
} else if (summary) {
|
|
450
|
-
writer.writeLine(summary);
|
|
492
|
+
writer.writeLine(docblockEscape(wordWrap(summary)));
|
|
451
493
|
}
|
|
452
494
|
|
|
453
495
|
writer.newLineIfLastNot();
|
|
@@ -455,7 +497,7 @@ sdk.server('https://eu.api.example.com/v14');`)
|
|
|
455
497
|
};
|
|
456
498
|
|
|
457
499
|
if (summary && description) {
|
|
458
|
-
docblock.tags
|
|
500
|
+
docblock.tags = [{ tagName: 'summary', text: docblockEscape(wordWrap(summary)) }];
|
|
459
501
|
}
|
|
460
502
|
}
|
|
461
503
|
|
|
@@ -474,9 +516,7 @@ sdk.server('https://eu.api.example.com/v14');`)
|
|
|
474
516
|
|
|
475
517
|
parameters.body = {
|
|
476
518
|
name: 'body',
|
|
477
|
-
type: paramTypes.body
|
|
478
|
-
? this.schemas.get(paramTypes.body).tsType
|
|
479
|
-
: this.schemas.get(paramTypes.formData).tsType,
|
|
519
|
+
type: paramTypes.body ? paramTypes.body : paramTypes.formData,
|
|
480
520
|
hasQuestionToken: hasOptionalBody,
|
|
481
521
|
};
|
|
482
522
|
}
|
|
@@ -486,28 +526,39 @@ sdk.server('https://eu.api.example.com/v14');`)
|
|
|
486
526
|
|
|
487
527
|
parameters.metadata = {
|
|
488
528
|
name: 'metadata',
|
|
489
|
-
type:
|
|
529
|
+
type: paramTypes.metadata,
|
|
490
530
|
hasQuestionToken: hasOptionalMetadata,
|
|
491
531
|
};
|
|
492
532
|
}
|
|
493
533
|
}
|
|
494
534
|
|
|
495
|
-
let returnType = 'Promise<
|
|
496
|
-
let typeParameters: (string | OptionalKind<TypeParameterDeclarationStructure>)[] = null;
|
|
535
|
+
let returnType = 'Promise<FetchResponse<number, unknown>>';
|
|
497
536
|
if (responseTypes) {
|
|
498
|
-
returnType = `Promise<${Object.
|
|
499
|
-
.map(
|
|
537
|
+
returnType = `Promise<${Object.entries(responseTypes)
|
|
538
|
+
.map(([status, responseType]) => {
|
|
539
|
+
if (status.toLowerCase() === 'default') {
|
|
540
|
+
return `FetchResponse<number, ${responseType}>`;
|
|
541
|
+
} else if (status.length === 3 && status.toUpperCase().endsWith('XX')) {
|
|
542
|
+
const statusPrefix = status.slice(0, 1);
|
|
543
|
+
if (!Number.isInteger(Number(statusPrefix))) {
|
|
544
|
+
// If this matches the `_XX` format, but it isn't `{number}XX` then we can't handle
|
|
545
|
+
// it and should instead fall back to treating it as an unknown number.
|
|
546
|
+
return `FetchResponse<number, ${responseType}>`;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
this.usesHTTPMethodRangeInterface = true;
|
|
550
|
+
return `FetchResponse<HTTPMethodRange<${statusPrefix}00, ${statusPrefix}99>, ${responseType}>`;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
return `FetchResponse<${status}, ${responseType}>`;
|
|
554
|
+
})
|
|
500
555
|
.join(' | ')}>`;
|
|
501
|
-
} else {
|
|
502
|
-
// We should only add the `<T>` method typing if we don't have any response types present.
|
|
503
|
-
typeParameters = ['T = unknown'];
|
|
504
556
|
}
|
|
505
557
|
|
|
506
558
|
const operationIdAccessor = this.sdk.addMethod({
|
|
507
559
|
name: operationId,
|
|
508
|
-
typeParameters,
|
|
509
560
|
returnType,
|
|
510
|
-
docs: docblock ? [docblock] : null,
|
|
561
|
+
docs: Object.keys(docblock).length ? [docblock] : null,
|
|
511
562
|
statements: writer => {
|
|
512
563
|
/**
|
|
513
564
|
* @example return this.core.fetch('/pet/findByStatus', 'get', body, metadata);
|
|
@@ -548,21 +599,19 @@ sdk.server('https://eu.api.example.com/v14');`)
|
|
|
548
599
|
if (shouldAddAltTypedOverloads) {
|
|
549
600
|
// Create an overload that has both `body` and `metadata` parameters as required.
|
|
550
601
|
operationIdAccessor.addOverload({
|
|
551
|
-
typeParameters,
|
|
552
602
|
parameters: [
|
|
553
603
|
{ ...parameters.body, hasQuestionToken: false },
|
|
554
604
|
{ ...parameters.metadata, hasQuestionToken: false },
|
|
555
605
|
],
|
|
556
606
|
returnType,
|
|
557
|
-
docs: docblock ? [docblock] : null,
|
|
607
|
+
docs: Object.keys(docblock).length ? [docblock] : null,
|
|
558
608
|
});
|
|
559
609
|
|
|
560
610
|
// Create an overload that just has a single `metadata` parameter.
|
|
561
611
|
operationIdAccessor.addOverload({
|
|
562
|
-
typeParameters,
|
|
563
612
|
parameters: [{ ...parameters.metadata }],
|
|
564
613
|
returnType,
|
|
565
|
-
docs: docblock ? [docblock] : null,
|
|
614
|
+
docs: Object.keys(docblock).length ? [docblock] : null,
|
|
566
615
|
});
|
|
567
616
|
|
|
568
617
|
// Create an overload that has both `body` and `metadata` parameters as optional. Even though
|
|
@@ -573,88 +622,20 @@ sdk.server('https://eu.api.example.com/v14');`)
|
|
|
573
622
|
// see if what the user is supplying is `metadata` or `body` content when they supply one or
|
|
574
623
|
// both.
|
|
575
624
|
operationIdAccessor.addParameters([
|
|
576
|
-
{
|
|
625
|
+
{
|
|
626
|
+
...parameters.body,
|
|
627
|
+
// Overloads have to be the most distilled version of the method so that's why we need to
|
|
628
|
+
// type `body` as either `body` or `metadata`. If we didn't do this, if `body` was a JSON
|
|
629
|
+
// Schema type that didn't allow `additionalProperties` then the implementation overload
|
|
630
|
+
// would throw type errors.
|
|
631
|
+
type: `${parameters.body.type} | ${parameters.metadata.type}`,
|
|
632
|
+
hasQuestionToken: true,
|
|
633
|
+
},
|
|
577
634
|
{ ...parameters.metadata, hasQuestionToken: true },
|
|
578
635
|
]);
|
|
579
636
|
} else {
|
|
580
637
|
operationIdAccessor.addParameters(Object.values(parameters));
|
|
581
638
|
}
|
|
582
|
-
|
|
583
|
-
// Add a typed generic HTTP method overload for this operation.
|
|
584
|
-
if (this.methodGenerics.has(operation.method)) {
|
|
585
|
-
// If we created alternate overloads for the operation accessor then we need to do the same
|
|
586
|
-
// for its generic HTTP counterpart.
|
|
587
|
-
if (shouldAddAltTypedOverloads) {
|
|
588
|
-
// Create an overload that has both `body` and `metadata` parameters as required.
|
|
589
|
-
this.methodGenerics.get(operation.method).addOverload({
|
|
590
|
-
typeParameters,
|
|
591
|
-
parameters: [
|
|
592
|
-
{ name: 'path', type: `'${operation.path}'` },
|
|
593
|
-
{ ...parameters.body, hasQuestionToken: false },
|
|
594
|
-
{ ...parameters.metadata, hasQuestionToken: false },
|
|
595
|
-
],
|
|
596
|
-
returnType,
|
|
597
|
-
docs: docblock ? [docblock] : null,
|
|
598
|
-
});
|
|
599
|
-
|
|
600
|
-
// Create an overload that just has a single `metadata` parameter.
|
|
601
|
-
this.methodGenerics.get(operation.method).addOverload({
|
|
602
|
-
typeParameters,
|
|
603
|
-
parameters: [{ name: 'path', type: `'${operation.path}'` }, parameters.metadata],
|
|
604
|
-
returnType,
|
|
605
|
-
docs: docblock ? [docblock] : null,
|
|
606
|
-
});
|
|
607
|
-
} else {
|
|
608
|
-
this.methodGenerics.get(operation.method).addOverload({
|
|
609
|
-
typeParameters: responseTypes ? null : ['T = unknown'],
|
|
610
|
-
parameters: [{ name: 'path', type: `'${operation.path}'` }, ...Object.values(parameters)],
|
|
611
|
-
returnType,
|
|
612
|
-
docs: docblock ? [docblock] : null,
|
|
613
|
-
});
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
/**
|
|
619
|
-
* Convert a JSON Schema object into a readily available TypeScript type or interface along with
|
|
620
|
-
* any `$ref` pointers that are in use and turn those into TS types too.
|
|
621
|
-
*
|
|
622
|
-
* Under the hood this uses https://npm.im/json-schema-to-typescript for all composition and
|
|
623
|
-
* conversion.
|
|
624
|
-
*
|
|
625
|
-
* @param schema
|
|
626
|
-
* @param name
|
|
627
|
-
*/
|
|
628
|
-
async convertJSONSchemaToTypescript(schema: JSONSchema, name: string) {
|
|
629
|
-
// Though our JSON Schema type exposes JSONSchema4, which `json-schema-to-typescript` wants, it
|
|
630
|
-
// won't accept our custom union type of JSON Schema 4, JSON Schema 6, and JSON Schema 7.
|
|
631
|
-
const ts = await compile(schema as any, name, {
|
|
632
|
-
bannerComment: '',
|
|
633
|
-
|
|
634
|
-
// Running Prettier here for every JSON Schema object we're generating is way too slow so
|
|
635
|
-
// we're instead running it at the very end after we've constructed the SDK.
|
|
636
|
-
format: false,
|
|
637
|
-
});
|
|
638
|
-
|
|
639
|
-
let primaryType: string;
|
|
640
|
-
const tempProject = this.project.createSourceFile(`${name}.types.tmp.ts`, ts);
|
|
641
|
-
const declarations = tempProject.getExportedDeclarations();
|
|
642
|
-
|
|
643
|
-
Array.from(declarations.keys()).forEach(declarationName => {
|
|
644
|
-
if (!primaryType) {
|
|
645
|
-
primaryType = declarationName;
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
declarations.get(declarationName).forEach(declaration => {
|
|
649
|
-
this.types.set(declarationName, declaration.getText());
|
|
650
|
-
});
|
|
651
|
-
});
|
|
652
|
-
|
|
653
|
-
this.project.removeSourceFile(tempProject);
|
|
654
|
-
|
|
655
|
-
return {
|
|
656
|
-
primaryType,
|
|
657
|
-
};
|
|
658
639
|
}
|
|
659
640
|
|
|
660
641
|
/**
|
|
@@ -663,7 +644,7 @@ sdk.server('https://eu.api.example.com/v14');`)
|
|
|
663
644
|
* along with every HTTP method that's in use.
|
|
664
645
|
*
|
|
665
646
|
*/
|
|
666
|
-
|
|
647
|
+
loadOperationsAndMethods() {
|
|
667
648
|
const operations: Record</* operationId */ string, OperationTypeHousing> = {};
|
|
668
649
|
const methods = new Set();
|
|
669
650
|
|
|
@@ -679,32 +660,19 @@ sdk.server('https://eu.api.example.com/v14');`)
|
|
|
679
660
|
camelCase: true,
|
|
680
661
|
});
|
|
681
662
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
responses,
|
|
690
|
-
},
|
|
691
|
-
operation,
|
|
692
|
-
};
|
|
693
|
-
}
|
|
663
|
+
operations[operationId] = {
|
|
664
|
+
types: {
|
|
665
|
+
params: this.prepareParameterTypesForOperation(operation, operationId),
|
|
666
|
+
responses: this.prepareResponseTypesForOperation(operation, operationId),
|
|
667
|
+
},
|
|
668
|
+
operation,
|
|
669
|
+
};
|
|
694
670
|
});
|
|
695
671
|
});
|
|
696
672
|
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
const ts = await this.convertJSONSchemaToTypescript(schema as JSONSchema, schemaName);
|
|
701
|
-
|
|
702
|
-
this.schemas.set(hash, {
|
|
703
|
-
...this.schemas.get(hash),
|
|
704
|
-
tsType: ts.primaryType,
|
|
705
|
-
});
|
|
706
|
-
})
|
|
707
|
-
);
|
|
673
|
+
if (!Object.keys(operations).length) {
|
|
674
|
+
throw new Error('Sorry, this OpenAPI definition does not have any operation paths to generate an SDK for.');
|
|
675
|
+
}
|
|
708
676
|
|
|
709
677
|
return {
|
|
710
678
|
operations,
|
|
@@ -716,13 +684,24 @@ sdk.server('https://eu.api.example.com/v14');`)
|
|
|
716
684
|
* Compile the parameter (path, query, cookie, and header) schemas for an API operation into
|
|
717
685
|
* usable TypeScript types.
|
|
718
686
|
*
|
|
719
|
-
* @param operation
|
|
720
|
-
* @param operationId
|
|
721
687
|
*/
|
|
722
688
|
prepareParameterTypesForOperation(operation: Operation, operationId: string) {
|
|
723
|
-
const schemas = operation.
|
|
689
|
+
const schemas = operation.getParametersAsJSONSchema({
|
|
690
|
+
includeDiscriminatorMappingRefs: false,
|
|
724
691
|
mergeIntoBodyAndMetadata: true,
|
|
725
692
|
retainDeprecatedProperties: true,
|
|
693
|
+
transformer: (s: SchemaObject) => {
|
|
694
|
+
// As our schemas are dereferenced in the `oas` library we don't want to pollute our
|
|
695
|
+
// codegen'd schemas file with duplicate schemas.
|
|
696
|
+
if ('x-readme-ref-name' in s) {
|
|
697
|
+
const typeName = generateTypeName(s['x-readme-ref-name']);
|
|
698
|
+
this.addSchemaToExport(s, typeName, typeName);
|
|
699
|
+
|
|
700
|
+
return `::convert::${typeName}` as SchemaObject;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
return s;
|
|
704
|
+
},
|
|
726
705
|
});
|
|
727
706
|
|
|
728
707
|
if (!schemas || !schemas.length) {
|
|
@@ -734,22 +713,22 @@ sdk.server('https://eu.api.example.com/v14');`)
|
|
|
734
713
|
.reduce((prev, next) => Object.assign(prev, next));
|
|
735
714
|
|
|
736
715
|
return Object.entries(res)
|
|
737
|
-
.map(([paramType, schema]) => {
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
schema
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
name: schemaName,
|
|
748
|
-
});
|
|
716
|
+
.map(([paramType, schema]: [string, string | unknown]) => {
|
|
717
|
+
let typeName;
|
|
718
|
+
|
|
719
|
+
if (typeof schema === 'string' && schema.startsWith('::convert::')) {
|
|
720
|
+
// If this schema is a string and has our conversion prefix then we've already created
|
|
721
|
+
// a type for it.
|
|
722
|
+
typeName = schema.replace('::convert::', '');
|
|
723
|
+
} else {
|
|
724
|
+
typeName = generateTypeName(operationId, paramType, 'param');
|
|
725
|
+
this.addSchemaToExport(schema, typeName, `${generateTypeName(operationId)}.${paramType}`);
|
|
749
726
|
}
|
|
750
727
|
|
|
751
728
|
return {
|
|
752
|
-
|
|
729
|
+
// Types are prefixed with `types.` because that's how we're importing them from
|
|
730
|
+
// `types.d.ts`.
|
|
731
|
+
[paramType]: `types.${typeName}`,
|
|
753
732
|
};
|
|
754
733
|
})
|
|
755
734
|
.reduce((prev, next) => Object.assign(prev, next), {}) as Record<'body' | 'formData' | 'metadata', string>;
|
|
@@ -758,9 +737,6 @@ sdk.server('https://eu.api.example.com/v14');`)
|
|
|
758
737
|
/**
|
|
759
738
|
* Compile the response schemas for an API operation into usable TypeScript types.
|
|
760
739
|
*
|
|
761
|
-
* @todo what does this do for a spec that has no responses?
|
|
762
|
-
* @param operation
|
|
763
|
-
* @param operationId
|
|
764
740
|
*/
|
|
765
741
|
prepareResponseTypesForOperation(operation: Operation, operationId: string) {
|
|
766
742
|
const responseStatusCodes = operation.getResponseStatusCodes();
|
|
@@ -770,7 +746,22 @@ sdk.server('https://eu.api.example.com/v14');`)
|
|
|
770
746
|
|
|
771
747
|
const schemas = responseStatusCodes
|
|
772
748
|
.map(status => {
|
|
773
|
-
const schema = operation.
|
|
749
|
+
const schema = operation.getResponseAsJSONSchema(status, {
|
|
750
|
+
includeDiscriminatorMappingRefs: false,
|
|
751
|
+
transformer: (s: SchemaObject) => {
|
|
752
|
+
// As our schemas are dereferenced in the `oas` library we don't want to pollute our
|
|
753
|
+
// codegen'd schemas file with duplicate schemas.
|
|
754
|
+
if ('x-readme-ref-name' in s) {
|
|
755
|
+
const typeName = generateTypeName(s['x-readme-ref-name']);
|
|
756
|
+
this.addSchemaToExport(s, typeName, `${typeName}`);
|
|
757
|
+
|
|
758
|
+
return `::convert::${typeName}` as SchemaObject;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
return s;
|
|
762
|
+
},
|
|
763
|
+
});
|
|
764
|
+
|
|
774
765
|
if (!schema) {
|
|
775
766
|
return false;
|
|
776
767
|
}
|
|
@@ -783,25 +774,42 @@ sdk.server('https://eu.api.example.com/v14');`)
|
|
|
783
774
|
|
|
784
775
|
const res = Object.entries(schemas)
|
|
785
776
|
.map(([status, { schema }]) => {
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
schema
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
777
|
+
let typeName;
|
|
778
|
+
|
|
779
|
+
if (typeof schema === 'string' && schema.startsWith('::convert::')) {
|
|
780
|
+
// If this schema is a string and has our conversion prefix then we've already created
|
|
781
|
+
// a type for it.
|
|
782
|
+
typeName = schema.replace('::convert::', '');
|
|
783
|
+
} else {
|
|
784
|
+
typeName = generateTypeName(operationId, 'response', status);
|
|
785
|
+
|
|
786
|
+
// Because `status` will usually be a number here we need to set the pointer for it
|
|
787
|
+
// within an `[]` as if we do `FromSchema<typeof schemas.operation.response.200>`,
|
|
788
|
+
// TypeScript will throw a compilation error.
|
|
789
|
+
this.addSchemaToExport(schema, typeName, `${generateTypeName(operationId)}.response['${status}']`);
|
|
797
790
|
}
|
|
798
791
|
|
|
799
792
|
return {
|
|
800
|
-
|
|
793
|
+
// Types are prefixed with `types.` because that's how we're importing them from
|
|
794
|
+
// `types.d.ts`.
|
|
795
|
+
[status]: `types.${typeName}`,
|
|
801
796
|
};
|
|
802
797
|
})
|
|
803
798
|
.reduce((prev, next) => Object.assign(prev, next), {});
|
|
804
799
|
|
|
805
800
|
return Object.keys(res).length ? res : undefined;
|
|
806
801
|
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Add a given schema into our schema dataset that we'll be be exporting as types.
|
|
805
|
+
*
|
|
806
|
+
*/
|
|
807
|
+
addSchemaToExport(schema: any, typeName: string, pointer: string) {
|
|
808
|
+
if (this.types.has(typeName)) {
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
setWith(this.schemas, pointer, schema, Object);
|
|
813
|
+
this.types.set(typeName, `FromSchema<typeof schemas.${pointer}>`);
|
|
814
|
+
}
|
|
807
815
|
}
|