New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

moleculer-web

Package Overview
Dependencies
Maintainers
1
Versions
68
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

moleculer-web - npm Package Compare versions

Comparing version

to
0.9.0-beta1

src/alias.js

104

.eslintrc.js
module.exports = {
root: true,
"env": {
"node": true,
"commonjs": true,
"es6": true,
"jquery": false,
"jest": true,
"jasmine": true
},
"extends": "eslint:recommended",
"parserOptions": {
"sourceType": "module"
},
"rules": {
"indent": [
"warn",
"tab"
],
"quotes": [
"warn",
"double"
],
"semi": [
"error",
"always"
],
"no-var": [
"error"
],
"no-console": [
"off"
],
"no-unused-vars": [
"warn"
]
}
};
"env": {
"node": true,
"commonjs": true,
"es6": true,
"jquery": false,
"jest": true,
"jasmine": true
},
"extends": [
"eslint:recommended",
"plugin:security/recommended"
],
"parserOptions": {
"sourceType": "module",
"ecmaVersion": "2017"
},
"plugins": [
"promise",
"security"
],
"rules": {
"indent": [
"warn",
"tab",
{ SwitchCase: 1 }
],
"quotes": [
"warn",
"double"
],
"semi": [
"error",
"always"
],
"no-var": [
"error"
],
"no-console": [
"error"
],
"no-unused-vars": [
"warn"
],
"no-trailing-spaces": [
"error"
],
"no-alert": 0,
"no-shadow": 0,
"security/detect-object-injection": ["off"],
"security/detect-non-literal-require": ["off"],
"security/detect-non-literal-fs-filename": ["off"],
"no-process-exit": ["off"],
"node/no-unpublished-require": 0,
"space-before-function-paren": [
"warn",
{
"anonymous": "never",
"named": "never",
"asyncArrow": "always"
}
],
"object-curly-spacing": [
"warn",
"always"
]
}
};
-----------------------------
<a name="0.9.0"></a>
# 0.9.0 (2018-xx-xx)
## Breaking changes
### Use `server` property instead of `middleware`
We have removed the `middleware` service setting because it was not straightforward. Therefore, we have created a new `server` setting.
If `server: true` (which is the default value), API Gateway will create a HTTP(s) server. If `server: false`, it won't create a HTTP server, so you can use API Gateway as an Express middleware.
#### Migration guide
**Before**
```js
const ApiGateway = require("moleculer-web");
module.exports = {
mixins: [ApiGateway],
settings: {
middleware: true
}
}
```
**After**
```js
const ApiGateway = require("moleculer-web");
module.exports = {
mixins: [ApiGateway],
settings: {
server: false
}
}
```
## New
### File upload aliases
API Gateway has implemented file uploads. You can upload files as a multipart form data (thanks for busboy library) or as a raw request body. In both cases, the file is transferred to an action as a Stream. In multipart form data mode you can upload multiple files, as well.
> Please note, you have to disable other body parsers in order to accept files.
**Example**
```js
const ApiGateway = require("moleculer-web");
module.exports = {
mixins: [ApiGateway],
settings: {
path: "/upload",
routes: [
{
path: "",
// You should disable body parsers
bodyParsers: {
json: false,
urlencoded: false
},
aliases: {
// File upload from HTML multipart form
"POST /": "multipart:file.save",
// File upload from AJAX or cURL
"PUT /": "stream:file.save",
// File upload from HTML form and overwrite busboy config
"POST /multi": {
type: "multipart",
// Action level busboy config
busboyConfig: {
limits: {
files: 3
}
},
action: "file.save"
}
},
// Route level busboy config.
// More info: https://github.com/mscdex/busboy#busboy-methods
busboyConfig: {
limits: {
files: 1
}
},
mappingPolicy: "restrict"
}
]
}
});
```
### HTTP2 server
HTTP2 experimental server has been implemented into API Gateway. You can turn it on with `http2: true` service setting.
**Example**
```js
const ApiGateway = require("moleculer-web");
module.exports = {
mixins: [ApiGateway],
settings: {
port: 8443,
// HTTPS server with certificate
https: {
key: fs.readFileSync("key.pem"),
cert: fs.readFileSync("cert.pem")
},
// Use HTTP2 server
http2: true
}
});
```
### Dynamic routing
The `this.addRoute(opts, toBottom = true)` new service method is added to add/replace routes. You can call it from your mixins to define new routes _(e.g. swagger route, graphql route...etc)_.
The function detects that the route is defined early. In this case, it will replace the previous route configuration with the new one.
To remove a route, use the `this.removeRoute("/admin")` method. It removes the route by path.
### ETag supporting
Thank to tiaod for ETag implementation. PR: [#92](https://github.com/moleculerjs/moleculer-web/pull/92)
**Example**
```js
const ApiGateway = require("moleculer-web");
module.exports = {
mixins: [ApiGateway],
settings: {
// Service-level option
etag: false,
routes: [
{
path: "/",
// Route-level option.
etag: true
}
]
}
}
```
The `etag` option value can be `false`, `true`, `weak`, `strong`, or a custom Function.
**Custom ETag generator function**
```js
module.exports = {
mixins: [ApiGateway],
settings: {
// Service-level option
etag: (body) => generateHash(body)
}
}
```
Please note, it doesn't work with stream responses. In this case, you should generate the etag by yourself.
**Example**
```js
module.exports = {
name: "export",
actions: {
// Download response as a file in the browser
downloadCSV(ctx) {
ctx.meta.$responseType = "text/csv";
ctx.meta.$responseHeaders = {
"Content-Disposition": `attachment; filename="data-${ctx.params.id}.csv"`,
"ETag": '<your etag here>'
};
return csvFileStream;
}
}
}
```
### Auto-aliasing feature
The auto-aliasing means you don't have to add all service aliases to the routes, the Gateway can generate it from service schema. If a new service is entered or leaved, Gateway regenerate aliases.
To configure which services are used in route use the whitelist.
**Example**
```js
// api.service.js
module.exports = {
mixins: [ApiGateway],
settings: {
routes: [
{
path: "/api",
whitelist: [
"posts.*",
"test.*"
],
aliases: {
"GET /hi": "test.hello"
},
autoAliases: true
}
]
}
};
```
```js
// posts.service.js
module.exports = {
name: "posts",
version: 2,
settings: {
// Base path
rest: "posts/"
},
actions: {
list: {
// Expose as "/v2/posts/"
rest: "GET /",
handler(ctx) {}
},
get: {
// Expose as "/v2/posts/:id"
rest: "GET /:id",
handler(ctx) {}
},
create: {
rest: "POST /",
handler(ctx) {}
},
update: {
rest: "PUT /:id",
handler(ctx) {}
},
remove: {
rest: "DELETE /:id",
handler(ctx) {}
}
}
};
```
**The generated aliases**
```
GET /api/hi => test.hello
GET /api/v2/posts => v2.posts.list
GET /api/v2/posts/:id => v2.posts.get
POST /api/v2/posts => v2.posts.create
PUT /api/v2/posts/:id => v2.posts.update
DELETE /api/v2/posts/:id => v2.posts.remove
```
> If `rest: true` in service settings, API Gateway will use the service name (with version) as base path.
> If `rest: true` in action definition, API Gateway will use action name in path.
## Changes
- new `optimizeOrder: true` setting in order to optimize route paths (deeper first). Defaults: `true`.
-----------------------------
<a name="0.8.5"></a>

@@ -6,2 +282,9 @@ # 0.8.5 (2018-11-28)

## Changes
- allow multiple whitespaces between method & path in aliases.
-----------------------------
<a name="0.8.4"></a>
# 0.8.4 (2018-11-18)
## Changes
- fix `req.url`, add `req.originalUrl` and `req.baseUrl` for better middleware support (e.g. support static serving in subpath).

@@ -8,0 +291,0 @@ - update deps

{
"name": "moleculer-web",
"version": "0.8.5",
"version": "0.9.0-beta1",
"description": "Official API Gateway service for Moleculer framework",

@@ -12,4 +12,5 @@ "main": "index.js",

"ci": "jest --watch",
"test": "jest --coverage",
"test": "jest --coverage --forceExit",
"lint": "eslint --ext=.js src",
"lint:fix": "eslint --ext=.js --fix src",
"deps": "npm-check -u",

@@ -38,2 +39,5 @@ "postdeps": "npm test",

"eslint": "5.9.0",
"eslint-plugin-node": "8.0.0",
"eslint-plugin-promise": "4.0.1",
"eslint-plugin-security": "1.4.0",
"express": "4.16.4",

@@ -49,11 +53,10 @@ "fakerator": "0.3.0",

"moleculer-console-tracer": "0.2.0",
"moleculer-repl": "0.5.2",
"multer": "1.4.1",
"multiparty": "4.2.1",
"moleculer-repl": "0.5.3",
"nats": "1.0.1",
"nodemon": "1.18.7",
"npm-check": "5.9.0",
"socket.io": "2.1.1",
"socket.io": "2.2.0",
"spdy": "4.0.0",
"supertest": "3.3.0",
"webpack": "4.26.1",
"webpack": "4.27.1",
"webpack-dev-middleware": "3.4.0"

@@ -66,4 +69,7 @@ },

"body-parser": "1.18.3",
"busboy": "0.2.14",
"chalk": "2.4.1",
"es6-error": "4.1.1",
"etag": "1.8.1",
"fresh": "0.5.2",
"isstream": "0.1.2",

@@ -70,0 +76,0 @@ "lodash": "4.17.11",

@@ -21,2 +21,3 @@ [![Moleculer logo](http://moleculer.services/images/banner.png)](https://github.com/moleculerjs/moleculer)

* support global, route, alias middlewares
* support file uploading
* alias names (with named parameters & REST shorthand)

@@ -26,2 +27,4 @@ * whitelist

* CORS headers
* ETags
* HTTP2
* Rate limiter

@@ -28,0 +31,0 @@ * before & after call hooks

@@ -19,27 +19,22 @@ /*

const isReadableStream = require("isstream").isReadable;
const pathToRegexp = require("path-to-regexp");
const { MoleculerError, MoleculerServerError, ServiceNotFoundError } = require("moleculer").Errors;
const { BadRequestError, NotFoundError, ForbiddenError, RateLimitExceeded, ERR_UNABLE_DECODE_PARAM, ERR_ORIGIN_NOT_ALLOWED } = require("./errors");
const { NotFoundError, ForbiddenError, RateLimitExceeded, ERR_ORIGIN_NOT_ALLOWED } = require("./errors");
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";
function decodeParam(param) {
try {
return decodeURIComponent(param);
} catch (_) {
/* istanbul ignore next */
throw BadRequestError(ERR_UNABLE_DECODE_PARAM, { param });
}
}
/**
* Official API Gateway service for Moleculer
* Official API Gateway service for Moleculer microservices framework.
*
* @service
*/
module.exports = {
// Service name
// Default service name
name: "api",

@@ -49,4 +44,2 @@

settings: {
// Middleware mode for ExpressJS
middleware: false,

@@ -59,4 +52,9 @@ // Exposed port

// Used server instance. If null, it will create a new HTTP(s)(2) server
// If false, it will start without server in middleware mode
server: true,
// Routes
routes: [
// TODO: should remove it and add only in `created` if it's empty
{

@@ -78,4 +76,11 @@ // Path prefix to this route

// If set to false, error responses with a status code indicating a client error will not be logged
log4XXResponses: true
// If set to true, it will log 4xx client errors, as well
log4XXResponses: false,
// Use HTTP2 server (experimental)
http2: false,
// Optimize route order
optimizeOrder: true,
},

@@ -87,10 +92,10 @@

created() {
if (!this.settings.middleware) {
// Create HTTP or HTTPS server (if not running as middleware)
if (this.settings.https && this.settings.https.key && this.settings.https.cert) {
this.server = https.createServer(this.settings.https, this.httpHandler);
this.isHTTPS = true;
if (this.settings.server !== false) {
if (_.isObject(this.settings.server)) {
// Use an existing server instance
this.server = this.settings.server;
} else {
this.server = http.createServer(this.httpHandler);
this.isHTTPS = false;
// Create a new HTTP/HTTPS/HTTP2 server instance
this.createServer();
}

@@ -102,2 +107,4 @@

});
this.logger.info("API Gateway server created.");
}

@@ -112,11 +119,12 @@

// Process routes
if (Array.isArray(this.settings.routes)) {
this.routes = this.settings.routes.map(route => this.createRoute(route));
}
this.logger.info("API Gateway created!");
this.routes = [];
if (Array.isArray(this.settings.routes))
this.settings.routes.forEach(route => this.addRoute(route));
},
actions: {
// REST request handler
/**
* REST request handler
*/
rest: {

@@ -179,2 +187,31 @@ visibility: "private",

/**
* Create HTTP server
*/
createServer() {
/* istanbul ignore next */
if (this.server) return;
if (this.settings.https && this.settings.https.key && this.settings.https.cert) {
this.server = this.settings.http2 ? this.tryLoadHTTP2Lib().createSecureServer(this.settings.https, this.httpHandler) : https.createServer(this.settings.https, this.httpHandler);
this.isHTTPS = true;
} else {
this.server = this.settings.http2 ? this.tryLoadHTTP2Lib().createServer(this.httpHandler) : http.createServer(this.httpHandler);
this.isHTTPS = false;
}
},
/**
* Try to require HTTP2 servers
*/
tryLoadHTTP2Lib() {
/* istanbul ignore next */
try {
return require("http2");
} catch (err) {
/* istanbul ignore next */
this.broker.fatal("HTTP2 server is not available. (>= Node 8.8.1)");
}
},
/**
* HTTP request handler. It is called from native NodeJS HTTP server.

@@ -228,3 +265,4 @@ *

/**
* Handle request in the matched route
* Handle request in the matched route.
*
* @param {Context} ctx

@@ -243,3 +281,3 @@ * @param {Route} route

return this.composeThen(req, res, ...route.middlewares)
return composeThen(req, res, ...route.middlewares)
.then(() => {

@@ -287,4 +325,4 @@ let params = {};

if (found) {
let alias = found.alias;
this.logger.debug(` Alias: ${req.method} ${urlPath} -> ${alias.action}`);
const alias = found.alias;
this.logger.debug(" Alias:", alias.toString());

@@ -321,3 +359,3 @@ if (route.opts.mergeParams === false) {

return this.aliasHandler(req, res, { action });
return this.aliasHandler(req, res, { action, _generated: true }); // To handle #27
})

@@ -381,4 +419,8 @@ .then(resolve)

const endpoint = this.broker.findNextActionEndpoint(alias.action);
if (endpoint instanceof Error)
if (endpoint instanceof Error) {
// TODO: #27
// if (alias._generated && endpoint instanceof ServiceNotFoundError)
// throw 503 - Service unavailable
throw endpoint;
}

@@ -433,3 +475,3 @@ if (endpoint.action.publish === false) {

// Call custom alias handler
this.logger.info(` Call custom function in '${req.$alias.method} ${req.$alias.path}' alias`);
this.logger.info(` Call custom function in '${alias.toString()}' alias`);
return new this.Promise((resolve, reject) => {

@@ -444,9 +486,9 @@ alias.handler.call(this, req, res, err => {

if (alias.action)
return this.callAction(route, alias.action, req, res, req.$params);
return this.callAction(route, alias.action, req, res, alias.type == "stream" ? req : req.$params);
else
throw new MoleculerServerError("No alias handler", 500, "NO_ALIAS_HANDLER", { alias });
throw new MoleculerServerError("No alias handler", 500, "NO_ALIAS_HANDLER", { path: req.originalUrl });
});
} else if (alias.action) {
return this.callAction(route, alias.action, req, res, req.$params);
return this.callAction(route, alias.action, req, res, alias.type == "stream" ? req : req.$params);
}

@@ -487,8 +529,5 @@ });

// Process the response
// Post-process the response
.then(data => {
//if (ctx.cachedResult)
// res.setHeader("X-From-Cache", "true");
// onAfterCall handling

@@ -503,3 +542,3 @@ if (route.onAfterCall)

.then(data => {
this.sendResponse(ctx, route, req, res, data, req.$endpoint.action);
this.sendResponse(req, res, data, req.$endpoint.action);

@@ -513,2 +552,3 @@ this.logResponse(req, res, data);

.catch(err => {
/* istanbul ignore next */
if (!err)

@@ -524,4 +564,2 @@ return;

*
* @param {Context} ctx
* @param {Object} route
* @param {HttpIncomingMessage} req

@@ -532,4 +570,7 @@ * @param {HttpResponse} res

*/
sendResponse(ctx, route, req, res, data, action = {}) {
sendResponse(req, res, data, action) {
const ctx = req.$ctx;
const route = req.$route;
/* istanbul ignore next */
if (res.headersSent) {

@@ -540,2 +581,3 @@ this.logger.warn("Headers have already sent");

/* istanbul ignore next */
if (!res.statusCode)

@@ -553,4 +595,5 @@ res.statusCode = 200;

// Redirect
if (res.statusCode >= 300 && res.statusCode < 400) {
if (res.statusCode >= 300 && res.statusCode < 400 && res.statusCode !== 304) {
const location = ctx.meta.$location;
/* istanbul ignore next */
if (!location)

@@ -563,9 +606,12 @@ this.logger.warn(`The 'ctx.meta.$location' is missing for status code ${res.statusCode}!`);

// Override responseType by action (Deprecated)
let responseType = action.responseType;
if (responseType) {
let responseType;
/* istanbul ignore next */
if (action && action.responseType) {
deprecate("The 'responseType' action property has been deprecated. Use 'ctx.meta.$responseType' instead");
responseType = action.responseType;
}
// Custom headers (Deprecated)
if (action.responseHeaders) {
/* istanbul ignore next */
if (action && action.responseHeaders) {
deprecate("The 'responseHeaders' action property has been deprecated. Use 'ctx.meta.$responseHeaders' instead");

@@ -593,6 +639,6 @@ Object.keys(action.responseHeaders).forEach(key => {

}
if (data == null)
return res.end();
let chunk;
// Buffer

@@ -602,3 +648,3 @@ if (Buffer.isBuffer(data)) {

res.setHeader("Content-Length", data.length);
res.end(data);
chunk = data;
}

@@ -610,3 +656,3 @@ // Buffer from Object

res.setHeader("Content-Length", buf.length);
res.end(buf);
chunk = buf;
}

@@ -616,22 +662,51 @@ // Stream

res.setHeader("Content-Type", responseType || "application/octet-stream");
data.pipe(res);
chunk = data;
}
// Object or Array
// Object or Array (stringify)
else if (_.isObject(data) || Array.isArray(data)) {
res.setHeader("Content-Type", responseType || "application/json; charset=utf-8");
res.end(JSON.stringify(data));
chunk = JSON.stringify(data);
}
// Other
// Other (stringify or raw text)
else {
if (!responseType) {
res.setHeader("Content-Type", "application/json; charset=utf-8");
res.end(JSON.stringify(data));
chunk = JSON.stringify(data);
} else {
res.setHeader("Content-Type", responseType);
if (_.isString(data))
res.end(data);
chunk = data;
else
res.end(data.toString());
chunk = data.toString();
}
}
// Auto generate & add ETag
if(route.etag && chunk && !res.getHeader("ETag") && !isReadableStream(chunk)) {
res.setHeader("ETag", generateETag.call(this, chunk, route.etag));
}
// Freshness
if (isFresh(req, res))
res.statusCode = 304;
if (res.statusCode === 204 || res.statusCode === 304) {
res.removeHeader("Content-Type");
res.removeHeader("Content-Length");
res.removeHeader("Transfer-Encoding");
chunk = "";
}
if (req.method === "HEAD") {
// skip body for HEAD
res.end();
} else {
// respond
if (isReadableStream(data)) { //Stream response
data.pipe(res);
} else {
res.end(chunk);
}
}
},

@@ -649,56 +724,2 @@

/**
* Compose middlewares
*
* @param {...Function} mws
*/
compose(...mws) {
return (req, res, done) => {
const next = (i, err) => {
if (i >= mws.length) {
if (_.isFunction(done))
return done.call(this, err);
return;
}
if (err) {
// Call only error middlewares (err, req, res, next)
if (mws[i].length == 4)
mws[i].call(this, err, req, res, err => next(i + 1, err));
else
next(i + 1, err);
} else {
if (mws[i].length < 4)
mws[i].call(this, req, res, err => next(i + 1, err));
else
next(i + 1);
}
};
return next(0);
};
},
/**
* Compose middlewares and return Promise
* @param {...Function} mws
* @returns {Promise}
*/
composeThen(req, res, ...mws) {
return new this.Promise((resolve, reject) => {
this.compose(...mws)(req, res, err => {
if (err) {
if (err instanceof MoleculerError)
return reject(err);
if (err instanceof Error)
return reject(new MoleculerError(err.message, err.code || err.status, err.type));
return reject(new MoleculerError(err));
}
resolve();
});
});
},
/**
* Send 404 response

@@ -736,7 +757,9 @@ *

/* istanbul ignore next */
if (res.headersSent) {
this.logger.warn("Headers have already sent");
this.logger.warn("Headers have already sent", req.url, err);
return;
}
/* istanbul ignore next */
if (!err || !(err instanceof Error)) {

@@ -750,2 +773,3 @@ res.writeHead(500);

/* istanbul ignore next */
if (!(err instanceof MoleculerError)) {

@@ -799,3 +823,3 @@ const e = err;

}
return {query, url};
return { query, url };
},

@@ -827,2 +851,4 @@

return chalk.green.bold(code);
/* istanbul ignore next */
return code;

@@ -850,2 +876,3 @@ },

/* istanbul ignore next */
if (this.settings.logResponseData && this.settings.logResponseData in this.logger) {

@@ -871,2 +898,3 @@ this.logger[this.settings.logResponseData](" Data:", data);

// Based on: https://github.com/hapijs/hapi
// eslint-disable-next-line
const wildcard = new RegExp(`^${_.escapeRegExp(settings).replace(/\\\*/g, ".*").replace(/\\\?/g, ".")}$`);

@@ -897,2 +925,4 @@ return origin.match(wildcard);

writeCorsHeaders(route, req, res, isPreFlight) {
/* istanbul ignore next */
if (!route.cors) return;

@@ -984,9 +1014,6 @@

const alias = route.aliases[i];
if (alias.method === "*" || alias.method === method) {
const res = alias.match(url);
if (res) {
return {
alias,
params: res
};
if (alias.isMethod(method)) {
const params = alias.match(url);
if (params) {
return { alias, params };
}

@@ -999,2 +1026,46 @@ }

/**
* Add & prepare route from options
* @param {Object} opts
* @param {Boolean} [toBottom=true]
*/
addRoute(opts, toBottom = true) {
const route = this.createRoute(opts);
const idx = this.routes.findIndex(r => r.path == route.path);
if (idx !== -1) {
// Replace the previous
this.routes[idx] = route;
} else {
// Add new route
if (toBottom)
this.routes.push(route);
else
this.routes.unshift(route);
// Reordering routes
if (this.settings.optimizeOrder)
this.optimizeRouteOrder();
}
return route;
},
/**
* Remove a route by path
* @param {String} path
*/
removeRoute(path) {
const idx = this.routes.findIndex(r => r.opts.path == path);
if (idx !== -1)
this.routes.splice(idx, 1);
},
/**
* Optimize route order by route path depth
*/
optimizeRouteOrder() {
this.routes.sort((a,b) => addSlashes(b.path).split("/").length - addSlashes(a.path).split("/").length);
this.logger.debug("Optimized path order: ", this.routes.map(r => r.path));
},
/**
* Create route object from options

@@ -1039,2 +1110,5 @@ *

// ETag
route.etag = opts.etag != null ? opts.etag : this.settings.etag;
// Middlewares

@@ -1105,96 +1179,149 @@ let mw = [];

const globalPath = this.settings.path && this.settings.path != "/" ? this.settings.path : "";
route.path = globalPath + (opts.path || "");
route.path = route.path || "/";
route.path = addSlashes(globalPath) + (opts.path || "");
route.path = normalizePath(route.path);
// Helper for aliased routes
const createAlias = (matchPath, action) => {
let method = "*";
if (matchPath.indexOf(" ") !== -1) {
// Create aliases
this.createRouteAliases(route, opts.aliases);
// Set alias mapping policy
route.mappingPolicy = opts.mappingPolicy || MAPPING_POLICY_ALL;
this.logger.info("");
return route;
},
/**
* Create all aliases for route.
* @param {Object} route
* @param {Object} aliases
*/
createRouteAliases(route, aliases) {
route.aliases = [];
_.forIn(aliases, (action, matchPath) => {
if (matchPath.startsWith("REST ")) {
const p = matchPath.split(/\s+/);
method = p[0];
matchPath = p[1];
}
if (matchPath.startsWith("/"))
matchPath = matchPath.slice(1);
const pathName = p[1];
let alias;
if (_.isString(action))
alias = { action };
else if (_.isFunction(action))
alias = { handler: action };
else if (Array.isArray(action)) {
alias = {};
const mws = _.compact(action.map(mw => {
if (_.isString(mw))
alias.action = mw;
else if(_.isFunction(mw))
return mw;
}));
alias.handler = this.compose(...mws);
// Generate RESTful API. More info http://www.restapitutorial.com/
route.aliases.push(
this.createAlias(route, `GET ${pathName}`, `${action}.list`),
this.createAlias(route, `GET ${pathName}/:id`, `${action}.get`),
this.createAlias(route, `POST ${pathName}`, `${action}.create`),
this.createAlias(route, `PUT ${pathName}/:id`, `${action}.update`),
this.createAlias(route, `PATCH ${pathName}/:id`, `${action}.patch`),
this.createAlias(route, `DELETE ${pathName}/:id`, `${action}.remove`)
);
} else {
alias = action;
route.aliases.push(this.createAlias(route, matchPath, action));
}
});
alias.path = matchPath;
alias.method = method;
if (route.opts.autoAliases) {
this.regenerateAutoAliases(route);
}
let keys = [];
alias.re = pathToRegexp(matchPath, keys, {}); // Options: https://github.com/pillarjs/path-to-regexp#usage
this.logger.info(` Alias: ${method} ${route.path + (route.path.endsWith("/") ? "": "/")}${matchPath} -> ${alias.handler != null ? "<Function>" : alias.action}`);
return route.aliases;
},
alias.match = url => {
const m = alias.re.exec(url);
if (!m) return false;
/**
* Regenerate aliases automatically if service registry has been changed.
*
* @param {Route} route
*/
regenerateAutoAliases(route) {
this.logger.info(`♻ Generate aliases for '${route.path}' route...`);
const params = {};
route.aliases = route.aliases.filter(alias => !alias._generated);
let key, param;
for (let i = 0; i < keys.length; i++) {
key = keys[i];
param = m[i + 1];
if (!param) continue;
const processedServices = new Set();
params[key.name] = decodeParam(param);
const services = this.broker.registry.getServiceList({ withActions: true });
services.forEach(service => {
const serviceName = service.version ? `v${service.version}.${service.name}` : service.name;
const basePath = addSlashes(_.isString(service.settings.rest) ? service.settings.rest : serviceName.replace(/\./g, "/"));
if (key.repeat)
params[key.name] = params[key.name].split(key.delimiter);
}
// Skip multiple instances of services
if (processedServices.has(serviceName)) return;
return params;
};
_.forIn(service.actions, action => {
if (action.rest) {
let alias = null;
return alias;
};
// Check visibility
if (action.visibility != null && action.visibility != "published") return;
// Handle aliases
if (opts.aliases && Object.keys(opts.aliases).length > 0) {
route.aliases = [];
_.forIn(opts.aliases, (action, matchPath) => {
if (matchPath.startsWith("REST ")) {
const p = matchPath.split(/\s+/);
const pathName = p[1];
// Check whitelist
if (route.hasWhitelist && !this.checkWhitelist(route, action.name)) return;
// Generate RESTful API. More info http://www.restapitutorial.com/
route.aliases.push(createAlias(`GET ${pathName}`, `${action}.list`));
route.aliases.push(createAlias(`GET ${pathName}/:id`, `${action}.get`));
route.aliases.push(createAlias(`POST ${pathName}`, `${action}.create`));
route.aliases.push(createAlias(`PUT ${pathName}/:id`, `${action}.update`));
route.aliases.push(createAlias(`PATCH ${pathName}/:id`, `${action}.patch`));
route.aliases.push(createAlias(`DELETE ${pathName}/:id`, `${action}.remove`));
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 (action.rest === true) {
// "route: true" is converted to "* {baseName}/{action.rawName}"
alias = {
method: "*",
path: basePath + action.rawName
};
} else if (_.isObject(action.rest)) {
// Handle route: { method: "POST", route: "/other" }
alias = Object.assign({}, action.rest, {
method: action.rest.method || "*",
path: basePath + action.rest.path ? action.rest.path : action.rawName
});
}
} else {
route.aliases.push(createAlias(matchPath, action));
if (alias) {
alias.path = removeTrailingSlashes(normalizePath(alias.path));
alias._generated = true;
route.aliases.push(this.createAlias(route, alias, action.name));
}
}
processedServices.add(serviceName);
});
}
route.mappingPolicy = opts.mappingPolicy || MAPPING_POLICY_ALL;
});
},
return route;
/**
* Create alias for route.
*
* @param {Object} route
* @param {String|Object} matchPath
* @param {String|Object} action
*/
createAlias(route, path, action) {
const alias = new Alias(this, route, path, action);
this.logger.info(" " + alias.toString());
return alias;
},
// Regenerate all auto aliases routes
regenerateAllAutoAliases: _.debounce(function() {
/* istanbul ignore next */
this.routes.forEach(route => route.opts.autoAliases && this.regenerateAutoAliases(route));
}, 500)
},
events: {
"$services.changed"() {
this.regenerateAllAutoAliases();
}
},
/**

@@ -1204,4 +1331,4 @@ * Service started lifecycle event handler

started() {
if (this.settings.middleware)
return;
if (this.settings.server === false)
return this.Promise.resolve();

@@ -1225,6 +1352,3 @@ /* istanbul ignore next */

stopped() {
if (this.settings.middleware)
return;
if (this.server.listening) {
if (this.settings.server !== false && this.server.listening) {
/* istanbul ignore next */

@@ -1241,2 +1365,4 @@ return new this.Promise((resolve, reject) => {

}
return this.Promise.resolve();
},

@@ -1243,0 +1369,0 @@

@@ -14,2 +14,3 @@ "use strict";

item.author = fake.random.number(1, 10);
item.created = item.created.toISOString();

@@ -28,2 +29,3 @@ rows.push(item);

cache: true,
rest: "GET /",
handler(ctx) {

@@ -39,2 +41,3 @@ return this.rows;

},
rest: "GET /:id",
handler(ctx) {

@@ -50,2 +53,3 @@ const post = this.findByID(ctx.params.id);

create: {
rest: "POST /",
handler(ctx) {

@@ -61,2 +65,3 @@ this.rows.push(ctx.params);

update: {
rest: "PUT /:id",
handler(ctx) {

@@ -80,2 +85,3 @@ const post = this.findByID(ctx.params.id);

patch: {
rest: "PATCH /:id",
handler(ctx) {

@@ -87,2 +93,3 @@ return this.actions.update(ctx.params, { parentCtx: ctx });

remove: {
rest: "DELETE /:id",
handler(ctx) {

@@ -89,0 +96,0 @@ this.rows = this.rows.filter(row => row.id != ctx.params.id);

@@ -12,8 +12,17 @@ "use strict";

name: "test",
settings: {
rest: "/"
},
actions: {
hello(ctx) {
return "Hello Moleculer";
hello: {
rest: "GET /hi",
handler(ctx) {
return "Hello Moleculer";
}
},
greeter: {
rest: "/greeter",
params: {

@@ -194,2 +203,16 @@ name: "string"

freshness(ctx){
ctx.meta.$responseHeaders = {
"Last-Modified": "Mon, 06 Aug 2018 14:23:28 GMT"
};
return "fresh";
},
etag(ctx){
ctx.meta.$responseHeaders = {
"ETag": "my custom etag"
};
return {};
},
error() {

@@ -196,0 +219,0 @@ throw new MoleculerServerError("I'm dangerous", 500);

@@ -43,3 +43,3 @@ "use strict";

expect(err.message).toBe("Unauthorized");
expect(err.data).toEqual({ a: 5});
expect(err.data).toEqual({ a: 5 });
});

@@ -56,3 +56,3 @@

expect(err.message).toBe("Forbidden");
expect(err.data).toEqual({ a: 5});
expect(err.data).toEqual({ a: 5 });
});

@@ -69,3 +69,3 @@

expect(err.message).toBe("Bad request");
expect(err.data).toEqual({ a: 5});
expect(err.data).toEqual({ a: 5 });
});

@@ -83,4 +83,4 @@

expect(err.message).toBe("Rate limit exceeded");
expect(err.data).toEqual({ a: 5});
expect(err.data).toEqual({ a: 5 });
});
});

@@ -21,3 +21,3 @@ "use strict";

const MockRequest = () => Object.assign(jest.fn(), {headers: {}});
const MockRequest = () => Object.assign(jest.fn(), { headers: {} });

@@ -94,3 +94,3 @@ describe("WebGateway", () => {

return handler.bind(context)(req, res, next).then(() => {
expect(context.actions.rest.mock.calls[0]).toEqual([{req, res}, {requestID: "foobar"}]);
expect(context.actions.rest.mock.calls[0]).toEqual([{ req, res }, { requestID: "foobar" }]);
});

@@ -110,3 +110,3 @@ });

return handler.bind(context)(req, res, next).then(() => {
expect(context.actions.rest.mock.calls[0]).toEqual([{req, res}, {requestID: "barfoo"}]);
expect(context.actions.rest.mock.calls[0]).toEqual([{ req, res }, { requestID: "barfoo" }]);
});

@@ -121,3 +121,3 @@ });

const context = MockContext();
context.actions.rest.mockReturnValueOnce(Promise.resolve({foo: "bar"}));
context.actions.rest.mockReturnValueOnce(Promise.resolve({ foo: "bar" }));

@@ -182,3 +182,3 @@ return handler.bind(context)(req, res, next).then(result => {

let error = new Error("Something went wrong while invoking the rest action");
error.code = 419;
error.code = 503;
context.actions.rest.mockReturnValueOnce(Promise.reject(error));

@@ -185,0 +185,0 @@

Sorry, the diff of this file is too big to display