UNPKG

lemon-devkit

Version:

Lemon Serverless Micro-Service Platform for local development

381 lines 17.4 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 () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __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; /** * Express Server Application. * - standalone http service with express. * * * ```bash * #run-server (use lemon.yml:local) * $ npm install -g nodemon * $ ENV=lemon STAGE=local nodemon express.js * ``` * * [TODO] * - [ ] 190801 proper content type `text/plain` * - [x] 190801 change router underscore char like `loopers_front` -> `loopers-front` * * * @author Steve Jung <steve@lemoncloud.io> * @date 2019-07-31 support ECMA 2016. * @date 2019-08-01a auto register api with pattern. `/^[a-z][a-z0-9\-_]+$/` * @date 2019-08-07 ignore `engine.dt` function. * @date 2019-11-26 cleanup and optimized for `lemon-core#v2` * @date 2020-01-23 improve context information via headers. * @date 2025-05-20 optimize for `lemon-core#v4` * * @copyright (C) lemoncloud.io 2025 - All Rights Reserved. */ const shared_1 = require("./shared"); const environ_1 = require("../environ"); 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. * * @param $engine - must be LemonEngine from `lemon-core` * @param $web - must be LambdaWEBHandler from `lemon-core` */ const buildExpress = ($engine, $web, options) => { const errScope = `buildExpress()`; // STEP.0 validate parameters. if (!$engine) throw new Error(`$engine(LemonEngine) is required - ${errScope}`); if (!$web) throw new Error(`$web(LambdaWEBHandler) is required - ${errScope}`); /** **************************************************************************************************************** * 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) const _loadCred = async () => { //NOTE! - DO NOT CHANGE CONFIG IN LAMBDA ENV (USE ROLE CONFIG). const ALFN = $engine.environ('AWS_LAMBDA_FUNCTION_NAME', ''); if (ALFN) { _log(NS, '!WARN! profile ignored due to AWS_LAMBDA_FUNCTION_NAME =', 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.SharedIniFileCredentials({ profile }); // if (profile) AWS.config.credentials = credentials; return (0, environ_1.asyncCredentials)(profile).catch((e) => { const error = `${e?.message ?? e}`.toLowerCase(); if (error.includes('could not resolve credentials') && error.includes('[default]')) return; throw e; }); }; /** **************************************************************************************************************** * 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, //WARN! - DO NOT USE THIS DUE TO CIRCULAR STRUCTURE. url: req.url, headers: req.headers, body: req.body, requestContext: { source: 'express', domainName: host, accountId, requestId, stage: $engine.environ('STAGE', ''), identity: { sourceIp, userAgent, cognitoIdentityPoolId: null, apiKey: undefined, // api-key if applicable. caller: undefined, // caller string like AROXXXXXX accessKey: undefined, // access-key used to sign }, }, }; _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()); } else if (key == 'x-api-key') { console.log('>>> found x-api-key header =', val); event.requestContext.identity.apiKey = `${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; }, { ...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) => { const $env = process?.env || {}; 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?.dependencies['lemon-core'] || $pack?.devDependencies['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?.prefix || ''}`; //* handle request to handler. const next_middle = (type) => async (req) => { const callback = req.$callback; const $profile = await _loadCred(); req.$event.requestContext = { ...req.$event.requestContext, profile: $profile?.profile }; // make sure `profile` req.$event.pathParameters = { type, ...req.$event.pathParameters }; // make sure `type` //* echo back `event` if no handler. if (!$web) return callback && callback(null, req); return $web .packContext(req.$event, req.$context) .then(($) => $web.handle(req.$event, $)) .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 = $map ? Object.keys($map) : []; const handlers = keys .filter(isValidName) .map(name => { //* check if valid name && function. const main = $map[name]; // const type = `${name}`.split('_').join('-'); // change '_' to '-'. const type = `${name}`; 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