You're Invited: Meet the Socket team at BSidesSF and RSAC - April 27 - May 1.RSVP

moleculer-web

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.6.0-beta4

<a name="0.6.0"></a>
# 0.6.0 (2017-xx-xx)
## Changes
## Breaking changes
### Alias custom function arguments is changed
The `route` first argument is removed. The new signature of function is `function(req, res) {}`. To access to route use the `req.$route` property.
However you can use an array of `Function` for aliases. With it you can call middlewares. In this case the third argument is `next`. I.e.: `function(req, res, next) {}`.
## Other changes
- better error handling. Always returns with JSON error response.

@@ -9,7 +15,9 @@ - The charset is `UTF-8` for `application/json` responses.

- `logResponseData` setting to log the response data. Use log level value i.e. `"debug"`, `"info"` or `null` to disable.
- alias function has a new 4th parameter, the `params`.
- the `req.service` is pointed to the service instance.
- `req.$service` & `res.$service` is pointed to the service instance.
- `req.$route` & `res.$route` is pointed to the route definition.
- `req.$params` is pointed to the resolved parameters (from query string & post body)
- `req.$alias` is pointed to the alias definition.
## Middlewares
Support middlewares in routes.
Support middlewares in routes & aliases

@@ -20,2 +28,7 @@ ```js

settings: {
// Global middlewares. Applied to all routes.
use: [
cookieParser()
],
routes: [

@@ -25,7 +38,16 @@ {

// Middlewares
// Route-level middlewares
use: [
compression(),
serveStatic(path.join(__dirname, "public"))
]
],
aliases: {
"GET /secret": [
// Alias-level middlewares
auth.isAuthenticated(),
auth.hasRole("admin"),
"top.secret" // Call the `top.secret` action
]
}
}

@@ -32,0 +54,0 @@ ]

{
"name": "moleculer-web",
"version": "0.6.0-beta3",
"version": "0.6.0-beta4",
"description": "Official API Gateway service for Moleculer framework",

@@ -58,3 +58,2 @@ "main": "index.js",

"body-parser": "1.18.2",
"depd": "1.1.1",
"es6-error": "4.0.2",

@@ -61,0 +60,0 @@ "isstream": "0.1.2",

@@ -20,7 +20,5 @@ /*

const deprecate = require("depd")("moleculer-web");
const { Context } = require("moleculer");
const { MoleculerError, ServiceNotFoundError } = require("moleculer").Errors;
const { InvalidRequestBodyError, BadRequestError, RateLimitExceeded, ERR_UNABLE_DECODE_PARAM } = require("./errors");
const { MoleculerError, MoleculerServerError, ServiceNotFoundError } = require("moleculer").Errors;
const { BadRequestError, RateLimitExceeded, ERR_UNABLE_DECODE_PARAM } = require("./errors");

@@ -120,2 +118,23 @@ const MemoryStore = require("./memory-store");

/**
* Compose middlewares
*
* @param {...Function} mws
*/
compose(...mws) {
return (req, res, done) => {
const next = (i, err) => {
if (i >= mws.length || err) {
if (_.isFunction(done))
return done.call(this, err);
return;
}
mws[i].call(this, req, res, err => next(i + 1, err));
};
return next(0);
};
},
/**
* Create route object from options

@@ -154,20 +173,14 @@ *

// Middlewares
if (opts.use && Array.isArray(opts.use) && opts.use.length > 0) {
let mw = [];
if (this.settings.use && Array.isArray(this.settings.use) && this.settings.use.length > 0)
mw.push(...this.settings.use);
route.middlewares.push(...opts.use);
if (opts.use && Array.isArray(opts.use) && opts.use.length > 0)
mw.push(...opts.use);
this.logger.info(` Registered ${opts.use.length.length} middlewares.`);
if (mw.length > 0) {
route.middlewares.push(...mw);
this.logger.info(` Registered ${mw.length} middlewares.`);
}
route.callMiddlewares = (req, res, next) => {
const nextFn = (i, err) => {
if (i >= route.middlewares.length || err)
return next.call(this, req, res, err);
route.middlewares[i].call(this, req, res, err => nextFn(i + 1, err));
};
return nextFn(0);
};
// CORS

@@ -224,3 +237,3 @@ if (this.settings.cors || opts.cors) {

// Helper for aliased routes
const createAliasedRoute = (action, matchPath) => {
const createAlias = (matchPath, action) => {
let method = "*";

@@ -235,31 +248,49 @@ if (matchPath.indexOf(" ") !== -1) {

let alias;
if (_.isString(action))
alias = { action };
else if (_.isFunction(action))
alias = { handler: action };
else if (Array.isArray(action)) {
let mws = action.map(mw => {
if (_.isString(mw))
return (req, res) => this.preActionCall(route, req, res, mw);
else if(_.isFunction(mw))
return mw;
});
alias = { handler: this.compose(...mws) };
} else {
alias = action;
}
alias.path = matchPath;
alias.method = method;
let keys = [];
const re = pathToRegexp(matchPath, keys, {}); // Options: https://github.com/pillarjs/path-to-regexp#usage
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} -> ${_.isFunction(action) ? "<Function>" : action}`);
return {
action,
method,
re,
match: url => {
const m = re.exec(url);
if (!m) return false;
this.logger.info(` Alias: ${method} ${route.path + (route.path.endsWith("/") ? "": "/")}${matchPath} -> ${alias.handler != null ? "<Function>" : alias.action}`);
const params = {};
alias.match = url => {
const m = alias.re.exec(url);
if (!m) return false;
let key, param;
for (let i = 0; i < keys.length; i++) {
key = keys[i];
param = m[i + 1];
if (!param) continue;
const params = {};
params[key.name] = decodeParam(param);
let key, param;
for (let i = 0; i < keys.length; i++) {
key = keys[i];
param = m[i + 1];
if (!param) continue;
if (key.repeat)
params[key.name] = params[key.name].split(key.delimiter);
}
params[key.name] = decodeParam(param);
return params;
if (key.repeat)
params[key.name] = params[key.name].split(key.delimiter);
}
return params;
};
return alias;
};

@@ -276,11 +307,11 @@

// Generate RESTful API. More info http://www.restapitutorial.com/
route.aliases.push(createAliasedRoute(`${action}.list`, `GET ${pathName}`));
route.aliases.push(createAliasedRoute(`${action}.get`, `GET ${pathName}/:id`));
route.aliases.push(createAliasedRoute(`${action}.create`, `POST ${pathName}`));
route.aliases.push(createAliasedRoute(`${action}.update`, `PUT ${pathName}/:id`));
//route.aliases.push(createAliasedRoute(`${action}.update`, `PATCH ${pathName}/:id`));
route.aliases.push(createAliasedRoute(`${action}.remove`, `DELETE ${pathName}/:id`));
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}.update`));
route.aliases.push(createAlias(`DELETE ${pathName}/:id`, `${action}.remove`));
} else {
route.aliases.push(createAliasedRoute(action, matchPath));
route.aliases.push(createAlias(matchPath, action));
}

@@ -299,7 +330,6 @@ });

* @param {HttpResponse} res
* @param {Function} next - `next` callback in middleware mode
*/
send404(req, res, next) {
if (next)
return next();
send404(req, res) {
if (req.$next)
return req.$next();

@@ -314,7 +344,6 @@ this.sendError(req, res, new MoleculerError("Not found", 404));

* @param {Error} err
* @param {Function} next - `next` callback in middleware mode
*/
sendError(req, res, err, next) {
if (next)
return next(err);
sendError(req, res, err) {
if (req.$next)
return req.$next(err);

@@ -349,3 +378,4 @@ if (!err || !(err instanceof Error)) {

res.writeHead(code, {
"Location": url
"Location": url,
"Content-Length": "0"
});

@@ -362,3 +392,3 @@ res.end();

*/
processQueryString(req) {
parseQueryString(req) {
// Split URL & query params

@@ -384,3 +414,6 @@ let url = req.url;

httpHandler(req, res, next) {
req.service = this; // pointer to this service
req.$service = this; // pointer to this service
res.$service = this; // pointer to this service
req.$next = next;
req.locals = req.locals || {};

@@ -391,7 +424,8 @@ this.logRequest(req);

// Split URL & query params
let {query, url} = this.processQueryString(req);
let parsed = this.parseQueryString(req);
let url = parsed.url;
if (!req.query)
req.query = query;
req.query = parsed.query;
let params = Object.assign({}, query);
let params = {};

@@ -409,18 +443,72 @@ // Trim trailing slash

req.route = route;
// Pointer to the matched route
req.$route = route;
res.$route = route;
// Call middlewares
if (route.middlewares.length > 0) {
route.callMiddlewares(req, res, (req, res, err) => {
if (err) {
const error = new MoleculerError(err.message, err.status, err.type);
this.logger.error("Middleware error!", error);
return this.sendError(req, res, error, next);
this.compose(...route.middlewares)(req, res, err => {
if (err) {
const error = new MoleculerError(err.message, err.status, err.type);
this.logger.error("Middleware error!", error);
return this.sendError(req, res, error);
}
// Merge params
const body = _.isObject(req.body) ? req.body : {};
Object.assign(params, body, req.query);
req.$params = params;
// Resolve action name
let urlPath = url.slice(route.path.length);
if (urlPath.startsWith("/"))
urlPath = urlPath.slice(1);
urlPath = urlPath.replace(/~/, "$");
let actionName = urlPath;
// Resolve aliases
if (route.aliases && route.aliases.length > 0) {
const found = this.resolveAlias(route, urlPath, req.method);
if (found) {
let alias = found.alias;
this.logger.debug(` Alias: ${req.method} ${urlPath} -> ${alias.action}`);
Object.assign(params, found.params);
req.$alias = alias;
// Custom Action handler
if (alias.handler) {
return alias.handler.call(this, req, res, err => {
if (err) {
const error = new MoleculerError(err.message, err.status, err.type);
this.logger.error("Alias middleware error!", error);
return this.sendError(req, res, error);
}
if (req.$next)
return req.$next();
// If it is reached, there is no real handler for this alias.
const error = new MoleculerServerError("No alias handler", 500);
this.logger.error(error);
return this.sendError(req, res, error);
});
}
actionName = alias.action;
} else if (route.mappingPolicy == MAPPING_POLICY_RESTRICT) {
// Blocking direct access
return this.send404(req, res);
}
this.routeHandler(route, req, res, url, params, next);
});
} else {
this.routeHandler(route, req, res, url, params, next);
}
}
actionName = actionName.replace(/\//g, ".");
if (route.opts.camelCaseNames) {
actionName = actionName.split(".").map(_.camelCase).join(".");
}
this.preActionCall(route, req, res, actionName);
});
return;

@@ -435,3 +523,3 @@ }

this.logger.debug(err);
this.send404(req, res, next);
this.send404(req, res);
});

@@ -442,7 +530,7 @@ return;

// If no route, send 404
this.send404(req, res, next);
this.send404(req, res);
} catch(err) {
this.logger.error("Handler error!", err);
return this.sendError(req, res, err, next);
return this.sendError(req, res, err);
}

@@ -453,3 +541,2 @@ },

* Route handler.
* - resolve aliases
* - check whitelist

@@ -462,39 +549,7 @@ * - CORS

* @param {HttpResponse} res
* @param {String} url
* @param {Object} params
* @param {Function} next Call next middleware (for Express)
* @param {String} actionName
* @returns
*/
routeHandler(route, req, res, url, params, next) {
// Resolve action name
let urlPath = url.slice(route.path.length);
if (urlPath.startsWith("/"))
urlPath = urlPath.slice(1);
preActionCall(route, req, res, actionName) {
urlPath = urlPath.replace(/~/, "$");
let actionName = urlPath;
// Resolve aliases
if (route.aliases && route.aliases.length > 0) {
const alias = this.resolveAlias(route, urlPath, req.method);
if (alias) {
this.logger.debug(` Alias: ${req.method} ${urlPath} -> ${alias.action}`);
actionName = alias.action;
Object.assign(params, alias.params);
// Custom Action handler
if (_.isFunction(alias.action)) {
return alias.action.call(this, route, req, res, params);
}
} else if (route.mappingPolicy == MAPPING_POLICY_RESTRICT) {
// Blocking direct access
return this.send404(req, res, next);
}
}
actionName = actionName.replace(/\//g, ".");
if (route.opts.camelCaseNames) {
actionName = actionName.split(".").map(part => _.camelCase(part)).join(".");
}
// Whitelist check

@@ -504,3 +559,3 @@ if (route.hasWhitelist) {

this.logger.debug(` The '${actionName}' action is not in the whitelist!`);
return this.sendError(req, res, new ServiceNotFoundError(actionName), next);
return this.sendError(req, res, new ServiceNotFoundError(actionName));
}

@@ -515,15 +570,13 @@ }

const key = opts.key(req);
if (!key)
/* istanbul ignore next */
return;
const remaining = opts.limit - store.inc(key);
if (opts.headers) {
res.setHeader("X-Rate-Limit-Limit", opts.limit);
res.setHeader("X-Rate-Limit-Remaining", Math.max(0, remaining));
res.setHeader("X-Rate-Limit-Reset", store.resetTime);
if (key) {
const remaining = opts.limit - store.inc(key);
if (opts.headers) {
res.setHeader("X-Rate-Limit-Limit", opts.limit);
res.setHeader("X-Rate-Limit-Remaining", Math.max(0, remaining));
res.setHeader("X-Rate-Limit-Reset", store.resetTime);
}
if (remaining < 0) {
return this.sendError(req, res, new RateLimitExceeded());
}
}
if (remaining < 0) {
return this.sendError(req, res, new RateLimitExceeded(), next);
}
}

@@ -551,8 +604,4 @@

// Merge params
const body = _.isObject(req.body) ? req.body : {};
params = Object.assign({}, body, params);
// Call the action
return this.callAction(route, actionName, req, res, params);
return this.callAction(route, actionName, req, res, req.$params);
},

@@ -612,2 +661,12 @@

// Pass the `req` & `res` vars to ctx.params.
if (req.$alias && req.$alias.passReqResToParams) {
if (endpoint.local) {
params.$req = req;
params.$res = res;
} else {
this.logger.warn("Don't use the `passReqResToParams` option in aliases if you call a remote service.");
}
}
// Create a new context to wrap the request

@@ -617,2 +676,3 @@ const ctx = Context.create(this.broker, restAction, this.broker.nodeID, params, route.callOptions || {});

return ctx;

@@ -897,3 +957,3 @@ })

return {
action: alias.action,
alias,
params: res

@@ -900,0 +960,0 @@ };

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

describe("Test alias", () => {
describe("Test aliases", () => {
let broker;

@@ -610,6 +610,21 @@ let service;

let customAlias = jest.fn((route, req, res, params) => {
res.end(`Custom Alias by ${params.name}`);
let customAlias = jest.fn((req, res) => {
expect(req.$route).toBeDefined();
expect(req.$service).toBe(service);
expect(req.$params).toEqual({
name: "Ben"
});
expect(res.$route).toBeDefined();
expect(res.$service).toBe(service);
res.end(`Custom Alias by ${req.$params.name}`);
});
let customMiddlewares = [
jest.fn((req, res, next) => next()),
jest.fn((req, res, next) => next()),
"test.greeter"
];
beforeAll(() => {

@@ -624,6 +639,13 @@ [ broker, service, server] = setup({

"GET greeter/:name": "test.greeter",
"POST greeting/:name": "test.greeter",
"opt-test/:name?": "test.echo",
"/repeat-test/:args*": "test.echo",
"GET /": "test.hello",
"GET custom": customAlias
"GET custom": customAlias,
"GET /middleware": customMiddlewares,
"GET /wrong-middleware": [customMiddlewares[0], customMiddlewares[1]],
"GET reqres": {
action: "test.reqres",
passReqResToParams: true
},
}

@@ -728,2 +750,14 @@ }]

it("POST /api/greeting/Norbert", () => {
return request(server)
.post("/api/greeting/Norbert")
.query({ name: "John" })
.send({ name: "Adam" })
.expect(200)
.expect("Content-Type", "application/json; charset=utf-8")
.then(res => {
expect(res.body).toBe("Hello Norbert");
});
});
it("GET opt-test/:name? with name", () => {

@@ -781,5 +815,56 @@ return request(server)

expect(customAlias).toHaveBeenCalledTimes(1);
expect(customAlias).toHaveBeenCalledWith(service.routes[0], jasmine.any(http.IncomingMessage), jasmine.any(http.ServerResponse), { name: "Ben" });
expect(customAlias).toHaveBeenCalledWith(jasmine.any(http.IncomingMessage), jasmine.any(http.ServerResponse), jasmine.any(Function));
});
});
it("GET /api/middleware", () => {
return request(server)
.get("/api/middleware")
.query({ name: "Ben" })
.expect(200)
.then(res => {
expect(res.body).toBe("Hello Ben");
expect(customMiddlewares[0]).toHaveBeenCalledTimes(1);
expect(customMiddlewares[0]).toHaveBeenCalledWith(jasmine.any(http.IncomingMessage), jasmine.any(http.ServerResponse), jasmine.any(Function));
expect(customMiddlewares[1]).toHaveBeenCalledTimes(1);
expect(customMiddlewares[1]).toHaveBeenCalledWith(jasmine.any(http.IncomingMessage), jasmine.any(http.ServerResponse), jasmine.any(Function));
});
});
it("GET /api/wrong-middleware", () => {
customMiddlewares[0].mockClear();
customMiddlewares[1].mockClear();
return request(server)
.get("/api/wrong-middleware")
.expect(500)
.then(res => {
expect(res.body).toEqual({
"name": "MoleculerServerError",
"message": "No alias handler",
"code": 500,
});
expect(customMiddlewares[0]).toHaveBeenCalledTimes(1);
expect(customMiddlewares[0]).toHaveBeenCalledWith(jasmine.any(http.IncomingMessage), jasmine.any(http.ServerResponse), jasmine.any(Function));
expect(customMiddlewares[1]).toHaveBeenCalledTimes(1);
expect(customMiddlewares[1]).toHaveBeenCalledWith(jasmine.any(http.IncomingMessage), jasmine.any(http.ServerResponse), jasmine.any(Function));
});
});
it("GET /api/reqres with name", () => {
return request(server)
.get("/api/reqres")
.expect(200)
.expect("Content-Type", "application/json; charset=utf-8")
.then(res => {
expect(res.body).toEqual({
hasReq: true,
hasRes: true
});
});
});
});

@@ -1548,6 +1633,8 @@

it("should call both middlewares", () => {
it("should call global & route middlewares", () => {
const broker = new ServiceBroker();
broker.loadService("./test/services/test.service");
const mwg = jest.fn((req, res, next) => next());
const mw1 = jest.fn((req, res, next) => {

@@ -1564,2 +1651,3 @@ res.setHeader("X-Custom-Header", "middleware");

settings: {
use: [mwg],
routes: [{

@@ -1580,2 +1668,5 @@ path: "/",

expect(res.body).toBe("Hello Moleculer");
expect(mwg).toHaveBeenCalledTimes(1);
expect(mwg).toHaveBeenCalledWith(jasmine.any(http.IncomingMessage), jasmine.any(http.ServerResponse), jasmine.any(Function));
expect(mw1).toHaveBeenCalledTimes(1);

@@ -1582,0 +1673,0 @@ expect(mw1).toHaveBeenCalledWith(jasmine.any(http.IncomingMessage), jasmine.any(http.ServerResponse), jasmine.any(Function));

@@ -36,2 +36,12 @@ "use strict";

reqres: {
handler(ctx) {
return {
hasReq: !!ctx.params.$req,
hasRes: !!ctx.params.$res,
a: ctx.params.a
};
}
},
dangerZone: {

@@ -38,0 +48,0 @@ publish: false,