api 5.0.0-beta.3 → 5.0.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 +7 -7
- package/dist/bin.js +1 -1
- package/dist/cache.d.ts +37 -2
- 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 +31 -38
- package/dist/cli/codegen/languages/typescript.js +390 -478
- package/dist/cli/commands/install.js +6 -6
- 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/index.d.ts +11 -3
- 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 +81 -57
- package/dist/fetcher.js +3 -3
- package/dist/index.js +24 -40
- package/dist/packageInfo.d.ts +1 -1
- package/dist/packageInfo.js +1 -1
- package/package.json +28 -17
- package/src/bin.ts +2 -1
- package/src/cache.ts +8 -30
- 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 +340 -402
- package/src/cli/commands/install.ts +6 -8
- package/src/cli/storage.ts +3 -3
- package/src/core/errors/fetchError.ts +31 -0
- package/src/core/getJSONSchemaDefaults.ts +2 -1
- package/src/core/index.ts +52 -17
- package/src/core/parseResponse.ts +8 -2
- package/src/core/prepareAuth.ts +55 -31
- package/src/core/prepareParams.ts +88 -55
- package/src/fetcher.ts +4 -3
- 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,28 +1,21 @@
|
|
|
1
|
-
import type Oas from 'oas';
|
|
2
|
-
import type { Operation } from 'oas';
|
|
3
|
-
import type { HttpMethods, JSONSchema, SchemaObject } from 'oas/dist/rmoas.types';
|
|
4
|
-
import type {
|
|
5
|
-
ClassDeclaration,
|
|
6
|
-
JSDocStructure,
|
|
7
|
-
MethodDeclaration,
|
|
8
|
-
OptionalKind,
|
|
9
|
-
ParameterDeclarationStructure,
|
|
10
|
-
TypeParameterDeclarationStructure,
|
|
11
|
-
VariableStatement,
|
|
12
|
-
} from 'ts-morph';
|
|
13
|
-
import type { Options as JSONSchemaToTypescriptOptions } from 'json-schema-to-typescript';
|
|
14
1
|
import type Storage from '../../storage';
|
|
15
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';
|
|
16
7
|
|
|
17
8
|
import fs from 'fs';
|
|
18
9
|
import path from 'path';
|
|
19
|
-
|
|
20
|
-
import logger from '../../logger';
|
|
21
|
-
import objectHash from 'object-hash';
|
|
22
|
-
import { IndentationText, Project, QuoteKind, ScriptTarget, VariableDeclarationKind } from 'ts-morph';
|
|
23
|
-
import { compile } from 'json-schema-to-typescript';
|
|
24
|
-
import { format as prettier } from 'json-schema-to-typescript/dist/src/formatter';
|
|
10
|
+
|
|
25
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';
|
|
26
19
|
|
|
27
20
|
export type TSGeneratorOptions = {
|
|
28
21
|
outputJS?: boolean;
|
|
@@ -37,11 +30,6 @@ type OperationTypeHousing = {
|
|
|
37
30
|
operation: Operation;
|
|
38
31
|
};
|
|
39
32
|
|
|
40
|
-
// https://www.30secondsofcode.org/js/s/word-wrap
|
|
41
|
-
function wordWrap(str: string, max = 88) {
|
|
42
|
-
return str.replace(new RegExp(`(?![^\\n]{1,${max}}$)([^\\n]{1,${max}})\\s`, 'g'), '$1\n');
|
|
43
|
-
}
|
|
44
|
-
|
|
45
33
|
export default class TSGenerator extends CodeGeneratorLanguage {
|
|
46
34
|
project: Project;
|
|
47
35
|
|
|
@@ -53,21 +41,22 @@ export default class TSGenerator extends CodeGeneratorLanguage {
|
|
|
53
41
|
|
|
54
42
|
files: Record<string, string>;
|
|
55
43
|
|
|
56
|
-
methodGenerics: Map<string, MethodDeclaration>;
|
|
57
|
-
|
|
58
44
|
sdk: ClassDeclaration;
|
|
59
45
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
schemas: Map<
|
|
46
|
+
schemas: Record<
|
|
63
47
|
string,
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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>
|
|
69
56
|
>;
|
|
70
57
|
|
|
58
|
+
usesHTTPMethodRangeInterface = false;
|
|
59
|
+
|
|
71
60
|
constructor(spec: Oas, specPath: string, identifier: string, opts: TSGeneratorOptions = {}) {
|
|
72
61
|
const options: { outputJS: boolean; compilerTarget: 'cjs' | 'esm' } = {
|
|
73
62
|
outputJS: false,
|
|
@@ -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,7 +92,9 @@ 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,
|
|
103
98
|
outDir: 'dist',
|
|
104
99
|
resolveJsonModule: true,
|
|
105
100
|
target: options.compilerTarget === 'cjs' ? ScriptTarget.ES5 : ScriptTarget.ES2020,
|
|
@@ -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
|
|
145
|
+
return execa('npm', [...npmInstall].filter(Boolean)).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,80 +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();
|
|
163
|
+
|
|
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();
|
|
172
|
+
|
|
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();
|
|
175
256
|
|
|
176
|
-
const
|
|
257
|
+
const sourceFile = this.project.createSourceFile('index.ts', '');
|
|
177
258
|
|
|
178
|
-
|
|
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',
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
declarationKind: VariableDeclarationKind.Const,
|
|
191
|
-
declarations: [
|
|
192
|
-
{
|
|
193
|
-
name: 'createSDK',
|
|
194
|
-
initializer: writer => {
|
|
195
|
-
// `ts-morph` doesn't have any way to cleanly create an IFEE.
|
|
196
|
-
writer.writeLine('(() => { return new SDK(); })()');
|
|
197
|
-
return writer;
|
|
198
|
-
},
|
|
199
|
-
},
|
|
275
|
+
properties: [
|
|
276
|
+
{ name: 'spec', type: 'Oas' },
|
|
277
|
+
{ name: 'core', type: 'APICore' },
|
|
200
278
|
],
|
|
201
279
|
});
|
|
202
280
|
|
|
203
|
-
/**
|
|
204
|
-
* There's an annoying quirk with `ts-morph` where if we set the `createSDK` function to be the
|
|
205
|
-
* default export with `isDefaultExport` then when we compile it to an ES5 target for CJS
|
|
206
|
-
* environments it'll be exported as `export.default = createSDK`, which when you try to load it
|
|
207
|
-
* you'll need to run `require('@api/sdk').default`.
|
|
208
|
-
*
|
|
209
|
-
* Instead here by plainly creating `createSDK` in the source file and then setting this export
|
|
210
|
-
* assignment it'll export the SDK IFEE initializer as `module.exports = createSDK` so people
|
|
211
|
-
* can cleanly load their SDK with `require('@api/sdk)`.
|
|
212
|
-
*
|
|
213
|
-
* A whole lot of debugging went into here to let people not have to worry about `.default`
|
|
214
|
-
* messes. I hope it's worth it!
|
|
215
|
-
*/
|
|
216
|
-
if (this.compilerTarget === 'cjs') {
|
|
217
|
-
sdkSource.addExportAssignment({
|
|
218
|
-
expression: 'createSDK',
|
|
219
|
-
});
|
|
220
|
-
} else {
|
|
221
|
-
/**
|
|
222
|
-
* Because `createSDK` above is an IFEE constant we can't use `setIsDefaultExport` on it due
|
|
223
|
-
* to `ts-morph` not having great handling for IFEE's.
|
|
224
|
-
*
|
|
225
|
-
* If we were to call `setIsDefaultExport` on our IFEE to attempt to compile it as
|
|
226
|
-
* `export default createSDK` then `ts-morph` hard crashes with a "Error replacing tree: The
|
|
227
|
-
* children of the old and new trees were expected to have the same count" exception due to
|
|
228
|
-
* it not being able properly handle IFEE's. It's for that reason that we need to manually
|
|
229
|
-
* write a statement expression to set `createSDK` as the default export.
|
|
230
|
-
*
|
|
231
|
-
* Another quirk that this work avoids is there being an empty `export {};` at the very end
|
|
232
|
-
* of our compiled `d.ts` declaration file. I'm not sure why it was being added, and it
|
|
233
|
-
* didn't appear to be harming anything, but us manually creating this export statement
|
|
234
|
-
* causes it to go away.
|
|
235
|
-
*
|
|
236
|
-
* Thankfully, fortunately, and curiously, these are all only problems in non-CJS compiled
|
|
237
|
-
* targets. ¯\_(ツ)_/¯
|
|
238
|
-
*/
|
|
239
|
-
sdkSource.addStatements('export default createSDK');
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
this.sdk.addProperties([
|
|
243
|
-
{ name: 'spec', type: 'Oas' },
|
|
244
|
-
{ name: 'core', type: 'APICore' },
|
|
245
|
-
{ name: 'authKeys', type: '(number | string)[][]', initializer: '[]' },
|
|
246
|
-
]);
|
|
247
|
-
|
|
248
281
|
this.sdk.addConstructor({
|
|
249
282
|
statements: writer => {
|
|
250
283
|
writer.writeLine('this.spec = Oas.init(definition);');
|
|
@@ -254,21 +287,6 @@ export default class TSGenerator extends CodeGeneratorLanguage {
|
|
|
254
287
|
});
|
|
255
288
|
|
|
256
289
|
// Add our core API methods for controlling auth, servers, and various configurable abilities.
|
|
257
|
-
sdkSource.addInterface({
|
|
258
|
-
name: 'ConfigOptions',
|
|
259
|
-
properties: [
|
|
260
|
-
{
|
|
261
|
-
name: 'parseResponse',
|
|
262
|
-
type: 'boolean',
|
|
263
|
-
docs: [
|
|
264
|
-
wordWrap(
|
|
265
|
-
'By default we parse the response based on the `Content-Type` header of the request. You can disable this functionality by negating this option.'
|
|
266
|
-
),
|
|
267
|
-
],
|
|
268
|
-
},
|
|
269
|
-
],
|
|
270
|
-
});
|
|
271
|
-
|
|
272
290
|
this.sdk.addMethods([
|
|
273
291
|
{
|
|
274
292
|
name: 'config',
|
|
@@ -277,14 +295,14 @@ export default class TSGenerator extends CodeGeneratorLanguage {
|
|
|
277
295
|
docs: [
|
|
278
296
|
{
|
|
279
297
|
description: writer =>
|
|
280
|
-
writer.writeLine(
|
|
281
|
-
wordWrap('Optionally configure various options, such as response parsing, that the SDK allows.')
|
|
282
|
-
),
|
|
298
|
+
writer.writeLine(wordWrap('Optionally configure various options that the SDK allows.')),
|
|
283
299
|
tags: [
|
|
284
300
|
{ tagName: 'param', text: 'config Object of supported SDK options and toggles.' },
|
|
285
301
|
{
|
|
286
302
|
tagName: 'param',
|
|
287
|
-
text:
|
|
303
|
+
text: wordWrap(
|
|
304
|
+
'config.timeout Override the default `fetch` request timeout of 30 seconds. This number should be represented in milliseconds.'
|
|
305
|
+
),
|
|
288
306
|
},
|
|
289
307
|
],
|
|
290
308
|
},
|
|
@@ -357,148 +375,102 @@ sdk.server('https://eu.api.example.com/v14');`)
|
|
|
357
375
|
},
|
|
358
376
|
]);
|
|
359
377
|
|
|
360
|
-
// Add all common method accessors into the SDK.
|
|
361
|
-
Array.from(methods).forEach((method: string) => this.createGenericMethodAccessor(method));
|
|
362
|
-
|
|
363
378
|
// Add all available operation ID accessors into the SDK.
|
|
364
379
|
Object.entries(operations).forEach(([operationId, data]: [string, OperationTypeHousing]) => {
|
|
365
380
|
this.createOperationAccessor(data.operation, operationId, data.types.params, data.types.responses);
|
|
366
381
|
});
|
|
367
382
|
|
|
368
|
-
//
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
* that library exports its data a raw string containing multiple types and interfaces.
|
|
382
|
-
* The only way we're able to capture and use them in our codegenerated SDK is because
|
|
383
|
-
* we're ingesting that string into `ts-morph` and then using its APIs to extract exported
|
|
384
|
-
* declarations (which are still strings) and then they're re-inserted into our main
|
|
385
|
-
* source file here.
|
|
386
|
-
* 2. Though `ts-morph` has APIs for adding type aliases and interfaces to a source file what
|
|
387
|
-
* it doesn't have is the ability to pass in a string, or a `Writer` class that exposes,
|
|
388
|
-
* to write raw strings to a type or an interface. If it did we'd be able to replace this
|
|
389
|
-
* `addStatements` call with an `addTypeAlias` and `addInterface` call for each of our
|
|
390
|
-
* JSON Schema schemas that we've got along with an `isExported` flag for `ts-morph` to
|
|
391
|
-
* export it.
|
|
392
|
-
*
|
|
393
|
-
* Because neither of these are solvable problems right now we're instead opting to **not**
|
|
394
|
-
* export types and interfaces from these SDKs. This isn't a great solution because it
|
|
395
|
-
* /slightly/ reduces the usability of the TS codegen functionality but in order for the TS
|
|
396
|
-
* declaration files that we generate to be valid this is the only option that we've got.
|
|
397
|
-
*
|
|
398
|
-
* However, that said, if somebody needs an interface or type exported they can export it
|
|
399
|
-
* themselves in the SDK code that we compile for them.
|
|
400
|
-
*
|
|
401
|
-
* @fixme
|
|
402
|
-
*/
|
|
403
|
-
sdkSource.addStatements(
|
|
404
|
-
// All expressions coming out of `json-schema-to-typescript` are exported so by popping this
|
|
405
|
-
// off we'll just be inserting plain interfaces and types into the SDK source.
|
|
406
|
-
exp.substring('export '.length)
|
|
407
|
-
);
|
|
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
|
+
],
|
|
408
396
|
});
|
|
409
397
|
|
|
410
|
-
|
|
411
|
-
return this.project
|
|
412
|
-
.emitToMemory()
|
|
413
|
-
.getFiles()
|
|
414
|
-
.map(sourceFile => ({
|
|
415
|
-
[path.basename(sourceFile.filePath)]: TSGenerator.formatter(sourceFile.text),
|
|
416
|
-
}))
|
|
417
|
-
.reduce((prev, next) => Object.assign(prev, next));
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
return [
|
|
421
|
-
...this.project.getSourceFiles().map(sourceFile => ({
|
|
422
|
-
[sourceFile.getBaseName()]: TSGenerator.formatter(sourceFile.getFullText()),
|
|
423
|
-
})),
|
|
398
|
+
sourceFile.addExportAssignment({ isExportEquals: false, expression: 'createSDK' });
|
|
424
399
|
|
|
425
|
-
|
|
426
|
-
// emit out our declaration files so we can put those into a separate file in the installed
|
|
427
|
-
// SDK directory.
|
|
428
|
-
...this.project
|
|
429
|
-
.emitToMemory({ emitOnlyDtsFiles: true })
|
|
430
|
-
.getFiles()
|
|
431
|
-
.map(sourceFile => ({
|
|
432
|
-
[path.basename(sourceFile.filePath)]: TSGenerator.formatter(sourceFile.text),
|
|
433
|
-
})),
|
|
434
|
-
].reduce((prev, next) => Object.assign(prev, next));
|
|
400
|
+
return sourceFile;
|
|
435
401
|
}
|
|
436
402
|
|
|
437
403
|
/**
|
|
438
|
-
* 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.
|
|
439
406
|
*
|
|
440
|
-
* @param method
|
|
441
407
|
*/
|
|
442
|
-
|
|
443
|
-
const
|
|
444
|
-
const docblock: OptionalKind<JSDocStructure> = {
|
|
445
|
-
description: writer => {
|
|
446
|
-
writer.writeLine(`Access any ${method.toUpperCase()} endpoint on your API.`);
|
|
447
|
-
return writer;
|
|
448
|
-
},
|
|
449
|
-
tags: [{ tagName: 'param', text: 'path API path to make a request against.' }],
|
|
450
|
-
};
|
|
408
|
+
createSchemasFile() {
|
|
409
|
+
const sourceFile = this.project.createSourceFile('schemas.ts', '');
|
|
451
410
|
|
|
452
|
-
|
|
453
|
-
if (method !== 'get') {
|
|
454
|
-
parameters.push({ name: 'body', type: 'unknown', hasQuestionToken: true });
|
|
455
|
-
docblock.tags.push({ tagName: 'param', text: 'body Request body payload data.' });
|
|
456
|
-
}
|
|
411
|
+
const sortedSchemas = new Map(Array.from(Object.entries(this.schemas)).sort());
|
|
457
412
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|
+
});
|
|
462
442
|
});
|
|
463
443
|
|
|
464
|
-
|
|
465
|
-
method,
|
|
466
|
-
this.sdk.addMethod({
|
|
467
|
-
name: method,
|
|
468
|
-
returnType: 'Promise<T>',
|
|
469
|
-
parameters,
|
|
470
|
-
typeParameters: ['T = unknown'],
|
|
471
|
-
docs: [docblock],
|
|
472
|
-
statements: writer => {
|
|
473
|
-
/**
|
|
474
|
-
* @example return this.core.fetch(path, 'get', body, metadata);
|
|
475
|
-
* @example return this.core.fetch(path, 'get', metadata);
|
|
476
|
-
*/
|
|
477
|
-
const fetchStmt = writer.write('return this.core.fetch(path, ').quote(method).write(', ');
|
|
478
|
-
|
|
479
|
-
const fetchArgs = parameters.slice(1).map(p => p.name);
|
|
480
|
-
fetchArgs.forEach((arg, i) => {
|
|
481
|
-
fetchStmt.write(arg);
|
|
482
|
-
if (fetchArgs.length > 1 && i !== fetchArgs.length) {
|
|
483
|
-
fetchStmt.write(', ');
|
|
484
|
-
}
|
|
485
|
-
});
|
|
444
|
+
sourceFile.addStatements(`export { ${Array.from(sortedSchemas.keys()).join(', ')} }`);
|
|
486
445
|
|
|
487
|
-
|
|
446
|
+
return sourceFile;
|
|
447
|
+
}
|
|
488
448
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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;
|
|
493
469
|
}
|
|
494
470
|
|
|
495
471
|
/**
|
|
496
472
|
* Create operation accessors on the SDK.
|
|
497
473
|
*
|
|
498
|
-
* @param operation
|
|
499
|
-
* @param operationId
|
|
500
|
-
* @param paramTypes
|
|
501
|
-
* @param responseTypes
|
|
502
474
|
*/
|
|
503
475
|
createOperationAccessor(
|
|
504
476
|
operation: Operation,
|
|
@@ -506,7 +478,7 @@ sdk.server('https://eu.api.example.com/v14');`)
|
|
|
506
478
|
paramTypes?: OperationTypeHousing['types']['params'],
|
|
507
479
|
responseTypes?: OperationTypeHousing['types']['responses']
|
|
508
480
|
) {
|
|
509
|
-
const docblock: OptionalKind<JSDocStructure> = {
|
|
481
|
+
const docblock: OptionalKind<JSDocStructure> = {};
|
|
510
482
|
const summary = operation.getSummary();
|
|
511
483
|
const description = operation.getDescription();
|
|
512
484
|
if (summary || description) {
|
|
@@ -515,9 +487,9 @@ sdk.server('https://eu.api.example.com/v14');`)
|
|
|
515
487
|
// what we surface the main docblock description.
|
|
516
488
|
docblock.description = writer => {
|
|
517
489
|
if (description) {
|
|
518
|
-
writer.writeLine(description);
|
|
490
|
+
writer.writeLine(docblockEscape(wordWrap(description)));
|
|
519
491
|
} else if (summary) {
|
|
520
|
-
writer.writeLine(summary);
|
|
492
|
+
writer.writeLine(docblockEscape(wordWrap(summary)));
|
|
521
493
|
}
|
|
522
494
|
|
|
523
495
|
writer.newLineIfLastNot();
|
|
@@ -525,7 +497,7 @@ sdk.server('https://eu.api.example.com/v14');`)
|
|
|
525
497
|
};
|
|
526
498
|
|
|
527
499
|
if (summary && description) {
|
|
528
|
-
docblock.tags
|
|
500
|
+
docblock.tags = [{ tagName: 'summary', text: docblockEscape(wordWrap(summary)) }];
|
|
529
501
|
}
|
|
530
502
|
}
|
|
531
503
|
|
|
@@ -544,9 +516,7 @@ sdk.server('https://eu.api.example.com/v14');`)
|
|
|
544
516
|
|
|
545
517
|
parameters.body = {
|
|
546
518
|
name: 'body',
|
|
547
|
-
type: paramTypes.body
|
|
548
|
-
? this.schemas.get(paramTypes.body).tsType
|
|
549
|
-
: this.schemas.get(paramTypes.formData).tsType,
|
|
519
|
+
type: paramTypes.body ? paramTypes.body : paramTypes.formData,
|
|
550
520
|
hasQuestionToken: hasOptionalBody,
|
|
551
521
|
};
|
|
552
522
|
}
|
|
@@ -556,28 +526,39 @@ sdk.server('https://eu.api.example.com/v14');`)
|
|
|
556
526
|
|
|
557
527
|
parameters.metadata = {
|
|
558
528
|
name: 'metadata',
|
|
559
|
-
type:
|
|
529
|
+
type: paramTypes.metadata,
|
|
560
530
|
hasQuestionToken: hasOptionalMetadata,
|
|
561
531
|
};
|
|
562
532
|
}
|
|
563
533
|
}
|
|
564
534
|
|
|
565
|
-
let returnType = 'Promise<
|
|
566
|
-
let typeParameters: (string | OptionalKind<TypeParameterDeclarationStructure>)[] = null;
|
|
535
|
+
let returnType = 'Promise<FetchResponse<number, unknown>>';
|
|
567
536
|
if (responseTypes) {
|
|
568
|
-
returnType = `Promise<${Object.
|
|
569
|
-
.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
|
+
})
|
|
570
555
|
.join(' | ')}>`;
|
|
571
|
-
} else {
|
|
572
|
-
// We should only add the `<T>` method typing if we don't have any response types present.
|
|
573
|
-
typeParameters = ['T = unknown'];
|
|
574
556
|
}
|
|
575
557
|
|
|
576
558
|
const operationIdAccessor = this.sdk.addMethod({
|
|
577
559
|
name: operationId,
|
|
578
|
-
typeParameters,
|
|
579
560
|
returnType,
|
|
580
|
-
docs: docblock ? [docblock] : null,
|
|
561
|
+
docs: Object.keys(docblock).length ? [docblock] : null,
|
|
581
562
|
statements: writer => {
|
|
582
563
|
/**
|
|
583
564
|
* @example return this.core.fetch('/pet/findByStatus', 'get', body, metadata);
|
|
@@ -618,21 +599,19 @@ sdk.server('https://eu.api.example.com/v14');`)
|
|
|
618
599
|
if (shouldAddAltTypedOverloads) {
|
|
619
600
|
// Create an overload that has both `body` and `metadata` parameters as required.
|
|
620
601
|
operationIdAccessor.addOverload({
|
|
621
|
-
typeParameters,
|
|
622
602
|
parameters: [
|
|
623
603
|
{ ...parameters.body, hasQuestionToken: false },
|
|
624
604
|
{ ...parameters.metadata, hasQuestionToken: false },
|
|
625
605
|
],
|
|
626
606
|
returnType,
|
|
627
|
-
docs: docblock ? [docblock] : null,
|
|
607
|
+
docs: Object.keys(docblock).length ? [docblock] : null,
|
|
628
608
|
});
|
|
629
609
|
|
|
630
610
|
// Create an overload that just has a single `metadata` parameter.
|
|
631
611
|
operationIdAccessor.addOverload({
|
|
632
|
-
typeParameters,
|
|
633
612
|
parameters: [{ ...parameters.metadata }],
|
|
634
613
|
returnType,
|
|
635
|
-
docs: docblock ? [docblock] : null,
|
|
614
|
+
docs: Object.keys(docblock).length ? [docblock] : null,
|
|
636
615
|
});
|
|
637
616
|
|
|
638
617
|
// Create an overload that has both `body` and `metadata` parameters as optional. Even though
|
|
@@ -643,88 +622,20 @@ sdk.server('https://eu.api.example.com/v14');`)
|
|
|
643
622
|
// see if what the user is supplying is `metadata` or `body` content when they supply one or
|
|
644
623
|
// both.
|
|
645
624
|
operationIdAccessor.addParameters([
|
|
646
|
-
{
|
|
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
|
+
},
|
|
647
634
|
{ ...parameters.metadata, hasQuestionToken: true },
|
|
648
635
|
]);
|
|
649
636
|
} else {
|
|
650
637
|
operationIdAccessor.addParameters(Object.values(parameters));
|
|
651
638
|
}
|
|
652
|
-
|
|
653
|
-
// Add a typed generic HTTP method overload for this operation.
|
|
654
|
-
if (this.methodGenerics.has(operation.method)) {
|
|
655
|
-
// If we created alternate overloads for the operation accessor then we need to do the same
|
|
656
|
-
// for its generic HTTP counterpart.
|
|
657
|
-
if (shouldAddAltTypedOverloads) {
|
|
658
|
-
// Create an overload that has both `body` and `metadata` parameters as required.
|
|
659
|
-
this.methodGenerics.get(operation.method).addOverload({
|
|
660
|
-
typeParameters,
|
|
661
|
-
parameters: [
|
|
662
|
-
{ name: 'path', type: `'${operation.path}'` },
|
|
663
|
-
{ ...parameters.body, hasQuestionToken: false },
|
|
664
|
-
{ ...parameters.metadata, hasQuestionToken: false },
|
|
665
|
-
],
|
|
666
|
-
returnType,
|
|
667
|
-
docs: docblock ? [docblock] : null,
|
|
668
|
-
});
|
|
669
|
-
|
|
670
|
-
// Create an overload that just has a single `metadata` parameter.
|
|
671
|
-
this.methodGenerics.get(operation.method).addOverload({
|
|
672
|
-
typeParameters,
|
|
673
|
-
parameters: [{ name: 'path', type: `'${operation.path}'` }, parameters.metadata],
|
|
674
|
-
returnType,
|
|
675
|
-
docs: docblock ? [docblock] : null,
|
|
676
|
-
});
|
|
677
|
-
} else {
|
|
678
|
-
this.methodGenerics.get(operation.method).addOverload({
|
|
679
|
-
typeParameters: responseTypes ? null : ['T = unknown'],
|
|
680
|
-
parameters: [{ name: 'path', type: `'${operation.path}'` }, ...Object.values(parameters)],
|
|
681
|
-
returnType,
|
|
682
|
-
docs: docblock ? [docblock] : null,
|
|
683
|
-
});
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
/**
|
|
689
|
-
* Convert a JSON Schema object into a readily available TypeScript type or interface along with
|
|
690
|
-
* any `$ref` pointers that are in use and turn those into TS types too.
|
|
691
|
-
*
|
|
692
|
-
* Under the hood this uses https://npm.im/json-schema-to-typescript for all composition and
|
|
693
|
-
* conversion.
|
|
694
|
-
*
|
|
695
|
-
* @param schema
|
|
696
|
-
* @param name
|
|
697
|
-
*/
|
|
698
|
-
async convertJSONSchemaToTypescript(schema: JSONSchema, name: string) {
|
|
699
|
-
// Though our JSON Schema type exposes JSONSchema4, which `json-schema-to-typescript` wants, it
|
|
700
|
-
// won't accept our custom union type of JSON Schema 4, JSON Schema 6, and JSON Schema 7.
|
|
701
|
-
const ts = await compile(schema as any, name, {
|
|
702
|
-
bannerComment: '',
|
|
703
|
-
|
|
704
|
-
// Running Prettier here for every JSON Schema object we're generating is way too slow so
|
|
705
|
-
// we're instead running it at the very end after we've constructed the SDK.
|
|
706
|
-
format: false,
|
|
707
|
-
});
|
|
708
|
-
|
|
709
|
-
let primaryType: string;
|
|
710
|
-
const tempProject = this.project.createSourceFile(`${name}.types.tmp.ts`, ts);
|
|
711
|
-
const declarations = tempProject.getExportedDeclarations();
|
|
712
|
-
|
|
713
|
-
Array.from(declarations.keys()).forEach(declarationName => {
|
|
714
|
-
if (!primaryType) {
|
|
715
|
-
primaryType = declarationName;
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
declarations.get(declarationName).forEach(declaration => {
|
|
719
|
-
this.types.set(declarationName, declaration.getText());
|
|
720
|
-
});
|
|
721
|
-
});
|
|
722
|
-
|
|
723
|
-
this.project.removeSourceFile(tempProject);
|
|
724
|
-
|
|
725
|
-
return {
|
|
726
|
-
primaryType,
|
|
727
|
-
};
|
|
728
639
|
}
|
|
729
640
|
|
|
730
641
|
/**
|
|
@@ -733,7 +644,7 @@ sdk.server('https://eu.api.example.com/v14');`)
|
|
|
733
644
|
* along with every HTTP method that's in use.
|
|
734
645
|
*
|
|
735
646
|
*/
|
|
736
|
-
|
|
647
|
+
loadOperationsAndMethods() {
|
|
737
648
|
const operations: Record</* operationId */ string, OperationTypeHousing> = {};
|
|
738
649
|
const methods = new Set();
|
|
739
650
|
|
|
@@ -749,32 +660,19 @@ sdk.server('https://eu.api.example.com/v14');`)
|
|
|
749
660
|
camelCase: true,
|
|
750
661
|
});
|
|
751
662
|
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
responses,
|
|
760
|
-
},
|
|
761
|
-
operation,
|
|
762
|
-
};
|
|
763
|
-
}
|
|
663
|
+
operations[operationId] = {
|
|
664
|
+
types: {
|
|
665
|
+
params: this.prepareParameterTypesForOperation(operation, operationId),
|
|
666
|
+
responses: this.prepareResponseTypesForOperation(operation, operationId),
|
|
667
|
+
},
|
|
668
|
+
operation,
|
|
669
|
+
};
|
|
764
670
|
});
|
|
765
671
|
});
|
|
766
672
|
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
const ts = await this.convertJSONSchemaToTypescript(schema as JSONSchema, schemaName);
|
|
771
|
-
|
|
772
|
-
this.schemas.set(hash, {
|
|
773
|
-
...this.schemas.get(hash),
|
|
774
|
-
tsType: ts.primaryType,
|
|
775
|
-
});
|
|
776
|
-
})
|
|
777
|
-
);
|
|
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
|
+
}
|
|
778
676
|
|
|
779
677
|
return {
|
|
780
678
|
operations,
|
|
@@ -786,13 +684,24 @@ sdk.server('https://eu.api.example.com/v14');`)
|
|
|
786
684
|
* Compile the parameter (path, query, cookie, and header) schemas for an API operation into
|
|
787
685
|
* usable TypeScript types.
|
|
788
686
|
*
|
|
789
|
-
* @param operation
|
|
790
|
-
* @param operationId
|
|
791
687
|
*/
|
|
792
688
|
prepareParameterTypesForOperation(operation: Operation, operationId: string) {
|
|
793
|
-
const schemas = operation.
|
|
689
|
+
const schemas = operation.getParametersAsJSONSchema({
|
|
690
|
+
includeDiscriminatorMappingRefs: false,
|
|
794
691
|
mergeIntoBodyAndMetadata: true,
|
|
795
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
|
+
},
|
|
796
705
|
});
|
|
797
706
|
|
|
798
707
|
if (!schemas || !schemas.length) {
|
|
@@ -804,22 +713,22 @@ sdk.server('https://eu.api.example.com/v14');`)
|
|
|
804
713
|
.reduce((prev, next) => Object.assign(prev, next));
|
|
805
714
|
|
|
806
715
|
return Object.entries(res)
|
|
807
|
-
.map(([paramType, schema]) => {
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
schema
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
name: schemaName,
|
|
818
|
-
});
|
|
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}`);
|
|
819
726
|
}
|
|
820
727
|
|
|
821
728
|
return {
|
|
822
|
-
|
|
729
|
+
// Types are prefixed with `types.` because that's how we're importing them from
|
|
730
|
+
// `types.d.ts`.
|
|
731
|
+
[paramType]: `types.${typeName}`,
|
|
823
732
|
};
|
|
824
733
|
})
|
|
825
734
|
.reduce((prev, next) => Object.assign(prev, next), {}) as Record<'body' | 'formData' | 'metadata', string>;
|
|
@@ -828,9 +737,6 @@ sdk.server('https://eu.api.example.com/v14');`)
|
|
|
828
737
|
/**
|
|
829
738
|
* Compile the response schemas for an API operation into usable TypeScript types.
|
|
830
739
|
*
|
|
831
|
-
* @todo what does this do for a spec that has no responses?
|
|
832
|
-
* @param operation
|
|
833
|
-
* @param operationId
|
|
834
740
|
*/
|
|
835
741
|
prepareResponseTypesForOperation(operation: Operation, operationId: string) {
|
|
836
742
|
const responseStatusCodes = operation.getResponseStatusCodes();
|
|
@@ -840,7 +746,22 @@ sdk.server('https://eu.api.example.com/v14');`)
|
|
|
840
746
|
|
|
841
747
|
const schemas = responseStatusCodes
|
|
842
748
|
.map(status => {
|
|
843
|
-
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
|
+
|
|
844
765
|
if (!schema) {
|
|
845
766
|
return false;
|
|
846
767
|
}
|
|
@@ -853,25 +774,42 @@ sdk.server('https://eu.api.example.com/v14');`)
|
|
|
853
774
|
|
|
854
775
|
const res = Object.entries(schemas)
|
|
855
776
|
.map(([status, { schema }]) => {
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
schema
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
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}']`);
|
|
867
790
|
}
|
|
868
791
|
|
|
869
792
|
return {
|
|
870
|
-
|
|
793
|
+
// Types are prefixed with `types.` because that's how we're importing them from
|
|
794
|
+
// `types.d.ts`.
|
|
795
|
+
[status]: `types.${typeName}`,
|
|
871
796
|
};
|
|
872
797
|
})
|
|
873
798
|
.reduce((prev, next) => Object.assign(prev, next), {});
|
|
874
799
|
|
|
875
800
|
return Object.keys(res).length ? res : undefined;
|
|
876
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
|
+
}
|
|
877
815
|
}
|