UNPKG

lemon-core

Version:
322 lines 15 kB
"use strict"; 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