strong-remotingnext
Version:
StrongLoop Remoting Module
279 lines (235 loc) • 7.7 kB
JavaScript
// Copyright IBM Corp. 2013,2016. All Rights Reserved.
// Node module: strong-remoting
// This file is licensed under the Artistic License 2.0.
// License text available at https://opensource.org/licenses/Artistic-2.0
/*!
* Expose `JsonRpcAdapter`.
*/
module.exports = JsonRpcAdapter;
/*!
* Module dependencies.
*/
var EventEmitter = require('events').EventEmitter;
var debug = require('debug')('strong-remoting:jsonrpc-adapter');
var util = require('util');
var inherits = util.inherits;
var jayson = require('jayson');
var express = require('express');
var bodyParser = require('body-parser');
var cors = require('cors');
var HttpContext = require('./http-context');
var json = bodyParser.json;
var urlencoded = bodyParser.urlencoded;
/**
* Create a new `JsonRpcAdapter` with the given `options`.
*
* @param {Object} options
* @return {JsonRpcAdapter}
*/
function JsonRpcAdapter(remotes) {
EventEmitter.call(this);
this.remotes = remotes;
this.Context = HttpContext;
}
/**
* Inherit from `EventEmitter`.
*/
inherits(JsonRpcAdapter, EventEmitter);
/*!
* Simplified APIs
*/
JsonRpcAdapter.create =
JsonRpcAdapter.createJsonRpcAdapter = function(remotes) {
// add simplified construction / sugar here
return new JsonRpcAdapter(remotes);
};
/**
* Get the path for the given method.
*/
JsonRpcAdapter.prototype.getRoutes = function(obj) {
// build default route
var routes = [
{
verb: 'POST',
path: obj.name ? ('/' + obj.name) : ''
}
];
return routes;
};
JsonRpcAdapter.errorHandler = function() {
return function restErrorHandler(err, req, res, next) {
if (typeof err === 'string') {
err = new Error(err);
err.status = err.statusCode = 500;
}
res.statusCode = err.statusCode || err.status || 500;
debug('Error in %s %s: %s', req.method, req.url, err.stack);
var data = {
name: err.name,
status: res.statusCode,
message: err.message || 'An unknown error occurred'
};
for (var prop in err) {
data[prop] = err[prop];
}
// TODO(bajtos) Remove stack info when running in production
data.stack = err.stack;
res.send({
jsonrpc: '2.0',
error: { code: -32000, message: 'Server error', data: data },
id: null
});
};
};
// A mock wrapper function to help code generation.
// Note that we can't make it a real function and use .toString() on it because
// that causes a whole world of trouble when we run strong-remoting's unit tests
// with code coverage.
var mockWrapper = [
'function mockWrapper(method) {',
' return function(__args__) {',
' var args = Array.prototype.slice.call(arguments);',
' if (method.isStatic) {',
' method.getFunction().apply(method.ctor, args);',
' } else {',
' method.sharedCtor.invoke(method, function(err, instance) {',
' method.getFunction().apply(instance, args);',
' });',
' }',
' };',
'}',
].join('\n');
/* istanbul ignore next */
JsonRpcAdapter.prototype.createHandler = function() {
var root = express.Router();
var classes = this.remotes.classes();
// Add a handler to tolerate empty json as connect's json middleware throws an error
root.use(function(req, res, next) {
if (req.is('application/json')) {
if (req.get('Content-Length') === '0') {
// This doesn't cover the transfer-encoding: chunked
req._body = true; // Mark it as parsed
req.body = {};
}
}
next();
});
// Set strict to be `false` so that anything `JSON.parse()` accepts will be parsed
debug('remoting options: %j', this.remotes.options);
var jsonOptions = this.remotes.options.json || {strict: false};
var corsOptions = this.remotes.options.cors;
if (corsOptions === undefined) corsOptions = {origin: true, credentials: true};
// Optimize the cors handler
var corsHandler = function(req, res, next) {
var reqUrl = req.protocol + '://' + req.get('host');
if (req.method === 'OPTIONS' || reqUrl !== req.get('origin')) {
cors(corsOptions)(req, res, next);
} else {
next();
}
};
// Set up CORS first so that it's always enabled even when parsing errors
// happen in urlencoded/json
if (corsOptions)
root.use(corsHandler);
root.use(json(jsonOptions));
root.use(JsonRpcAdapter.errorHandler());
classes.forEach(function(sc) {
var server = new jayson.server();
root.post('/' + sc.name + '/jsonrpc',
new jayson.server.interfaces.middleware(server, {}));
var methods = sc.methods();
methods.forEach(function(method) {
// Wrap the method so that it will keep its own receiver - the shared class
var argsNames = '';
if (method.accepts) {
argsNames = method.accepts.map(function(item) {
return item.arg;
});
argsNames = argsNames.join(',');
} else {
var m = method.getFunction();
if (m.length > 1) {
// The method has more args than cb
// Build dummy param names
var names = [];
for (var i = 0; i < m.length - 1; i++) {
names.push('param' + i);
}
argsNames = names.join(',');
}
}
argsNames = argsNames ? argsNames + ',cb' : 'cb';
// Generate the function based on the wrapper
// We need to remove the header/footer to get the function body
var funcBody = mockWrapper.toString().
replace('function mockWrapper(method) {', '').
replace('__args__', argsNames).
replace(/}$/, '');
/*jslint evil: true */
var fn = new Function('method', funcBody)(method);
if (debug.enabled) {
debug('Generated function: %s', fn.toString());
}
server.method(method.name, fn);
});
});
return root;
};
JsonRpcAdapter.prototype.allRoutes = function() {
var routes = [];
var adapter = this;
var classes = this.remotes.classes();
var currentRoot = '';
classes.forEach(function(sc) {
adapter
.getRoutes(sc)
.forEach(function(classRoute) {
currentRoot = classRoute.path;
var methods = sc.methods();
var functions = [];
methods.forEach(function(method) {
// Use functions to keep track of JS functions to dedupe
if (functions.indexOf(method.fn) === -1) {
functions.push(method.fn);
} else {
return; // Skip duplicate methods such as X.m1 = X.m2 = function() {...}
}
adapter.getRoutes(method).forEach(function(route) {
if (method.isStatic) {
addRoute(route.verb, route.path, method);
} else {
adapter
.getRoutes(method.sharedCtor)
.forEach(function(sharedCtorRoute) {
addRoute(route.verb, sharedCtorRoute.path + route.path, method);
});
}
});
});
});
});
return routes;
function addRoute(verb, path, method) {
if (path === '/' || path === '//') {
path = currentRoot;
} else {
path = currentRoot + path;
}
if (path[path.length - 1] === '/') {
path = path.substr(0, path.length - 1);
}
// TODO this could be cleaner
path = path.replace(/\/\//g, '/');
routes.push({
verb: verb,
path: path,
description: method.description,
notes: method.notes,
method: method.stringName,
accepts: (method.accepts && method.accepts.length) ? method.accepts : undefined,
returns: (method.returns && method.returns.length) ? method.returns : undefined,
errors: (method.errors && method.errors.length) ? method.errors : undefined
});
}
};