UNPKG

unified-api

Version:

Easily create and call APIs in Typescript

326 lines (325 loc) 16.1 kB
"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 __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 = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); return g.next = verb(0), g["throw"] = verb(1), g["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 }; } }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ServerApi = exports.ErrorLogMode = exports.ApiTemplate = exports.RequestHelper = exports.RequestMethod = exports.ApiErrors = void 0; exports.createRoute = createRoute; /** * @tested_by tests/index.test.ts */ var ApiErrors; (function (ApiErrors) { var Error = /** @class */ (function () { function Error(res, errorCode, description) { if (errorCode === void 0) { errorCode = 500; } if (description === void 0) { description = "The server encountered an error while processing the request"; } this.errorCode = errorCode; this.description = description; res.error(errorCode, description); } Error.prototype.toString = function () { return "".concat(this.errorCode, ": ").concat(this.description); }; return Error; }()); ApiErrors.Error = Error; var NotFoundError = /** @class */ (function (_super) { __extends(NotFoundError, _super); function NotFoundError(res, routeName) { return _super.call(this, res, 404, "This API Route (/".concat(routeName, ") does not exist")) || this; } return NotFoundError; }(Error)); ApiErrors.NotFoundError = NotFoundError; var InvalidRequestError = /** @class */ (function (_super) { __extends(InvalidRequestError, _super); function InvalidRequestError(res) { return _super.call(this, res, 400, "Invalid Request") || this; } return InvalidRequestError; }(Error)); ApiErrors.InvalidRequestError = InvalidRequestError; var UnauthorizedError = /** @class */ (function (_super) { __extends(UnauthorizedError, _super); function UnauthorizedError(res) { return _super.call(this, res, 403, "You are not authorized to execute this route") || this; } return UnauthorizedError; }(Error)); ApiErrors.UnauthorizedError = UnauthorizedError; var InternalServerError = /** @class */ (function (_super) { __extends(InternalServerError, _super); function InternalServerError(res) { return _super.call(this, res, 500, "The server encountered an error while processing the request") || this; } return InternalServerError; }(Error)); ApiErrors.InternalServerError = InternalServerError; })(ApiErrors || (exports.ApiErrors = ApiErrors = {})); var RequestMethod; (function (RequestMethod) { RequestMethod["POST"] = "POST"; RequestMethod["GET"] = "GET"; })(RequestMethod || (exports.RequestMethod = RequestMethod = {})); var RequestHelper = /** @class */ (function () { function RequestHelper(baseUrl, onError, logPrefix) { if (logPrefix === void 0) { logPrefix = false; } this.baseUrl = baseUrl; this.onError = onError; if (logPrefix) { console.log("Constructed RequestHelper with baseUrl: ".concat(this.baseUrl)); } } RequestHelper.prototype.request = function (route_1, body_1) { return __awaiter(this, arguments, void 0, function (route, body, method) { function getRes(raw) { return __awaiter(this, void 0, void 0, function () { var text, e_1; return __generator(this, function (_a) { switch (_a.label) { case 0: if ("error" in raw) { return [2 /*return*/, raw]; } _a.label = 1; case 1: _a.trys.push([1, 3, , 4]); return [4 /*yield*/, raw.text()]; case 2: text = _a.sent(); // Null and undefined are sent as an empty string that we can't parse as JSON return [2 /*return*/, text.length ? JSON.parse(text) : undefined]; case 3: e_1 = _a.sent(); return [2 /*return*/, { error: "Failed to parse response (".concat(method, " ").concat(parsedRoute.subUrl, "): ").concat(e_1), }]; case 4: return [2 /*return*/]; } }); }); } var parsedRoute, deps, rawResponse, res; var _a, _b, _c; if (method === void 0) { method = RequestMethod.POST; } return __generator(this, function (_d) { switch (_d.label) { case 0: parsedRoute = typeof route === "string" ? { subUrl: route } : route; return [4 /*yield*/, this.getLocalDependencies()]; case 1: deps = _d.sent(); (_a = parsedRoute.beforeCall) === null || _a === void 0 ? void 0 : _a.call(parsedRoute, deps, body); return [4 /*yield*/, fetch(this.baseUrl + parsedRoute.subUrl, { method: method, headers: { Accept: "application/json", "Content-Type": "application/json", }, body: JSON.stringify(body), }).catch(function (e) { return ({ error: e, }); })]; case 2: rawResponse = _d.sent(); return [4 /*yield*/, getRes(rawResponse)]; case 3: res = _d.sent(); if (res === null || res === void 0 ? void 0 : res.error) { if (parsedRoute.fallback) { return [2 /*return*/, (_b = parsedRoute.fallback) === null || _b === void 0 ? void 0 : _b.call(parsedRoute, deps, body).then(function (res) { var _a; (_a = parsedRoute.afterResponse) === null || _a === void 0 ? void 0 : _a.call(parsedRoute, deps, res, true); return res; })]; } this.onError(parsedRoute.subUrl, res.error); } (_c = parsedRoute.afterResponse) === null || _c === void 0 ? void 0 : _c.call(parsedRoute, deps, res, false); return [2 /*return*/, res]; } }); }); }; RequestHelper.prototype.getLocalDependencies = function () { return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) { return [2 /*return*/, {}]; }); }); }; return RequestHelper; }()); exports.RequestHelper = RequestHelper; /** * There's no easy one-liner to create a function with properties while maintaining typing, so I made this shortcut */ function createRoute(config, clientHandler) { return Object.assign(clientHandler !== null && clientHandler !== void 0 ? clientHandler : { subUrl: "newRoute" }, config); } var ApiTemplate = /** @class */ (function () { /** * You need to pass false in subclasses and then call this.init() * @param init Whether to call init() on construction. Pass false if calling super() */ function ApiTemplate(requestHelper, init) { if (init === void 0) { init = true; } this.requestHelper = requestHelper; if (init) { this.init(); } } ApiTemplate.prototype.initSegment = function (requestHelper, segment, subUrl) { var _loop_1 = function (key, value) { if (typeof value === "function") { value.subUrl = (subUrl ? subUrl + "/" : "") + key; } else if (value .subUrl === "newRoute") { var route_1 = value; route_1.subUrl = (subUrl ? subUrl + "/" : "") + key; segment[key] = createRoute(route_1, function () { var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } return requestHelper.request(route_1, args); }); } else if (typeof value === "object") { this_1.initSegment(requestHelper, value, subUrl + "/" + key); } }; var this_1 = this; for (var _i = 0, _a = Object.entries(segment); _i < _a.length; _i++) { var _b = _a[_i], key = _b[0], value = _b[1]; _loop_1(key, value); } }; ApiTemplate.prototype.init = function () { this.initSegment(this.requestHelper, this, ""); }; return ApiTemplate; }()); exports.ApiTemplate = ApiTemplate; var ErrorLogMode; (function (ErrorLogMode) { ErrorLogMode[ErrorLogMode["Throw"] = 0] = "Throw"; ErrorLogMode[ErrorLogMode["Log"] = 1] = "Log"; ErrorLogMode[ErrorLogMode["None"] = 2] = "None"; })(ErrorLogMode || (exports.ErrorLogMode = ErrorLogMode = {})); var ServerApi = /** @class */ (function () { function ServerApi(api, errorLogMode, logPrefix) { if (errorLogMode === void 0) { errorLogMode = ErrorLogMode.Log; } if (logPrefix === void 0) { logPrefix = false; } this.api = api; this.errorLogMode = errorLogMode; this.urlPrefix = api.requestHelper.baseUrl; if (logPrefix) { console.log("Constructed ServerApi with urlPrefix: ".concat(this.urlPrefix)); } } ServerApi.prototype.handle = function (req, rawRes) { return __awaiter(this, void 0, void 0, function () { var res, path, route, deps, body, _a, authorized, authData, e_2; return __generator(this, function (_b) { switch (_b.label) { case 0: res = this.parseRawResponse(rawRes); if (!req.url) { throw new ApiErrors.InvalidRequestError(res); } path = req.url.slice(this.urlPrefix.length).split("/"); _b.label = 1; case 1: _b.trys.push([1, 4, , 5]); route = path.reduce(function (segment, route) { return Object(segment)[route]; }, this.api); if (!(route === null || route === void 0 ? void 0 : route.handler)) throw new ApiErrors.NotFoundError(res, path.join("/")); deps = this.getDependencies(req, res); body = req.body; return [4 /*yield*/, route.isAuthorized(req, res, deps, body)]; case 2: _a = _b.sent(), authorized = _a.authorized, authData = _a.authData; if (!authorized) throw new ApiErrors.UnauthorizedError(res); return [4 /*yield*/, route.handler(req, res, deps, authData, body)]; case 3: _b.sent(); return [3 /*break*/, 5]; case 4: e_2 = _b.sent(); e_2.route = path.join("/"); if (this.errorLogMode === ErrorLogMode.None) return [2 /*return*/]; if (this.errorLogMode === ErrorLogMode.Throw) throw e_2; console.error(e_2); // If it's an error we've already handled, don't do anything if (e_2 instanceof ApiErrors.Error) { return [2 /*return*/]; } new ApiErrors.InternalServerError(res); return [3 /*break*/, 5]; case 5: return [2 /*return*/]; } }); }); }; ServerApi.prototype.parseRawResponse = function (rawRes) { return rawRes; }; return ServerApi; }()); exports.ServerApi = ServerApi;