UNPKG

node-rest-server

Version:
387 lines (368 loc) 14.3 kB
import * as https from 'node:https'; import pino from 'pino'; import express from 'express'; import cors from 'cors'; import errorHandler$1 from 'errorhandler'; import FastValidator from 'fastest-validator'; let logger; const initializeLogger = (serverConfig) => { const pinoConfig = { name: 'node-rest-server', level: 'info' }; const transportConfig = { targets: [{ target: 'pino-pretty', options: { colorize: true, translateTime: 'SYS:dd-mm-yyyy HH:MM:ss' } }] }; if (typeof serverConfig.logger === 'boolean') { pinoConfig.enabled = serverConfig.logger; } else { const loggerConfig = serverConfig.logger; pinoConfig.name = loggerConfig.name ?? pinoConfig.name; pinoConfig.enabled = loggerConfig.enable; pinoConfig.level = (loggerConfig.level ?? loggerConfig.debug) ? 'debug' : 'info'; if (loggerConfig.file) { transportConfig.targets = [...transportConfig.targets, { target: 'pino/file', options: { destination: loggerConfig.file, mkdir: true } }]; } } // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const transport = pino.transport(transportConfig); // eslint-disable-next-line @typescript-eslint/no-unsafe-argument logger = pino(pinoConfig, transport); }; const configProcessor = (app, serverConfig) => { app.set('port', serverConfig.port || 8000); app.set('x-powered-by', false); }; const registerPreprocessor = (app, serverConfig) => { logger.debug('loading json processor'); app.use(express.json()); logger.debug('loading URL encoder'); app.use(express.urlencoded({ extended: true })); logger.debug('loading cors request handler'); app.use(cors(serverConfig.cors)); }; const initPreProcessors = (app, serverConfig) => { configProcessor(app, serverConfig); registerPreprocessor(app, serverConfig); }; const getServerReturnHandlers = (server) => ({ close: (forced) => new Promise((resolve) => { if (!server) { resolve(new Error('Server instance not found')); return; } if (forced) { server.closeIdleConnections(); server.closeAllConnections(); } server.close(resolve); }), // eslint-disable-next-line @typescript-eslint/no-explicit-any addListener: (event, listener) => { if (!server) { return; } return server.addListener(event, listener); }, }); const GLOBAL_API_ERROR = 500; const extractIfAvailable = (object, attributes) => { if (object[attributes]) { return { [attributes]: object[attributes] }; } if (Array.isArray(attributes)) { return attributes.reduce((result, attribute) => { if (object[attribute]) { // @ts-expect-error result is aan object which contains attribute result[attribute] = object[attribute]; } return result; }, {}); } return {}; }; const getRequestData = (request) => ({ url: `${request.protocol}://${request.hostname}${request.originalUrl}`, body: request.body, pathParams: request.params, queryParams: request.query, getHeader: (name) => request.get(name), headers: request.headers, method: request.method, }); const getFilterData = (response) => ({ filter: response.locals, }); const getControllerOptions = (options) => { return extractIfAvailable(options, 'getDatabaseConnection'); }; const extractResponseData = (routeConfig, controllerResponseData = {}, serverConfigHeaders) => { const { status, payload, headers, ...userData } = controllerResponseData; return { status: status || routeConfig.status || 200, payload: payload || userData, headers: { ...serverConfigHeaders, ...routeConfig.headers, ...headers }, }; }; const publishErrorResponse = (response, status, payload) => { publishResponse(response, { status, payload, headers: {} }); }; const publishResponse = (response, finalResponse) => { const { status, payload, headers } = finalResponse; if (headers && Object.keys(headers).length !== 0) { response.set(headers); } if (status) { response.status(status); } if (payload && Object.keys(payload).length !== 0) { response.json(payload); } else { response.end(); } }; const sendResponse = (routeConfig, serverConfig, response, controllerResponseData, serverConfigHeaders) => { const finalResponse = extractResponseData(routeConfig, controllerResponseData, serverConfigHeaders); logger.debug(`Response sent : ${JSON.stringify(finalResponse.payload)}`); if (serverConfig.delay && serverConfig.delay > 0) { setTimeout(() => { publishResponse(response, finalResponse); }, serverConfig.delay * 1000); } else { publishResponse(response, finalResponse); } }; const errorHandler = (err) => { logger.error(err); }; const buildRequestData = (request, response) => ({ ...getRequestData(request), ...getFilterData(response) }); const handleResponseHeaders = (serverConfig, requestData) => { if (serverConfig.headers && typeof serverConfig.headers === 'function') { return serverConfig.headers(requestData); } return serverConfig.headers; }; const handleControllerResponse = (routeConfig, controllerOptions, requestData) => { if (typeof routeConfig.controller === 'function') { return routeConfig.controller(requestData, controllerOptions); } else if (typeof routeConfig.controller === 'object') { return routeConfig.controller; } throw new Error('Controller should be either object or a function'); }; var RouteProvider = (routeConfig, controllerOptions, serverConfig) => (request, response) => { try { const requestData = buildRequestData(request, response); const serverConfigHeaders = handleResponseHeaders(serverConfig, requestData); const responseData = handleControllerResponse(routeConfig, controllerOptions, requestData); if (responseData instanceof Promise) { responseData.then((resolvedResponseData) => { sendResponse(routeConfig, serverConfig, response, resolvedResponseData, serverConfigHeaders); }, (error) => { errorHandler(error); publishErrorResponse(response, GLOBAL_API_ERROR, error.message); }); return; } sendResponse(routeConfig, serverConfig, response, responseData, serverConfigHeaders); } catch (error) { errorHandler(error); publishErrorResponse(response, GLOBAL_API_ERROR, error.message); } }; const registerRequestLogger = (app) => { logger.debug('Registering request logger'); app.use((request, _response, next) => { const data = getRequestData(request); logger.info(`Request URL: ${data.method} ${new URL(data.url).pathname}`); logger.debug(`Request headers: ${JSON.stringify(data.headers)}`); logger.debug(`Request body: ${JSON.stringify(data.body)}`); next(); }); }; const registerFilters = (app, serverConfig) => { logger.debug('Registering global filter'); app.use((request, response, next) => { const data = getRequestData(request); if (typeof serverConfig.filter === 'function') { logger.info('Executing filter...'); const filterData = serverConfig.filter(data); if (filterData instanceof Promise) { filterData.then((filterDataResponse) => { response.locals = filterDataResponse || {}; next(); }, errorHandler); return; } response.locals = filterData; } next(); }); }; const registerStatusEndpoint = (app) => { logger.debug('Registering /status endpoint to get routes information'); app.get('/status', (_request, response) => { const { stack } = app._router; response.send(stack); }); }; const registerMiddlewares = (app, serverConfig) => { serverConfig.middlewares?.forEach((middleware) => { if (typeof middleware === 'function') { app.use(middleware); } else { throw new Error('Middleware should be a function'); } }); }; const registerDevErrorHandler = (app) => { if (app.get('env') === 'development') { logger.debug('Loading Error handler'); app.use(errorHandler$1()); } }; const serverSettingsProperties = { basePath: { type: 'string', default: '', }, port: { type: 'number', positive: true, integer: true, default: 8000, }, headers: { type: 'multi', rules: [{ type: 'object' }, { type: 'function' }], optional: true, }, delay: { type: 'number', positive: true, integer: true, default: 0, }, logger: { type: 'multi', rules: [ { type: 'boolean' }, { type: 'object', props: { enable: { type: 'boolean', default: true }, debug: { type: 'boolean', default: false }, name: { type: 'string', default: 'node-rest-server' }, level: { type: 'string', optional: true }, }, }, ], default: true, }, filter: { type: 'function', optional: true }, cors: { type: 'any', optional: true }, getDatabaseConnection: { type: 'function', optional: true }, https: { type: 'any', optional: true }, middlewares: { type: 'function', optional: true }, }; const serverSettingsSchema = { $$root: true, strict: true, type: 'object', optional: true, messages: { objectStrict: "Server settings contains forbidden keys: '{actual}', valid properties are '{expected}'", }, properties: serverSettingsProperties, }; const validator = new FastValidator(); const serverSettingsValidator = validator.compile(serverSettingsSchema); const ERROR = { VALIDATION_MESSAGE: 'occurred during validation of', }; const commonResultProcessor = (result, type) => { if (result !== true && !(result instanceof Promise)) { const formattedMessages = result.map(({ message }) => `\n${message}`); throw Error([`${ERROR.VALIDATION_MESSAGE} ${type}`, ...formattedMessages].join('')); } }; const validateServerSettings = (serverConfig) => { console.log('Validating Server settings'); const validationStatus = serverSettingsValidator(serverConfig); commonResultProcessor(validationStatus, 'server settings'); }; const hasUniqueMethods = (endpointList = []) => endpointList.map((endpointHandler) => endpointHandler.method).filter((method, index, methods) => methods.indexOf(method) === index).length === endpointList.length; const registerMethod = (app, endpoint, endpointHandlerConfigItem, controllerOptions, serverConfig) => { const uri = `${serverConfig.basePath || ''}${endpoint}`; if (typeof endpointHandlerConfigItem.method === 'string') { const method = String(endpointHandlerConfigItem.method); logger.info(`Registering route path: ${method.toUpperCase()} ${uri}`); if (endpointHandlerConfigItem.middlewares?.length) { // @ts-expect-error unsafe call to support dynamic generator // eslint-disable-next-line @typescript-eslint/no-unsafe-call app[method.toLowerCase()](uri, ...endpointHandlerConfigItem.middlewares, RouteProvider(endpointHandlerConfigItem, controllerOptions, serverConfig)); } else { // @ts-expect-error unsafe call to support dynamic generator // eslint-disable-next-line @typescript-eslint/no-unsafe-call app[method.toLowerCase()](uri, RouteProvider(endpointHandlerConfigItem, controllerOptions, serverConfig)); } } }; function buildNodeRestServer(routeConfig, serverConfig = {}) { validateServerSettings(serverConfig); initializeLogger(serverConfig); logger.info('Loading resources and starting server'); const app = express(); logger.debug('Applying preprocessors'); initPreProcessors(app, serverConfig); logger.debug('Applying global middlewares'); registerRequestLogger(app); registerFilters(app, serverConfig); registerStatusEndpoint(app); registerMiddlewares(app, serverConfig); const controllerOptions = getControllerOptions(serverConfig); Object.keys(routeConfig).forEach((endpoint) => { const endpointHandlerConfigs = routeConfig[endpoint]; if (Array.isArray(endpointHandlerConfigs)) { if (hasUniqueMethods(endpointHandlerConfigs)) { endpointHandlerConfigs.forEach((endpointHandlerConfigItem) => { registerMethod(app, endpoint, endpointHandlerConfigItem, controllerOptions, serverConfig); }); } else { logger.error(`Multiple handlers for same http method found for endpoint : ${endpoint}`); } } else { registerMethod(app, endpoint, endpointHandlerConfigs, controllerOptions, serverConfig); } }); registerDevErrorHandler(app); return app; } function NodeRestServer(routeConfig, serverConfig = {}) { try { const app = buildNodeRestServer(routeConfig, serverConfig); let server = app; if (serverConfig.https) { // eslint-disable-next-line @typescript-eslint/no-misused-promises server = https.createServer(serverConfig.https, app); } const serverInstance = server.listen(app.get('port'), (error) => { if (error) { logger.error(`Error starting server: ${error}`); } else { logger.info(`Server started listening on port ${app.get('port')}`); } }); return getServerReturnHandlers(serverInstance); } catch (error) { console.error(error); return getServerReturnHandlers(); } } export { NodeRestServer, NodeRestServer as default };