api 7.0.0-beta.0 → 7.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/codegen/codegenerator.d.ts +9 -11
- package/dist/codegen/codegenerator.d.ts.map +1 -1
- package/dist/codegen/codegenerator.js +11 -0
- package/dist/codegen/codegenerator.js.map +1 -1
- package/dist/codegen/factory.d.ts +19 -3
- package/dist/codegen/factory.d.ts.map +1 -1
- package/dist/codegen/factory.js +12 -5
- package/dist/codegen/factory.js.map +1 -1
- package/dist/codegen/languages/typescript/index.d.ts +8 -2
- package/dist/codegen/languages/typescript/index.d.ts.map +1 -1
- package/dist/codegen/languages/typescript/index.js +63 -17
- package/dist/codegen/languages/typescript/index.js.map +1 -1
- package/dist/commands/index.d.ts +2 -0
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +4 -0
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/install.d.ts.map +1 -1
- package/dist/commands/install.js +5 -5
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/list.d.ts +4 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +37 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/uninstall.d.ts +4 -0
- package/dist/commands/uninstall.d.ts.map +1 -0
- package/dist/commands/uninstall.js +72 -0
- package/dist/commands/uninstall.js.map +1 -0
- package/dist/lockfileSchema.d.ts +125 -0
- package/dist/lockfileSchema.d.ts.map +1 -0
- package/dist/lockfileSchema.js +78 -0
- package/dist/lockfileSchema.js.map +1 -0
- package/dist/packageInfo.d.ts +1 -1
- package/dist/packageInfo.js +1 -1
- package/dist/storage.d.ts +75 -60
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +85 -25
- package/dist/storage.js.map +1 -1
- package/package.json +14 -7
- package/schema.json +69 -0
- package/src/bin.ts +0 -21
- package/src/codegen/codegenerator.ts +0 -75
- package/src/codegen/factory.ts +0 -23
- package/src/codegen/languages/typescript/index.ts +0 -984
- package/src/codegen/languages/typescript/util.ts +0 -174
- package/src/commands/index.ts +0 -5
- package/src/commands/install.ts +0 -196
- package/src/fetcher.ts +0 -145
- package/src/lib/prompt.ts +0 -29
- package/src/logger.ts +0 -10
- package/src/packageInfo.ts +0 -3
- package/src/storage.ts +0 -333
- package/tsconfig.json +0 -10
|
@@ -1,984 +0,0 @@
|
|
|
1
|
-
import type Storage from '../../../storage.js';
|
|
2
|
-
import type { InstallerOptions } from '../../codegenerator.js';
|
|
3
|
-
import type Oas from 'oas';
|
|
4
|
-
import type Operation from 'oas/operation';
|
|
5
|
-
import type { HttpMethods, SchemaObject } from 'oas/rmoas.types';
|
|
6
|
-
import type { SemVer } from 'semver';
|
|
7
|
-
import type {
|
|
8
|
-
ClassDeclaration,
|
|
9
|
-
Directory,
|
|
10
|
-
JSDocStructure,
|
|
11
|
-
JSDocTagStructure,
|
|
12
|
-
OptionalKind,
|
|
13
|
-
ParameterDeclarationStructure,
|
|
14
|
-
} from 'ts-morph';
|
|
15
|
-
import type { Options } from 'tsup';
|
|
16
|
-
import type { JsonObject, PackageJson, TsConfigJson } from 'type-fest';
|
|
17
|
-
|
|
18
|
-
import path from 'node:path';
|
|
19
|
-
|
|
20
|
-
import corePkg from '@readme/api-core/package.json' assert { type: 'json' };
|
|
21
|
-
import { execa } from 'execa';
|
|
22
|
-
import setWith from 'lodash.setwith';
|
|
23
|
-
import semver from 'semver';
|
|
24
|
-
import { IndentationText, Project, QuoteKind, ScriptTarget, VariableDeclarationKind } from 'ts-morph';
|
|
25
|
-
|
|
26
|
-
import logger from '../../../logger.js';
|
|
27
|
-
import CodeGenerator from '../../codegenerator.js';
|
|
28
|
-
|
|
29
|
-
import { docblockEscape, generateTypeName, wordWrap } from './util.js';
|
|
30
|
-
|
|
31
|
-
interface OperationTypeHousing {
|
|
32
|
-
operation: Operation;
|
|
33
|
-
types: {
|
|
34
|
-
params?: false | Record<'body' | 'formData' | 'metadata', string>;
|
|
35
|
-
responses?: Record<
|
|
36
|
-
string | number,
|
|
37
|
-
{
|
|
38
|
-
description?: string;
|
|
39
|
-
type: string;
|
|
40
|
-
}
|
|
41
|
-
>;
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* This is the conversion prefix that we add to all `$ref` pointers we find in generated JSON
|
|
47
|
-
* Schema.
|
|
48
|
-
*
|
|
49
|
-
* Because the pointer name is a string we want to have it reference the schema constant we're
|
|
50
|
-
* adding into the codegen'd schema file. As there's no way, not even using `eval()` in this case,
|
|
51
|
-
* to convert a string to a constant we're prefixing them with this so we can later remove it and
|
|
52
|
-
* rewrite the value to a literal. eg. `'Pet'` becomes `Pet`.
|
|
53
|
-
*
|
|
54
|
-
* And because our TypeScript type name generator properly ignores `:`, this is safe to prepend to
|
|
55
|
-
* all generated type names.
|
|
56
|
-
*/
|
|
57
|
-
const REF_PLACEHOLDER = '::convert::';
|
|
58
|
-
const REF_PLACEHOLDER_REGEX = /"::convert::([a-zA-Z_$\\d]*)"/g;
|
|
59
|
-
|
|
60
|
-
export default class TSGenerator extends CodeGenerator {
|
|
61
|
-
project: Project;
|
|
62
|
-
|
|
63
|
-
types: Map<string, string>;
|
|
64
|
-
|
|
65
|
-
sdk!: ClassDeclaration;
|
|
66
|
-
|
|
67
|
-
schemas: Record<
|
|
68
|
-
string,
|
|
69
|
-
// Operation-level type
|
|
70
|
-
| {
|
|
71
|
-
body?: unknown;
|
|
72
|
-
metadata?: unknown;
|
|
73
|
-
response?: Record<string, unknown>;
|
|
74
|
-
}
|
|
75
|
-
// Wholesale collection of `$ref` pointer types
|
|
76
|
-
| Record<string, unknown>
|
|
77
|
-
>;
|
|
78
|
-
|
|
79
|
-
usesHTTPMethodRangeInterface = false;
|
|
80
|
-
|
|
81
|
-
constructor(spec: Oas, specPath: string, identifier: string) {
|
|
82
|
-
super(spec, specPath, identifier);
|
|
83
|
-
|
|
84
|
-
this.requiredPackages = {
|
|
85
|
-
'@readme/api-core': {
|
|
86
|
-
dependencyType: 'production',
|
|
87
|
-
reason: "The core magic of your codegen'd SDK and is what is used for making requests.",
|
|
88
|
-
url: 'https://npm.im/api',
|
|
89
|
-
version:
|
|
90
|
-
// When running unit tests we're installing `@readme/api-core` but because that package
|
|
91
|
-
// source lives in this repository NPM will throw a gnarly "Cannot set properties of null
|
|
92
|
-
// (setting 'dev')" workspace error message because we're creating a funky circular
|
|
93
|
-
// dependency.
|
|
94
|
-
process.env.NODE_ENV === 'test'
|
|
95
|
-
? // eslint-disable-next-line unicorn/prefer-module
|
|
96
|
-
`file:${path.relative(__dirname, path.dirname(require.resolve('@readme/api-core/package.json')))}`
|
|
97
|
-
: corePkg.version,
|
|
98
|
-
},
|
|
99
|
-
tsup: {
|
|
100
|
-
dependencyType: 'development',
|
|
101
|
-
reason: "Used for compiling your codegen'd SDK into code that can be used in JS environments.",
|
|
102
|
-
url: 'https://tsup.egoist.dev/',
|
|
103
|
-
version: '^7.2.0',
|
|
104
|
-
},
|
|
105
|
-
typescript: {
|
|
106
|
-
dependencyType: 'development',
|
|
107
|
-
reason: 'Required for `tsup`.',
|
|
108
|
-
version: '^5.2.2',
|
|
109
|
-
},
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
this.project = new Project({
|
|
113
|
-
compilerOptions: {
|
|
114
|
-
outDir: 'dist',
|
|
115
|
-
resolveJsonModule: true,
|
|
116
|
-
target: ScriptTarget.ES2022,
|
|
117
|
-
},
|
|
118
|
-
manipulationSettings: {
|
|
119
|
-
indentationText: IndentationText.TwoSpaces,
|
|
120
|
-
quoteKind: QuoteKind.Single,
|
|
121
|
-
},
|
|
122
|
-
useInMemoryFileSystem: true,
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
this.types = new Map();
|
|
126
|
-
this.schemas = {};
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// eslint-disable-next-line class-methods-use-this
|
|
130
|
-
async install(storage: Storage, opts: InstallerOptions = {}): Promise<void> {
|
|
131
|
-
const installDir = storage.getIdentifierStorageDir();
|
|
132
|
-
|
|
133
|
-
const npmInstall = ['install', '--save', opts.dryRun ? '--dry-run' : ''].filter(Boolean);
|
|
134
|
-
|
|
135
|
-
// This will install the installed SDK as a dependency within the current working directory,
|
|
136
|
-
// adding `@api/<sdk identifier>` as a dependency there so you can load it with
|
|
137
|
-
// `require('@api/<sdk identifier>)`.
|
|
138
|
-
await execa('npm', [...npmInstall, installDir].filter(Boolean))
|
|
139
|
-
.then(res => {
|
|
140
|
-
if (opts.dryRun) {
|
|
141
|
-
(opts.logger ? opts.logger : logger)(res.command);
|
|
142
|
-
(opts.logger ? opts.logger : logger)(res.stdout);
|
|
143
|
-
}
|
|
144
|
-
})
|
|
145
|
-
.catch(err => {
|
|
146
|
-
// If `npm install` throws this error it always happens **after** our dependencies have been
|
|
147
|
-
// installed and is an annoying quirk that sometimes occurs when installing a package within
|
|
148
|
-
// our workspace as we're creating a circular dependency on `@readme/api-core`.
|
|
149
|
-
if (
|
|
150
|
-
process.env.NODE_ENV === 'test' &&
|
|
151
|
-
err.message.includes("npm ERR! Cannot set properties of null (setting 'dev')")
|
|
152
|
-
) {
|
|
153
|
-
(opts.logger ? opts.logger : logger)("npm threw an error but we're ignoring it");
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (opts.dryRun) {
|
|
158
|
-
(opts.logger ? opts.logger : logger)(err.message);
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
throw err;
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Compile the TS code we generated into JS for use in CJS and ESM environments.
|
|
168
|
-
*
|
|
169
|
-
*/
|
|
170
|
-
// eslint-disable-next-line class-methods-use-this
|
|
171
|
-
async compile(storage: Storage, opts: InstallerOptions = {}): Promise<void> {
|
|
172
|
-
const installDir = storage.getIdentifierStorageDir();
|
|
173
|
-
|
|
174
|
-
await execa('npx', ['tsup'], {
|
|
175
|
-
cwd: installDir,
|
|
176
|
-
})
|
|
177
|
-
.then(res => {
|
|
178
|
-
if (opts.dryRun) {
|
|
179
|
-
(opts.logger ? opts.logger : logger)(res.command);
|
|
180
|
-
(opts.logger ? opts.logger : logger)(res.stdout);
|
|
181
|
-
}
|
|
182
|
-
})
|
|
183
|
-
.catch(err => {
|
|
184
|
-
if (opts.dryRun) {
|
|
185
|
-
(opts.logger ? opts.logger : logger)(err.message);
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
throw err;
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Generate the current OpenAPI definition into a TypeScript library.
|
|
195
|
-
*
|
|
196
|
-
*/
|
|
197
|
-
async generate() {
|
|
198
|
-
const srcDirectory = this.project.createDirectory('src');
|
|
199
|
-
const sdkSource = this.createSDKSource(srcDirectory);
|
|
200
|
-
|
|
201
|
-
this.createPackageJSON();
|
|
202
|
-
this.createTSConfig();
|
|
203
|
-
|
|
204
|
-
if (Object.keys(this.schemas).length) {
|
|
205
|
-
this.createSchemasFile(srcDirectory);
|
|
206
|
-
this.createTypesFile(srcDirectory);
|
|
207
|
-
} else {
|
|
208
|
-
// If we don't have any schemas then we shouldn't import a `types` file that doesn't exist.
|
|
209
|
-
sdkSource
|
|
210
|
-
.getImportDeclarations()
|
|
211
|
-
.find(id => id.getText() === "import type * as types from './types';")
|
|
212
|
-
?.remove();
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// If this SDK doesn't use the `HTTPMethodRange` interface for handling `2XX` response status
|
|
216
|
-
// codes then we should remove it from being imported.
|
|
217
|
-
if (!this.usesHTTPMethodRangeInterface) {
|
|
218
|
-
sdkSource
|
|
219
|
-
.getImportDeclarations()
|
|
220
|
-
.find(id => id.getText().includes('HTTPMethodRange'))
|
|
221
|
-
?.replaceWithText("import type { ConfigOptions, FetchResponse } from '@readme/api-core';");
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
return [
|
|
225
|
-
...this.project.getSourceFiles().map(sourceFile => {
|
|
226
|
-
// `getFilePath` will always return a string that contains a preceeding directory separator
|
|
227
|
-
// however when we're creating these codegen'd files that may cause us to create that file
|
|
228
|
-
// in the root directory (because it's preceeded by a `/`). We don't want that to happen so
|
|
229
|
-
// we're slicing off that first character.
|
|
230
|
-
let filePath = sourceFile.getFilePath().toString();
|
|
231
|
-
filePath = filePath.substring(1);
|
|
232
|
-
|
|
233
|
-
return {
|
|
234
|
-
[filePath]: sourceFile.getFullText(),
|
|
235
|
-
};
|
|
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)]: sourceFile.text,
|
|
246
|
-
})),
|
|
247
|
-
].reduce((prev, next) => Object.assign(prev, next));
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* Create our main SDK source file.
|
|
252
|
-
*
|
|
253
|
-
*/
|
|
254
|
-
private createSDKSource(sourceDirectory: Directory) {
|
|
255
|
-
const { operations } = this.loadOperationsAndMethods();
|
|
256
|
-
|
|
257
|
-
const sourceFile = sourceDirectory.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: '@readme/api-core',
|
|
266
|
-
},
|
|
267
|
-
{ defaultImport: 'APICore', moduleSpecifier: '@readme/api-core' },
|
|
268
|
-
{ defaultImport: 'definition', moduleSpecifier: this.specPath },
|
|
269
|
-
]);
|
|
270
|
-
|
|
271
|
-
// @todo add TOS, License, info.* to a docblock at the top of the SDK.
|
|
272
|
-
this.sdk = sourceFile.addClass({
|
|
273
|
-
name: 'SDK',
|
|
274
|
-
properties: [{ name: 'core', type: 'APICore' }],
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
this.sdk.addConstructor({
|
|
278
|
-
statements: writer => {
|
|
279
|
-
writer.write('this.core = new APICore(definition, ').quote(this.userAgent).write(');');
|
|
280
|
-
return writer;
|
|
281
|
-
},
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
// Add our core API methods for controlling auth, servers, and various configurable abilities.
|
|
285
|
-
this.sdk.addMethods([
|
|
286
|
-
{
|
|
287
|
-
name: 'config',
|
|
288
|
-
parameters: [{ name: 'config', type: 'ConfigOptions' }],
|
|
289
|
-
statements: writer => writer.writeLine('this.core.setConfig(config);'),
|
|
290
|
-
docs: [
|
|
291
|
-
{
|
|
292
|
-
description: writer =>
|
|
293
|
-
writer.writeLine(wordWrap('Optionally configure various options that the SDK allows.')),
|
|
294
|
-
tags: [
|
|
295
|
-
{ tagName: 'param', text: 'config Object of supported SDK options and toggles.' },
|
|
296
|
-
{
|
|
297
|
-
tagName: 'param',
|
|
298
|
-
text: wordWrap(
|
|
299
|
-
'config.timeout Override the default `fetch` request timeout of 30 seconds. This number should be represented in milliseconds.',
|
|
300
|
-
),
|
|
301
|
-
},
|
|
302
|
-
],
|
|
303
|
-
},
|
|
304
|
-
],
|
|
305
|
-
},
|
|
306
|
-
{
|
|
307
|
-
name: 'auth',
|
|
308
|
-
parameters: [{ name: '...values', type: 'string[] | number[]' }],
|
|
309
|
-
statements: writer => {
|
|
310
|
-
writer.writeLine('this.core.setAuth(...values);');
|
|
311
|
-
writer.writeLine('return this;');
|
|
312
|
-
return writer;
|
|
313
|
-
},
|
|
314
|
-
docs: [
|
|
315
|
-
{
|
|
316
|
-
description: writer =>
|
|
317
|
-
writer.writeLine(
|
|
318
|
-
wordWrap(`If the API you're using requires authentication you can supply the required credentials through this method and the library will magically determine how they should be used within your API request.
|
|
319
|
-
|
|
320
|
-
With the exception of OpenID and MutualTLS, it supports all forms of authentication supported by the OpenAPI specification.
|
|
321
|
-
|
|
322
|
-
@example <caption>HTTP Basic auth</caption>
|
|
323
|
-
sdk.auth('username', 'password');
|
|
324
|
-
|
|
325
|
-
@example <caption>Bearer tokens (HTTP or OAuth 2)</caption>
|
|
326
|
-
sdk.auth('myBearerToken');
|
|
327
|
-
|
|
328
|
-
@example <caption>API Keys</caption>
|
|
329
|
-
sdk.auth('myApiKey');`),
|
|
330
|
-
),
|
|
331
|
-
tags: [
|
|
332
|
-
{ tagName: 'see', text: '{@link https://spec.openapis.org/oas/v3.0.3#fixed-fields-22}' },
|
|
333
|
-
{ tagName: 'see', text: '{@link https://spec.openapis.org/oas/v3.1.0#fixed-fields-22}' },
|
|
334
|
-
{
|
|
335
|
-
tagName: 'param',
|
|
336
|
-
text: 'values Your auth credentials for the API; can specify up to two strings or numbers.',
|
|
337
|
-
},
|
|
338
|
-
],
|
|
339
|
-
},
|
|
340
|
-
],
|
|
341
|
-
},
|
|
342
|
-
{
|
|
343
|
-
name: 'server',
|
|
344
|
-
parameters: [
|
|
345
|
-
{ name: 'url', type: 'string' },
|
|
346
|
-
{ name: 'variables', initializer: '{}' },
|
|
347
|
-
],
|
|
348
|
-
statements: writer => writer.writeLine('this.core.setServer(url, variables);'),
|
|
349
|
-
docs: [
|
|
350
|
-
{
|
|
351
|
-
description: writer =>
|
|
352
|
-
writer.writeLine(
|
|
353
|
-
wordWrap(`If the API you're using offers alternate server URLs, and server variables, you can tell the SDK which one to use with this method. To use it you can supply either one of the server URLs that are contained within the OpenAPI definition (along with any server variables), or you can pass it a fully qualified URL to use (that may or may not exist within the OpenAPI definition).
|
|
354
|
-
|
|
355
|
-
@example <caption>Server URL with server variables</caption>
|
|
356
|
-
sdk.server('https://{region}.api.example.com/{basePath}', {
|
|
357
|
-
name: 'eu',
|
|
358
|
-
basePath: 'v14',
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
@example <caption>Fully qualified server URL</caption>
|
|
362
|
-
sdk.server('https://eu.api.example.com/v14');`),
|
|
363
|
-
),
|
|
364
|
-
tags: [
|
|
365
|
-
{ tagName: 'param', text: 'url Server URL' },
|
|
366
|
-
{ tagName: 'param', text: 'variables An object of variables to replace into the server URL.' },
|
|
367
|
-
],
|
|
368
|
-
},
|
|
369
|
-
],
|
|
370
|
-
},
|
|
371
|
-
]);
|
|
372
|
-
|
|
373
|
-
// Add all available operation ID accessors into the SDK.
|
|
374
|
-
Object.entries(operations).forEach(([operationId, data]: [string, OperationTypeHousing]) => {
|
|
375
|
-
this.createOperationAccessor(data.operation, operationId, data.types.params, data.types.responses);
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
// Export our SDK into the source file.
|
|
379
|
-
sourceFile.addVariableStatement({
|
|
380
|
-
declarationKind: VariableDeclarationKind.Const,
|
|
381
|
-
declarations: [
|
|
382
|
-
{
|
|
383
|
-
name: 'createSDK',
|
|
384
|
-
initializer: writer => {
|
|
385
|
-
// `ts-morph` doesn't have any way to cleanly create an IIFE.
|
|
386
|
-
writer.writeLine('(() => { return new SDK(); })()');
|
|
387
|
-
return writer;
|
|
388
|
-
},
|
|
389
|
-
},
|
|
390
|
-
],
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
sourceFile.addExportAssignment({
|
|
394
|
-
// Because we're exporting `createSDK` as an IIFE constant we need to have it exported as
|
|
395
|
-
// `export default createSDK`. `addExportAssignment` by default wants it exported as
|
|
396
|
-
// `export = createSDK`, which will throw TS errors because we may also be exporting types in
|
|
397
|
-
// the `./types.ts` file.
|
|
398
|
-
isExportEquals: false,
|
|
399
|
-
expression: 'createSDK',
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
return sourceFile;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
/**
|
|
406
|
-
* Create the `tsconfig.json` file that will allow this SDK to be compiled for use.
|
|
407
|
-
*
|
|
408
|
-
*/
|
|
409
|
-
createTSConfig() {
|
|
410
|
-
const sourceFile = this.project.createSourceFile('tsconfig.json', '');
|
|
411
|
-
|
|
412
|
-
const config: TsConfigJson = {
|
|
413
|
-
compilerOptions: {
|
|
414
|
-
module: 'NodeNext',
|
|
415
|
-
resolveJsonModule: true,
|
|
416
|
-
},
|
|
417
|
-
include: ['./src/**/*'],
|
|
418
|
-
};
|
|
419
|
-
|
|
420
|
-
sourceFile.addStatements(JSON.stringify(config, null, 2));
|
|
421
|
-
|
|
422
|
-
return sourceFile;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
/**
|
|
426
|
-
* Create the `package.json` file that will ultimately make this SDK available to use.
|
|
427
|
-
*
|
|
428
|
-
*/
|
|
429
|
-
createPackageJSON() {
|
|
430
|
-
const sourceFile = this.project.createSourceFile('package.json', '');
|
|
431
|
-
|
|
432
|
-
const hasTypes = !!Object.keys(this.schemas).length;
|
|
433
|
-
|
|
434
|
-
const info = this.spec.getDefinition().info;
|
|
435
|
-
let pkgVersion = semver.coerce(info.version);
|
|
436
|
-
if (!pkgVersion) {
|
|
437
|
-
// If the version that's in `info.version` isn't compatible with semver NPM won't be able to
|
|
438
|
-
// handle it properly so we need to fallback to something it can.
|
|
439
|
-
pkgVersion = semver.coerce('0.0.0') as SemVer;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
const tsupOptions: Options = {
|
|
443
|
-
cjsInterop: true,
|
|
444
|
-
clean: true,
|
|
445
|
-
dts: true,
|
|
446
|
-
entry: [
|
|
447
|
-
'./src/index.ts',
|
|
448
|
-
// If this SDK has schemas and generated types then we should also export those too so
|
|
449
|
-
// they're available to use.
|
|
450
|
-
hasTypes ? './src/types.ts' : '',
|
|
451
|
-
].filter(Boolean),
|
|
452
|
-
format: ['esm', 'cjs'],
|
|
453
|
-
minify: false,
|
|
454
|
-
shims: true,
|
|
455
|
-
sourcemap: true,
|
|
456
|
-
splitting: true,
|
|
457
|
-
};
|
|
458
|
-
|
|
459
|
-
const dependencies = Object.entries(this.requiredPackages)
|
|
460
|
-
.map(([dep, { dependencyType, version }]) => (dependencyType === 'production' ? { [dep]: version } : {}))
|
|
461
|
-
.reduce((prev, next) => Object.assign(prev, next));
|
|
462
|
-
|
|
463
|
-
const devDependencies = Object.entries(this.requiredPackages)
|
|
464
|
-
.map(([dep, { dependencyType, version }]) => (dependencyType === 'development' ? { [dep]: version } : {}))
|
|
465
|
-
.reduce((prev, next) => Object.assign(prev, next));
|
|
466
|
-
|
|
467
|
-
const pkg: PackageJson = {
|
|
468
|
-
name: `@api/${this.identifier}`,
|
|
469
|
-
version: pkgVersion.version,
|
|
470
|
-
main: './dist/index.js',
|
|
471
|
-
types: './dist/index.d.ts',
|
|
472
|
-
module: './dist/index.mts',
|
|
473
|
-
exports: {
|
|
474
|
-
'.': {
|
|
475
|
-
import: './dist/index.mjs',
|
|
476
|
-
require: './dist/index.js',
|
|
477
|
-
},
|
|
478
|
-
...(hasTypes
|
|
479
|
-
? {
|
|
480
|
-
'./types': {
|
|
481
|
-
import: './dist/types.d.mts',
|
|
482
|
-
require: './dist/types.d.ts',
|
|
483
|
-
},
|
|
484
|
-
}
|
|
485
|
-
: {}),
|
|
486
|
-
},
|
|
487
|
-
files: ['dist'],
|
|
488
|
-
scripts: {
|
|
489
|
-
prepare: 'tsup',
|
|
490
|
-
},
|
|
491
|
-
dependencies,
|
|
492
|
-
devDependencies,
|
|
493
|
-
tsup: tsupOptions as JsonObject,
|
|
494
|
-
};
|
|
495
|
-
|
|
496
|
-
sourceFile.addStatements(JSON.stringify(pkg, null, 2));
|
|
497
|
-
|
|
498
|
-
return sourceFile;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
/**
|
|
502
|
-
* Create our main schemas file. This is where all of the JSON Schema that our TypeScript typing
|
|
503
|
-
* infrastructure sources its data from. Without this there are no types.
|
|
504
|
-
*
|
|
505
|
-
*/
|
|
506
|
-
private createSchemasFile(sourceDirectory: Directory) {
|
|
507
|
-
const sourceFile = sourceDirectory.createSourceFile('schemas.ts', '');
|
|
508
|
-
const schemasDir = sourceDirectory.createDirectory('schemas');
|
|
509
|
-
|
|
510
|
-
const sortedSchemas = new Map(Array.from(Object.entries(this.schemas)).sort());
|
|
511
|
-
|
|
512
|
-
Array.from(sortedSchemas).forEach(([schemaName, schema]) => {
|
|
513
|
-
const schemaFile = schemasDir.createSourceFile(`${schemaName}.ts`);
|
|
514
|
-
|
|
515
|
-
// Because we're chunking our schemas into a `schemas/` directory we need to add imports
|
|
516
|
-
// for these schemas into our main `schemas.ts` file.`
|
|
517
|
-
sourceFile.addImportDeclaration({
|
|
518
|
-
defaultImport: schemaName,
|
|
519
|
-
moduleSpecifier: `./schemas/${schemaName}`,
|
|
520
|
-
});
|
|
521
|
-
|
|
522
|
-
let str = JSON.stringify(schema);
|
|
523
|
-
let referencedSchemas = str.match(REF_PLACEHOLDER_REGEX)?.map(s => s.replace(REF_PLACEHOLDER_REGEX, '$1'));
|
|
524
|
-
if (referencedSchemas) {
|
|
525
|
-
referencedSchemas.sort();
|
|
526
|
-
|
|
527
|
-
// Remove any duplicates so we don't add the same import multiple times into this schema
|
|
528
|
-
// file.
|
|
529
|
-
referencedSchemas = Array.from(new Set(referencedSchemas));
|
|
530
|
-
|
|
531
|
-
referencedSchemas.forEach(ref => {
|
|
532
|
-
// Because this schema is referenced from another file we need to create an `import`
|
|
533
|
-
// declaration for it.
|
|
534
|
-
schemaFile.addImportDeclaration({
|
|
535
|
-
defaultImport: ref,
|
|
536
|
-
moduleSpecifier: `./${ref}`,
|
|
537
|
-
});
|
|
538
|
-
});
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
// Load the schema into the schema file within the `schemas/` directory.
|
|
542
|
-
schemaFile.addVariableStatement({
|
|
543
|
-
declarationKind: VariableDeclarationKind.Const,
|
|
544
|
-
declarations: [
|
|
545
|
-
{
|
|
546
|
-
name: schemaName,
|
|
547
|
-
initializer: writer => {
|
|
548
|
-
// We can't have `::convert::<schemaName>` variables within these schema files so we
|
|
549
|
-
// need to clean them up.
|
|
550
|
-
str = str.replace(REF_PLACEHOLDER_REGEX, '$1');
|
|
551
|
-
|
|
552
|
-
writer.writeLine(`${str} as const`);
|
|
553
|
-
return writer;
|
|
554
|
-
},
|
|
555
|
-
},
|
|
556
|
-
],
|
|
557
|
-
});
|
|
558
|
-
|
|
559
|
-
schemaFile.addStatements(`export default ${schemaName}`);
|
|
560
|
-
});
|
|
561
|
-
|
|
562
|
-
// Export all of our schemas from inside the main `schemas.ts` file.
|
|
563
|
-
sourceFile.addStatements(`export { ${Array.from(sortedSchemas.keys()).join(', ')} }`);
|
|
564
|
-
|
|
565
|
-
return sourceFile;
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
/**
|
|
569
|
-
* Create our main types file. This sources its data from the JSON Schema `schemas.ts` file and
|
|
570
|
-
* will re-export types to be used in TypeScript implementations and IDE intellisense. This
|
|
571
|
-
* typing work is functional with the `json-schema-to-ts` library.
|
|
572
|
-
*
|
|
573
|
-
* @see {@link https://npm.im/json-schema-to-ts}
|
|
574
|
-
*/
|
|
575
|
-
private createTypesFile(sourceDirectory: Directory) {
|
|
576
|
-
const sourceFile = sourceDirectory.createSourceFile('types.ts', '');
|
|
577
|
-
|
|
578
|
-
sourceFile.addImportDeclarations([
|
|
579
|
-
{ defaultImport: 'type { FromSchema }', moduleSpecifier: '@readme/api-core/lib' },
|
|
580
|
-
{ defaultImport: '* as schemas', moduleSpecifier: './schemas' },
|
|
581
|
-
]);
|
|
582
|
-
|
|
583
|
-
Array.from(new Map(Array.from(this.types.entries()).sort())).forEach(([typeName, typeExpression]) => {
|
|
584
|
-
sourceFile.addTypeAlias({ isExported: true, name: typeName, type: typeExpression });
|
|
585
|
-
});
|
|
586
|
-
|
|
587
|
-
return sourceFile;
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
/**
|
|
591
|
-
* Create operation accessors on the SDK.
|
|
592
|
-
*
|
|
593
|
-
*/
|
|
594
|
-
private createOperationAccessor(
|
|
595
|
-
operation: Operation,
|
|
596
|
-
operationId: string,
|
|
597
|
-
paramTypes?: OperationTypeHousing['types']['params'],
|
|
598
|
-
responseTypes?: OperationTypeHousing['types']['responses'],
|
|
599
|
-
) {
|
|
600
|
-
let docblock: OptionalKind<JSDocStructure> = {};
|
|
601
|
-
const summary = operation.getSummary();
|
|
602
|
-
const description = operation.getDescription();
|
|
603
|
-
if (summary || description) {
|
|
604
|
-
// To keep our generated docblocks clean we should only add the `@summary` tag if we've
|
|
605
|
-
// got both a summary and a description present on the operation, otherwise we can alternate
|
|
606
|
-
// what we surface the main docblock description.
|
|
607
|
-
docblock.description = writer => {
|
|
608
|
-
if (description) {
|
|
609
|
-
writer.writeLine(docblockEscape(wordWrap(description)));
|
|
610
|
-
} else if (summary) {
|
|
611
|
-
writer.writeLine(docblockEscape(wordWrap(summary)));
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
writer.newLineIfLastNot();
|
|
615
|
-
return writer;
|
|
616
|
-
};
|
|
617
|
-
|
|
618
|
-
if (summary && description) {
|
|
619
|
-
docblock = TSGenerator.#addTagToDocblock(docblock, {
|
|
620
|
-
tagName: 'summary',
|
|
621
|
-
text: docblockEscape(wordWrap(summary)),
|
|
622
|
-
});
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
let hasOptionalBody = false;
|
|
627
|
-
let hasOptionalMetadata = false;
|
|
628
|
-
const parameters = {} as {
|
|
629
|
-
body: OptionalKind<ParameterDeclarationStructure>;
|
|
630
|
-
metadata: OptionalKind<ParameterDeclarationStructure>;
|
|
631
|
-
};
|
|
632
|
-
|
|
633
|
-
if (paramTypes) {
|
|
634
|
-
// If an operation has a request body payload it will only ever have `body` or `formData`,
|
|
635
|
-
// never both, as these are determined upon the media type that's in use.
|
|
636
|
-
if (paramTypes.body || paramTypes.formData) {
|
|
637
|
-
hasOptionalBody = !operation.hasRequiredRequestBody();
|
|
638
|
-
|
|
639
|
-
parameters.body = {
|
|
640
|
-
name: 'body',
|
|
641
|
-
type: paramTypes.body ? paramTypes.body : paramTypes.formData,
|
|
642
|
-
hasQuestionToken: hasOptionalBody,
|
|
643
|
-
};
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
if (paramTypes.metadata) {
|
|
647
|
-
hasOptionalMetadata = !operation.hasRequiredParameters();
|
|
648
|
-
|
|
649
|
-
parameters.metadata = {
|
|
650
|
-
name: 'metadata',
|
|
651
|
-
type: paramTypes.metadata,
|
|
652
|
-
hasQuestionToken: hasOptionalMetadata,
|
|
653
|
-
};
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
let returnType = 'Promise<FetchResponse<number, unknown>>';
|
|
658
|
-
if (responseTypes) {
|
|
659
|
-
const returnTypes = Object.entries(responseTypes)
|
|
660
|
-
.map(([status, { description: responseDescription, type: responseType }]) => {
|
|
661
|
-
if (status.toLowerCase() === 'default') {
|
|
662
|
-
return `FetchResponse<number, ${responseType}>`;
|
|
663
|
-
} else if (status.length === 3 && status.toUpperCase().endsWith('XX')) {
|
|
664
|
-
const statusPrefix = status.slice(0, 1);
|
|
665
|
-
if (!Number.isInteger(Number(statusPrefix))) {
|
|
666
|
-
// If this matches the `_XX` format, but it isn't `{number}XX` then we can't handle
|
|
667
|
-
// it and should instead fall back to treating it as an unknown number.
|
|
668
|
-
return `FetchResponse<number, ${responseType}>`;
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
if (Number(statusPrefix) >= 4) {
|
|
672
|
-
docblock = TSGenerator.#addTagToDocblock(docblock, {
|
|
673
|
-
tagName: 'throws',
|
|
674
|
-
text: `FetchError<${status}, ${responseType}>${
|
|
675
|
-
responseDescription ? docblockEscape(wordWrap(` ${responseDescription}`)) : ''
|
|
676
|
-
}`,
|
|
677
|
-
});
|
|
678
|
-
|
|
679
|
-
return false;
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
this.usesHTTPMethodRangeInterface = true;
|
|
683
|
-
return `FetchResponse<HTTPMethodRange<${statusPrefix}00, ${statusPrefix}99>, ${responseType}>`;
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
// 400 and 500 status code families are thrown as exceptions so adding them as a possible
|
|
687
|
-
// return type isn't valid.
|
|
688
|
-
if (Number(status) >= 400) {
|
|
689
|
-
docblock = TSGenerator.#addTagToDocblock(docblock, {
|
|
690
|
-
tagName: 'throws',
|
|
691
|
-
text: `FetchError<${status}, ${responseType}>${
|
|
692
|
-
responseDescription ? docblockEscape(wordWrap(` ${responseDescription}`)) : ''
|
|
693
|
-
}`,
|
|
694
|
-
});
|
|
695
|
-
|
|
696
|
-
return false;
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
return `FetchResponse<${status}, ${responseType}>`;
|
|
700
|
-
})
|
|
701
|
-
.filter(Boolean)
|
|
702
|
-
.join(' | ');
|
|
703
|
-
|
|
704
|
-
// If all of our documented responses are for error status codes then all we can document for
|
|
705
|
-
// anything else that might happen is `unknown`.
|
|
706
|
-
returnType = `Promise<${returnTypes.length ? returnTypes : 'FetchResponse<number, unknown>'}>`;
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
const shouldAddAltTypedOverloads = Object.keys(parameters).length === 2 && hasOptionalBody && !hasOptionalMetadata;
|
|
710
|
-
const operationIdAccessor = this.sdk.addMethod({
|
|
711
|
-
name: operationId,
|
|
712
|
-
returnType,
|
|
713
|
-
|
|
714
|
-
// If we're going to be creating typed method overloads for optional body an metadata handling
|
|
715
|
-
// we should only add a docblock to the first overload we create because IDE Intellisense will
|
|
716
|
-
// always use that and adding a docblock to all three will bloat the SDK with unused and
|
|
717
|
-
// unsurfaced method documentation.
|
|
718
|
-
docs: shouldAddAltTypedOverloads ? undefined : Object.keys(docblock).length ? [docblock] : undefined,
|
|
719
|
-
statements: writer => {
|
|
720
|
-
/**
|
|
721
|
-
* @example return this.core.fetch('/pet/findByStatus', 'get', body, metadata);
|
|
722
|
-
* @example return this.core.fetch('/pet/findByStatus', 'get', metadata);
|
|
723
|
-
*/
|
|
724
|
-
const fetchStmt = writer
|
|
725
|
-
.write('return this.core.fetch(')
|
|
726
|
-
.quote(operation.path)
|
|
727
|
-
.write(', ')
|
|
728
|
-
.quote(operation.method);
|
|
729
|
-
|
|
730
|
-
const totalParams = Object.keys(parameters).length;
|
|
731
|
-
if (totalParams) {
|
|
732
|
-
Object.values(parameters).forEach((arg, i) => {
|
|
733
|
-
if (i === 0) {
|
|
734
|
-
fetchStmt.write(', ');
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
fetchStmt.write(arg.name);
|
|
738
|
-
if (i !== totalParams - 1) {
|
|
739
|
-
fetchStmt.write(', ');
|
|
740
|
-
}
|
|
741
|
-
});
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
fetchStmt.write(');');
|
|
745
|
-
return fetchStmt;
|
|
746
|
-
},
|
|
747
|
-
});
|
|
748
|
-
|
|
749
|
-
// If we have both body and metadata parameters but only body is optional we need to create
|
|
750
|
-
// a couple function overloads as Typescript doesn't let us have an optional method parameter
|
|
751
|
-
// come before one that's required.
|
|
752
|
-
if (shouldAddAltTypedOverloads) {
|
|
753
|
-
// Create an overload that has both `body` and `metadata` parameters as required.
|
|
754
|
-
operationIdAccessor.addOverload({
|
|
755
|
-
parameters: [
|
|
756
|
-
{ ...parameters.body, hasQuestionToken: false },
|
|
757
|
-
{ ...parameters.metadata, hasQuestionToken: false },
|
|
758
|
-
],
|
|
759
|
-
returnType,
|
|
760
|
-
docs: Object.keys(docblock).length ? [docblock] : undefined,
|
|
761
|
-
});
|
|
762
|
-
|
|
763
|
-
// Create an overload that just has a single `metadata` parameter.
|
|
764
|
-
operationIdAccessor.addOverload({
|
|
765
|
-
parameters: [{ ...parameters.metadata }],
|
|
766
|
-
returnType,
|
|
767
|
-
});
|
|
768
|
-
|
|
769
|
-
// Create an overload that has both `body` and `metadata` parameters as optional. Even though
|
|
770
|
-
// our `metadata` parameter is actually required for this operation this is the only way we're
|
|
771
|
-
// able to have an optional `body` parameter be present before `metadata`.
|
|
772
|
-
//
|
|
773
|
-
// Thankfully our core fetch work in `@readme/api-core` is able to do the proper determination to
|
|
774
|
-
// see if what the user is supplying is `metadata` or `body` content when they supply one or
|
|
775
|
-
// both.
|
|
776
|
-
operationIdAccessor.addParameters([
|
|
777
|
-
{
|
|
778
|
-
...parameters.body,
|
|
779
|
-
// Overloads have to be the most distilled version of the method so that's why we need to
|
|
780
|
-
// type `body` as either `body` or `metadata`. If we didn't do this, if `body` was a JSON
|
|
781
|
-
// Schema type that didn't allow `additionalProperties` then the implementation overload
|
|
782
|
-
// would throw type errors.
|
|
783
|
-
type: `${parameters.body.type} | ${parameters.metadata.type}`,
|
|
784
|
-
hasQuestionToken: true,
|
|
785
|
-
},
|
|
786
|
-
{ ...parameters.metadata, hasQuestionToken: true },
|
|
787
|
-
]);
|
|
788
|
-
} else {
|
|
789
|
-
operationIdAccessor.addParameters(Object.values(parameters));
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
/**
|
|
794
|
-
* Scour through the current OpenAPI definition and compile a store of every operation, along
|
|
795
|
-
* with every HTTP method that's in use, and their available TypeScript types that we can use,
|
|
796
|
-
* along with every HTTP method that's in use.
|
|
797
|
-
*
|
|
798
|
-
*/
|
|
799
|
-
private loadOperationsAndMethods() {
|
|
800
|
-
const operations: Record</* operationId */ string, OperationTypeHousing> = {};
|
|
801
|
-
const methods = new Set<HttpMethods>();
|
|
802
|
-
|
|
803
|
-
// Prepare all of the schemas that we need to process for every operation within this API
|
|
804
|
-
// definition.
|
|
805
|
-
Object.entries(this.spec.getPaths()).forEach(([, ops]) => {
|
|
806
|
-
Object.entries(ops).forEach(([method, operation]: [string, Operation]) => {
|
|
807
|
-
methods.add(method as HttpMethods);
|
|
808
|
-
|
|
809
|
-
const operationId = operation.getOperationId({
|
|
810
|
-
// This `camelCase` option will clean up any weird characters that might be present in
|
|
811
|
-
// the `operationId` so as we don't break TS compilation with an invalid method accessor.
|
|
812
|
-
camelCase: true,
|
|
813
|
-
});
|
|
814
|
-
|
|
815
|
-
operations[operationId] = {
|
|
816
|
-
types: {
|
|
817
|
-
params: this.prepareParameterTypesForOperation(operation, operationId),
|
|
818
|
-
responses: this.prepareResponseTypesForOperation(operation, operationId),
|
|
819
|
-
},
|
|
820
|
-
operation,
|
|
821
|
-
};
|
|
822
|
-
});
|
|
823
|
-
});
|
|
824
|
-
|
|
825
|
-
if (!Object.keys(operations).length) {
|
|
826
|
-
throw new Error('Sorry, this OpenAPI definition does not have any operation paths to generate an SDK for.');
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
return {
|
|
830
|
-
operations,
|
|
831
|
-
methods,
|
|
832
|
-
};
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
/**
|
|
836
|
-
* Compile the parameter (path, query, cookie, and header) schemas for an API operation into
|
|
837
|
-
* usable TypeScript types.
|
|
838
|
-
*
|
|
839
|
-
*/
|
|
840
|
-
private prepareParameterTypesForOperation(operation: Operation, operationId: string) {
|
|
841
|
-
const schemas = operation.getParametersAsJSONSchema({
|
|
842
|
-
includeDiscriminatorMappingRefs: false,
|
|
843
|
-
mergeIntoBodyAndMetadata: true,
|
|
844
|
-
retainDeprecatedProperties: true,
|
|
845
|
-
transformer: (s: SchemaObject) => {
|
|
846
|
-
// As our schemas are dereferenced in the `oas` library we don't want to pollute our
|
|
847
|
-
// codegen'd schemas file with duplicate schemas.
|
|
848
|
-
if ('x-readme-ref-name' in s && typeof s['x-readme-ref-name'] !== 'undefined') {
|
|
849
|
-
const typeName = generateTypeName(s['x-readme-ref-name']);
|
|
850
|
-
this.addSchemaToExport(s, typeName, typeName);
|
|
851
|
-
|
|
852
|
-
return `${REF_PLACEHOLDER}${typeName}` as SchemaObject;
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
return s;
|
|
856
|
-
},
|
|
857
|
-
});
|
|
858
|
-
|
|
859
|
-
if (!schemas || !schemas.length) {
|
|
860
|
-
return false;
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
const res = schemas
|
|
864
|
-
.map(param => ({ [param.type]: param.schema }))
|
|
865
|
-
.reduce((prev, next) => Object.assign(prev, next));
|
|
866
|
-
|
|
867
|
-
return Object.entries(res)
|
|
868
|
-
.map(([paramType, schema]: [string, string | SchemaObject]) => {
|
|
869
|
-
let typeName;
|
|
870
|
-
|
|
871
|
-
if (typeof schema === 'string' && schema.startsWith(REF_PLACEHOLDER)) {
|
|
872
|
-
// If this schema is a string and has our conversion prefix then we've already created
|
|
873
|
-
// a type for it.
|
|
874
|
-
typeName = schema.replace(REF_PLACEHOLDER, '');
|
|
875
|
-
} else {
|
|
876
|
-
typeName = generateTypeName(operationId, paramType, 'param');
|
|
877
|
-
this.addSchemaToExport(schema as SchemaObject, typeName, `${generateTypeName(operationId)}.${paramType}`);
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
return {
|
|
881
|
-
// Types are prefixed with `types.` because that's how we're importing them from
|
|
882
|
-
// `types.d.ts`.
|
|
883
|
-
[paramType]: `types.${typeName}`,
|
|
884
|
-
};
|
|
885
|
-
})
|
|
886
|
-
.reduce((prev, next) => Object.assign(prev, next), {}) as Record<'body' | 'formData' | 'metadata', string>;
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
/**
|
|
890
|
-
* Compile the response schemas for an API operation into usable TypeScript types.
|
|
891
|
-
*
|
|
892
|
-
*/
|
|
893
|
-
private prepareResponseTypesForOperation(operation: Operation, operationId: string) {
|
|
894
|
-
const responseStatusCodes = operation.getResponseStatusCodes();
|
|
895
|
-
if (!responseStatusCodes.length) {
|
|
896
|
-
return undefined;
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
const schemas = responseStatusCodes
|
|
900
|
-
.map(status => {
|
|
901
|
-
const schema = operation.getResponseAsJSONSchema(status, {
|
|
902
|
-
includeDiscriminatorMappingRefs: false,
|
|
903
|
-
transformer: (s: SchemaObject) => {
|
|
904
|
-
// As our schemas are dereferenced in the `oas` library we don't want to pollute our
|
|
905
|
-
// codegen'd schemas file with duplicate schemas.
|
|
906
|
-
if ('x-readme-ref-name' in s && typeof s['x-readme-ref-name'] !== 'undefined') {
|
|
907
|
-
const typeName = generateTypeName(s['x-readme-ref-name']);
|
|
908
|
-
this.addSchemaToExport(s, typeName, `${typeName}`);
|
|
909
|
-
|
|
910
|
-
return `${REF_PLACEHOLDER}${typeName}` as SchemaObject;
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
return s;
|
|
914
|
-
},
|
|
915
|
-
});
|
|
916
|
-
|
|
917
|
-
if (!schema) {
|
|
918
|
-
return false;
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
return {
|
|
922
|
-
[status]: schema.shift(),
|
|
923
|
-
};
|
|
924
|
-
})
|
|
925
|
-
.reduce((prev, next) => Object.assign(prev, next));
|
|
926
|
-
|
|
927
|
-
const res = Object.entries(schemas)
|
|
928
|
-
.map(([status, { description, schema }]) => {
|
|
929
|
-
let typeName;
|
|
930
|
-
|
|
931
|
-
if (typeof schema === 'string' && schema.startsWith(REF_PLACEHOLDER)) {
|
|
932
|
-
// If this schema is a string and has our conversion prefix then we've already created
|
|
933
|
-
// a type for it.
|
|
934
|
-
typeName = schema.replace(REF_PLACEHOLDER, '');
|
|
935
|
-
} else {
|
|
936
|
-
typeName = generateTypeName(operationId, 'response', status);
|
|
937
|
-
|
|
938
|
-
// Because `status` will usually be a number here we need to set the pointer for it
|
|
939
|
-
// within an `[]` as if we do `FromSchema<typeof schemas.operation.response.200>`,
|
|
940
|
-
// TypeScript will throw a compilation error.
|
|
941
|
-
this.addSchemaToExport(schema, typeName, `${generateTypeName(operationId)}.response['${status}']`);
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
return {
|
|
945
|
-
// Types are prefixed with `types.` because that's how we're importing them from
|
|
946
|
-
// `types.d.ts`.
|
|
947
|
-
[status]: {
|
|
948
|
-
type: `types.${typeName}`,
|
|
949
|
-
description,
|
|
950
|
-
},
|
|
951
|
-
};
|
|
952
|
-
})
|
|
953
|
-
.reduce((prev, next) => Object.assign(prev, next), {});
|
|
954
|
-
|
|
955
|
-
return Object.keys(res).length ? res : undefined;
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
/**
|
|
959
|
-
* Add a given schema into our schema dataset that we'll be be exporting as types.
|
|
960
|
-
*
|
|
961
|
-
*/
|
|
962
|
-
private addSchemaToExport(schema: SchemaObject, typeName: string, pointer: string) {
|
|
963
|
-
if (this.types.has(typeName)) {
|
|
964
|
-
return;
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
setWith(this.schemas, pointer, schema, Object);
|
|
968
|
-
this.types.set(typeName, `FromSchema<typeof schemas.${pointer}>`);
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
/**
|
|
972
|
-
* Add a new JSDoc `@tag` to an existing docblock.
|
|
973
|
-
*
|
|
974
|
-
*/
|
|
975
|
-
static #addTagToDocblock(docblock: OptionalKind<JSDocStructure>, tag: OptionalKind<JSDocTagStructure>) {
|
|
976
|
-
const tags = docblock.tags ?? [];
|
|
977
|
-
tags.push(tag);
|
|
978
|
-
|
|
979
|
-
return {
|
|
980
|
-
...docblock,
|
|
981
|
-
tags,
|
|
982
|
-
};
|
|
983
|
-
}
|
|
984
|
-
}
|