lemon-core
Version:
Lemon Serverless Micro-Service Platform
322 lines • 15 kB
JavaScript
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildExpress = exports.buildHeaderGetter = void 0;
const shared_1 = require("./shared");
const aws_sdk_1 = __importDefault(require("aws-sdk"));
const express_1 = __importDefault(require("express"));
const cors_1 = __importDefault(require("cors"));
const body_parser_1 = __importDefault(require("body-parser"));
const cookie_parser_1 = __importDefault(require("cookie-parser"));
const multer_1 = __importDefault(require("multer"));
const http_1 = __importDefault(require("http"));
const fs_1 = __importDefault(require("fs"));
const requestIp = __importStar(require("request-ip"));
//! helper to catch header value w/o case-sensitive
const buildHeaderGetter = (headers) => (name) => {
name = `${name || ''}`.toLowerCase();
headers = headers || {};
return Object.keys(headers).reduce((found, key) => {
const val = headers[key];
key = `${key || ''}`.toLowerCase();
if (key == name)
return val;
return found;
}, '');
};
exports.buildHeaderGetter = buildHeaderGetter;
//! create Server Instance.
//NOTE - avoid external reference of type.
const buildExpress = ($engine, $web, options) => {
if (!$engine)
throw new Error('$engine is required!');
options = options || {};
/** ****************************************************************************************************************
* Common Constants
** ****************************************************************************************************************/
//! re-use core modules.
const $U = $engine.U;
if (!$U)
throw new Error('$U(utilities) is required!');
//! load common(log) functions
const useEngineLog = !!1; //NOTE - turn off to print log in jest test.
const _log = useEngineLog ? $engine.log : console.log;
const _inf = useEngineLog ? $engine.inf : console.info;
const _err = useEngineLog ? $engine.err : console.error;
const NS = $U.NS('EXPR', 'cyan');
const $pack = (0, shared_1.loadJsonSync)('package.json');
const argv = options.argv || process.argv || [];
const NAME = $pack.name || 'LEMON API';
const VERS = $pack.version || '0.0.0';
const PORT = (0, shared_1.getRunParam)('-port', $U.N($pack.port, 8081), argv); // default server port.
const IS_WSC = (0, shared_1.getRunParam)('-wsc', false, argv); // default server port.
_inf(NS, `###### express[${NAME}@${$U.NS(VERS, 'cyan')}] ######`);
IS_WSC && _inf(NS, `! IS_WSC=`, IS_WSC);
//! dynamic loading credentials by profile. (search PROFILE -> NAME)
(() => {
//NOTE! - DO NOT CHANGE CONFIG IN LAMBDA ENV (USE ROLE CONFIG).
const ALFN = $engine.environ('AWS_LAMBDA_FUNCTION_NAME', '');
if (ALFN)
return;
//NOTE! - OR, TRY TO LOAD CREDENTIALS BY PROFILE NAME.
const NAME = $engine.environ('NAME', '');
const profile = $engine.environ('PROFILE', NAME);
const credentials = new aws_sdk_1.default.SharedIniFileCredentials({ profile });
if (profile)
aws_sdk_1.default.config.credentials = credentials;
})();
/** ****************************************************************************************************************
* Initialize Express
** ****************************************************************************************************************/
//! create express app.
const app = (0, express_1.default)();
const uploader = (0, multer_1.default)({ dest: '../tmp/' });
const genRequestId = options.genRequestId ||
(() => {
const msec = new Date().getMilliseconds() % 1000;
return `${$U.ts()}.${msec < 10 ? '00' : msec < 100 ? '0' : ''}${msec}`;
});
app.use((0, cors_1.default)());
app.use(body_parser_1.default.json({ limit: '10mb' })); // default limit 10mb
app.use(body_parser_1.default.urlencoded({ extended: true }));
app.use((0, cookie_parser_1.default)());
//! middle ware
const middle = (req, res, next) => {
// _log(NS, `! req =`, req);
// _log(NS, `! header =`, req.headers);
const getHeader = (0, exports.buildHeaderGetter)(req.headers || {});
const host = getHeader('host').split(':')[0];
const accountId = $engine.environ('USER', $engine.environ('LOGNAME', ''));
const requestId = genRequestId();
const clientIp = typeof req.clientIp == 'string' ? req.clientIp : ''; //NOTE! - use `request-ip`
const sourceIp = clientIp || `${requestIp.getClientIp(req) || ''}`;
const userAgent = getHeader('user-agent');
//! prepare event compartible with API-Gateway Event.
const event = {
path: req.path,
queryStringParameters: req.query || {},
pathParameters: req.params,
httpMethod: req.method,
connection: 1 ? null : req.connection,
url: req.url,
headers: req.headers,
body: req.body,
requestContext: {
source: 'express',
domainName: host,
accountId,
requestId,
stage: $engine.environ('STAGE', ''),
identity: {
sourceIp,
userAgent,
},
},
};
_log(NS, `! req-ctx =`, $U.json(event.requestContext));
//! prepare internal-context
const context = { source: 'express', domain: host };
//! catch cookie
if (req.headers) {
Object.keys(req.headers).forEach(_key => {
const val = req.headers[_key];
const key = `${_key}`.toLowerCase();
if (key == 'cookie') {
const parseCookies = (str) => {
const rx = /([^;=\s]*)=([^;]*)/g;
const obj = {};
for (let m; (m = rx.exec(str));)
obj[m[1]] = decodeURIComponent(m[2]);
return obj;
};
context.cookie = parseCookies(`${Array.isArray(val) ? val.join('; ') : val || ''}`.trim());
}
});
}
if (req.cookies && typeof req.cookies == 'object') {
context.cookie = Object.keys(req.cookies).reduce((M, key) => {
const val = req.cookies[key];
M[key] = `${val || ''}`.trim();
return M;
}, Object.assign({}, context.cookie));
}
const callback = (err, data) => {
err && _err(NS, '! err@callback =', err);
data && _inf(NS, `! res@callback[${(data && data.statusCode) || 0}] =`, $U.S(data && data.body, 1024));
let contentType = null;
if (data.headers) {
Object.keys(data.headers).map(k => {
if (`${k}`.toLowerCase() == 'content-type') {
contentType = data.headers[k];
}
else {
res.setHeader(k, data.headers[k]);
}
});
}
const statusCode = (data && data.statusCode) || (err ? 503 : 200);
res.setHeader('Content-Type', contentType || 'application/json');
res.status(statusCode).send(data.body);
};
//! attach to req.
req.$event = event;
req.$context = context; //! save the
req.$callback = callback;
//! use json parser or multer.
const method = req.method || '';
const ctype = getHeader('content-type');
_log(NS, `! ${method} ${req.url} =`, ctype);
if (ctype.indexOf('multipart/') >= 0) {
const parser = uploader.single('file');
parser(req, res, () => {
// _inf(NS, '> body =', req.body);
event.body = req.body || {};
event.body.file = req.file;
next();
});
}
else if (method === 'POST' || method === 'PUT') {
const parser = body_parser_1.default.json({ limit: '10mb' });
parser(req, res, () => {
// _inf(NS, '> body =', req.body);
event.body = req.body;
next();
});
}
else {
next();
}
};
/** ********************************************************************************************************************
* ROUTE SETTING
** *******************************************************************************************************************/
//! default app.
app.get('', (req, res) => {
//WARN! - must be matched with the `LambdaWEBHandler.handleProtocol()`.
const $env = (process && process.env) || {};
// const $pack = JSON.parse(fs.readFileSync('package.json', { encoding: 'utf8' }).toString());
// _log(NS, `stat =`, $stat);
// _log(NS, `pack =`, $pack);
const $stat = fs_1.default.statSync('package.json');
const modified = $U.ts($U.F($stat.ctimeMs, 0));
const name = $pack.name || 'LEMON API';
const version = $pack.version || '0.0.0';
const core = $pack && $pack.dependencies && $pack.dependencies['lemon-core'];
const msgs = [
`${name}/${version}`,
`lemon-core/${core || ''}`,
`modified/${modified}`,
`env/ENV=${$env.ENV || ''} NAME=${$env.NAME || ''} STAGE=${$env.STAGE || ''}`,
`env/REPORT_ERROR_ARN=${$env.REPORT_ERROR_ARN || ''}`,
];
res.status(200).send(msgs.join('\n'));
});
//! handler map.
if (true) {
//! route prefix
const ROUTE_PREFIX = `${(options && options.prefix) || ''}`;
//! handle request to handler.
const next_middle = (type) => (req) => {
const callback = req.$callback;
req.$event.pathParameters = Object.assign({ type }, req.$event.pathParameters); // make sure `type`
return $web
.packContext(req.$event, req.$context)
.then(context => $web.handle(req.$event, context))
.then(_ => callback && callback(null, _))
.catch(e => {
_err(NS, '! exp.err =', e);
callback && callback(e);
});
};
//! register automatically endpont.
const RESERVES = 'id,log,inf,err,extend,ts,dt,environ'.split(',');
// support single char path.
const isValidName = (name) => /^[a-z][a-z0-9\-_]*$/.test(name) && RESERVES.indexOf(name) < 0;
const $map = $web.getHandlerDecoders();
const keys = Object.keys($map);
// _inf(NS, '! express.keys =', keys);
const handlers = keys
.filter(isValidName)
.map(name => {
//! check if valid name && function.
const main = $map[name];
const type = `${name}`.split('_').join('-'); // change '_' to '-'.
if (typeof main !== 'function')
throw new Error(`.${name} should be function handler. but type=` + typeof main);
//! route pattern with `/<type>/<id>/<cmd?>`
app.get(`/${ROUTE_PREFIX}${type}`, middle, next_middle(type));
app.get(`/${ROUTE_PREFIX}${type}/:id`, middle, next_middle(type));
app.get(`/${ROUTE_PREFIX}${type}/:id/:cmd`, middle, next_middle(type));
app.put(`/${ROUTE_PREFIX}${type}/:id`, middle, next_middle(type));
app.put(`/${ROUTE_PREFIX}${type}/:id/:cmd`, middle, next_middle(type));
app.patch(`/${ROUTE_PREFIX}${type}/:id`, middle, next_middle(type));
app.patch(`/${ROUTE_PREFIX}${type}/:id/:cmd`, middle, next_middle(type));
app.post(`/${ROUTE_PREFIX}${type}/:id`, middle, next_middle(type));
app.post(`/${ROUTE_PREFIX}${type}/:id/:cmd`, middle, next_middle(type));
app.delete(`/${ROUTE_PREFIX}${type}/:id`, middle, next_middle(type));
app.delete(`/${ROUTE_PREFIX}${type}/:id/:cmd`, middle, next_middle(type));
const _NS = (name, color) => $U.NS(name, color, 4, '');
_inf(NS, `! api[${_NS(name, 'yellow')}] is routed as ${_NS(`/${ROUTE_PREFIX}${type}`, 'cyan')}`);
return { name, type, main };
})
.reduce((M, N) => {
M[N.name] = N;
return M;
}, {});
// _inf(NS, '! express.handlers =', Object.keys(handlers).join(', '));
_inf(NS, '! express.handlers.len =', Object.keys(handlers).length);
}
//! create server by port.
const createServer = () => {
//! logging options.
const NS = $U.NS('main', 'cyan');
const $pack = (0, shared_1.loadJsonSync)('package.json');
const name = $pack.name || 'LEMON API';
const version = $pack.version || '0.0.0';
const server = http_1.default
.createServer(app)
.listen(PORT, () => {
_inf(NS, `###### express[${name}@${$U.NS(version, 'cyan')}] ######`);
})
.on('listening', () => {
const addr = server.address();
const port = $U.NS(`${addr && addr.port}`, 'yellow').split(':')[0];
_log(NS, `Server[${process.env.NAME}:${process.env.STAGE}] is listening on Port:${port}`);
//TODO - improve way to initialize $engine.
$engine.initialize();
})
.on('error', (e) => {
_inf(NS, '!ERR - listen.err = ', e);
});
return server;
};
//! export
return { express: express_1.default, app, createServer };
};
exports.buildExpress = buildExpress;
//# sourceMappingURL=express.js.map
;