"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()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.writeError = exports.TwirpServer = void 0; const hooks_1 = require("./hooks"); const request_1 = require("./request"); const errors_1 = require("./errors"); /** * Runtime server implementation of a TwirpServer */ class TwirpServer { constructor(options) { this.pathPrefix = "/twirp"; this.hooks = []; this.interceptors = []; this.packageName = options.packageName; this.serviceName = options.serviceName; this.methodList = options.methodList; this.matchRoute = options.matchRoute; this.service = options.service; } /** * Returns the prefix for this server */ get prefix() { return this.pathPrefix; } /** * The http handler for twirp complaint endpoints * @param options */ httpHandler(options) { return (req, resp) => { // setup prefix if ((options === null || options === void 0 ? void 0 : options.prefix) !== undefined) { this.withPrefix(options.prefix); } return this._httpHandler(req, resp); }; } /** * Adds interceptors or hooks to the request stack * @param middlewares */ use(...middlewares) { middlewares.forEach((middleware) => { if (hooks_1.isHook(middleware)) { this.hooks.push(middleware); return this; } this.interceptors.push(middleware); }); return this; } /** * Adds a prefix to the service url path * @param prefix */ withPrefix(prefix) { if (prefix === false) { this.pathPrefix = ""; } else { this.pathPrefix = prefix; } return this; } /** * Returns the regex matching path for this twirp server */ matchingPath() { const baseRegex = this.baseURI().replace(/\./g, "\\."); return new RegExp(`${baseRegex}\/(${this.methodList.join("|")})`); } /** * Returns the base URI for this twirp server */ baseURI() { return `${this.pathPrefix}/${this.packageName ? this.packageName + "." : ""}${this.serviceName}`; } /** * Create a twirp context * @param req * @param res * @private */ createContext(req, res) { return { packageName: this.packageName, serviceName: this.serviceName, methodName: "", contentType: request_1.getContentType(req.headers["content-type"]), req: req, res: res, }; } /** * Twrip server http handler implementation * @param req * @param resp * @private */ _httpHandler(req, resp) { return __awaiter(this, void 0, void 0, function* () { const ctx = this.createContext(req, resp); try { yield this.invokeHook("requestReceived", ctx); const { method, mimeContentType } = request_1.validateRequest(ctx, req, this.pathPrefix || ""); const handler = this.matchRoute(method, { onMatch: (ctx) => { return this.invokeHook("requestRouted", ctx); }, onNotFound: () => { const msg = `no handler for path ${req.url}`; throw new errors_1.BadRouteError(msg, req.method || "", req.url || ""); }, }); const body = yield request_1.getRequestData(req); const response = yield handler(ctx, this.service, body, this.interceptors); yield Promise.all([ this.invokeHook("responsePrepared", ctx), // keep backwards compatibility till next release this.invokeHook("requestPrepared", ctx), ]); resp.statusCode = 200; resp.setHeader("Content-Type", mimeContentType); resp.end(response); } catch (e) { yield this.invokeHook("error", ctx, mustBeTwirpError(e)); if (!resp.headersSent) { writeError(resp, e); } } finally { yield Promise.all([ this.invokeHook("responseSent", ctx), // keep backwards compatibility till next release this.invokeHook("requestSent", ctx), ]); } }); } /** * Invoke a hook * @param hookName * @param ctx * @param err * @protected */ invokeHook(hookName, ctx, err) { return __awaiter(this, void 0, void 0, function* () { if (this.hooks.length === 0) { return; } const chainedHooks = hooks_1.chainHooks(...this.hooks); const hook = chainedHooks === null || chainedHooks === void 0 ? void 0 : chainedHooks[hookName]; if (hook) { yield hook(ctx, err || new errors_1.InternalServerError("internal server error")); } }); } } exports.TwirpServer = TwirpServer; /** * Write http error response * @param res * @param error */ function writeError(res, error) { const twirpError = mustBeTwirpError(error); res.setHeader("Content-Type", "application/json"); res.statusCode = errors_1.httpStatusFromErrorCode(twirpError.code); res.end(twirpError.toJSON()); } exports.writeError = writeError; /** * Make sure that the error passed is a TwirpError * otherwise it will wrap it into an InternalError * @param err */ function mustBeTwirpError(err) { if (err instanceof errors_1.TwirpError) { return err; } return new errors_1.InternalServerErrorWith(err); }