From ebfe3e0a1f38a972ce77deb18e08cf8ae780e570 Mon Sep 17 00:00:00 2001 From: mariusbackes Date: Fri, 20 Nov 2020 13:53:57 +0100 Subject: [PATCH 1/3] added multiple REST routes in service settings --- src/index.js | 151 ++++++++-------- test/integration/index.spec.js | 264 ++++++++++++++++++---------- test/services/multiRoute.service.js | 56 ++++++ 3 files changed, 308 insertions(+), 163 deletions(-) create mode 100644 test/services/multiRoute.service.js diff --git a/src/index.js b/src/index.js index d289e56a..a8bfaf14 100644 --- a/src/index.js +++ b/src/index.js @@ -6,38 +6,40 @@ "use strict"; -const http = require("http"); -const http2 = require("http2"); -const https = require("https"); -const queryString = require("qs"); -const os = require("os"); -const kleur = require("kleur"); -const { match } = require("moleculer").Utils; -const pkg = require("../package.json"); - -const _ = require("lodash"); -const bodyParser = require("body-parser"); -const serveStatic = require("serve-static"); -const isReadableStream = require("isstream").isReadable; +const http = require("http"); +const http2 = require("http2"); +const https = require("https"); +const queryString = require("qs"); +const os = require("os"); +const kleur = require("kleur"); +const { match } = require("moleculer").Utils; +const pkg = require("../package.json"); + +const _ = require("lodash"); +const bodyParser = require("body-parser"); +const serveStatic = require("serve-static"); +const isReadableStream = require("isstream").isReadable; const { MoleculerError, MoleculerServerError, ServiceNotFoundError } = require("moleculer").Errors; const { ServiceUnavailableError, NotFoundError, ForbiddenError, RateLimitExceeded, ERR_ORIGIN_NOT_ALLOWED } = require("./errors"); -const Alias = require("./alias"); -const MemoryStore = require("./memory-store"); +const Alias = require("./alias"); +const MemoryStore = require("./memory-store"); const { removeTrailingSlashes, addSlashes, normalizePath, composeThen, generateETag, isFresh } = require("./utils"); -const MAPPING_POLICY_ALL = "all"; -const MAPPING_POLICY_RESTRICT = "restrict"; +const MAPPING_POLICY_ALL = "all"; +const MAPPING_POLICY_RESTRICT = "restrict"; function getServiceFullname(svc) { if (svc.version != null && svc.settings.$noVersionPrefix !== true) - return (typeof(svc.version) == "number" ? "v" + svc.version : svc.version) + "." + svc.name; + return (typeof (svc.version) == "number" ? "v" + svc.version : svc.version) + "." + svc.name; return svc.name; } +const SLASH_REGEX = new RegExp(/\./g); + /** * Official API Gateway service for Moleculer microservices framework. * @@ -163,7 +165,7 @@ module.exports = { } // Check routes - for(let i = 0; i < this.routes.length; i++) { + for (let i = 0; i < this.routes.length; i++) { const route = this.routes[i]; if (url.startsWith(route.path)) { @@ -275,7 +277,7 @@ module.exports = { } // HTTP server timeout - if (this.settings.httpServerTimeout){ + if (this.settings.httpServerTimeout) { this.logger.debug("Override default http(s) server timeout:", this.settings.httpServerTimeout); this.server.setTimeout(this.settings.httpServerTimeout); } @@ -319,7 +321,7 @@ module.exports = { // If not routed and not served static asset, send 404 this.send404(req, res); } - } catch(err) { + } catch (err) { // don't log client side errors only it's configured if (this.settings.log4XXResponses || (err && !_.inRange(err.code, 400, 500))) { this.logger.error(" Request error!", err.name, ":", err.message, "\n", err.stack, "\nData:", err.data); @@ -423,7 +425,7 @@ module.exports = { const result = await this.aliasHandler(req, res, { action, _notDefined: true }); resolve(result); - } catch(err) { + } catch (err) { reject(err); } }); @@ -587,7 +589,7 @@ module.exports = { return true; - } catch(err) { + } catch (err) { /* istanbul ignore next */ if (!err) return; // Cancelling promise chain, no error @@ -721,7 +723,7 @@ module.exports = { } // Auto generate & add ETag - if(route.etag && chunk && !res.getHeader("ETag") && !isReadableStream(chunk)) { + if (route.etag && chunk && !res.getHeader("ETag") && !isReadableStream(chunk)) { res.setHeader("ETag", generateETag.call(this, chunk, route.etag)); } @@ -966,7 +968,7 @@ module.exports = { return origin.match(wildcard); } } else if (Array.isArray(settings)) { - for(let i = 0; i < settings.length; i++) { + for (let i = 0; i < settings.length; i++) { if (this.checkOrigin(origin, settings[i])) { return true; } @@ -1072,7 +1074,7 @@ module.exports = { * @returns {Object} Resolved alas & params */ resolveAlias(url, method = "GET") { - for(let i = 0; i < this.aliases.length; i++) { + for (let i = 0; i < this.aliases.length; i++) { const alias = this.aliases[i]; if (alias.isMethod(method)) { const params = alias.match(url); @@ -1134,7 +1136,7 @@ module.exports = { * Optimize route order by route path depth */ optimizeRouteOrder() { - this.routes.sort((a,b) => { + this.routes.sort((a, b) => { let c = addSlashes(b.path).split("/").length - addSlashes(a.path).split("/").length; if (c == 0) { // Second level ordering (considering URL params) @@ -1365,54 +1367,65 @@ module.exports = { const services = this.broker.registry.getServiceList({ withActions: true, grouping: true }); services.forEach(service => { const serviceName = service.fullName || getServiceFullname(service); - const basePath = addSlashes(service.settings && _.isString(service.settings.rest) ? service.settings.rest : serviceName.replace(/\./g, "/")); + + let basePaths = []; + if (_.isString(service.settings.rest)) { + basePaths = [service.settings.rest]; + } else if (_.isArray(service.settings.rest)) { + basePaths = service.settings.rest; + } else { + basePaths = [serviceName.replace(SLASH_REGEX, "/")]; + } // Skip multiple instances of services if (processedServices.has(serviceName)) return; - _.forIn(service.actions, action => { - if (action.rest) { - let alias = null; - - // Check visibility - if (action.visibility != null && action.visibility != "published") return; - - // Check whitelist - if (route.hasWhitelist && !this.checkWhitelist(route, action.name)) return; - - if (_.isString(action.rest)) { - if (action.rest.indexOf(" ") !== -1) { - // Handle route: "POST /import" - const p = action.rest.split(/\s+/); - alias = { - method: p[0], - path: basePath + p[1] - }; - } else { - // Handle route: "/import". In this case apply to all methods as "* /import" - alias = { - method: "*", - path: basePath + action.rest - }; + for (let basePath of basePaths) { + basePath = addSlashes(_.isString(basePath) ? basePath : serviceName.replace(SLASH_REGEX, "/")); + + _.forIn(service.actions, action => { + if (action.rest) { + let alias = null; + + // Check visibility + if (action.visibility != null && action.visibility != "published") return; + + // Check whitelist + if (route.hasWhitelist && !this.checkWhitelist(route, action.name)) return; + + if (_.isString(action.rest)) { + if (action.rest.indexOf(" ") !== -1) { + // Handle route: "POST /import" + const p = action.rest.split(/\s+/); + alias = { + method: p[0], + path: basePath + p[1] + }; + } else { + // Handle route: "/import". In this case apply to all methods as "* /import" + alias = { + method: "*", + path: basePath + action.rest + }; + } + } else if (_.isObject(action.rest)) { + // Handle route: { method: "POST", path: "/other", basePath: "newBasePath" } + alias = Object.assign({}, action.rest, { + method: action.rest.method || "*", + path: (action.rest.basePath ? action.rest.basePath : basePath) + (action.rest.path ? action.rest.path : action.rawName) + }); } - } else if (_.isObject(action.rest)) { - // Handle route: { method: "POST", path: "/other", basePath: "newBasePath" } - alias = Object.assign({}, action.rest, { - method: action.rest.method || "*", - path: (action.rest.basePath ? action.rest.basePath : basePath) + (action.rest.path ? action.rest.path : action.rawName) - }); - } - if (alias) { - alias.path = removeTrailingSlashes(normalizePath(alias.path)); - alias._generated = true; - this.aliases.push(this.createAlias(route, alias, action.name)); + if (alias) { + alias.path = removeTrailingSlashes(normalizePath(alias.path)); + alias._generated = true; + this.aliases.push(this.createAlias(route, alias, action.name)); + } } - } - - processedServices.add(serviceName); - }); + processedServices.add(serviceName); + }); + } }); if (this.settings.optimizeOrder) { @@ -1424,10 +1437,10 @@ module.exports = { * Optimize order of alias path. */ optimizeAliasesOrder() { - this.aliases.sort((a,b) => { + this.aliases.sort((a, b) => { let c = addSlashes(b.path).split("/").length - addSlashes(a.path).split("/").length; if (c == 0) { - // Second level ordering (considering URL params) + // Second level ordering (considering URL params) c = a.path.split(":").length - b.path.split(":").length; } diff --git a/test/integration/index.spec.js b/test/integration/index.spec.js index b1a2edc9..7798e3ee 100644 --- a/test/integration/index.spec.js +++ b/test/integration/index.spec.js @@ -44,7 +44,7 @@ describe("Test default settings", () => { let server; beforeAll(() => { - [ broker, service, server] = setup(); + [broker, service, server] = setup(); return broker.start(); }); @@ -164,8 +164,8 @@ describe("Test responses", () => { beforeAll(() => { - [ broker, service, server] = setup({ - routes:[{ + [broker, service, server] = setup({ + routes: [{ camelCaseNames: true }] }, { metrics: true }); @@ -414,7 +414,7 @@ describe("Test with `path` prefix", () => { let server; beforeAll(() => { - [ broker, service, server] = setup({ + [broker, service, server] = setup({ path: "/my-api" }); //broker.loadService("./test/services/math.service"); @@ -470,7 +470,7 @@ describe("Test with `/` path prefix", () => { let server; beforeAll(() => { - [ broker, service, server] = setup({ + [broker, service, server] = setup({ path: "/" }); //broker.loadService("./test/services/math.service"); @@ -511,7 +511,7 @@ describe("Test only assets", () => { let server; beforeAll(() => { - [ broker, service, server] = setup({ + [broker, service, server] = setup({ assets: { folder: path.join(__dirname, "..", "assets") }, @@ -565,7 +565,7 @@ describe("Test assets & API route", () => { let server; beforeAll(() => { - [ broker, service, server] = setup({ + [broker, service, server] = setup({ assets: { folder: path.join(__dirname, "..", "assets") }, @@ -625,7 +625,7 @@ describe("Test whitelist", () => { let server; beforeAll(() => { - [ broker, service, server] = setup({ + [broker, service, server] = setup({ routes: [{ path: "/api", whitelist: [ @@ -747,7 +747,7 @@ describe("Test aliases", () => { }); beforeAll(() => { - [ broker, service, server] = setup({ + [broker, service, server] = setup({ routes: [ { path: "/api", @@ -1125,7 +1125,7 @@ describe("Test un-merged params", () => { let server; beforeAll(() => { - [ broker, service, server] = setup({ + [broker, service, server] = setup({ routes: [{ path: "/api", mergeParams: false, @@ -1305,7 +1305,7 @@ describe("Test REST shorthand aliases", () => { let server; beforeAll(() => { - [ broker, service, server] = setup({ + [broker, service, server] = setup({ routes: [{ path: "/api", aliases: { @@ -1417,7 +1417,7 @@ describe("Test REST shorthand aliases and only filter", () => { let server; beforeAll(() => { - [ broker, service, server] = setup({ + [broker, service, server] = setup({ routes: [{ path: "/api", aliases: { @@ -1492,13 +1492,13 @@ describe("Test REST shorthand aliases and only filter", () => { }); }); -describe("Test REST shorthand aliases and execpt filter", () => { +describe("Test REST shorthand aliases and except filter", () => { let broker; let service; let server; beforeAll(() => { - [ broker, service, server] = setup({ + [broker, service, server] = setup({ routes: [{ path: "/api", aliases: { @@ -1581,7 +1581,7 @@ describe("Test REST shorthand aliases and only, execpt filter", () => { let server; beforeAll(() => { - [ broker, service, server] = setup({ + [broker, service, server] = setup({ routes: [{ path: "/api", aliases: { @@ -1661,7 +1661,7 @@ describe("Test alias & whitelist", () => { let server; beforeAll(() => { - [ broker, service, server] = setup({ + [broker, service, server] = setup({ routes: [{ path: "/api", whitelist: [ @@ -1742,7 +1742,7 @@ describe("Test body-parsers", () => { }); it("POST /api/test.gretter without bodyParsers", () => { - [ broker, service, server] = setup({ + [broker, service, server] = setup({ routes: [{ bodyParsers: false }] @@ -1772,7 +1772,7 @@ describe("Test body-parsers", () => { }); it("POST /api/test.gretter with JSON parser", () => { - [ broker, service, server] = setup({ + [broker, service, server] = setup({ routes: [{ bodyParsers: { json: true @@ -1792,7 +1792,7 @@ describe("Test body-parsers", () => { }); it("POST /api/test.gretter with JSON parser & invalid JSON", () => { - [ broker, service, server] = setup({ + [broker, service, server] = setup({ routes: [{ bodyParsers: { json: true @@ -1819,7 +1819,7 @@ describe("Test body-parsers", () => { it("POST /api/test.gretter with JSON parser & urlEncoded body", () => { - [ broker, service, server] = setup({ + [broker, service, server] = setup({ routes: [{ bodyParsers: { json: true @@ -1839,7 +1839,7 @@ describe("Test body-parsers", () => { }); it("POST /api/test.gretter with urlencoder parser", () => { - [ broker, service, server] = setup({ + [broker, service, server] = setup({ routes: [{ bodyParsers: { urlencoded: { extended: true } @@ -1866,7 +1866,7 @@ describe("Test multiple routes", () => { let server; beforeAll(() => { - [ broker, service, server] = setup({ + [broker, service, server] = setup({ routes: [ { path: "/api1", @@ -1965,7 +1965,7 @@ describe("Test mappingPolicy route option", () => { describe("'all' option", () => { beforeAll(() => { - [ broker, service, server] = setup({ + [broker, service, server] = setup({ routes: [ { path: "/api", @@ -2021,7 +2021,7 @@ describe("Test mappingPolicy route option", () => { describe("'restrict' option", () => { beforeAll(() => { - [ broker, service, server] = setup({ + [broker, service, server] = setup({ routes: [ { path: "/api", @@ -2071,7 +2071,7 @@ describe("Test mappingPolicy route option", () => { describe("'restrict' option without aliases", () => { beforeAll(() => { - [ broker, service, server] = setup({ + [broker, service, server] = setup({ routes: [ { path: "/", @@ -2113,7 +2113,7 @@ describe("Test CORS", () => { }); it("no errors if missing origin header", () => { - [ broker, service, server] = setup({ + [broker, service, server] = setup({ cors: {} }); return broker.start() @@ -2126,7 +2126,7 @@ describe("Test CORS", () => { }); it("errors on mismatching origin header", () => { - [ broker, service, server] = setup({ + [broker, service, server] = setup({ cors: { origin: "a" } @@ -2141,12 +2141,13 @@ describe("Test CORS", () => { "message": "Forbidden", "code": 403, "type": "ORIGIN_NOT_ALLOWED", - "name": "ForbiddenError" }); + "name": "ForbiddenError" + }); }).then(() => broker.stop()).catch(err => broker.stop().then(() => { throw err; })); }); it("with default settings", () => { - [ broker, service, server] = setup({ + [broker, service, server] = setup({ cors: {} }); @@ -2165,7 +2166,7 @@ describe("Test CORS", () => { }); it("with custom global settings (string)", () => { - [ broker, service, server] = setup({ + [broker, service, server] = setup({ cors: { origin: "http://localhost:3000", exposedHeaders: "X-Response-Time", @@ -2190,7 +2191,7 @@ describe("Test CORS", () => { }); it("with custom global settings (array)", () => { - [ broker, service, server] = setup({ + [broker, service, server] = setup({ cors: { origin: ["http://localhost:3000", "https://localhost:4000"], exposedHeaders: ["X-Custom-Header", "X-Response-Time"], @@ -2215,7 +2216,7 @@ describe("Test CORS", () => { }); it("with custom route settings", () => { - [ broker, service, server] = setup({ + [broker, service, server] = setup({ cors: { origin: ["http://localhost:3000", "https://localhost:4000"], exposedHeaders: ["X-Custom-Header", "X-Response-Time"], @@ -2246,7 +2247,7 @@ describe("Test CORS", () => { }); it("returns matching CORS origin wildcard with single origin", () => { - [ broker, service, server] = setup({ + [broker, service, server] = setup({ cors: { origin: "http://localhost:*", } @@ -2268,7 +2269,7 @@ describe("Test CORS", () => { }); it("returns matching CORS origin wildcard", () => { - [ broker, service, server] = setup({ + [broker, service, server] = setup({ cors: { origin: ["http://test.example.com", "http://www.example.com", "http://*.a.com"], } @@ -2290,7 +2291,7 @@ describe("Test CORS", () => { }); it("returns matching CORS origin wildcard when more than one wildcard", () => { - [ broker, service, server] = setup({ + [broker, service, server] = setup({ cors: { origin: ["http://test.example.com", "http://*.b.com", "http://*.a.com"], } @@ -2312,7 +2313,7 @@ describe("Test CORS", () => { }); it("preflight request with custom route settings", () => { - [ broker, service, server] = setup({ + [broker, service, server] = setup({ cors: { origin: ["http://localhost:3000"], exposedHeaders: ["X-Custom-Header", "X-Response-Time"] @@ -2349,7 +2350,7 @@ describe("Test CORS", () => { }); it("preflight request with default settings", () => { - [ broker, service, server] = setup({ + [broker, service, server] = setup({ cors: { allowedHeaders: ["X-Custom-Header", "X-Response-Time"] } @@ -2371,7 +2372,7 @@ describe("Test CORS", () => { }); it("preflight request with 'Access-Control-Request-Headers'", () => { - [ broker, service, server] = setup({ + [broker, service, server] = setup({ cors: { origin: "http://localhost:3000", exposedHeaders: ["X-Custom-Header", "X-Response-Time"], @@ -2414,7 +2415,7 @@ describe("Test Rate Limiter", () => { beforeAll(() => { clock = lolex.install(); - [ broker, service, server] = setup({ + [broker, service, server] = setup({ rateLimit: { window: 10000, limit: 3, @@ -2508,7 +2509,7 @@ describe("Test Rate Limiter", () => { it("Test StoreFactory", () => { let factory = jest.fn(); - [ broker, service, server] = setup({ + [broker, service, server] = setup({ rateLimit: { window: 10000, limit: 3, @@ -2531,8 +2532,8 @@ describe("Test onBeforeCall & onAfterCall", () => { expect(ctx.action.name).toBe("api.rest"); ctx.meta.custom = "John"; - ctx.meta.endpoint = req.$endpoint ? req.$endpoint.name: null; - ctx.meta.action = req.$action ? req.$action.name: null; + ctx.meta.endpoint = req.$endpoint ? req.$endpoint.name : null; + ctx.meta.action = req.$action ? req.$action.name : null; }); const afterCall = jest.fn((ctx, route, req, res, data) => { expect(req.$service).toBeDefined(); @@ -3064,7 +3065,8 @@ describe("Test authorization", () => { "message": "Unauthorized", "code": 401, "type": "NO_TOKEN", - "name": "UnAuthorizedError" }); + "name": "UnAuthorizedError" + }); expect(authorize).toHaveBeenCalledTimes(1); expect(authorize).toHaveBeenCalledWith(jasmine.any(Context), jasmine.any(Object), jasmine.any(http.IncomingMessage), jasmine.any(http.ServerResponse)); }).then(() => broker.stop()).catch(err => broker.stop().then(() => { throw err; })); @@ -3238,7 +3240,7 @@ describe("Test route.path and aliases", () => { res.end("/api/te/test"); } } - },{ + }, { path: "", aliases: { "GET test"(req, res) { @@ -3390,7 +3392,7 @@ describe("Test file uploading", () => { } }, onAfterCall(ctx, route, req, res, data) { - if(ctx.meta.$multipart && "name" in ctx.meta.$multipart) + if (ctx.meta.$multipart && "name" in ctx.meta.$multipart) data = { name: ctx.meta.$multipart.name, files: data }; return Promise.resolve(data); }, @@ -3518,7 +3520,7 @@ describe("Test dynamic routing", () => { let server; beforeAll(() => { - [ broker, service, server] = setup({ + [broker, service, server] = setup({ routes: false }); return broker.start(); @@ -3610,7 +3612,7 @@ describe("Test dynamic routing with actions", () => { let server; beforeAll(() => { - [ broker, service, server] = setup({ + [broker, service, server] = setup({ routes: false }); return broker.start(); @@ -3634,12 +3636,14 @@ describe("Test dynamic routing with actions", () => { }); it("create route & should find '/my/hello'", async () => { - await broker.call("api.addRoute", { route: { - path: "/my", - aliases: { - "hello": "test.hello" + await broker.call("api.addRoute", { + route: { + path: "/my", + aliases: { + "hello": "test.hello" + } } - } }); + }); return request(server) .get("/my/hello") @@ -3651,12 +3655,14 @@ describe("Test dynamic routing with actions", () => { }); it("change route & should find '/other/hello'", async () => { - await broker.call("api.addRoute", { route: { - path: "/other", - aliases: { - "hello": "test.hello" + await broker.call("api.addRoute", { + route: { + path: "/other", + aliases: { + "hello": "test.hello" + } } - } }); + }); return request(server) .get("/other/hello") @@ -3701,15 +3707,17 @@ describe("Test route path optimization", () => { let server; beforeAll(() => { - [ broker, service, server] = setup({ + [broker, service, server] = setup({ routes: [ { path: "/", aliases: { "b": "test.hello" } }, { path: "/a", aliases: { "c": "test.hello" } }, - { path: "/a/b", aliases: { - "c": "test.hello", - "d/:id": "test.params", - "d/e": "test.params", - } }, + { + path: "/a/b", aliases: { + "c": "test.hello", + "d/:id": "test.params", + "d/e": "test.params", + } + }, ] }); return broker.start(); @@ -3963,7 +3971,7 @@ describe("Test ETag cache control", () => { let server; beforeAll(() => { - [ broker, service, server] = setup({ + [broker, service, server] = setup({ etag: false, routes: [{ path: "/", @@ -3975,48 +3983,48 @@ describe("Test ETag cache control", () => { }); afterAll(() => broker.stop()); - it("should add Etag to response", ()=>{ + it("should add Etag to response", () => { return request(server) .get("/test/greeter") .query({ name: "tiaod" }) - .then(res=>{ + .then(res => { expect(res.statusCode).toBe(200); expect(res.headers["etag"]).toBe("W/\"d-q+AO2Lbr8LT+rw9AWUCOel9HJU4\""); }); }); - it("should response status 304 when etag matched", ()=>{ + it("should response status 304 when etag matched", () => { return request(server) .get("/test/greeter") .query({ name: "tiaod" }) .set("If-None-Match", "\"d-q+AO2Lbr8LT+rw9AWUCOel9HJU4\"") - .then(res=>{ + .then(res => { expect(res.statusCode).toBe(304); expect(res.headers["etag"]).toBe("W/\"d-q+AO2Lbr8LT+rw9AWUCOel9HJU4\""); }); }); - it("should return status 200 when response is not fresh", ()=>{ + it("should return status 200 when response is not fresh", () => { return request(server) .get("/test/freshness") .set("If-Modified-Since", "Thu, 31 Mar 2016 07:07:52 GMT") - .then(res=>{ + .then(res => { expect(res.statusCode).toBe(200); expect(res.headers["last-modified"]).toBe("Mon, 06 Aug 2018 14:23:28 GMT"); }); }); - it("should return status 304 when response is fresh", ()=>{ + it("should return status 304 when response is fresh", () => { return request(server) .get("/test/freshness") .set("If-Modified-Since", "Mon, 06 Aug 2018 14:28:00 GMT") - .then(res=>{ + .then(res => { expect(res.statusCode).toBe(304); expect(res.headers["last-modified"]).toBe("Mon, 06 Aug 2018 14:23:28 GMT"); }); }); - it("should not generate etag when streaming response", ()=>{ + it("should not generate etag when streaming response", () => { return request(server) .get("/test/stream") .then(res => { @@ -4025,7 +4033,7 @@ describe("Test ETag cache control", () => { }); }); - it("should use the etag in ctx.meta.$responseHeaders['ETag']", ()=>{ + it("should use the etag in ctx.meta.$responseHeaders['ETag']", () => { return request(server) .get("/test/etag") .then(res => { @@ -4034,7 +4042,7 @@ describe("Test ETag cache control", () => { }); }); - it("should skip body for HEAD", ()=>{ + it("should skip body for HEAD", () => { return request(server) .head("/test/greeter") .query({ name: "tiaod" }) @@ -4043,7 +4051,7 @@ describe("Test ETag cache control", () => { }); }); - it("should strip irrelevant headers when sending 304 response", ()=>{ + it("should strip irrelevant headers when sending 304 response", () => { return request(server) .head("/test/greeter") .query({ name: "tiaod" }) @@ -4062,7 +4070,7 @@ describe("Test ETag cache control", () => { let server; beforeAll(() => { - [ broker, service, server] = setup({ + [broker, service, server] = setup({ etag: "weak", routes: [{ path: "/", @@ -4073,11 +4081,11 @@ describe("Test ETag cache control", () => { }); afterAll(() => broker.stop()); - it("should add Etag to response", ()=>{ + it("should add Etag to response", () => { return request(server) .get("/test/greeter") .query({ name: "tiaod" }) - .then(res=>{ + .then(res => { expect(res.statusCode).toBe(200); expect(res.headers["etag"]).toBe("W/\"d-q+AO2Lbr8LT+rw9AWUCOel9HJU4\""); }); @@ -4090,7 +4098,7 @@ describe("Test ETag cache control", () => { let server; beforeAll(() => { - [ broker, service, server] = setup({ + [broker, service, server] = setup({ etag: "strong", routes: [{ path: "/", @@ -4101,11 +4109,11 @@ describe("Test ETag cache control", () => { }); afterAll(() => broker.stop()); - it("should add Etag to response", ()=>{ + it("should add Etag to response", () => { return request(server) .get("/test/greeter") .query({ name: "tiaod" }) - .then(res=>{ + .then(res => { expect(res.statusCode).toBe(200); expect(res.headers["etag"]).toBe("\"d-q+AO2Lbr8LT+rw9AWUCOel9HJU4\""); }); @@ -4120,7 +4128,7 @@ describe("Test ETag cache control", () => { let custETag = jest.fn(() => "123abc"); beforeAll(() => { - [ broker, service, server] = setup({ + [broker, service, server] = setup({ etag: true, routes: [{ path: "/", @@ -4133,11 +4141,11 @@ describe("Test ETag cache control", () => { afterAll(() => broker.stop()); - it("should add Etag to response", ()=>{ + it("should add Etag to response", () => { return request(server) .get("/test/greeter") .query({ name: "tiaod" }) - .then(res=>{ + .then(res => { expect(res.statusCode).toBe(200); expect(res.headers["etag"]).toBe("123abc"); @@ -4147,19 +4155,19 @@ describe("Test ETag cache control", () => { }); }); - describe("with disabled ETag", ()=>{ + describe("with disabled ETag", () => { let broker; let service; let server; beforeAll(() => { - [ broker, service, server] = setup(); + [broker, service, server] = setup(); broker.loadService("./test/services/test.service"); return broker.start(); }); afterAll(() => broker.stop()); - it("should not add ETag to response", ()=>{ + it("should not add ETag to response", () => { return request(server) .get("/test/greeter") .query({ name: "tiaod" }) @@ -4177,7 +4185,7 @@ describe("Test new alias handling", () => { let service; beforeAll(() => { - [ broker, service, server] = setup({ + [broker, service, server] = setup({ path: "/api", routes: [ { @@ -4272,7 +4280,7 @@ describe("Test internal service special char", () => { let service; beforeAll(() => { - [ broker, service, server] = setup({ + [broker, service, server] = setup({ path: "/api", internalServiceSpecialChar: "@", }); @@ -4306,7 +4314,7 @@ describe("Test httpServerTimeout setting", () => { let service; beforeAll(() => { - [ broker, service, server] = setup({ + [broker, service, server] = setup({ httpServerTimeout: 500 }); @@ -4318,7 +4326,7 @@ describe("Test httpServerTimeout setting", () => { it("should return response", () => { return request(server) .post("/test/apitimeout") - .send({ counter:2, sleeptime: 100 }) + .send({ counter: 2, sleeptime: 100 }) .then(res => { expect(res.statusCode).toBe(200); expect(res.body).toEqual({ @@ -4331,7 +4339,7 @@ describe("Test httpServerTimeout setting", () => { it("should throw timeout error", () => { return request(server) .post("/test/apitimeout") - .send({ counter:6, sleeptime: 100 }) + .send({ counter: 6, sleeptime: 100 }) .then(res => { expect(true).toBe(false); }) @@ -4349,7 +4357,7 @@ describe("Test listAliases action", () => { let service; beforeAll(() => { - [ broker, service, server] = setup({ + [broker, service, server] = setup({ routes: [ { path: "/api", @@ -4474,3 +4482,71 @@ describe("Test listAliases action", () => { }); }); }); + +describe("Test multi REST interfaces in service settings", () => { + let broker; + let service; + let server; + + function regenerate() { + service.routes.forEach(route => route.opts.autoAliases && service.regenerateAutoAliases(route)); + } + + beforeAll(() => { + [broker, service, server] = setup({ + routes: [{ + path: "/", + autoAliases: true + }] + }); + broker.loadService("./test/services/multiRoute.service"); + return broker.start().then(() => regenerate()); + }); + afterAll(() => broker.stop()); + + it("should call both 'GET /route/hello' and 'GET /route/multi/hello'", () => { + return Promise.all([ + request(server).get("/route/hello"), + request(server).get("/route/multi/hello") + ]).then((results) => { + results.forEach(result => { + expect(result.statusCode).toBe(200); + expect(result.headers["content-type"]).toBe("application/json; charset=utf-8"); + expect(result.body).toBe("Hello Moleculer"); + }); + }) + }); + + it("should call both 'GET /route/greet' and 'GET /route/multi/greet' with parameter", () => { + return Promise.all([ + request(server).get("/route/greet").query({ name: "John" }), + request(server).get("/route/multi/greet").query({ name: "John" }) + ]).then((results) => { + results.forEach(result => { + expect(result.statusCode).toBe(200); + expect(result.headers["content-type"]).toBe("application/json; charset=utf-8"); + expect(result.body).toBe("Hello John"); + }); + }) + }); + + it("should call 'GET /fullPath'", () => { + return request(server) + .get("/fullPath") + .then(res => { + expect(res.statusCode).toBe(200); + expect(res.headers["content-type"]).toBe("application/json; charset=utf-8"); + expect(res.body).toBe("Full path"); + }); + }); + + it("should call both 'GET /route/custom-base-path/base-path' and 'GET /route/multi/custom-base-path/base-path'", () => { + return request(server) + .get("/custom-base-path/base-path") + .then(res => { + expect(res.statusCode).toBe(200); + expect(res.headers["content-type"]).toBe("application/json; charset=utf-8"); + expect(res.body).toBe("Hello Custom Moleculer Root Path"); + }); + }); +}); diff --git a/test/services/multiRoute.service.js b/test/services/multiRoute.service.js new file mode 100644 index 00000000..ee20ebb7 --- /dev/null +++ b/test/services/multiRoute.service.js @@ -0,0 +1,56 @@ +"use strict"; + +const fs = require("fs"); +const path = require("path"); + +const _ = require("lodash"); + +const { MoleculerServerError } = require("moleculer").Errors; + +module.exports = { + name: "multiRoute", + + settings: { + rest: ["/route", "/route/multi"] + }, + + actions: { + hello: { + rest: "GET /hello", + handler(ctx) { + return "Hello Moleculer"; + } + }, + + greet: { + rest: "/greet", + params: { + name: "string" + }, + handler(ctx) { + return `Hello ${ctx.params.name}`; + } + }, + + fullPath: { + rest: { + method: "GET", + fullPath: "/fullPath", + }, + handler(ctx) { + return "Full path"; + } + }, + + basePath: { + rest: { + method: "GET", + path: "/base-path", + basePath: "custom-base-path" + }, + handler(ctx) { + return "Hello Custom Moleculer Root Path"; + } + }, + } +} \ No newline at end of file From 03847a6fd9634231e4a0dffd1c513a25f8337eb2 Mon Sep 17 00:00:00 2001 From: mariusbackes Date: Mon, 23 Nov 2020 09:31:12 +0100 Subject: [PATCH 2/3] removed deprecated node 8 version for testing --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 5b180fb3..4f52680c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,5 @@ cache: node_js: - "12" - "10" - - "8" after_success: - npm run coverall From ab2bb78e6acd90f2e50ac54055f0de284f13484f Mon Sep 17 00:00:00 2001 From: mariusbackes Date: Mon, 23 Nov 2020 13:38:15 +0100 Subject: [PATCH 3/3] added possibility for multiple REST interfaces on each action --- src/index.js | 76 +++++++++++++++++++---------- test/integration/index.spec.js | 31 +++++++++++- test/services/multiRoute.service.js | 10 ++++ test/services/test.service.js | 28 +++++++---- 4 files changed, 108 insertions(+), 37 deletions(-) diff --git a/src/index.js b/src/index.js index a8bfaf14..10dfdb03 100644 --- a/src/index.js +++ b/src/index.js @@ -1385,41 +1385,33 @@ module.exports = { _.forIn(service.actions, action => { if (action.rest) { - let alias = null; - // Check visibility if (action.visibility != null && action.visibility != "published") return; // Check whitelist if (route.hasWhitelist && !this.checkWhitelist(route, action.name)) return; - if (_.isString(action.rest)) { - if (action.rest.indexOf(" ") !== -1) { - // Handle route: "POST /import" - const p = action.rest.split(/\s+/); - alias = { - method: p[0], - path: basePath + p[1] - }; - } else { - // Handle route: "/import". In this case apply to all methods as "* /import" - alias = { - method: "*", - path: basePath + action.rest - }; - } - } else if (_.isObject(action.rest)) { - // Handle route: { method: "POST", path: "/other", basePath: "newBasePath" } - alias = Object.assign({}, action.rest, { - method: action.rest.method || "*", - path: (action.rest.basePath ? action.rest.basePath : basePath) + (action.rest.path ? action.rest.path : action.rawName) - }); + let restRoutes = []; + if (!_.isArray(action.rest)) { + restRoutes = [action.rest]; + } else { + restRoutes = action.rest; } - if (alias) { - alias.path = removeTrailingSlashes(normalizePath(alias.path)); - alias._generated = true; - this.aliases.push(this.createAlias(route, alias, action.name)); + for (let restRoute of restRoutes) { + let alias = null; + + if (_.isString(restRoute)) { + alias = this.parseActionRestString(restRoute, basePath); + } else if (_.isObject(restRoute)) { + alias = this.parseActionRestObject(restRoute, action.rawName, basePath); + } + + if (alias) { + alias.path = removeTrailingSlashes(normalizePath(alias.path)); + alias._generated = true; + this.aliases.push(this.createAlias(route, alias, action.name)); + } } } @@ -1433,6 +1425,36 @@ module.exports = { } }, + /** + * + */ + parseActionRestString(restRoute, basePath) { + if (restRoute.indexOf(" ") !== -1) { + // Handle route: "POST /import" + const p = restRoute.split(/\s+/); + return { + method: p[0], + path: basePath + p[1] + }; + } + // Handle route: "/import". In this case apply to all methods as "* /import" + return { + method: "*", + path: basePath + restRoute + }; + }, + + /** + * + */ + parseActionRestObject(restRoute, rawName, basePath) { + // Handle route: { method: "POST", path: "/other", basePath: "newBasePath" } + return Object.assign({}, restRoute, { + method: restRoute.method || "*", + path: (restRoute.basePath ? restRoute.basePath : basePath) + (restRoute.path ? restRoute.path : rawName) + }); + }, + /** * Optimize order of alias path. */ diff --git a/test/integration/index.spec.js b/test/integration/index.spec.js index 7798e3ee..316fbdbf 100644 --- a/test/integration/index.spec.js +++ b/test/integration/index.spec.js @@ -3794,7 +3794,8 @@ describe("Test auto aliasing", () => { "posts.*", "test.hello", "test.full*", - "test.base*" + "test.base*", + "test.update*" ], autoAliases: true, @@ -3961,6 +3962,19 @@ describe("Test auto aliasing", () => { expect(res.body).toBe("Full path"); }); }); + + it("should call PUT /update and PATCH /update", () => { + return Promise.all([ + request(server).put("/api/update").query({ name: "John" }), + request(server).patch("/api/update").query({ name: "John" }) + ]).then((results) => { + results.forEach(result => { + expect(result.statusCode).toBe(200); + expect(result.headers["content-type"]).toBe("application/json; charset=utf-8"); + expect(result.body).toBe("Hello John"); + }); + }) + }); }); describe("Test ETag cache control", () => { @@ -4549,4 +4563,19 @@ describe("Test multi REST interfaces in service settings", () => { expect(res.body).toBe("Hello Custom Moleculer Root Path"); }); }); + + it("should call multiple PUT /update and PATCH /update on /route and /route/multi base path ", () => { + return Promise.all([ + request(server).put("/route/update").query({ name: "John" }), + request(server).put("/route/multi/update").query({ name: "John" }), + request(server).patch("/route/update").query({ name: "John" }), + request(server).patch("/route/multi/update").query({ name: "John" }) + ]).then((results) => { + results.forEach(result => { + expect(result.statusCode).toBe(200); + expect(result.headers["content-type"]).toBe("application/json; charset=utf-8"); + expect(result.body).toBe("Hello John"); + }); + }) + }); }); diff --git a/test/services/multiRoute.service.js b/test/services/multiRoute.service.js index ee20ebb7..dfc6ee73 100644 --- a/test/services/multiRoute.service.js +++ b/test/services/multiRoute.service.js @@ -52,5 +52,15 @@ module.exports = { return "Hello Custom Moleculer Root Path"; } }, + + update: { + rest: ["PUT /update", "PATCH /update"], + params: { + name: "string" + }, + handler(ctx) { + return `Hello ${ctx.params.name}`; + } + }, } } \ No newline at end of file diff --git a/test/services/test.service.js b/test/services/test.service.js index d2b1d492..29190340 100644 --- a/test/services/test.service.js +++ b/test/services/test.service.js @@ -1,9 +1,9 @@ "use strict"; -const fs = require("fs"); -const path = require("path"); +const fs = require("fs"); +const path = require("path"); -const _ = require("lodash"); +const _ = require("lodash"); const { MoleculerServerError } = require("moleculer").Errors; @@ -85,7 +85,7 @@ module.exports = { } try { let c = 0; - while(c != ctx.params.counter) { + while (c != ctx.params.counter) { await sleep(ctx.params.sleeptime); c++; } @@ -93,7 +93,7 @@ module.exports = { status: 200, msg: "apitimeout response" }; - } catch(e) { + } catch (e) { return { status: 500, msg: "apitimeout response", @@ -178,10 +178,10 @@ module.exports = { }, function(ctx) { - return () => {}; + return () => { }; }, - nothing(ctx) {}, + nothing(ctx) { }, null(ctx) { return null; @@ -259,14 +259,14 @@ module.exports = { return stream; }, - freshness(ctx){ + freshness(ctx) { ctx.meta.$responseHeaders = { "Last-Modified": "Mon, 06 Aug 2018 14:23:28 GMT" }; return "fresh"; }, - etag(ctx){ + etag(ctx) { ctx.meta.$responseHeaders = { "ETag": "my custom etag" }; @@ -285,5 +285,15 @@ module.exports = { throw new MoleculerServerError("It is a wrong action! I always throw error!"); }, + + update: { + rest: ["PUT /update", "PATCH /update"], + params: { + name: "string" + }, + handler(ctx) { + return `Hello ${ctx.params.name}`; + } + }, } };