Skip to content

Commit

Permalink
init commit
Browse files Browse the repository at this point in the history
  • Loading branch information
dimapaloskin committed Mar 11, 2017
0 parents commit bba37ad
Show file tree
Hide file tree
Showing 13 changed files with 4,415 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"plugins": ["transform-async-to-generator"]
}
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules
npm-debug.log
.DS_Store
dist
57 changes: 57 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# micro-chain

Helps to build chains from your [micro](https://github.com/zeit/micro/) services.

### Example

**index.js**
```js
const chain = require('micro-chain');

const options = {
micro: 'app.js', // or just your micro async function required before
location: /\/some-path$/, // optional. chain will be executed only if request url matched
chain: [{
target: 'account.api.your-domain.com',
mergeJson: true // response json will be merged with
// request json and will be passed down the chain
}, {
target: 'storage.api.your-domain.com/s3/save',
mergeJson: true,
// will modify request data and return result if declared
// receive Buffer
transformRequestBody: body => {
if (!body || !body.length) return body;
body = JSON.parse(body);
delete body.secret;
return body;
},
// will modify response body if declared
// receive Buffer
transformReponseBody: body => {
if (!body || !body.length) return body;
body = JSON.parse(body);
body.newProp = true;
delete body.token;
return body;
}
}, {
// if host is not declared will extract host from original request
target: '/notify'
}, {
target: 'some-stuff.api.your-domain.com',
sendOriginalBody: true // modifed body passed down the chain.
// use this option if you want send original data
}]
};

module.exports = chain(options);
```

Run:
```bash
micro index.js
```

### Author
[Dmitry Pavlovsky](http://palosk.in)
19 changes: 19 additions & 0 deletions example/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const { json } = require('micro');

module.exports = async req => {
const uri = req.url;
switch (uri) {
case '/auth': {
return { id: '123', username: 'paloskin', email: '[email protected]' };
}
case '/private': {
return { message: 'Nice to see you :)' };
}
case '/public': {
const body = await json(req);
return Object.assign({}, body, { final: true });
}
default:
return { message: 'hi' };
}
};
31 changes: 31 additions & 0 deletions example/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
const chain = require('./../lib');

const options = {
micro: './app.js',
location: /\/public$/,
chain: [{
target: '/private'
}, {
target: '/auth',
allowedStatuses: [200],
mergeJson: true,
transformRequestBody: body => {
body = JSON.parse(body);
delete body.password;
return body;
},
transformResponseBody: body => {
body = JSON.parse(body);
delete body.email;
return body;
}
}, {
target: 'https://ping.pong.global',
mergeJson: true
}, {
target: 'http://date.jsontest.com',
mergeJson: true
}]
};

module.exports = chain(options);
94 changes: 94 additions & 0 deletions lib/chain-request.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
const http = require('http');
const https = require('https');
const url = require('url');
const querystring = require('querystring');
const Promise = require('bluebird');
const typer = require('media-typer');
const bl = require('bl');
const fullUrl = require('full-url');
const ip = require('ip');

module.exports = (req, body, {
target,
useSearchQuery = false,
isOriginal = false
} = {}) => {
return new Promise((resolve, reject) => {
const type = req.headers['content-type'];
const encoding = (type) ? typer.parse(type).parameters.charset : undefined;

const full = fullUrl(req);
const parsedOriginal = url.parse(full);

if (!target.startsWith('http:') && !target.startsWith('https:') && !target.startsWith('/')) {
target = `${parsedOriginal.protocol}//${target}`;
}

const parsedTarget = url.parse(target);
if (!parsedTarget.protocol) {
parsedTarget.protocol = parsedOriginal.protocol;
}

// Don't DOS Yourself!
if (!parsedTarget.hostname) {
const p = url.parse(full);
parsedTarget.hostname = p.hostname;
parsedTarget.port = p.port;
}

const headers = Object.assign({}, req.headers, {
host: parsedTarget.host || parsedOriginal.host
});

if (body && body.length !== 0) {
headers['content-length'] = body.length;
} else if (headers['content-length']) {
delete headers['content-length'];
}

let resolvedUrl = parsedTarget.path;
if (useSearchQuery) {
const queryObj = Object.assign({}, querystring.parse(parsedTarget.query), querystring.parse(parsedOriginal.query));
resolvedUrl = url.format({
pathname: parsedTarget.pathname,
search: querystring.stringify(queryObj)
});
}

const requestOptions = {
protocol: isOriginal ? 'http:' : parsedTarget.protocol,
hostname: isOriginal ? ip.address() : parsedTarget.hostname,
port: parsedTarget.port || ((parsedTarget.protocol === 'http:') ? 80 : 443),
path: resolvedUrl,
method: req.method,
headers
};

const request = (requestOptions.protocol === 'https:' ? https : http).request(requestOptions, res => {
res.setEncoding(encoding);
res.pipe(bl((err, body) => {
if (err) {
return reject(err);
}

resolve({
body,
statusCode: res.statusCode,
headers: res.headers
});
}));
});

request.on('error', err => {
reject(err);
});

request.on('abort', request.abort);

if (body) {
request.write(body);
}

request.end();
});
};
91 changes: 91 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
const { URL } = require('url');
const { resolve } = require('path');
const micro = require('micro');
const getRawBody = require('raw-body');
const getPort = require('get-port');
const fullUrl = require('full-url');
const doRequest = require('./chain-request');

module.exports = opts => {
const mod = (typeof opts.micro === 'function') ? opts.micro : require(resolve(opts.micro));
let isReady = false;
let port;

getPort()
.then(p => {
port = p;
micro(mod).listen(port, err => {
if (err) {
console.error(err);
process.exit(1);
}

isReady = true;
});
}).catch(err => {
console.error(err);
process.exit(1);
});

return async (req, res) => {
const originalBody = await getRawBody(req);
let body = originalBody;

if (!isReady) {
return micro.send(res, 503, 'Service Unavailable');
}

if (opts.chain &&
(!opts.location || (opts.location.test(req.url)))
) {
try {
for (let link of opts.chain) {
let bodyToSend = body;
if (link.transformRequestBody) {
bodyToSend = link.transformRequestBody(bodyToSend);
if (typeof bodyToSend === 'object' && !(bodyToSend instanceof Buffer)) {
bodyToSend = JSON.stringify(bodyToSend);
}
}

const result = await doRequest(req, (link.sendOriginalBody ? originalBody : bodyToSend), link);
const allowedStatuses = link.allowedStatuses || [200];

if (!allowedStatuses.includes(result.statusCode)) {
return micro.send(res, result.statusCode, result.body);
}

if (typeof link.transformResponseBody === 'function') {
result.body = link.transformResponseBody(result.body);
if (typeof result.body === 'object') {
result.body = JSON.stringify(result.body);
}
}

if (link.mergeJson) {
try {
body = Object.assign({}, JSON.parse(body), JSON.parse(result.body));
body = JSON.stringify(body);
} catch (err) {}
}
}
} catch (err) {
console.error(err);
throw micro.createError(500, 'Internal Server Error');
}
}

const full = new URL(fullUrl(req));
full.port = port;

const originalRequest = await doRequest(req, body, { target: full.toString(), isOriginal: true });

for (let key in originalRequest.headers) {
if ({}.hasOwnProperty.call(originalRequest.headers, key)) {
res.setHeader(key, originalRequest.headers[key]);
}
}

micro.send(res, originalRequest.statusCode, originalRequest.body);
};
};
56 changes: 56 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
{
"name": "micro-chain",
"version": "0.0.1",
"description": "",
"main": "dist/index.js",
"scripts": {
"start": "cd example && micro index.js",
"build": "babel lib -d dist",
"test": "xo && ava -v test/*.test.js"
},
"files": [
"dist",
"README.md"
],
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"bl": "^1.2.0",
"bluebird": "^3.5.0",
"full-url": "^1.0.0",
"get-port": "^2.1.0",
"media-typer": "^0.3.0",
"micro": "latest",
"raw-body": "^2.2.0"
},
"devDependencies": {
"async-sleep": "^0.1.0",
"ava": "^0.18.2",
"babel-cli": "^6.23.0",
"babel-plugin-transform-async-to-generator": "^6.22.0",
"ip": "^1.1.5",
"request-promise": "^4.1.1",
"xo": "^0.17.1"
},
"xo": {
"space": true,
"rules": {
"object-curly-spacing": [
0
],
"import/no-dynamic-require": [
0
],
"unicorn/no-process-exit": [
0
],
"max-depth": [
0
],
"no-use-extend-native/no-use-extend-native": [
0
]
}
}
}
Loading

0 comments on commit bba37ad

Please sign in to comment.