unified-api
Version:
Easily create and call APIs in Typescript
326 lines (325 loc) • 16.1 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 __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;