Skip to content

Commit d387034

Browse files
committed
proxy implementation, tests
1 parent a2ccd9d commit d387034

8 files changed

+710
-1
lines changed

.eslintrc.yml

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
env:
2+
node: true
3+
extends: 'eslint:recommended'
4+
globals:
5+
Promise: false
6+
parserOptions:
7+
ecmaVersion: 6
8+
rules:
9+
indent: [ 2, 2, { SwitchCase: 1 } ]
10+
no-trailing-spaces: 2
11+
quotes: [ 2, single, avoid-escape ]
12+
linebreak-style: [ 2, unix ]
13+
semi: [ 2, always ]
14+
valid-jsdoc: [ 2, { requireReturn: false } ]
15+
no-invalid-this: 2
16+
no-unused-vars: [ 2, { args: none } ]
17+
# no-console: [ 2, { allow: [ warn, error ] } ]
18+
no-console: 0
19+
block-scoped-var: 2
20+
complexity: [ 2, 8 ]
21+
curly: [ 2, multi-or-nest, consistent ]
22+
dot-location: [ 2, property ]
23+
dot-notation: 2
24+
no-else-return: 2
25+
no-eq-null: 2
26+
no-fallthrough: 2
27+
no-return-assign: 2
28+
strict: [ 2, global ]
29+
no-shadow: 1
30+
no-use-before-define: [ 2, nofunc ]
31+
callback-return: 2
32+
no-path-concat: 2

README.md

+96-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,97 @@
11
# jsonscript-proxy
2-
Proxy server for scripted processing of other services
2+
3+
Proxy server for batch processing of other services using [JSONScript](https://github.com/JSONScript/jsonscript).
4+
5+
6+
## Install
7+
8+
To run proxy server using configuration file (not implemented yet):
9+
10+
```
11+
npm install -g jsonscript-proxy
12+
```
13+
14+
To add proxy to the existing express app
15+
16+
```
17+
npm install jsonscript-proxy
18+
```
19+
20+
21+
## Getting started
22+
23+
Sample proxy:
24+
25+
```JavaScript
26+
var express = require('express');
27+
var app = express();
28+
var bodyParser = require('body-parser');
29+
var jsonscriptProxy = require('jsonscript-proxy');
30+
31+
// app needs body parser for JSON even if no endpoint uses it.
32+
// it is needed for JSONScript middleware
33+
app.use(bodyParser.json());
34+
35+
/**
36+
* The code below adds JSONScript proxy on the endpoint '/js'
37+
* that allows processing any scripts combining existing services
38+
*/
39+
app.post('/js', jsonscriptProxy({
40+
services: {
41+
service1: { basePath: 'http://localhost:3001' },
42+
service2: { basePath: 'http://localhost:3002' },
43+
}
44+
}));
45+
46+
app.listen(3000);
47+
```
48+
49+
Now you can send POST requests to `/js` endpoint with the body containing the script and an optional data instance that will be processed by JSONScript interpreter. For example, with this request:
50+
51+
```json
52+
{
53+
"script": {
54+
"res1": { "$$service1.get": { "path": "/resource/1" } },
55+
"res2": { "$$service2.get": { "path": "/resource/2" } }
56+
}
57+
}
58+
```
59+
60+
the response will be a combination of two responses (both requests are processed in parallel):
61+
62+
```javascript
63+
{
64+
"res1": {
65+
"statusCode": 200,
66+
"headers": { /* response headers for the 1st request */ },
67+
"request": { "method": "get", "path": "/resource/1" },
68+
"body": { /* response body 1 */ }
69+
},
70+
"res2": {
71+
"statusCode": 200,
72+
"headers": { /* response headers for the 2nd request */ },
73+
"request": { "method": "get", "path": "/resource/2" },
74+
"body": { /* response body 2 */ }
75+
}
76+
}
77+
```
78+
79+
If option `processResponse: "body"` were used the result would have been:
80+
81+
```javascript
82+
{
83+
"res1": { /* response body 1 */ },
84+
"res2": { /* response body 2 */ }
85+
}
86+
```
87+
88+
Options passed to proxy middleware should be valid according to the [options schema](https://github.com/JSONScript/jsonscript-proxy/blob/master/config_schema.json).
89+
90+
JSONScript also supports sequential evaluation, conditionals, data manipulation, functions etc. So you can implement an advanced logic in your script and it will be executed in the server without sending responses of individual requests to the client.
91+
92+
See [JSONScript Language](https://github.com/JSONScript/jsonscript/blob/master/LANGUAGE.md) for more information.
93+
94+
95+
## License
96+
97+
[MIT](https://github.com/JSONScript/jsonscript-proxy/blob/master/LICENSE)

config_schema.json

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
{
2+
"id": "https://raw.githubusercontent.com/JSONScript/jsonscript-proxy/master/config_schema.json",
3+
"$schema": "http://json-schema.org/draft-04/schema#",
4+
"title": "Configuration",
5+
"description": "Schema for proxy server configuration file (and for config passed to proxy middleware)",
6+
"type": "object",
7+
"required": ["services"],
8+
"properties": {
9+
"services": {
10+
"title": "proxied services",
11+
"description": "each property name will be an executor exposed to JSONScript interpreter",
12+
"type": "object",
13+
"minProperties": 1,
14+
"additionalProperties": false,
15+
"patternProperties": {
16+
"^[A-Za-z_$][A-Za-z_$0-9]+$": { "$ref": "#/definitions/service" }
17+
}
18+
},
19+
"jsonscript": {
20+
"description": "options passed to JSONScript interpreter",
21+
"type": "object"
22+
},
23+
"processResponse": { "$ref": "#/definitions/processResponse" }
24+
},
25+
"definitions": {
26+
"service": {
27+
"description": "proxied service definition",
28+
"required": ["basePath"],
29+
"properties": {
30+
"basePath": {
31+
"type": "string",
32+
"format": "uri"
33+
},
34+
"processResponse": { "$ref": "#/definitions/processResponse" }
35+
}
36+
},
37+
"processResponse": {
38+
"description": "defines how response from service is processed",
39+
"anyOf": [
40+
{
41+
"description": "return only response body if status code is < 300, throw an exception otherwise",
42+
"type": "string",
43+
"enum": ["body"]
44+
},
45+
{
46+
"description": "uses custom keyword 'typeof', function should return result or throw exception",
47+
"not": { "type": ["string", "number", "array", "object", "boolean", "null"] },
48+
"typeof": "function"
49+
}
50+
]
51+
}
52+
}
53+
}

index.js

+125
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
'use strict';
2+
3+
var JSONScript = require('jsonscript-js')
4+
, request = require('request')
5+
, Ajv = require('ajv')
6+
, defineKeywords = require('ajv-keywords')
7+
, _ = require('lodash')
8+
, optionsSchema = require('./config_schema.json')
9+
, validateOptions = getValidator();
10+
11+
module.exports = jsonscriptProxy;
12+
13+
14+
var METHODS = ['get', 'post', 'put', 'delete'];
15+
function jsonscriptProxy(options) {
16+
if (!validateOptions(options)) {
17+
console.log('Invalid options:\n', validateOptions.errors);
18+
throw new Error('Invalid options');
19+
}
20+
21+
var js = JSONScript(options.jsonscript || { strict: true });
22+
for (var key in options.services)
23+
js.addExecutor(key, getExecutor(options.services[key]));
24+
evaluator.js = js;
25+
26+
return evaluator;
27+
28+
29+
function evaluator(req, res) {
30+
var script = req.body.script;
31+
var data = req.body.data;
32+
var valid = js.validate(script);
33+
if (valid) {
34+
js.evaluate(script, data)
35+
.then(function (value) {
36+
res.json(value);
37+
}, function (err) {
38+
res.status(err.errors ? 400 : err.statusCode || 500)
39+
.send({
40+
error: err.message,
41+
errors: err.errors
42+
});
43+
});
44+
} else {
45+
res.status(400)
46+
.send({
47+
error: 'script is invalid',
48+
errors: js.validate.errors
49+
});
50+
}
51+
}
52+
53+
54+
function getExecutor(service) {
55+
var processResponse = processResponseFunc(service.processResponse || options.processResponse);
56+
addExecutorMethods();
57+
return execRouter;
58+
59+
function execRouter(args) {
60+
var opts = {
61+
uri: service.basePath + args.path,
62+
headers: args.headers,
63+
json: args.body || true
64+
};
65+
66+
return new (options.Promise || Promise)(function (resolve, reject) {
67+
request[args.method](opts, function (err, resp) {
68+
if (err) return reject(err);
69+
resolve(processResponse(resp, args));
70+
});
71+
});
72+
}
73+
74+
function addExecutorMethods() {
75+
METHODS.forEach(function (method) {
76+
execRouter[method] = function(args) {
77+
if (args.method && args.method != method) {
78+
console.warn('method specified in args (' + args.method +
79+
') is different from $method in instruction (' + method + '), used ' + method);
80+
}
81+
args.method = method;
82+
return execRouter(args);
83+
};
84+
});
85+
}
86+
}
87+
}
88+
89+
90+
function getValidator() {
91+
var ajv = Ajv({ allErrors: true });
92+
defineKeywords(ajv, 'typeof');
93+
return ajv.compile(optionsSchema);
94+
}
95+
96+
97+
function processResponseFunc(processResponse) {
98+
return processResponse == 'body'
99+
? bodyProcessResponse
100+
: typeof processResponse == 'function'
101+
? processResponse
102+
: defaultProcessResponse;
103+
}
104+
105+
106+
function bodyProcessResponse(resp) {
107+
if (resp.statusCode < 300) return resp.body;
108+
throw new HttpError(resp);
109+
}
110+
111+
112+
function defaultProcessResponse(resp, args) {
113+
resp = _.pick(resp, 'statusCode', 'headers', 'body');
114+
resp.request = args;
115+
return resp;
116+
}
117+
118+
119+
function HttpError(resp) {
120+
this.message = resp.body ? JSON.stringify(resp.body) : 'Error';
121+
this.statusCode = resp.statusCode;
122+
}
123+
124+
HttpError.prototype = Object.create(Error.prototype);
125+
HttpError.prototype.constructor = HttpError;

package.json

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{
2+
"name": "jsonscript-proxy",
3+
"version": "0.1.0",
4+
"description": "Proxy server for scripted processing of other services using JSONScript",
5+
"main": "index.js",
6+
"scripts": {
7+
"test": "npm run eslint && npm run test-cov",
8+
"test-spec": "mocha spec/*.spec.js -R spec",
9+
"test-cov": "istanbul cover -x 'spec/**' node_modules/mocha/bin/_mocha -- spec/*.spec.js -R spec",
10+
"eslint": "eslint index.js"
11+
},
12+
"repository": {
13+
"type": "git",
14+
"url": "git+https://github.com/JSONScript/jsonscript-proxy.git"
15+
},
16+
"keywords": [
17+
"JSONScript",
18+
"proxy server",
19+
"batch processing"
20+
],
21+
"author": "Evgeny Poberezkin",
22+
"license": "MIT",
23+
"bugs": {
24+
"url": "https://github.com/JSONScript/jsonscript-proxy/issues"
25+
},
26+
"homepage": "https://github.com/JSONScript/jsonscript-proxy#readme",
27+
"dependencies": {
28+
"ajv": "^4.1.2",
29+
"ajv-keywords": "^0.2.0",
30+
"jsonscript-js": "^0.4.2",
31+
"lodash": "^4.13.1",
32+
"request": "^2.72.0"
33+
},
34+
"devDependencies": {
35+
"body-parser": "^1.15.1",
36+
"eslint": "^2.12.0",
37+
"express": "^4.13.4",
38+
"istanbul": "^0.4.3",
39+
"mocha": "^2.5.3",
40+
"supertest": "^1.2.0"
41+
}
42+
}

spec/proxy.js

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
'use strict';
2+
3+
var express = require('express');
4+
var bodyParser = require('body-parser');
5+
var jsonscriptProxy = require('..');
6+
var _ = require('lodash');
7+
8+
9+
module.exports = function createProxy(options) {
10+
var app = express();
11+
app.use(bodyParser.json());
12+
app.post('/js', jsonscriptProxy(options));
13+
return app;
14+
};

0 commit comments

Comments
 (0)