api 7.0.0-alpha.0 → 7.0.0-alpha.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.
@@ -0,0 +1,748 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const node_fs_1 = __importDefault(require("node:fs"));
7
+ const node_path_1 = __importDefault(require("node:path"));
8
+ const execa_1 = __importDefault(require("execa"));
9
+ const lodash_setwith_1 = __importDefault(require("lodash.setwith"));
10
+ const semver_1 = __importDefault(require("semver"));
11
+ const ts_morph_1 = require("ts-morph");
12
+ const logger_1 = __importDefault(require("../../logger"));
13
+ const language_1 = __importDefault(require("../language"));
14
+ const util_1 = require("./typescript/util");
15
+ class TSGenerator extends language_1.default {
16
+ project;
17
+ outputJS;
18
+ compilerTarget;
19
+ types;
20
+ sdk;
21
+ schemas;
22
+ usesHTTPMethodRangeInterface = false;
23
+ constructor(spec, specPath, identifier, opts = {}) {
24
+ const options = {
25
+ outputJS: false,
26
+ compilerTarget: 'cjs',
27
+ ...opts,
28
+ };
29
+ if (!options.outputJS) {
30
+ // TypeScript compilation will always target towards ESM-like imports and exports.
31
+ options.compilerTarget = 'esm';
32
+ }
33
+ super(spec, specPath, identifier);
34
+ this.requiredPackages = {
35
+ api: {
36
+ reason: "Required for the `@readme/api-core` library that the codegen'd SDK uses for making requests.",
37
+ url: 'https://npm.im/api',
38
+ },
39
+ 'json-schema-to-ts': {
40
+ reason: 'Required for TypeScript type handling.',
41
+ url: 'https://npm.im/json-schema-to-ts',
42
+ },
43
+ oas: {
44
+ reason: 'Used within `@readme/api-core` and is also loaded for TypeScript types.',
45
+ url: 'https://npm.im/oas',
46
+ },
47
+ };
48
+ this.project = new ts_morph_1.Project({
49
+ manipulationSettings: {
50
+ indentationText: ts_morph_1.IndentationText.TwoSpaces,
51
+ quoteKind: ts_morph_1.QuoteKind.Single,
52
+ },
53
+ compilerOptions: {
54
+ // If we're exporting a TypeScript SDK then we don't need to pollute the codegen directory
55
+ // with unnecessary declaration `.d.ts` files.
56
+ declaration: options.outputJS,
57
+ outDir: 'dist',
58
+ resolveJsonModule: true,
59
+ target: options.compilerTarget === 'cjs' ? ts_morph_1.ScriptTarget.ES5 : ts_morph_1.ScriptTarget.ES2020,
60
+ // If we're compiling to a CJS target then we need to include this compiler option
61
+ // otherwise TS will attempt to load our `openapi.json` import with a `.default` property
62
+ // which doesn't exist. `esModuleInterop` wraps imports in a small `__importDefault`
63
+ // function that does some determination to see if the module has a default export or not.
64
+ //
65
+ // Basically without this option CJS code will fail.
66
+ ...(options.compilerTarget === 'cjs' ? { esModuleInterop: true } : {}),
67
+ },
68
+ });
69
+ this.compilerTarget = options.compilerTarget;
70
+ this.outputJS = options.outputJS;
71
+ this.types = new Map();
72
+ this.schemas = {};
73
+ }
74
+ async installer(storage, opts = {}) {
75
+ const installDir = storage.getIdentifierStorageDir();
76
+ const info = this.spec.getDefinition().info;
77
+ let pkgVersion = semver_1.default.coerce(info.version);
78
+ if (!pkgVersion) {
79
+ // If the version that's in `info.version` isn't compatible with semver NPM won't be able to
80
+ // handle it properly so we need to fallback to something it can.
81
+ pkgVersion = semver_1.default.coerce('0.0.0');
82
+ }
83
+ const pkg = {
84
+ name: `@api/${storage.identifier}`,
85
+ version: pkgVersion.version,
86
+ main: `./index.${this.outputJS ? 'js' : 'ts'}`,
87
+ types: './index.d.ts', // Types are always present regardless if you're getting compiled JS.
88
+ };
89
+ node_fs_1.default.writeFileSync(node_path_1.default.join(installDir, 'package.json'), JSON.stringify(pkg, null, 2));
90
+ const npmInstall = ['install', '--save', opts.dryRun ? '--dry-run' : ''].filter(Boolean);
91
+ // This will install packages required for the SDK within its installed directory in `.apis/`.
92
+ await (0, execa_1.default)('npm', [...npmInstall, ...Object.keys(this.requiredPackages)].filter(Boolean), {
93
+ cwd: installDir,
94
+ }).then(res => {
95
+ if (opts.dryRun) {
96
+ (opts.logger ? opts.logger : logger_1.default)(res.command);
97
+ (opts.logger ? opts.logger : logger_1.default)(res.stdout);
98
+ }
99
+ });
100
+ // This will install the installed SDK as a dependency within the current working directory,
101
+ // adding `@api/<sdk identifier>` as a dependency there so you can load it with
102
+ // `require('@api/<sdk identifier>)`.
103
+ return (0, execa_1.default)('npm', [...npmInstall, installDir].filter(Boolean))
104
+ .then(res => {
105
+ if (opts.dryRun) {
106
+ (opts.logger ? opts.logger : logger_1.default)(res.command);
107
+ (opts.logger ? opts.logger : logger_1.default)(res.stdout);
108
+ }
109
+ })
110
+ .catch(err => {
111
+ if (opts.dryRun) {
112
+ (opts.logger ? opts.logger : logger_1.default)(err.message);
113
+ return;
114
+ }
115
+ throw err;
116
+ });
117
+ }
118
+ /**
119
+ * Compile the current OpenAPI definition into a TypeScript library.
120
+ *
121
+ */
122
+ async generator() {
123
+ const sdkSource = this.createSourceFile();
124
+ if (Object.keys(this.schemas).length) {
125
+ this.createSchemasFile();
126
+ this.createTypesFile();
127
+ /**
128
+ * Export all of our available types so they can be used in SDK implementations. Types are
129
+ * exported individually because TS has no way right now of allowing us to do
130
+ * `export type * from './types'` on a non-named entry.
131
+ *
132
+ * Types in the main entry point are only being exported for TS outputs as JS users won't be
133
+ * able to use them and it clashes with the default SDK export present.
134
+ *
135
+ * @see {@link https://github.com/microsoft/TypeScript/issues/37238}
136
+ * @see {@link https://github.com/readmeio/api/issues/588}
137
+ */
138
+ if (!this.outputJS) {
139
+ const types = Array.from(this.types.keys());
140
+ types.sort();
141
+ sdkSource.addExportDeclarations([
142
+ {
143
+ isTypeOnly: true,
144
+ namedExports: types,
145
+ moduleSpecifier: './types',
146
+ },
147
+ ]);
148
+ }
149
+ }
150
+ else {
151
+ // If we don't have any schemas then we shouldn't import a `types` file that doesn't exist.
152
+ sdkSource
153
+ .getImportDeclarations()
154
+ .find(id => id.getText() === "import type * as types from './types';")
155
+ ?.remove();
156
+ }
157
+ // If this SDK doesn't use the `HTTPMethodRange` interface for handling `2XX` response status
158
+ // codes then we should remove it from being imported.
159
+ if (!this.usesHTTPMethodRangeInterface) {
160
+ sdkSource
161
+ .getImportDeclarations()
162
+ .find(id => id.getText().includes('HTTPMethodRange'))
163
+ ?.replaceWithText("import type { ConfigOptions, FetchResponse } from '@readme/api-core';");
164
+ }
165
+ if (this.outputJS) {
166
+ return this.project
167
+ .emitToMemory()
168
+ .getFiles()
169
+ .map(sourceFile => {
170
+ const file = node_path_1.default.basename(sourceFile.filePath);
171
+ if (file === 'schemas.js' || file === 'types.js') {
172
+ // If we're generating a JS SDK then we don't need to generate these two files as the
173
+ // user will have `.d.ts` files for them instead.
174
+ return {};
175
+ }
176
+ let code = sourceFile.text;
177
+ if (file === 'index.js' && this.compilerTarget === 'cjs') {
178
+ /**
179
+ * There's an annoying quirk with `ts-morph` where if we're exporting a default export
180
+ * to a CJS environment, it'll export it as `exports.default`. Because we don't want
181
+ * folks in these environments to have to load their SDKs with
182
+ * `require('@api/sdk').default` we're overriding that here to change it to being the
183
+ * module exports.
184
+ *
185
+ * `ts-morph` unfortunately doesn't give us any options for programatically doing this
186
+ * so we need to resort to modifying the emitted JS code.
187
+ */
188
+ code = code
189
+ .replace(/Object\.defineProperty\(exports, '__esModule', { value: true }\);\n/, '')
190
+ .replace('exports.default = createSDK;', 'module.exports = createSDK;');
191
+ }
192
+ return {
193
+ [file]: code,
194
+ };
195
+ })
196
+ .reduce((prev, next) => Object.assign(prev, next));
197
+ }
198
+ return [
199
+ ...this.project.getSourceFiles().map(sourceFile => ({
200
+ [sourceFile.getBaseName()]: sourceFile.getFullText(),
201
+ })),
202
+ // Because we're returning the raw source files for TS generation we also need to separately
203
+ // emit out our declaration files so we can put those into a separate file in the installed
204
+ // SDK directory.
205
+ ...this.project
206
+ .emitToMemory({ emitOnlyDtsFiles: true })
207
+ .getFiles()
208
+ .map(sourceFile => ({
209
+ [node_path_1.default.basename(sourceFile.filePath)]: sourceFile.text,
210
+ })),
211
+ ].reduce((prev, next) => Object.assign(prev, next));
212
+ }
213
+ /**
214
+ * Create our main SDK source file.
215
+ *
216
+ */
217
+ createSourceFile() {
218
+ const { operations } = this.loadOperationsAndMethods();
219
+ const sourceFile = this.project.createSourceFile('index.ts', '');
220
+ sourceFile.addImportDeclarations([
221
+ // This import will be automatically removed later if the SDK ends up not having any types.
222
+ { defaultImport: 'type * as types', moduleSpecifier: './types' },
223
+ {
224
+ // `HTTPMethodRange` will be conditionally removed later if it ends up not being used.
225
+ defaultImport: 'type { ConfigOptions, FetchResponse, HTTPMethodRange }',
226
+ moduleSpecifier: '@readme/api-core',
227
+ },
228
+ { defaultImport: 'Oas', moduleSpecifier: 'oas' },
229
+ { defaultImport: 'APICore', moduleSpecifier: '@readme/api-core' },
230
+ { defaultImport: 'definition', moduleSpecifier: this.specPath },
231
+ ]);
232
+ // @todo add TOS, License, info.* to a docblock at the top of the SDK.
233
+ this.sdk = sourceFile.addClass({
234
+ name: 'SDK',
235
+ properties: [
236
+ { name: 'spec', type: 'Oas' },
237
+ { name: 'core', type: 'APICore' },
238
+ ],
239
+ });
240
+ this.sdk.addConstructor({
241
+ statements: writer => {
242
+ writer.writeLine('this.spec = Oas.init(definition);');
243
+ writer.write('this.core = new APICore(this.spec, ').quote(this.userAgent).write(');');
244
+ return writer;
245
+ },
246
+ });
247
+ // Add our core API methods for controlling auth, servers, and various configurable abilities.
248
+ this.sdk.addMethods([
249
+ {
250
+ name: 'config',
251
+ parameters: [{ name: 'config', type: 'ConfigOptions' }],
252
+ statements: writer => writer.writeLine('this.core.setConfig(config);'),
253
+ docs: [
254
+ {
255
+ description: writer => writer.writeLine((0, util_1.wordWrap)('Optionally configure various options that the SDK allows.')),
256
+ tags: [
257
+ { tagName: 'param', text: 'config Object of supported SDK options and toggles.' },
258
+ {
259
+ tagName: 'param',
260
+ text: (0, util_1.wordWrap)('config.timeout Override the default `fetch` request timeout of 30 seconds. This number should be represented in milliseconds.'),
261
+ },
262
+ ],
263
+ },
264
+ ],
265
+ },
266
+ {
267
+ name: 'auth',
268
+ parameters: [{ name: '...values', type: 'string[] | number[]' }],
269
+ statements: writer => {
270
+ writer.writeLine('this.core.setAuth(...values);');
271
+ writer.writeLine('return this;');
272
+ return writer;
273
+ },
274
+ docs: [
275
+ {
276
+ description: writer => writer.writeLine((0, util_1.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.
277
+
278
+ With the exception of OpenID and MutualTLS, it supports all forms of authentication supported by the OpenAPI specification.
279
+
280
+ @example <caption>HTTP Basic auth</caption>
281
+ sdk.auth('username', 'password');
282
+
283
+ @example <caption>Bearer tokens (HTTP or OAuth 2)</caption>
284
+ sdk.auth('myBearerToken');
285
+
286
+ @example <caption>API Keys</caption>
287
+ sdk.auth('myApiKey');`)),
288
+ tags: [
289
+ { tagName: 'see', text: '{@link https://spec.openapis.org/oas/v3.0.3#fixed-fields-22}' },
290
+ { tagName: 'see', text: '{@link https://spec.openapis.org/oas/v3.1.0#fixed-fields-22}' },
291
+ {
292
+ tagName: 'param',
293
+ text: 'values Your auth credentials for the API; can specify up to two strings or numbers.',
294
+ },
295
+ ],
296
+ },
297
+ ],
298
+ },
299
+ {
300
+ name: 'server',
301
+ parameters: [
302
+ { name: 'url', type: 'string' },
303
+ { name: 'variables', initializer: '{}' },
304
+ ],
305
+ statements: writer => writer.writeLine('this.core.setServer(url, variables);'),
306
+ docs: [
307
+ {
308
+ description: writer => writer.writeLine((0, util_1.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).
309
+
310
+ @example <caption>Server URL with server variables</caption>
311
+ sdk.server('https://{region}.api.example.com/{basePath}', {
312
+ name: 'eu',
313
+ basePath: 'v14',
314
+ });
315
+
316
+ @example <caption>Fully qualified server URL</caption>
317
+ sdk.server('https://eu.api.example.com/v14');`)),
318
+ tags: [
319
+ { tagName: 'param', text: 'url Server URL' },
320
+ { tagName: 'param', text: 'variables An object of variables to replace into the server URL.' },
321
+ ],
322
+ },
323
+ ],
324
+ },
325
+ ]);
326
+ // Add all available operation ID accessors into the SDK.
327
+ Object.entries(operations).forEach(([operationId, data]) => {
328
+ this.createOperationAccessor(data.operation, operationId, data.types.params, data.types.responses);
329
+ });
330
+ // Export our SDK into the source file.
331
+ sourceFile.addVariableStatement({
332
+ declarationKind: ts_morph_1.VariableDeclarationKind.Const,
333
+ declarations: [
334
+ {
335
+ name: 'createSDK',
336
+ initializer: writer => {
337
+ // `ts-morph` doesn't have any way to cleanly create an IFEE.
338
+ writer.writeLine('(() => { return new SDK(); })()');
339
+ return writer;
340
+ },
341
+ },
342
+ ],
343
+ });
344
+ sourceFile.addExportAssignment({
345
+ // Because CJS targets have `createSDK` exported with `module.exports`, but the TS type side
346
+ // of things to work right we need to set this as `export =`. Thankfully `ts-morph` will
347
+ // handle this accordingly and still create our JS file with `module.exports` and not
348
+ // `export =` -- only TS types will have this export style.
349
+ isExportEquals: this.compilerTarget === 'cjs' && this.outputJS,
350
+ expression: 'createSDK',
351
+ });
352
+ return sourceFile;
353
+ }
354
+ /**
355
+ * Create our main schemas file. This is where all of the JSON Schema that our TypeScript typing
356
+ * infrastructure sources its data from. Without this there are no types.
357
+ *
358
+ */
359
+ createSchemasFile() {
360
+ const sourceFile = this.project.createSourceFile('schemas.ts', '');
361
+ const sortedSchemas = new Map(Array.from(Object.entries(this.schemas)).sort());
362
+ Array.from(sortedSchemas).forEach(([schemaName, schema]) => {
363
+ sourceFile.addVariableStatement({
364
+ declarationKind: ts_morph_1.VariableDeclarationKind.Const,
365
+ declarations: [
366
+ {
367
+ name: schemaName,
368
+ initializer: writer => {
369
+ /**
370
+ * This is the conversion prefix that we add to all `$ref` pointers we find in
371
+ * generated JSON Schema.
372
+ *
373
+ * Because the pointer name is a string we want to have it reference the schema
374
+ * constant we're adding into the codegen'd schema file. As there's no way, not even
375
+ * using `eval()` in this case, to convert a string to a constant we're prefixing
376
+ * them with this so we can later remove it and rewrite the value to a literal.
377
+ * eg. `'Pet'` becomes `Pet`.
378
+ *
379
+ * And because our TypeScript type name generator properly ignores `:`, this is safe
380
+ * to prepend to all generated type names.
381
+ */
382
+ let str = JSON.stringify(schema);
383
+ str = str.replace(/"::convert::([a-zA-Z_$\\d]*)"/g, '$1');
384
+ writer.writeLine(`${str} as const`);
385
+ return writer;
386
+ },
387
+ },
388
+ ],
389
+ });
390
+ });
391
+ sourceFile.addStatements(`export { ${Array.from(sortedSchemas.keys()).join(', ')} }`);
392
+ return sourceFile;
393
+ }
394
+ /**
395
+ * Create our main types file. This sources its data from the JSON Schema `schemas.ts` file and
396
+ * will re-export types to be used in TypeScript implementations and IDE intellisense. This
397
+ * typing work is functional with the `json-schema-to-ts` library.
398
+ *
399
+ * @see {@link https://npm.im/json-schema-to-ts}
400
+ */
401
+ createTypesFile() {
402
+ const sourceFile = this.project.createSourceFile('types.ts', '');
403
+ sourceFile.addImportDeclarations([
404
+ { defaultImport: 'type { FromSchema }', moduleSpecifier: 'json-schema-to-ts' },
405
+ { defaultImport: '* as schemas', moduleSpecifier: './schemas' },
406
+ ]);
407
+ Array.from(new Map(Array.from(this.types.entries()).sort())).forEach(([typeName, typeExpression]) => {
408
+ sourceFile.addTypeAlias({ isExported: true, name: typeName, type: typeExpression });
409
+ });
410
+ return sourceFile;
411
+ }
412
+ /**
413
+ * Add a new JSDoc `@tag` to an existing docblock.
414
+ *
415
+ */
416
+ static addTagToDocblock(docblock, tag) {
417
+ const tags = docblock.tags ?? [];
418
+ tags.push(tag);
419
+ return {
420
+ ...docblock,
421
+ tags,
422
+ };
423
+ }
424
+ /**
425
+ * Create operation accessors on the SDK.
426
+ *
427
+ */
428
+ createOperationAccessor(operation, operationId, paramTypes, responseTypes) {
429
+ let docblock = {};
430
+ const summary = operation.getSummary();
431
+ const description = operation.getDescription();
432
+ if (summary || description) {
433
+ // To keep our generated docblocks clean we should only add the `@summary` tag if we've
434
+ // got both a summary and a description present on the operation, otherwise we can alternate
435
+ // what we surface the main docblock description.
436
+ docblock.description = writer => {
437
+ if (description) {
438
+ writer.writeLine((0, util_1.docblockEscape)((0, util_1.wordWrap)(description)));
439
+ }
440
+ else if (summary) {
441
+ writer.writeLine((0, util_1.docblockEscape)((0, util_1.wordWrap)(summary)));
442
+ }
443
+ writer.newLineIfLastNot();
444
+ return writer;
445
+ };
446
+ if (summary && description) {
447
+ docblock = TSGenerator.addTagToDocblock(docblock, {
448
+ tagName: 'summary',
449
+ text: (0, util_1.docblockEscape)((0, util_1.wordWrap)(summary)),
450
+ });
451
+ }
452
+ }
453
+ let hasOptionalBody = false;
454
+ let hasOptionalMetadata = false;
455
+ const parameters = {};
456
+ if (paramTypes) {
457
+ // If an operation has a request body payload it will only ever have `body` or `formData`,
458
+ // never both, as these are determined upon the media type that's in use.
459
+ if (paramTypes.body || paramTypes.formData) {
460
+ hasOptionalBody = !operation.hasRequiredRequestBody();
461
+ parameters.body = {
462
+ name: 'body',
463
+ type: paramTypes.body ? paramTypes.body : paramTypes.formData,
464
+ hasQuestionToken: hasOptionalBody,
465
+ };
466
+ }
467
+ if (paramTypes.metadata) {
468
+ hasOptionalMetadata = !operation.hasRequiredParameters();
469
+ parameters.metadata = {
470
+ name: 'metadata',
471
+ type: paramTypes.metadata,
472
+ hasQuestionToken: hasOptionalMetadata,
473
+ };
474
+ }
475
+ }
476
+ let returnType = 'Promise<FetchResponse<number, unknown>>';
477
+ if (responseTypes) {
478
+ const returnTypes = Object.entries(responseTypes)
479
+ .map(([status, { description: responseDescription, type: responseType }]) => {
480
+ if (status.toLowerCase() === 'default') {
481
+ return `FetchResponse<number, ${responseType}>`;
482
+ }
483
+ else if (status.length === 3 && status.toUpperCase().endsWith('XX')) {
484
+ const statusPrefix = status.slice(0, 1);
485
+ if (!Number.isInteger(Number(statusPrefix))) {
486
+ // If this matches the `_XX` format, but it isn't `{number}XX` then we can't handle
487
+ // it and should instead fall back to treating it as an unknown number.
488
+ return `FetchResponse<number, ${responseType}>`;
489
+ }
490
+ if (Number(statusPrefix) >= 4) {
491
+ docblock = TSGenerator.addTagToDocblock(docblock, {
492
+ tagName: 'throws',
493
+ text: `FetchError<${status}, ${responseType}>${responseDescription ? (0, util_1.docblockEscape)((0, util_1.wordWrap)(` ${responseDescription}`)) : ''}`,
494
+ });
495
+ return false;
496
+ }
497
+ this.usesHTTPMethodRangeInterface = true;
498
+ return `FetchResponse<HTTPMethodRange<${statusPrefix}00, ${statusPrefix}99>, ${responseType}>`;
499
+ }
500
+ // 400 and 500 status code families are thrown as exceptions so adding them as a possible
501
+ // return type isn't valid.
502
+ if (Number(status) >= 400) {
503
+ docblock = TSGenerator.addTagToDocblock(docblock, {
504
+ tagName: 'throws',
505
+ text: `FetchError<${status}, ${responseType}>${responseDescription ? (0, util_1.docblockEscape)((0, util_1.wordWrap)(` ${responseDescription}`)) : ''}`,
506
+ });
507
+ return false;
508
+ }
509
+ return `FetchResponse<${status}, ${responseType}>`;
510
+ })
511
+ .filter(Boolean)
512
+ .join(' | ');
513
+ // If all of our documented responses are for error status codes then all we can document for
514
+ // anything else that might happen is `unknown`.
515
+ returnType = `Promise<${returnTypes.length ? returnTypes : 'FetchResponse<number, unknown>'}>`;
516
+ }
517
+ const shouldAddAltTypedOverloads = Object.keys(parameters).length === 2 && hasOptionalBody && !hasOptionalMetadata;
518
+ const operationIdAccessor = this.sdk.addMethod({
519
+ name: operationId,
520
+ returnType,
521
+ // If we're going to be creating typed method overloads for optional body an metadata handling
522
+ // we should only add a docblock to the first overload we create because IDE Intellisense will
523
+ // always use that and adding a docblock to all three will bloat the SDK with unused and
524
+ // unsurfaced method documentation.
525
+ docs: shouldAddAltTypedOverloads ? undefined : Object.keys(docblock).length ? [docblock] : undefined,
526
+ statements: writer => {
527
+ /**
528
+ * @example return this.core.fetch('/pet/findByStatus', 'get', body, metadata);
529
+ * @example return this.core.fetch('/pet/findByStatus', 'get', metadata);
530
+ */
531
+ const fetchStmt = writer
532
+ .write('return this.core.fetch(')
533
+ .quote(operation.path)
534
+ .write(', ')
535
+ .quote(operation.method);
536
+ const totalParams = Object.keys(parameters).length;
537
+ if (totalParams) {
538
+ Object.values(parameters).forEach((arg, i) => {
539
+ if (i === 0) {
540
+ fetchStmt.write(', ');
541
+ }
542
+ fetchStmt.write(arg.name);
543
+ if (i !== totalParams - 1) {
544
+ fetchStmt.write(', ');
545
+ }
546
+ });
547
+ }
548
+ fetchStmt.write(');');
549
+ return fetchStmt;
550
+ },
551
+ });
552
+ // If we have both body and metadata parameters but only body is optional we need to create
553
+ // a couple function overloads as Typescript doesn't let us have an optional method parameter
554
+ // come before one that's required.
555
+ if (shouldAddAltTypedOverloads) {
556
+ // Create an overload that has both `body` and `metadata` parameters as required.
557
+ operationIdAccessor.addOverload({
558
+ parameters: [
559
+ { ...parameters.body, hasQuestionToken: false },
560
+ { ...parameters.metadata, hasQuestionToken: false },
561
+ ],
562
+ returnType,
563
+ docs: Object.keys(docblock).length ? [docblock] : undefined,
564
+ });
565
+ // Create an overload that just has a single `metadata` parameter.
566
+ operationIdAccessor.addOverload({
567
+ parameters: [{ ...parameters.metadata }],
568
+ returnType,
569
+ });
570
+ // Create an overload that has both `body` and `metadata` parameters as optional. Even though
571
+ // our `metadata` parameter is actually required for this operation this is the only way we're
572
+ // able to have an optional `body` parameter be present before `metadata`.
573
+ //
574
+ // Thankfully our core fetch work in `@readme/api-core` is able to do the proper determination to
575
+ // see if what the user is supplying is `metadata` or `body` content when they supply one or
576
+ // both.
577
+ operationIdAccessor.addParameters([
578
+ {
579
+ ...parameters.body,
580
+ // Overloads have to be the most distilled version of the method so that's why we need to
581
+ // type `body` as either `body` or `metadata`. If we didn't do this, if `body` was a JSON
582
+ // Schema type that didn't allow `additionalProperties` then the implementation overload
583
+ // would throw type errors.
584
+ type: `${parameters.body.type} | ${parameters.metadata.type}`,
585
+ hasQuestionToken: true,
586
+ },
587
+ { ...parameters.metadata, hasQuestionToken: true },
588
+ ]);
589
+ }
590
+ else {
591
+ operationIdAccessor.addParameters(Object.values(parameters));
592
+ }
593
+ }
594
+ /**
595
+ * Scour through the current OpenAPI definition and compile a store of every operation, along
596
+ * with every HTTP method that's in use, and their available TypeScript types that we can use,
597
+ * along with every HTTP method that's in use.
598
+ *
599
+ */
600
+ loadOperationsAndMethods() {
601
+ const operations = {};
602
+ const methods = new Set();
603
+ // Prepare all of the schemas that we need to process for every operation within this API
604
+ // definition.
605
+ Object.entries(this.spec.getPaths()).forEach(([, ops]) => {
606
+ Object.entries(ops).forEach(([method, operation]) => {
607
+ methods.add(method);
608
+ const operationId = operation.getOperationId({
609
+ // This `camelCase` option will clean up any weird characters that might be present in
610
+ // the `operationId` so as we don't break TS compilation with an invalid method accessor.
611
+ camelCase: true,
612
+ });
613
+ operations[operationId] = {
614
+ types: {
615
+ params: this.prepareParameterTypesForOperation(operation, operationId),
616
+ responses: this.prepareResponseTypesForOperation(operation, operationId),
617
+ },
618
+ operation,
619
+ };
620
+ });
621
+ });
622
+ if (!Object.keys(operations).length) {
623
+ throw new Error('Sorry, this OpenAPI definition does not have any operation paths to generate an SDK for.');
624
+ }
625
+ return {
626
+ operations,
627
+ methods,
628
+ };
629
+ }
630
+ /**
631
+ * Compile the parameter (path, query, cookie, and header) schemas for an API operation into
632
+ * usable TypeScript types.
633
+ *
634
+ */
635
+ prepareParameterTypesForOperation(operation, operationId) {
636
+ const schemas = operation.getParametersAsJSONSchema({
637
+ includeDiscriminatorMappingRefs: false,
638
+ mergeIntoBodyAndMetadata: true,
639
+ retainDeprecatedProperties: true,
640
+ transformer: (s) => {
641
+ // As our schemas are dereferenced in the `oas` library we don't want to pollute our
642
+ // codegen'd schemas file with duplicate schemas.
643
+ if ('x-readme-ref-name' in s && typeof s['x-readme-ref-name'] !== 'undefined') {
644
+ const typeName = (0, util_1.generateTypeName)(s['x-readme-ref-name']);
645
+ this.addSchemaToExport(s, typeName, typeName);
646
+ return `::convert::${typeName}`;
647
+ }
648
+ return s;
649
+ },
650
+ });
651
+ if (!schemas || !schemas.length) {
652
+ return false;
653
+ }
654
+ const res = schemas
655
+ .map(param => ({ [param.type]: param.schema }))
656
+ .reduce((prev, next) => Object.assign(prev, next));
657
+ return Object.entries(res)
658
+ .map(([paramType, schema]) => {
659
+ let typeName;
660
+ if (typeof schema === 'string' && schema.startsWith('::convert::')) {
661
+ // If this schema is a string and has our conversion prefix then we've already created
662
+ // a type for it.
663
+ typeName = schema.replace('::convert::', '');
664
+ }
665
+ else {
666
+ typeName = (0, util_1.generateTypeName)(operationId, paramType, 'param');
667
+ this.addSchemaToExport(schema, typeName, `${(0, util_1.generateTypeName)(operationId)}.${paramType}`);
668
+ }
669
+ return {
670
+ // Types are prefixed with `types.` because that's how we're importing them from
671
+ // `types.d.ts`.
672
+ [paramType]: `types.${typeName}`,
673
+ };
674
+ })
675
+ .reduce((prev, next) => Object.assign(prev, next), {});
676
+ }
677
+ /**
678
+ * Compile the response schemas for an API operation into usable TypeScript types.
679
+ *
680
+ */
681
+ prepareResponseTypesForOperation(operation, operationId) {
682
+ const responseStatusCodes = operation.getResponseStatusCodes();
683
+ if (!responseStatusCodes.length) {
684
+ return undefined;
685
+ }
686
+ const schemas = responseStatusCodes
687
+ .map(status => {
688
+ const schema = operation.getResponseAsJSONSchema(status, {
689
+ includeDiscriminatorMappingRefs: false,
690
+ transformer: (s) => {
691
+ // As our schemas are dereferenced in the `oas` library we don't want to pollute our
692
+ // codegen'd schemas file with duplicate schemas.
693
+ if ('x-readme-ref-name' in s && typeof s['x-readme-ref-name'] !== 'undefined') {
694
+ const typeName = (0, util_1.generateTypeName)(s['x-readme-ref-name']);
695
+ this.addSchemaToExport(s, typeName, `${typeName}`);
696
+ return `::convert::${typeName}`;
697
+ }
698
+ return s;
699
+ },
700
+ });
701
+ if (!schema) {
702
+ return false;
703
+ }
704
+ return {
705
+ [status]: schema.shift(),
706
+ };
707
+ })
708
+ .reduce((prev, next) => Object.assign(prev, next));
709
+ const res = Object.entries(schemas)
710
+ .map(([status, { description, schema }]) => {
711
+ let typeName;
712
+ if (typeof schema === 'string' && schema.startsWith('::convert::')) {
713
+ // If this schema is a string and has our conversion prefix then we've already created
714
+ // a type for it.
715
+ typeName = schema.replace('::convert::', '');
716
+ }
717
+ else {
718
+ typeName = (0, util_1.generateTypeName)(operationId, 'response', status);
719
+ // Because `status` will usually be a number here we need to set the pointer for it
720
+ // within an `[]` as if we do `FromSchema<typeof schemas.operation.response.200>`,
721
+ // TypeScript will throw a compilation error.
722
+ this.addSchemaToExport(schema, typeName, `${(0, util_1.generateTypeName)(operationId)}.response['${status}']`);
723
+ }
724
+ return {
725
+ // Types are prefixed with `types.` because that's how we're importing them from
726
+ // `types.d.ts`.
727
+ [status]: {
728
+ type: `types.${typeName}`,
729
+ description,
730
+ },
731
+ };
732
+ })
733
+ .reduce((prev, next) => Object.assign(prev, next), {});
734
+ return Object.keys(res).length ? res : undefined;
735
+ }
736
+ /**
737
+ * Add a given schema into our schema dataset that we'll be be exporting as types.
738
+ *
739
+ */
740
+ addSchemaToExport(schema, typeName, pointer) {
741
+ if (this.types.has(typeName)) {
742
+ return;
743
+ }
744
+ (0, lodash_setwith_1.default)(this.schemas, pointer, schema, Object);
745
+ this.types.set(typeName, `FromSchema<typeof schemas.${pointer}>`);
746
+ }
747
+ }
748
+ exports.default = TSGenerator;