node-rest-server
Version:
Configurable node rest server
387 lines (368 loc) • 14.3 kB
JavaScript
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 };