@bcgov/citz-imb-express-utilities
Version:
BCGov Utilities for Express API
463 lines (435 loc) • 16.6 kB
JavaScript
;
var zod = require('zod');
var express = require('express');
const stringParam = (param, optional = false) => optional
? zod.z
.string()
.min(1, { message: `\`${param}\` must be a non-empty string.` })
.optional()
: zod.z.string().min(1, { message: `\`${param}\` must be a non-empty string.` });
const booleanParam = (param, optional = false) => optional
? zod.z
.string()
.optional()
.refine((value) => value === undefined || value === 'true' || value === 'false', {
message: `\`${param}\` must be a boolean ('true' or 'false') or undefined.`,
})
.transform((value) => (value === undefined ? undefined : value === 'true'))
: zod.z
.string()
.refine((value) => value === 'true' || value === 'false', {
message: `\`${param}\` must be a boolean ('true' or 'false').`,
})
.transform((value) => value === 'true');
const integerParam = (param, optional = false) => optional
? zod.z
.string()
.optional()
.refine((value) => value === undefined ||
(!Number.isNaN(Number.parseInt(value, 10)) && Number.isInteger(Number.parseFloat(value))), {
message: `\`${param}\` must be an integer or undefined.`,
})
.transform((value) => (value === undefined ? undefined : Number.parseInt(value, 10)))
: zod.z
.string()
.refine((value) => !Number.isNaN(Number.parseInt(value, 10)) && Number.isInteger(Number.parseFloat(value)), {
message: `\`${param}\` must be an integer.`,
})
.transform((value) => Number.parseInt(value, 10));
const numberParam = (param, optional = false) => optional
? zod.z
.string()
.optional()
.refine((value) => value === undefined || (value !== '' && !Number.isNaN(Number.parseFloat(value))), {
message: `\`${param}\` must be a number.`,
})
.transform((value) => (value === undefined ? undefined : Number.parseFloat(value)))
: zod.z
.string()
.refine((value) => value !== '' && !Number.isNaN(Number.parseFloat(value)), {
message: `\`${param}\` must be a number.`,
})
.transform((value) => Number.parseFloat(value));
const refineAtLeastOneNonEmpty = (keys) => {
return (data) => {
const nonEmptyValues = keys.filter((key) => {
const value = data[key];
if (typeof value === 'string')
return value.trim() !== '';
if (typeof value === 'boolean' || typeof value === 'number')
return true;
return false;
});
return nonEmptyValues.length > 0;
};
};
const transformRemoveEmpty = (obj) => {
return Object.entries(obj).reduce((acc, [key, value]) => {
if (value !== undefined &&
value !== null &&
!(typeof value === 'string' && value.trim() === '')) {
acc[key] = value;
}
return acc;
}, {});
};
class HttpError extends Error {
statusCode;
constructor(statusCode, message) {
super(message);
this.statusCode = statusCode;
}
}
const HTTP_STATUS_CODES = {
OK: 200,
CREATED: 201,
ACCEPTED: 202,
NO_CONTENT: 204,
RESET_CONTENT: 205,
MOVED_PERMANENTLY: 301,
FOUND: 302,
SEE_OTHER: 303,
NOT_MODIFIED: 304,
TEMPORARY_REDIRECT: 307,
PERMANENT_REDIRECT: 308,
BAD_REQUEST: 400,
UNAUTHORIZED: 401,
PAYMENT_REQUIRED: 402,
FORBIDDEN: 403,
NOT_FOUND: 404,
METHOD_NOT_ALLOWED: 405,
CONFLICT: 409,
GONE: 410,
LENGTH_REQUIRED: 411,
PRECONDITION_FAILED: 412,
PAYLOAD_TOO_LARGE: 413,
URI_TOO_LONG: 414,
UNSUPPORTED_MEDIA_TYPE: 415,
IM_A_TEAPOT: 418,
MISDIRECTED_REQUEST: 421,
TOO_MANY_REQUESTS: 429,
REQUEST_HEADER_FIELDS_TOO_LARGE: 431,
UNAVAILABLE_FOR_LEGAL_REASONS: 451,
INTERNAL_SERVER_ERROR: 500,
NOT_IMPLEMENTED: 501,
BAD_GATEWAY: 502,
SERVICE_UNAVAIBLABLE: 503,
GATEWAY_TIMEOUT: 504,
NOT_EXTENDED: 510,
};
const DEFAULT_LOG_FUNCTION = ({ method, originalUrl, message, }) => {
console.error(`REQUEST ERROR: [${method}] ${originalUrl}: ${message}`);
};
const ANSI_CODES = {
FOREGROUND: {
BLACK: '\x1b[30m',
RED: '\x1b[31m',
GREEN: '\x1b[32m',
GOLD: '\x1b[33m',
BLUE: '\x1b[34m',
PURPLE: '\x1b[35m',
CYAN: '\x1b[36m',
WHITE: '\x1b[37m',
GREY: '\x1b[1m\x1b[30m',
PINK: '\x1b[1m\x1b[31m',
LIME: '\x1b[1m\x1b[32m',
YELLOW: '\x1b[1m\x1b[33m',
LIGHT_BLUE: '\x1b[1m\x1b[34m',
MAGENTA: '\x1b[1m\x1b[35m',
AQUA: '\x1b[1m\x1b[36m',
},
BACKGROUND: {
BLACK: '\x1b[40m',
RED: '\x1b[41m',
GREEN: '\x1b[42m',
GOLD: '\x1b[43m',
BLUE: '\x1b[44m',
PURPLE: '\x1b[45m',
CYAN: '\x1b[46m',
WHITE: '\x1b[47m',
GREY: '\x1b[1m\x1b[40m',
PINK: '\x1b[1m\x1b[41m',
LIME: '\x1b[1m\x1b[42m',
YELLOW: '\x1b[1m\x1b[43m',
LIGHT_BLUE: '\x1b[1m\x1b[44m',
MAGENTA: '\x1b[1m\x1b[45m',
AQUA: '\x1b[1m\x1b[46m',
},
FORMATTING: {
RESET: '\x1b[0m',
BRIGHT: '\x1b[1m',
DIM: '\x1b[2m',
UNDERSCORE: '\x1b[4m',
REVERSE: '\x1b[7m',
ITALIC: '\x1b[3m',
STRIKETHROUGH: '\x1b[9m',
},
};
const sanitize = (input, options = {
removeHTMLTags: true,
removeSQLInjectionPatterns: true,
removeScriptTags: true,
removeNoSQLInjectionPatterns: true,
}) => {
const removeHTMLTags = (str) => {
return str.replace(/<[^>]*>/g, '');
};
const removeSQLInjectionPatterns = (str) => {
return str.replace(/(\b(SELECT|INSERT|DELETE|UPDATE|DROP|CREATE|ALTER|TRUNCATE|EXEC|UNION|ALL)\b)/gi, '');
};
const removeScriptTags = (str) => {
return str.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, '');
};
const removeNoSQLInjectionPatterns = (str) => {
return str.replace(/(\$\b(where|regex|ne|eq|gt|gte|lt|lte|in|nin|exists)\b)/gi, '');
};
let sanitizedString = input;
if (options.removeHTMLTags) {
sanitizedString = removeHTMLTags(sanitizedString);
}
if (options.removeSQLInjectionPatterns) {
sanitizedString = removeSQLInjectionPatterns(sanitizedString);
}
if (options.removeScriptTags) {
sanitizedString = removeScriptTags(sanitizedString);
}
if (options.removeNoSQLInjectionPatterns) {
sanitizedString = removeNoSQLInjectionPatterns(sanitizedString);
}
return sanitizedString;
};
const validateZodRequestSchema = (obj, schema, errorMsgPrefix, options) => {
try {
const sanitizedSchema = schema.transform((data) => {
const sanitizedData = {};
Object.keys(data).forEach((key) => {
if (typeof data[key] === 'string') {
sanitizedData[key] = sanitize(data[key], options?.sanitizationOptions);
}
else {
sanitizedData[key] = data[key];
}
});
return sanitizedData;
});
return sanitizedSchema.parse(obj);
}
catch (error) {
if (error instanceof zod.ZodError) {
const formattedErrors = error.errors
.map((e) => {
const detail = {
path: e.path,
message: e.message,
expected: '',
received: '',
};
if (e.code === zod.ZodIssueCode.invalid_type) {
detail.expected = e.expected;
detail.received = e.received;
return `${detail.path.join('.')}: (Expected: ${detail.expected}, Received: ${detail.received}, Message: ${detail.message})`;
}
return `${detail.path.join('.')}: (Message: ${detail.message})`;
})
.join(', ');
throw new HttpError(HTTP_STATUS_CODES.BAD_REQUEST, `${errorMsgPrefix}${formattedErrors}`);
}
throw error;
}
};
const zodValidationMiddlewareFunctions = (req) => {
req.getZodValidatedParams = (schema, options) => validateZodRequestSchema(req.params, schema, 'Request is malformed. Invalid path parameters: ', options);
req.getZodValidatedQuery = (schema, options) => validateZodRequestSchema(req.query, schema, 'Request is malformed. Invalid query parameters: ', options);
req.getZodValidatedBody = (schema, options) => validateZodRequestSchema(req.body, schema, 'Request is malformed. Invalid request body: ', options);
};
const errorWrapper = (handler, options = {}) => {
const { logFunction = DEFAULT_LOG_FUNCTION } = options;
return async (req, res, next) => {
try {
await handler(req, res, next);
}
catch (error) {
const { method, originalUrl, getStandardResponse } = req;
let statusCode = HTTP_STATUS_CODES.INTERNAL_SERVER_ERROR;
let message = 'An unexpected error occurred';
if (error instanceof HttpError) {
statusCode = error.statusCode;
message = error.message;
}
else if (error instanceof Error) {
message = error.message;
}
logFunction({ method, originalUrl, statusCode, message });
const responseData = getStandardResponse({
success: false,
data: { method, originalUrl, statusCode },
message,
});
res.status(statusCode).json(responseData);
}
};
};
const isHealthy = errorWrapper(async (req, res) => {
res.send('Application is healthy!');
});
const router$1 = express.Router();
router$1.route('/').get(isHealthy);
const getConfig = (config) => {
return errorWrapper(async (req, res) => {
res.json(config);
});
};
const router = express.Router();
const configRouter = (config) => {
router.route('/').get(getConfig(config));
return router;
};
const healthModule = (app) => {
app.use('/health', router$1);
};
const configModule = (app, config) => {
app.use('/config', configRouter(config));
};
const getCurrentUTCComponents = () => {
const now = new Date();
return {
utcYear: now.getUTCFullYear(),
utcMonth: now.getUTCMonth(),
utcDate: now.getUTCDate(),
utcHours: now.getUTCHours(),
utcMinutes: now.getUTCMinutes(),
utcSeconds: now.getUTCSeconds(),
};
};
const formatDate = (year, month, date) => {
return `${year}-${String(month + 1).padStart(2, '0')}-${String(date).padStart(2, '0')}`;
};
const formatTime = (hours, minutes, seconds) => {
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
};
const isDaylightSavingTime = (month) => {
return month > 2 && month < 11;
};
const getPacificTimeComponents = (utcHours, utcDate, utcMonth, utcYear, isDST) => {
let pacificHours = utcHours - 8;
if (isDST)
pacificHours += 1;
const pacificTimeZone = isDST ? 'PDT' : 'PST';
let pacificDate = utcDate;
let pacificMonth = utcMonth;
let pacificYear = utcYear;
if (pacificHours < 0) {
pacificHours += 24;
pacificDate -= 1;
if (pacificDate < 1) {
pacificMonth -= 1;
if (pacificMonth < 0) {
pacificMonth = 11;
pacificYear -= 1;
}
pacificDate = new Date(pacificYear, pacificMonth + 1, 0).getDate();
}
}
return { pacificHours, pacificDate, pacificMonth, pacificYear, pacificTimeZone };
};
const getCurrentDateTime = () => {
const { utcYear, utcMonth, utcDate, utcHours, utcMinutes, utcSeconds } = getCurrentUTCComponents();
const formattedDateUTC = formatDate(utcYear, utcMonth, utcDate);
const formattedTimeUTC = formatTime(utcHours, utcMinutes, utcSeconds);
const isDST = isDaylightSavingTime(utcMonth);
const { pacificHours, pacificDate, pacificMonth, pacificYear, pacificTimeZone } = getPacificTimeComponents(utcHours, utcDate, utcMonth, utcYear, isDST);
const formattedDatePacific = formatDate(pacificYear, pacificMonth, pacificDate);
const formattedTimePacific = formatTime(pacificHours, utcMinutes, utcSeconds);
return {
formattedDateUTC,
formattedTimeUTC,
formattedDatePacific,
formattedTimePacific,
pacificTimeZone,
};
};
const elapsedTimeMiddlewareFunction = (req) => {
const startHrTime = process.hrtime();
req.getElapsedTimeInMs = () => {
const elapsedHrTime = process.hrtime(startHrTime);
const elapsedTimeInMs = elapsedHrTime[0] * 1000 + elapsedHrTime[1] / 1e6;
return elapsedTimeInMs.toFixed(3);
};
};
const { FOREGROUND, FORMATTING } = ANSI_CODES;
const { NODE_ENV } = process.env;
const NODE_VERSION = process.version;
const MEMORY_USAGE = process.memoryUsage();
const CPU_USAGE = process.cpuUsage();
const formatBytes = (bytes) => `${(bytes / 1024 / 1024).toFixed(2)} MB`;
const formatCPUUsage = (microseconds) => `${(microseconds / 1000000).toFixed(2)} seconds`;
const serverStartupLogs = (port) => {
if (port) {
console.info(`${FOREGROUND.LIGHT_BLUE}Express Server started on port ${FORMATTING.RESET}${port}${FOREGROUND.LIGHT_BLUE}${FORMATTING.RESET}.`);
}
else {
console.info(`${FOREGROUND.LIGHT_BLUE}Express Server started${FORMATTING.RESET}.`);
}
console.info(`${FOREGROUND.YELLOW}[NODE]${FORMATTING.RESET} Current version of node.js: ${NODE_VERSION}`);
console.info(`${FOREGROUND.YELLOW}[NODE]${FORMATTING.RESET} Current NODE_ENV environment: ${NODE_ENV}`);
console.info(`${FOREGROUND.YELLOW}[MEMORY]${FORMATTING.RESET} Startup memory usage:`);
console.log(` RSS: ${formatBytes(MEMORY_USAGE.rss)}`);
console.log(` Heap Total: ${formatBytes(MEMORY_USAGE.heapTotal)}`);
console.log(` Heap Used: ${formatBytes(MEMORY_USAGE.heapUsed)}`);
console.info(`${FOREGROUND.YELLOW}[CPU]${FORMATTING.RESET} Startup cpu usage:`);
console.log(` User: ${formatCPUUsage(CPU_USAGE.user)}`);
console.log(` System: ${formatCPUUsage(CPU_USAGE.system)}`);
console.info(`${FOREGROUND.YELLOW}[UTC]${FORMATTING.RESET} Current date and time: ${getCurrentDateTime().formattedDateUTC}, ${getCurrentDateTime().formattedTimeUTC}`);
console.info(`${FOREGROUND.YELLOW}[${getCurrentDateTime().pacificTimeZone}]${FORMATTING.RESET} Current date and time: ${getCurrentDateTime().formattedDatePacific}, ${getCurrentDateTime().formattedTimePacific}`);
};
const standardResponse = (dataInput, req) => {
const { success = true, data, message } = dataInput;
const dateTime = getCurrentDateTime();
return {
success,
data,
message: message ?? '',
responseDateUTC: dateTime.formattedDateUTC,
responseTimeUTC: dateTime.formattedTimeUTC,
responseTimeInMs: req.getElapsedTimeInMs(),
};
};
const expressUtilitiesMiddleware = (req, res, next) => {
elapsedTimeMiddlewareFunction(req);
zodValidationMiddlewareFunctions(req);
req.getStandardResponse = (inputData) => standardResponse(inputData, req);
next();
};
const safePromise = async (promise) => {
try {
const response = await promise;
const contentType = response.headers.get("content-type");
if (contentType?.includes("application/json")) {
const jsonData = (await response.json());
return [null, jsonData];
}
return [null, null];
}
catch (error) {
return [error, null];
}
};
exports.ANSI_CODES = ANSI_CODES;
exports.DEFAULT_LOG_FUNCTION = DEFAULT_LOG_FUNCTION;
exports.HTTP_STATUS_CODES = HTTP_STATUS_CODES;
exports.HttpError = HttpError;
exports.booleanParam = booleanParam;
exports.configModule = configModule;
exports.errorWrapper = errorWrapper;
exports.expressUtilitiesMiddleware = expressUtilitiesMiddleware;
exports.healthModule = healthModule;
exports.integerParam = integerParam;
exports.numberParam = numberParam;
exports.refineAtLeastOneNonEmpty = refineAtLeastOneNonEmpty;
exports.safePromise = safePromise;
exports.serverStartupLogs = serverStartupLogs;
exports.stringParam = stringParam;
exports.transformRemoveEmpty = transformRemoveEmpty;
exports.validateZodRequestSchema = validateZodRequestSchema;
exports.zodValidationMiddlewareFunctions = zodValidationMiddlewareFunctions;
//# sourceMappingURL=index.js.map