UNPKG

moleculer-api

Version:

A dynamic API Gateway for MoleculerJS which updates REST endpoints and aggregated GraphQL schema, access control policy for each action calls from metadata of remote services schema without restart or deployment.

390 lines 18.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.RESTProtocolPlugin = void 0; const tslib_1 = require("tslib"); const _ = tslib_1.__importStar(require("lodash")); const interface_1 = require("../../../../interface"); const server_1 = require("../../../../server"); const plugin_1 = require("../plugin"); const handler_1 = require("./handler"); const connector_1 = require("../../connector"); class RESTProtocolPlugin extends plugin_1.ProtocolPlugin { constructor(props, opts) { super(props); this.props = props; this.opts = _.defaultsDeep(opts || {}, RESTProtocolPlugin.autoLoadOptions); } start() { return tslib_1.__awaiter(this, void 0, void 0, function* () { }); } stop() { return tslib_1.__awaiter(this, void 0, void 0, function* () { }); } validateSchema(schema) { const routeMethodAndPaths = []; return interface_1.validateObject(schema, { description: { type: "string", optional: true, }, basePath: { type: "custom", check(value) { if (server_1.HTTPRoute.isNonRootStaticPath(value)) { return true; } return [{ type: "basePathInvalid", field: "basePath", actual: value, expected: server_1.HTTPRoute.nonRootStaticPathRegExp, message: `basePath should be a valid non-root static path: eg. "/players" | "/players/billings"`, }]; }, }, routes: { type: "array", empty: false, items: { type: "custom", check(value) { const idx = schema.routes.indexOf(value); if (typeof value !== "object") { return [{ field: `routes[${idx}]`, type: "type", message: "route definition should be an object", actual: value, }]; } const { path, deprecated, description, method } = value, restProps = tslib_1.__rest(value, ["path", "deprecated", "description", "method"]); // path: string; if (!server_1.HTTPRoute.isNonRootDynamicPath(path) && !server_1.HTTPRoute.isRootStaticPath(path)) { return [{ field: `routes[${idx}].path`, type: "routePathInvalid", actual: path, expected: [server_1.HTTPRoute.nonRootDynamicPath, server_1.HTTPRoute.rootStaticPathRegExp], message: `route path should be a valid path: eg. "/" | "/accounts" | "/accounts/:id"`, }]; } // description?: string; // deprecated?: boolean; // method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" const errors = interface_1.validateObject({ deprecated, description, method }, { description: { type: "string", optional: true, }, deprecated: { type: "boolean", optional: true, }, method: { type: "enum", values: ["GET", "POST", "PUT", "PATCH", "DELETE"], }, }, { strict: true, field: `routes[${idx}]`, }); if (errors.length === 0) { // check duplicate path const routePathAndMethod = `${method} ${path}`; if (routeMethodAndPaths.includes(routePathAndMethod)) { return [{ field: `routes[${idx}].path`, type: "routePathDuplicate", actual: routePathAndMethod, expected: undefined, message: `a pair of route method and path should be unique"`, }]; } routeMethodAndPaths.push(routePathAndMethod); } // validate method and each connector let rule; switch (method) { case "GET": if (typeof restProps.call !== "undefined") { rule = { type: "object", strict: true, props: { call: connector_1.ConnectorValidator.call, ignoreError: { type: "boolean", optional: true, }, }, messages: { objectStrict: "RESTCallableRouteResolverSchema cannot be with other connectors", }, }; } else if (typeof restProps.map !== "undefined") { rule = { type: "object", strict: true, props: { map: connector_1.ConnectorValidator.map, }, messages: { objectStrict: "RESTMappableRouteResolverSchema cannot be with other connectors", }, }; } else { errors.push({ type: "routeInvalid", field: `routes[${idx}]`, message: `${method} route should have either call or map property`, expected: "RESTCallableRouteResolverSchema | RESTMappableRouteResolverSchema", }); } break; case "POST": case "PUT": case "PATCH": case "DELETE": if (typeof restProps.call !== "undefined") { rule = { type: "object", strict: true, props: { call: connector_1.ConnectorValidator.call, }, messages: { objectStrict: "RESTCallableRouteResolverSchema cannot be with other connectors", }, }; } else if (restProps.publish !== "undefined") { rule = { type: "object", strict: true, props: { publish: connector_1.ConnectorValidator.publish, }, messages: { objectStrict: "RESTPublishableRouteResolverSchema cannot be with other connectors", }, }; } else { errors.push({ type: "routeInvalid", field: `routes[${idx}]`, message: `${method} route should have either call or publish property`, expected: `Omit<RESTCallableRouteResolverSchema, "ignoreError"> | RESTPublishableRouteResolverSchema`, }); } break; } // @ts-ignore if (rule) { errors.push(...interface_1.validateValue(restProps, // @ts-ignore rule, { strict: true, field: `routes[${idx}]`, })); } return errors; }, }, }, }, { strict: true, }); } compileSchemata(routeHashMapCache, integrations, branch) { const items = new Array(); // introspection routes if (this.opts.introspection) { const introspectionRouteHash = "static@introspection"; items.push({ hash: introspectionRouteHash, route: routeHashMapCache.get(introspectionRouteHash) || new server_1.HTTPRoute({ path: "/~status", method: "GET", description: `${branch.name} branch introspection endpoint`, handler: (context, req, res) => tslib_1.__awaiter(this, void 0, void 0, function* () { this.sendResponse(res, branch.getInformation(true)); }), }), }); } for (const integration of integrations) { const schema = integration.schema.protocol[this.key]; for (const routeSchema of schema.routes) { // the source object below hash contains properties which can make this route unique const routeHash = interface_1.hashObject([schema.basePath, routeSchema, integration.service.hash], true); // cache hit const cachedRoute = routeHashMapCache.get(routeHash); if (cachedRoute) { items.push({ hash: routeHash, route: cachedRoute }); continue; } const path = server_1.HTTPRoute.mergePaths(schema.basePath, routeSchema.path); const method = routeSchema.method; let route; switch (method) { case "GET": if (routeSchema.call) { route = this.createRouteFromCallConnectorScheme(path, method, routeSchema, integration); } else if (routeSchema.map) { route = this.createRouteFromMapConnectorScheme(path, method, routeSchema, integration); } break; default: if (routeSchema.call) { route = this.createRouteFromCallConnectorScheme(path, method, routeSchema, integration); } else if (routeSchema.publish) { route = this.createRouteFromPublishConnectorScheme(path, method, routeSchema, integration); } } if (route) { items.push({ hash: routeHash, route }); } } } return items; } createRouteFromMapConnectorScheme(path, method, schema, integration) { const mapConnector = connector_1.ConnectorCompiler.map(schema.map, integration, { mappableKeys: ["context", "path", "query", "body"], }); const handler = (context, req, res) => { const { params, query, body } = req; const mappableArgs = { context, path: params, query, body }; const result = mapConnector(mappableArgs); this.sendResponse(res, result); }; return new server_1.HTTPRoute({ path, method, description: schema.description || null, handler, }); } createRouteFromCallConnectorScheme(path, method, schema, integration) { const callConnector = connector_1.ConnectorCompiler.call(schema.call, integration, this.props.policyPlugins, { explicitMappableKeys: ["context", "path", "query", "body"], implicitMappableKeys: ["path"], batchingEnabled: false, disableCache: false, }); const { ignoreError } = schema; const multipart = new handler_1.MultipartFormDataHandler(this.opts.uploads); const handler = (context, req, res) => tslib_1.__awaiter(this, void 0, void 0, function* () { try { // process multipart/form-data const uploads = yield multipart.collect(req, res); if (uploads) { req.body = Object.assign(req.body || {}, uploads); } const { params, query, body } = req; const mappableArgs = { context, path: params, query, body }; const result = yield callConnector(context, mappableArgs); this.sendResponse(res, result); } catch (error) { if (ignoreError) { this.sendResponse(res, null); } else { throw error; } } }); return new server_1.HTTPRoute({ path, method, description: schema.description || null, handler, }); } createRouteFromPublishConnectorScheme(path, method, schema, integration) { const publishConnector = connector_1.ConnectorCompiler.publish(schema.publish, integration, this.props.policyPlugins, { mappableKeys: ["context", "path", "query", "body"], }); const handler = (context, req, res) => tslib_1.__awaiter(this, void 0, void 0, function* () { const { params, query, body } = req; const mappableArgs = { context, path: params, query, body }; const result = yield publishConnector(context, mappableArgs); this.sendResponse(res, result); }); return new server_1.HTTPRoute({ path, method, description: schema.description || null, handler, }); } // TODO: REST plugin catalog describeSchema(schema) { return {}; } sendResponse(res, result) { if (result === null || typeof result === "undefined") { res.status(200).end(); return; } if (typeof result === "object") { const { $status, $headers, $body, createReadStream } = result, resultProps = tslib_1.__rest(result, ["$status", "$headers", "$body", "createReadStream"]); // response header modification if (typeof $headers === "object") { for (const [k, v] of Object.entries($headers)) { if (typeof k !== "string") { continue; } res.setHeader(k, v); } } // response code modification if (typeof $status === "number") { res.status($status); } // streaming support if (typeof createReadStream === "function") { const stream = createReadStream(); if (!interface_1.isReadStream(stream)) { throw new Error("invalid read stream"); // TODO: normalize error } if (!res.hasHeader("Content-Type")) { res.setHeader("Content-Type", "application/octet-stream"); } if (!res.hasHeader("Transfer-Encoding")) { res.setHeader("Transfer-Encoding", "chunked"); } stream.pipe(res); return; } // raw body response support (eg. with text/html content-type) if (typeof $body !== "undefined") { res.send($body); return; } // normal response as json res.json(resultProps); return; } res.status(200).send(result); return; } } exports.RESTProtocolPlugin = RESTProtocolPlugin; RESTProtocolPlugin.key = "REST"; RESTProtocolPlugin.autoLoadOptions = { uploads: { maxFiles: Infinity, maxFileSize: Infinity, }, introspection: true, }; //# sourceMappingURL=plugin.js.map