api
Version:
Magical SDK generation from an OpenAPI definition 🪄
822 lines (821 loc) • 44.2 kB
JavaScript
"use strict";
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
if (typeof b !== "function" && b !== null)
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
if (ar || !(i in from)) {
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
ar[i] = from[i];
}
}
return to.concat(ar || Array.prototype.slice.call(from));
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
exports.__esModule = true;
var fs_1 = __importDefault(require("fs"));
var path_1 = __importDefault(require("path"));
var execa_1 = __importDefault(require("execa"));
var setWith_1 = __importDefault(require("lodash/setWith"));
var semver_1 = __importDefault(require("semver"));
var ts_morph_1 = require("ts-morph");
var logger_1 = __importDefault(require("../../logger"));
var language_1 = __importDefault(require("../language"));
var util_1 = require("./typescript/util");
var TSGenerator = /** @class */ (function (_super) {
__extends(TSGenerator, _super);
function TSGenerator(spec, specPath, identifier, opts) {
if (opts === void 0) { opts = {}; }
var _this = this;
var options = __assign({ outputJS: false, compilerTarget: 'cjs' }, opts);
if (!options.outputJS) {
// TypeScript compilation will always target towards ESM-like imports and exports.
options.compilerTarget = 'esm';
}
_this = _super.call(this, spec, specPath, identifier) || this;
_this.usesHTTPMethodRangeInterface = false;
_this.requiredPackages = {
api: {
reason: "Required for the `api/dist/core` library that the codegen'd SDK uses for making requests.",
url: 'https://npm.im/api'
},
'json-schema-to-ts@beta': {
reason: 'Required for TypeScript type handling.',
url: 'https://npm.im/json-schema-to-ts'
},
oas: {
reason: 'Used within `api/dist/core` and is also loaded for TypeScript types.',
url: 'https://npm.im/oas'
}
};
_this.project = new ts_morph_1.Project({
manipulationSettings: {
indentationText: ts_morph_1.IndentationText.TwoSpaces,
quoteKind: ts_morph_1.QuoteKind.Single
},
compilerOptions: __assign({
// If we're exporting a TypeScript SDK then we don't need to pollute the codegen directory
// with unnecessary declaration `.d.ts` files.
declaration: options.outputJS, outDir: 'dist', resolveJsonModule: true, target: options.compilerTarget === 'cjs' ? ts_morph_1.ScriptTarget.ES5 : ts_morph_1.ScriptTarget.ES2020 }, (options.compilerTarget === 'cjs' ? { esModuleInterop: true } : {}))
});
_this.compilerTarget = options.compilerTarget;
_this.outputJS = options.outputJS;
_this.types = new Map();
_this.schemas = {};
return _this;
}
TSGenerator.prototype.installer = function (storage, opts) {
if (opts === void 0) { opts = {}; }
return __awaiter(this, void 0, void 0, function () {
var installDir, info, pkgVersion, pkg, npmInstall;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
installDir = storage.getIdentifierStorageDir();
info = this.spec.getDefinition().info;
pkgVersion = semver_1["default"].coerce(info.version);
if (!pkgVersion) {
// If the version that's in `info.version` isn't compatible with semver NPM won't be able to
// handle it properly so we need to fallback to something it can.
pkgVersion = semver_1["default"].coerce('0.0.0');
}
pkg = {
name: "@api/".concat(storage.identifier),
version: pkgVersion.version,
main: "./index.".concat(this.outputJS ? 'js' : 'ts'),
types: './index.d.ts'
};
fs_1["default"].writeFileSync(path_1["default"].join(installDir, 'package.json'), JSON.stringify(pkg, null, 2));
npmInstall = ['install', '--save', opts.dryRun ? '--dry-run' : ''].filter(Boolean);
// This will install packages required for the SDK within its installed directory in `.apis/`.
return [4 /*yield*/, (0, execa_1["default"])('npm', __spreadArray(__spreadArray([], npmInstall, true), Object.keys(this.requiredPackages), true).filter(Boolean), {
cwd: installDir
}).then(function (res) {
if (opts.dryRun) {
(opts.logger ? opts.logger : logger_1["default"])(res.command);
(opts.logger ? opts.logger : logger_1["default"])(res.stdout);
}
})];
case 1:
// This will install packages required for the SDK within its installed directory in `.apis/`.
_a.sent();
// This will install the installed SDK as a dependency within the current working directory,
// adding `@api/<sdk identifier>` as a dependency there so you can load it with
// `require('@api/<sdk identifier>)`.
return [2 /*return*/, (0, execa_1["default"])('npm', __spreadArray(__spreadArray([], npmInstall, true), [installDir], false).filter(Boolean))
.then(function (res) {
if (opts.dryRun) {
(opts.logger ? opts.logger : logger_1["default"])(res.command);
(opts.logger ? opts.logger : logger_1["default"])(res.stdout);
}
})["catch"](function (err) {
if (opts.dryRun) {
(opts.logger ? opts.logger : logger_1["default"])(err.message);
return;
}
throw err;
})];
}
});
});
};
/**
* Compile the current OpenAPI definition into a TypeScript library.
*
*/
TSGenerator.prototype.generator = function () {
return __awaiter(this, void 0, void 0, function () {
var sdkSource, types;
var _this = this;
return __generator(this, function (_a) {
sdkSource = this.createSourceFile();
if (Object.keys(this.schemas).length) {
this.createSchemasFile();
this.createTypesFile();
/**
* Export all of our available types so they can be used in SDK implementations. Types are
* exported individually because TS has no way right now of allowing us to do
* `export type * from './types'` on a non-named entry.
*
* Types in the main entry point are only being exported for TS outputs as JS users won't be
* able to use them and it clashes with the default SDK export present.
*
* @see {@link https://github.com/microsoft/TypeScript/issues/37238}
* @see {@link https://github.com/readmeio/api/issues/588}
*/
if (!this.outputJS) {
types = Array.from(this.types.keys());
types.sort();
sdkSource.addExportDeclarations([
{
isTypeOnly: true,
namedExports: types,
moduleSpecifier: './types'
},
]);
}
}
else {
// If we don't have any schemas then we shouldn't import a `types` file that doesn't exist.
sdkSource
.getImportDeclarations()
.find(function (id) { return id.getText() === "import type * as types from './types';"; })
.remove();
}
// If this SDK doesn't use the `HTTPMethodRange` interface for handling `2XX` response status
// codes then we should remove it from being imported.
if (!this.usesHTTPMethodRangeInterface) {
sdkSource
.getImportDeclarations()
.find(function (id) { return id.getText().includes('HTTPMethodRange'); })
.replaceWithText("import type { ConfigOptions, FetchResponse } from 'api/dist/core'");
}
if (this.outputJS) {
return [2 /*return*/, this.project
.emitToMemory()
.getFiles()
.map(function (sourceFile) {
var _a;
var file = path_1["default"].basename(sourceFile.filePath);
if (file === 'schemas.js' || file === 'types.js') {
// If we're generating a JS SDK then we don't need to generate these two files as the
// user will have `.d.ts` files for them instead.
return {};
}
var code = sourceFile.text;
if (file === 'index.js' && _this.compilerTarget === 'cjs') {
/**
* There's an annoying quirk with `ts-morph` where if we're exporting a default export
* to a CJS environment, it'll export it as `exports.default`. Because we don't want
* folks in these environments to have to load their SDKs with
* `require('@api/sdk').default` we're overriding that here to change it to being the
* module exports.
*
* `ts-morph` unfortunately doesn't give us any options for programatically doing this
* so we need to resort to modifying the emitted JS code.
*/
code = code
.replace(/Object\.defineProperty\(exports, '__esModule', { value: true }\);\n/, '')
.replace('exports.default = createSDK;', 'module.exports = createSDK;');
}
return _a = {},
_a[file] = code,
_a;
})
.reduce(function (prev, next) { return Object.assign(prev, next); })];
}
return [2 /*return*/, __spreadArray(__spreadArray([], this.project.getSourceFiles().map(function (sourceFile) {
var _a;
return (_a = {},
_a[sourceFile.getBaseName()] = sourceFile.getFullText(),
_a);
}), true), this.project
.emitToMemory({ emitOnlyDtsFiles: true })
.getFiles()
.map(function (sourceFile) {
var _a;
return (_a = {},
_a[path_1["default"].basename(sourceFile.filePath)] = sourceFile.text,
_a);
}), true).reduce(function (prev, next) { return Object.assign(prev, next); })];
});
});
};
/**
* Create our main SDK source file.
*
*/
TSGenerator.prototype.createSourceFile = function () {
var _this = this;
var operations = this.loadOperationsAndMethods().operations;
var sourceFile = this.project.createSourceFile('index.ts', '');
sourceFile.addImportDeclarations([
// This import will be automatically removed later if the SDK ends up not having any types.
{ defaultImport: 'type * as types', moduleSpecifier: './types' },
{
// `HTTPMethodRange` will be conditionally removed later if it ends up not being used.
defaultImport: 'type { ConfigOptions, FetchResponse, HTTPMethodRange }',
moduleSpecifier: 'api/dist/core'
},
{ defaultImport: 'Oas', moduleSpecifier: 'oas' },
{ defaultImport: 'APICore', moduleSpecifier: 'api/dist/core' },
{ defaultImport: 'definition', moduleSpecifier: this.specPath },
]);
// @todo add TOS, License, info.* to a docblock at the top of the SDK.
this.sdk = sourceFile.addClass({
name: 'SDK',
properties: [
{ name: 'spec', type: 'Oas' },
{ name: 'core', type: 'APICore' },
]
});
this.sdk.addConstructor({
statements: function (writer) {
writer.writeLine('this.spec = Oas.init(definition);');
writer.write('this.core = new APICore(this.spec, ').quote(_this.userAgent).write(');');
return writer;
}
});
// Add our core API methods for controlling auth, servers, and various configurable abilities.
this.sdk.addMethods([
{
name: 'config',
parameters: [{ name: 'config', type: 'ConfigOptions' }],
statements: function (writer) { return writer.writeLine('this.core.setConfig(config);'); },
docs: [
{
description: function (writer) {
return writer.writeLine((0, util_1.wordWrap)('Optionally configure various options that the SDK allows.'));
},
tags: [
{ tagName: 'param', text: 'config Object of supported SDK options and toggles.' },
{
tagName: 'param',
text: (0, util_1.wordWrap)('config.timeout Override the default `fetch` request timeout of 30 seconds. This number should be represented in milliseconds.')
},
]
},
]
},
{
name: 'auth',
parameters: [{ name: '...values', type: 'string[] | number[]' }],
statements: function (writer) {
writer.writeLine('this.core.setAuth(...values);');
writer.writeLine('return this;');
return writer;
},
docs: [
{
description: function (writer) {
return 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.\n\nWith the exception of OpenID and MutualTLS, it supports all forms of authentication supported by the OpenAPI specification.\n\n@example <caption>HTTP Basic auth</caption>\nsdk.auth('username', 'password');\n\n@example <caption>Bearer tokens (HTTP or OAuth 2)</caption>\nsdk.auth('myBearerToken');\n\n@example <caption>API Keys</caption>\nsdk.auth('myApiKey');"));
},
tags: [
{ tagName: 'see', text: '{@link https://spec.openapis.org/oas/v3.0.3#fixed-fields-22}' },
{ tagName: 'see', text: '{@link https://spec.openapis.org/oas/v3.1.0#fixed-fields-22}' },
{
tagName: 'param',
text: 'values Your auth credentials for the API; can specify up to two strings or numbers.'
},
]
},
]
},
{
name: 'server',
parameters: [
{ name: 'url', type: 'string' },
{ name: 'variables', initializer: '{}' },
],
statements: function (writer) { return writer.writeLine('this.core.setServer(url, variables);'); },
docs: [
{
description: function (writer) {
return 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).\n\n@example <caption>Server URL with server variables</caption>\nsdk.server('https://{region}.api.example.com/{basePath}', {\n name: 'eu',\n basePath: 'v14',\n});\n\n@example <caption>Fully qualified server URL</caption>\nsdk.server('https://eu.api.example.com/v14');"));
},
tags: [
{ tagName: 'param', text: 'url Server URL' },
{ tagName: 'param', text: 'variables An object of variables to replace into the server URL.' },
]
},
]
},
]);
// Add all available operation ID accessors into the SDK.
Object.entries(operations).forEach(function (_a) {
var operationId = _a[0], data = _a[1];
_this.createOperationAccessor(data.operation, operationId, data.types.params, data.types.responses);
});
// Export our SDK into the source file.
sourceFile.addVariableStatement({
declarationKind: ts_morph_1.VariableDeclarationKind.Const,
declarations: [
{
name: 'createSDK',
initializer: function (writer) {
// `ts-morph` doesn't have any way to cleanly create an IFEE.
writer.writeLine('(() => { return new SDK(); })()');
return writer;
}
},
]
});
sourceFile.addExportAssignment({
// Because CJS targets have `createSDK` exported with `module.exports`, but the TS type side
// of things to work right we need to set this as `export =`. Thankfully `ts-morph` will
// handle this accordingly and still create our JS file with `module.exports` and not
// `export =` -- only TS types will have this export style.
isExportEquals: this.compilerTarget === 'cjs' && this.outputJS,
expression: 'createSDK'
});
return sourceFile;
};
/**
* Create our main schemas file. This is where all of the JSON Schema that our TypeScript typing
* infrastructure sources its data from. Without this there are no types.
*
*/
TSGenerator.prototype.createSchemasFile = function () {
var sourceFile = this.project.createSourceFile('schemas.ts', '');
var sortedSchemas = new Map(Array.from(Object.entries(this.schemas)).sort());
Array.from(sortedSchemas).forEach(function (_a) {
var schemaName = _a[0], schema = _a[1];
sourceFile.addVariableStatement({
declarationKind: ts_morph_1.VariableDeclarationKind.Const,
declarations: [
{
name: schemaName,
initializer: function (writer) {
/**
* This is the conversion prefix that we add to all `$ref` pointers we find in
* generated JSON Schema.
*
* Because the pointer name is a string we want to have it reference the schema
* constant we're adding into the codegen'd schema file. As there's no way, not even
* using `eval()` in this case, to convert a string to a constant we're prefixing
* them with this so we can later remove it and rewrite the value to a literal.
* eg. `'Pet'` becomes `Pet`.
*
* And because our TypeScript type name generator properly ignores `:`, this is safe
* to prepend to all generated type names.
*/
var str = JSON.stringify(schema);
str = str.replace(/"::convert::([a-zA-Z_$\\d]*)"/g, '$1');
writer.writeLine("".concat(str, " as const"));
return writer;
}
},
]
});
});
sourceFile.addStatements("export { ".concat(Array.from(sortedSchemas.keys()).join(', '), " }"));
return sourceFile;
};
/**
* Create our main types file. This sources its data from the JSON Schema `schemas.ts` file and
* will re-export types to be used in TypeScript implementations and IDE intellisense. This
* typing work is functional with the `json-schema-to-ts` library.
*
* @see {@link https://npm.im/json-schema-to-ts}
*/
TSGenerator.prototype.createTypesFile = function () {
var sourceFile = this.project.createSourceFile('types.ts', '');
sourceFile.addImportDeclarations([
{ defaultImport: 'type { FromSchema }', moduleSpecifier: 'json-schema-to-ts' },
{ defaultImport: '* as schemas', moduleSpecifier: './schemas' },
]);
Array.from(new Map(Array.from(this.types.entries()).sort())).forEach(function (_a) {
var typeName = _a[0], typeExpression = _a[1];
sourceFile.addTypeAlias({ isExported: true, name: typeName, type: typeExpression });
});
return sourceFile;
};
/**
* Add a new JSDoc `@tag` to an existing docblock.
*
*/
TSGenerator.addTagToDocblock = function (docblock, tag) {
var _a;
var tags = (_a = docblock.tags) !== null && _a !== void 0 ? _a : [];
tags.push(tag);
return __assign(__assign({}, docblock), { tags: tags });
};
/**
* Create operation accessors on the SDK.
*
*/
TSGenerator.prototype.createOperationAccessor = function (operation, operationId, paramTypes, responseTypes) {
var _this = this;
var docblock = {};
var summary = operation.getSummary();
var description = operation.getDescription();
if (summary || description) {
// To keep our generated docblocks clean we should only add the `@summary` tag if we've
// got both a summary and a description present on the operation, otherwise we can alternate
// what we surface the main docblock description.
docblock.description = function (writer) {
if (description) {
writer.writeLine((0, util_1.docblockEscape)((0, util_1.wordWrap)(description)));
}
else if (summary) {
writer.writeLine((0, util_1.docblockEscape)((0, util_1.wordWrap)(summary)));
}
writer.newLineIfLastNot();
return writer;
};
if (summary && description) {
docblock = TSGenerator.addTagToDocblock(docblock, {
tagName: 'summary',
text: (0, util_1.docblockEscape)((0, util_1.wordWrap)(summary))
});
}
}
var hasOptionalBody = false;
var hasOptionalMetadata = false;
var parameters = {};
if (paramTypes) {
// If an operation has a request body payload it will only ever have `body` or `formData`,
// never both, as these are determined upon the media type that's in use.
if (paramTypes.body || paramTypes.formData) {
hasOptionalBody = !operation.hasRequiredRequestBody();
parameters.body = {
name: 'body',
type: paramTypes.body ? paramTypes.body : paramTypes.formData,
hasQuestionToken: hasOptionalBody
};
}
if (paramTypes.metadata) {
hasOptionalMetadata = !operation.hasRequiredParameters();
parameters.metadata = {
name: 'metadata',
type: paramTypes.metadata,
hasQuestionToken: hasOptionalMetadata
};
}
}
var returnType = 'Promise<FetchResponse<number, unknown>>';
if (responseTypes) {
var returnTypes = Object.entries(responseTypes)
.map(function (_a) {
var status = _a[0], _b = _a[1], responseDescription = _b.description, responseType = _b.type;
if (status.toLowerCase() === 'default') {
return "FetchResponse<number, ".concat(responseType, ">");
}
else if (status.length === 3 && status.toUpperCase().endsWith('XX')) {
var statusPrefix = status.slice(0, 1);
if (!Number.isInteger(Number(statusPrefix))) {
// If this matches the `_XX` format, but it isn't `{number}XX` then we can't handle
// it and should instead fall back to treating it as an unknown number.
return "FetchResponse<number, ".concat(responseType, ">");
}
if (Number(statusPrefix) >= 4) {
docblock = TSGenerator.addTagToDocblock(docblock, {
tagName: 'throws',
text: "FetchError<".concat(status, ", ").concat(responseType, ">").concat(responseDescription ? (0, util_1.docblockEscape)((0, util_1.wordWrap)(" ".concat(responseDescription))) : '')
});
return false;
}
_this.usesHTTPMethodRangeInterface = true;
return "FetchResponse<HTTPMethodRange<".concat(statusPrefix, "00, ").concat(statusPrefix, "99>, ").concat(responseType, ">");
}
// 400 and 500 status code families are thrown as exceptions so adding them as a possible
// return type isn't valid.
if (Number(status) >= 400) {
docblock = TSGenerator.addTagToDocblock(docblock, {
tagName: 'throws',
text: "FetchError<".concat(status, ", ").concat(responseType, ">").concat(responseDescription ? (0, util_1.docblockEscape)((0, util_1.wordWrap)(" ".concat(responseDescription))) : '')
});
return false;
}
return "FetchResponse<".concat(status, ", ").concat(responseType, ">");
})
.filter(Boolean)
.join(' | ');
// If all of our documented responses are for error status codes then all we can document for
// anything else that might happen is `unknown`.
returnType = "Promise<".concat(returnTypes.length ? returnTypes : 'FetchResponse<number, unknown>', ">");
}
var shouldAddAltTypedOverloads = Object.keys(parameters).length === 2 && hasOptionalBody && !hasOptionalMetadata;
var operationIdAccessor = this.sdk.addMethod({
name: operationId,
returnType: returnType,
// If we're going to be creating typed method overloads for optional body an metadata handling
// we should only add a docblock to the first overload we create because IDE Intellisense will
// always use that and adding a docblock to all three will bloat the SDK with unused and
// unsurfaced method documentation.
docs: shouldAddAltTypedOverloads ? null : Object.keys(docblock).length ? [docblock] : null,
statements: function (writer) {
/**
* @example return this.core.fetch('/pet/findByStatus', 'get', body, metadata);
* @example return this.core.fetch('/pet/findByStatus', 'get', metadata);
*/
var fetchStmt = writer
.write('return this.core.fetch(')
.quote(operation.path)
.write(', ')
.quote(operation.method);
var totalParams = Object.keys(parameters).length;
if (totalParams) {
Object.values(parameters).forEach(function (arg, i) {
if (i === 0) {
fetchStmt.write(', ');
}
fetchStmt.write(arg.name);
if (i !== totalParams - 1) {
fetchStmt.write(', ');
}
});
}
fetchStmt.write(');');
return fetchStmt;
}
});
// If we have both body and metadata parameters but only body is optional we need to create
// a couple function overloads as Typescript doesn't let us have an optional method parameter
// come before one that's required.
if (shouldAddAltTypedOverloads) {
// Create an overload that has both `body` and `metadata` parameters as required.
operationIdAccessor.addOverload({
parameters: [
__assign(__assign({}, parameters.body), { hasQuestionToken: false }),
__assign(__assign({}, parameters.metadata), { hasQuestionToken: false }),
],
returnType: returnType,
docs: Object.keys(docblock).length ? [docblock] : null
});
// Create an overload that just has a single `metadata` parameter.
operationIdAccessor.addOverload({
parameters: [__assign({}, parameters.metadata)],
returnType: returnType
});
// Create an overload that has both `body` and `metadata` parameters as optional. Even though
// our `metadata` parameter is actually required for this operation this is the only way we're
// able to have an optional `body` parameter be present before `metadata`.
//
// Thankfully our core fetch work in `api/dist/core` is able to do the proper determination to
// see if what the user is supplying is `metadata` or `body` content when they supply one or
// both.
operationIdAccessor.addParameters([
__assign(__assign({}, parameters.body), {
// Overloads have to be the most distilled version of the method so that's why we need to
// type `body` as either `body` or `metadata`. If we didn't do this, if `body` was a JSON
// Schema type that didn't allow `additionalProperties` then the implementation overload
// would throw type errors.
type: "".concat(parameters.body.type, " | ").concat(parameters.metadata.type), hasQuestionToken: true }),
__assign(__assign({}, parameters.metadata), { hasQuestionToken: true }),
]);
}
else {
operationIdAccessor.addParameters(Object.values(parameters));
}
};
/**
* Scour through the current OpenAPI definition and compile a store of every operation, along
* with every HTTP method that's in use, and their available TypeScript types that we can use,
* along with every HTTP method that's in use.
*
*/
TSGenerator.prototype.loadOperationsAndMethods = function () {
var _this = this;
var operations = {};
var methods = new Set();
// Prepare all of the schemas that we need to process for every operation within this API
// definition.
Object.entries(this.spec.getPaths()).forEach(function (_a) {
var ops = _a[1];
Object.entries(ops).forEach(function (_a) {
var method = _a[0], operation = _a[1];
methods.add(method);
var operationId = operation.getOperationId({
// This `camelCase` option will clean up any weird characters that might be present in
// the `operationId` so as we don't break TS compilation with an invalid method accessor.
camelCase: true
});
operations[operationId] = {
types: {
params: _this.prepareParameterTypesForOperation(operation, operationId),
responses: _this.prepareResponseTypesForOperation(operation, operationId)
},
operation: operation
};
});
});
if (!Object.keys(operations).length) {
throw new Error('Sorry, this OpenAPI definition does not have any operation paths to generate an SDK for.');
}
return {
operations: operations,
methods: methods
};
};
/**
* Compile the parameter (path, query, cookie, and header) schemas for an API operation into
* usable TypeScript types.
*
*/
TSGenerator.prototype.prepareParameterTypesForOperation = function (operation, operationId) {
var _this = this;
var schemas = operation.getParametersAsJSONSchema({
includeDiscriminatorMappingRefs: false,
mergeIntoBodyAndMetadata: true,
retainDeprecatedProperties: true,
transformer: function (s) {
// As our schemas are dereferenced in the `oas` library we don't want to pollute our
// codegen'd schemas file with duplicate schemas.
if ('x-readme-ref-name' in s) {
var typeName = (0, util_1.generateTypeName)(s['x-readme-ref-name']);
_this.addSchemaToExport(s, typeName, typeName);
return "::convert::".concat(typeName);
}
return s;
}
});
if (!schemas || !schemas.length) {
return false;
}
var res = schemas
.map(function (param) {
var _a;
return (_a = {}, _a[param.type] = param.schema, _a);
})
.reduce(function (prev, next) { return Object.assign(prev, next); });
return Object.entries(res)
.map(function (_a) {
var _b;
var paramType = _a[0], schema = _a[1];
var typeName;
if (typeof schema === 'string' && schema.startsWith('::convert::')) {
// If this schema is a string and has our conversion prefix then we've already created
// a type for it.
typeName = schema.replace('::convert::', '');
}
else {
typeName = (0, util_1.generateTypeName)(operationId, paramType, 'param');
_this.addSchemaToExport(schema, typeName, "".concat((0, util_1.generateTypeName)(operationId), ".").concat(paramType));
}
return _b = {},
// Types are prefixed with `types.` because that's how we're importing them from
// `types.d.ts`.
_b[paramType] = "types.".concat(typeName),
_b;
})
.reduce(function (prev, next) { return Object.assign(prev, next); }, {});
};
/**
* Compile the response schemas for an API operation into usable TypeScript types.
*
*/
TSGenerator.prototype.prepareResponseTypesForOperation = function (operation, operationId) {
var _this = this;
var responseStatusCodes = operation.getResponseStatusCodes();
if (!responseStatusCodes.length) {
return undefined;
}
var schemas = responseStatusCodes
.map(function (status) {
var _a;
var schema = operation.getResponseAsJSONSchema(status, {
includeDiscriminatorMappingRefs: false,
transformer: function (s) {
// As our schemas are dereferenced in the `oas` library we don't want to pollute our
// codegen'd schemas file with duplicate schemas.
if ('x-readme-ref-name' in s) {
var typeName = (0, util_1.generateTypeName)(s['x-readme-ref-name']);
_this.addSchemaToExport(s, typeName, "".concat(typeName));
return "::convert::".concat(typeName);
}
return s;
}
});
if (!schema) {
return false;
}
return _a = {},
_a[status] = schema.shift(),
_a;
})
.reduce(function (prev, next) { return Object.assign(prev, next); });
var res = Object.entries(schemas)
.map(function (_a) {
var _b;
var status = _a[0], _c = _a[1], description = _c.description, schema = _c.schema;
var typeName;
if (typeof schema === 'string' && schema.startsWith('::convert::')) {
// If this schema is a string and has our conversion prefix then we've already created
// a type for it.
typeName = schema.replace('::convert::', '');
}
else {
typeName = (0, util_1.generateTypeName)(operationId, 'response', status);
// Because `status` will usually be a number here we need to set the pointer for it
// within an `[]` as if we do `FromSchema<typeof schemas.operation.response.200>`,
// TypeScript will throw a compilation error.
_this.addSchemaToExport(schema, typeName, "".concat((0, util_1.generateTypeName)(operationId), ".response['").concat(status, "']"));
}
return _b = {},
// Types are prefixed with `types.` because that's how we're importing them from
// `types.d.ts`.
_b[status] = {
type: "types.".concat(typeName),
description: description
},
_b;
})
.reduce(function (prev, next) { return Object.assign(prev, next); }, {});
return Object.keys(res).length ? res : undefined;
};
/**
* Add a given schema into our schema dataset that we'll be be exporting as types.
*
*/
TSGenerator.prototype.addSchemaToExport = function (schema, typeName, pointer) {
if (this.types.has(typeName)) {
return;
}
(0, setWith_1["default"])(this.schemas, pointer, schema, Object);
this.types.set(typeName, "FromSchema<typeof schemas.".concat(pointer, ">"));
};
return TSGenerator;
}(language_1["default"]));
exports["default"] = TSGenerator;