UNPKG

api

Version:

Magical SDK generation from an OpenAPI definition 🪄

278 lines (277 loc) • 13.5 kB
"use strict"; 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 __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 make_dir_1 = __importDefault(require("make-dir")); var ssri_1 = __importDefault(require("ssri")); var validate_npm_package_name_1 = __importDefault(require("validate-npm-package-name")); var fetcher_1 = __importDefault(require("../fetcher")); var packageInfo_1 = require("../packageInfo"); var Storage = /** @class */ (function () { function Storage(source, identifier) { Storage.setStorageDir(); this.fetcher = new fetcher_1["default"](source); this.source = source; this.identifier = identifier; // This should default to false so we have awareness if we've looked at the lockfile yet. Storage.lockfile = false; } Storage.getLockfilePath = function () { return path_1["default"].join(Storage.dir, 'api.json'); }; Storage.getAPIsDir = function () { return path_1["default"].join(Storage.dir, 'apis'); }; Storage.setStorageDir = function (dir) { if (dir) { Storage.dir = dir; return; } else if (Storage.dir) { // If we already have a storage dir set and aren't explicitly it to something new then we // shouldn't overwrite what we've already got. return; } Storage.dir = make_dir_1["default"].sync(path_1["default"].join(process.cwd(), '.api')); make_dir_1["default"].sync(Storage.getAPIsDir()); }; /** * Reset the state of the entire storage system. * * This will completely destroy the contents of the `.api/` directory! */ Storage.reset = function () { return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) { switch (_a.label) { case 0: if (!Storage.getLockfilePath()) return [3 /*break*/, 2]; return [4 /*yield*/, fs_1["default"].promises.writeFile(Storage.getLockfilePath(), JSON.stringify(Storage.getDefaultLockfile(), null, 2))]; case 1: _a.sent(); _a.label = 2; case 2: if (!Storage.getAPIsDir()) return [3 /*break*/, 5]; return [4 /*yield*/, fs_1["default"].promises.rm(Storage.getAPIsDir(), { recursive: true })]; case 3: _a.sent(); return [4 /*yield*/, fs_1["default"].promises.mkdir(Storage.getAPIsDir(), { recursive: true })]; case 4: _a.sent(); _a.label = 5; case 5: return [2 /*return*/]; } }); }); }; Storage.getDefaultLockfile = function () { return { version: '1.0', apis: [] }; }; Storage.generateIntegrityHash = function (definition) { return ssri_1["default"] .fromData(JSON.stringify(definition), { algorithms: ['sha512'] }) .toString(); }; Storage.getLockfile = function () { if (typeof Storage.lockfile === 'object') { return Storage.lockfile; } if (fs_1["default"].existsSync(Storage.getLockfilePath())) { var file = fs_1["default"].readFileSync(Storage.getLockfilePath(), 'utf8'); Storage.lockfile = JSON.parse(file); } else { Storage.lockfile = Storage.getDefaultLockfile(); } return Storage.lockfile; }; Storage.isIdentifierValid = function (identifier, prefixWithAPINamespace) { // Is this identifier already in storage? if (Storage.isInLockFile({ identifier: identifier })) { throw new Error("\"".concat(identifier, "\" is already taken in your `.api/` directory. Please try another identifier.")); } var isValidForNPM = (0, validate_npm_package_name_1["default"])(prefixWithAPINamespace ? "@api/".concat(identifier) : identifier); if (!isValidForNPM.validForNewPackages) { // `prompts` doesn't support surfacing multiple errors in a `validate` call so we can only // surface the first to the user. throw new Error("Identifier cannot be used for an NPM package: ".concat(isValidForNPM.errors[0])); } return true; }; Storage.isInLockFile = function (search) { // Because this method may run before we initialize a new storage object we should make sure // that we have a storage directory present. Storage.setStorageDir(); if (!search.identifier && !search.source) { throw new TypeError('An `identifier` or `source` must be supplied to this method to search in the lockfile.'); } var lockfile = Storage.getLockfile(); if (typeof lockfile !== 'object' || lockfile === null || !lockfile.apis) { return false; } var res = lockfile.apis.find(function (a) { if (search.identifier) { return a.identifier === search.identifier; } return a.source === search.source; }); return res === undefined ? false : res; }; Storage.prototype.setIdentifier = function (identifier) { this.identifier = identifier; }; /** * Determine if the current spec + identifier we're working with is already in the lockfile. */ Storage.prototype.isInLockfile = function () { return Boolean(this.getFromLockfile()); }; /** * Retrieve the lockfile record for the current spec + identifier if it exists in the lockfile. */ Storage.prototype.getFromLockfile = function () { var _this = this; var lockfile = Storage.getLockfile(); return lockfile.apis.find(function (a) { return a.identifier === _this.identifier; }); }; Storage.prototype.getIdentifierStorageDir = function () { if (!this.isInLockfile()) { throw new Error("".concat(this.source, " has not been saved to storage yet and must do so before being retrieved.")); } return path_1["default"].join(Storage.getAPIsDir(), this.identifier); }; Storage.prototype.getAPIDefinition = function () { var file = fs_1["default"].readFileSync(path_1["default"].join(this.getIdentifierStorageDir(), 'openapi.json'), 'utf8'); return JSON.parse(file); }; Storage.prototype.saveSourceFiles = function (files) { var _this = this; if (!this.isInLockfile()) { throw new Error("".concat(this.source, " has not been saved to storage yet and must do so before being retrieved.")); } return new Promise(function (resolve) { var savedSource = []; Object.entries(files).forEach(function (_a) { var fileName = _a[0], contents = _a[1]; var sourceFilePath = path_1["default"].join(_this.getIdentifierStorageDir(), fileName); fs_1["default"].writeFileSync(sourceFilePath, contents); savedSource.push(sourceFilePath); }); resolve(savedSource); }); }; Storage.prototype.load = function () { return __awaiter(this, void 0, void 0, function () { var _this = this; return __generator(this, function (_a) { return [2 /*return*/, this.fetcher.load().then(function (spec) { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { return [2 /*return*/, this.save(spec)]; }); }); })]; }); }); }; /** * @example <caption>Storage directory structure</caption> * .api/ * ├── api.json // The `package-lock.json` equivalent that records everything that's * | // installed, when it was installed, what the original source was, * | // and what version of `api` was used. * └── apis/ * ├── readme/ * | ├── node_modules/ * │ ├── index.js // We may offer the option to export a raw TS file for folks who want * | | // that, but for now it'll be a compiled JS file. * │ ├── index.d.ts // All types for their SDK, ready to use in an IDE. * │ |── openapi.json * │ └── package.json * └── petstore/ * ├── node_modules/ * ├── index.js * ├── index.d.ts * ├── openapi.json * └── package.json * */ Storage.prototype.save = function (spec) { if (!this.identifier) { throw new TypeError('An identifier must be set before saving the API definition into storage.'); } // Create our main `.api/` directory. if (!fs_1["default"].existsSync(Storage.dir)) { fs_1["default"].mkdirSync(Storage.dir, { recursive: true }); } // Create the `.api/apis/` diretory where we'll be storing API definitions. if (!fs_1["default"].existsSync(Storage.getAPIsDir())) { fs_1["default"].mkdirSync(Storage.getAPIsDir(), { recursive: true }); } if (!this.isInLockfile()) { // This API doesn't exist within our storage system yet so we need to record it in the // lockfile. var identifierStorageDir = path_1["default"].join(Storage.getAPIsDir(), this.identifier); var saved = JSON.stringify(spec, null, 2); // Create the `.api/apis/<identifier>` directory where we'll be storing this API definition // and eventually its codegen'd SDK. if (!fs_1["default"].existsSync(identifierStorageDir)) { fs_1["default"].mkdirSync(identifierStorageDir, { recursive: true }); } Storage.lockfile.apis.push({ identifier: this.identifier, source: this.source, integrity: Storage.generateIntegrityHash(spec), installerVersion: packageInfo_1.PACKAGE_VERSION }); fs_1["default"].writeFileSync(path_1["default"].join(identifierStorageDir, 'openapi.json'), saved); fs_1["default"].writeFileSync(Storage.getLockfilePath(), JSON.stringify(Storage.lockfile, null, 2)); } else { // Is this the same spec that we already have? Should we update it? // @todo } return spec; }; return Storage; }()); exports["default"] = Storage;