express-pro-toolkit 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +426 -0
- package/index.js +28 -0
- package/package.json +55 -0
- package/src/AppError.js +175 -0
- package/src/asyncHandler.js +48 -0
- package/src/correlationId.js +58 -0
- package/src/errorHandler.js +153 -0
- package/src/httpStatus.js +48 -0
- package/src/notFoundHandler.js +34 -0
- package/src/requestLogger.js +119 -0
- package/src/responseHelpers.js +108 -0
- package/types/index.d.ts +169 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wraps an async Express route handler to automatically catch
|
|
5
|
+
* rejected promises and forward them to Express error-handling middleware.
|
|
6
|
+
*
|
|
7
|
+
* Compatible with Express 4 and 5. Preserves the original function's
|
|
8
|
+
* `length` property so Express can distinguish middleware (3 args) from
|
|
9
|
+
* error handlers (4 args).
|
|
10
|
+
*
|
|
11
|
+
* @param {Function} fn - Async route handler `(req, res, next) => Promise<void>`
|
|
12
|
+
* @returns {Function} Express middleware that catches async errors
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* const { asyncHandler } = require('express-pro-toolkit');
|
|
16
|
+
*
|
|
17
|
+
* router.get('/users', asyncHandler(async (req, res) => {
|
|
18
|
+
* const users = await User.find();
|
|
19
|
+
* res.json(users);
|
|
20
|
+
* }));
|
|
21
|
+
*
|
|
22
|
+
* // Also works with error-handling middleware (4 args)
|
|
23
|
+
* app.use(asyncHandler(async (err, req, res, next) => {
|
|
24
|
+
* await logErrorToService(err);
|
|
25
|
+
* next(err);
|
|
26
|
+
* }));
|
|
27
|
+
*/
|
|
28
|
+
function asyncHandler(fn) {
|
|
29
|
+
if (typeof fn !== 'function') {
|
|
30
|
+
throw new TypeError('asyncHandler requires a function argument');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Express uses fn.length to distinguish regular middleware (3) from
|
|
34
|
+
// error-handling middleware (4). We must preserve it.
|
|
35
|
+
if (fn.length === 4) {
|
|
36
|
+
// Error-handling middleware: (err, req, res, next)
|
|
37
|
+
return function asyncErrorHandlerWrapper(err, req, res, next) {
|
|
38
|
+
Promise.resolve().then(() => fn(err, req, res, next)).catch(next);
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Standard middleware / route handler: (req, res, next)
|
|
43
|
+
return function asyncHandlerWrapper(req, res, next) {
|
|
44
|
+
Promise.resolve().then(() => fn(req, res, next)).catch(next);
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = asyncHandler;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { randomBytes } = require('crypto');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Default ID generator — 16 random hex characters.
|
|
7
|
+
* Highly performant; avoids UUID overhead.
|
|
8
|
+
* @returns {string}
|
|
9
|
+
*/
|
|
10
|
+
function defaultGenerateId() {
|
|
11
|
+
return randomBytes(8).toString('hex');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Creates middleware that attaches a unique correlation / request ID
|
|
16
|
+
* to every incoming request, enabling distributed tracing.
|
|
17
|
+
*
|
|
18
|
+
* The ID is:
|
|
19
|
+
* 1. Read from an incoming header (default `x-request-id`) — honours
|
|
20
|
+
* upstream load-balancers / API gateways.
|
|
21
|
+
* 2. Generated if not present.
|
|
22
|
+
* 3. Stored on `req.id` for downstream use.
|
|
23
|
+
* 4. Echoed back on the response in the same header.
|
|
24
|
+
*
|
|
25
|
+
* @param {object} [options]
|
|
26
|
+
* @param {string} [options.header='x-request-id'] - Header to read / write
|
|
27
|
+
* @param {Function} [options.generator] - Custom ID generator `() => string`
|
|
28
|
+
* @param {boolean} [options.setResponseHeader=true] - Whether to echo ID on response
|
|
29
|
+
* @returns {Function} Express middleware
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* const { correlationId } = require('express-pro-toolkit');
|
|
33
|
+
* app.use(correlationId());
|
|
34
|
+
* app.use(correlationId({ header: 'x-correlation-id' }));
|
|
35
|
+
*/
|
|
36
|
+
function correlationId(options) {
|
|
37
|
+
const opts = options && typeof options === 'object' ? options : {};
|
|
38
|
+
const header = (typeof opts.header === 'string' && opts.header) || 'x-request-id';
|
|
39
|
+
const headerLower = header.toLowerCase();
|
|
40
|
+
const generate = typeof opts.generator === 'function' ? opts.generator : defaultGenerateId;
|
|
41
|
+
const setResponse = opts.setResponseHeader !== false;
|
|
42
|
+
|
|
43
|
+
return function correlationIdMiddleware(req, res, next) {
|
|
44
|
+
const incoming = req.headers[headerLower];
|
|
45
|
+
const id = (typeof incoming === 'string' && incoming.length > 0) ? incoming : generate();
|
|
46
|
+
|
|
47
|
+
/** @type {string} Unique request identifier */
|
|
48
|
+
req.id = id;
|
|
49
|
+
|
|
50
|
+
if (setResponse) {
|
|
51
|
+
res.setHeader(header, id);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
next();
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = correlationId;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const AppError = require('./AppError');
|
|
4
|
+
|
|
5
|
+
/* ─── Default logger (console) ────────────────────────────────────── */
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {object} payload
|
|
9
|
+
*/
|
|
10
|
+
function defaultOnError(payload) {
|
|
11
|
+
console.error(
|
|
12
|
+
`[express-pro-toolkit] ERROR ${payload.statusCode} — ${payload.method} ${payload.url}`,
|
|
13
|
+
);
|
|
14
|
+
console.error(payload.stack || payload.message);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/* ─── Helpers ─────────────────────────────────────────────────────── */
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Resolves the HTTP status code from an error object.
|
|
21
|
+
* Supports err.statusCode, err.status, and defaults to 500.
|
|
22
|
+
* @param {Error} err
|
|
23
|
+
* @returns {number}
|
|
24
|
+
*/
|
|
25
|
+
function resolveStatus(err) {
|
|
26
|
+
const code = err.statusCode || err.status;
|
|
27
|
+
return typeof code === 'number' && code >= 100 && code < 600 ? code : 500;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Determines if an error message is safe to expose to clients.
|
|
32
|
+
* Non-operational / 5xx errors get a generic message in production.
|
|
33
|
+
* @param {Error} err
|
|
34
|
+
* @param {number} statusCode
|
|
35
|
+
* @param {boolean} isProduction
|
|
36
|
+
* @returns {string}
|
|
37
|
+
*/
|
|
38
|
+
function resolveMessage(err, statusCode, isProduction) {
|
|
39
|
+
if (!isProduction) return err.message || 'Internal Server Error';
|
|
40
|
+
|
|
41
|
+
// In production, only expose messages for operational / client errors
|
|
42
|
+
if (err instanceof AppError && err.isOperational) return err.message;
|
|
43
|
+
if (statusCode < 500) return err.message || 'Error';
|
|
44
|
+
|
|
45
|
+
return 'Internal Server Error';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* ─── Factory ─────────────────────────────────────────────────────── */
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Creates a configurable global Express error-handling middleware.
|
|
52
|
+
*
|
|
53
|
+
* Options:
|
|
54
|
+
* - `includeStack` — Include stack traces in the response.
|
|
55
|
+
* Defaults to `true` outside production.
|
|
56
|
+
* - `onError(payload)` — Custom error logging / alerting hook.
|
|
57
|
+
* Receives `{ err, statusCode, method, url, requestId, message, stack, timestamp }`.
|
|
58
|
+
* - `defaultStatusCode` — Fallback status code (default `500`).
|
|
59
|
+
*
|
|
60
|
+
* @param {object} [options]
|
|
61
|
+
* @param {boolean} [options.includeStack]
|
|
62
|
+
* @param {Function} [options.onError]
|
|
63
|
+
* @param {number} [options.defaultStatusCode=500]
|
|
64
|
+
* @returns {Function} Express error-handling middleware `(err, req, res, next)`
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* const { createErrorHandler } = require('express-pro-toolkit');
|
|
68
|
+
*
|
|
69
|
+
* // Basic
|
|
70
|
+
* app.use(createErrorHandler());
|
|
71
|
+
*
|
|
72
|
+
* // With external logging service
|
|
73
|
+
* app.use(createErrorHandler({
|
|
74
|
+
* onError: ({ err, requestId }) => Sentry.captureException(err, { tags: { requestId } }),
|
|
75
|
+
* }));
|
|
76
|
+
*/
|
|
77
|
+
function createErrorHandler(options) {
|
|
78
|
+
const opts = options && typeof options === 'object' ? options : {};
|
|
79
|
+
const isProduction = process.env.NODE_ENV === 'production';
|
|
80
|
+
const showStack = typeof opts.includeStack === 'boolean' ? opts.includeStack : !isProduction;
|
|
81
|
+
const onError = typeof opts.onError === 'function' ? opts.onError : defaultOnError;
|
|
82
|
+
|
|
83
|
+
return function errorHandlerMiddleware(err, req, res, _next) {
|
|
84
|
+
const statusCode = resolveStatus(err) || (opts.defaultStatusCode || 500);
|
|
85
|
+
const message = resolveMessage(err, statusCode, isProduction);
|
|
86
|
+
const requestId = req.id || null;
|
|
87
|
+
|
|
88
|
+
// ── Build payload for the logging hook ────────────────────────
|
|
89
|
+
const logPayload = {
|
|
90
|
+
err,
|
|
91
|
+
statusCode,
|
|
92
|
+
method: req.method,
|
|
93
|
+
url: req.originalUrl || req.url,
|
|
94
|
+
requestId,
|
|
95
|
+
message,
|
|
96
|
+
stack: err.stack || null,
|
|
97
|
+
timestamp: new Date().toISOString(),
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Fire logging / alerting hook (non-blocking)
|
|
101
|
+
try { onError(logPayload); } catch (_) { /* never let logger crash the response */ }
|
|
102
|
+
|
|
103
|
+
// ── Build JSON response ───────────────────────────────────────
|
|
104
|
+
/** @type {object} */
|
|
105
|
+
const body = {
|
|
106
|
+
success: false,
|
|
107
|
+
message,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// Error code (machine-readable)
|
|
111
|
+
if (err.code) {
|
|
112
|
+
body.code = err.code;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Detailed sub-errors (validation, etc.)
|
|
116
|
+
if (err.errors) {
|
|
117
|
+
body.errors = Array.isArray(err.errors) ? err.errors : [err.errors];
|
|
118
|
+
} else {
|
|
119
|
+
body.errors = null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Correlation ID for client-side tracing
|
|
123
|
+
if (requestId) {
|
|
124
|
+
body.requestId = requestId;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Stack trace (dev only by default)
|
|
128
|
+
if (showStack) {
|
|
129
|
+
body.stack = err.stack || null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Prevent double-send if headers already flushed
|
|
133
|
+
if (res.headersSent) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
res.status(statusCode).json(body);
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/* ─── Backward-compatible default instance ────────────────────────── */
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Pre-configured error handler using default options.
|
|
145
|
+
* Drop-in replacement for the v1 export.
|
|
146
|
+
*
|
|
147
|
+
* @type {Function}
|
|
148
|
+
*/
|
|
149
|
+
const errorHandler = createErrorHandler();
|
|
150
|
+
|
|
151
|
+
module.exports = errorHandler;
|
|
152
|
+
module.exports.errorHandler = errorHandler;
|
|
153
|
+
module.exports.createErrorHandler = createErrorHandler;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Common HTTP status code constants.
|
|
5
|
+
*
|
|
6
|
+
* Avoids magic numbers throughout application code.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* const { httpStatus } = require('express-pro-toolkit');
|
|
10
|
+
* res.status(httpStatus.CREATED).json(data);
|
|
11
|
+
*
|
|
12
|
+
* @readonly
|
|
13
|
+
* @enum {number}
|
|
14
|
+
*/
|
|
15
|
+
const httpStatus = Object.freeze({
|
|
16
|
+
// ── 2xx Success ──────────────────────────────
|
|
17
|
+
OK: 200,
|
|
18
|
+
CREATED: 201,
|
|
19
|
+
ACCEPTED: 202,
|
|
20
|
+
NO_CONTENT: 204,
|
|
21
|
+
|
|
22
|
+
// ── 3xx Redirection ──────────────────────────
|
|
23
|
+
MOVED_PERMANENTLY: 301,
|
|
24
|
+
FOUND: 302,
|
|
25
|
+
NOT_MODIFIED: 304,
|
|
26
|
+
TEMPORARY_REDIRECT: 307,
|
|
27
|
+
PERMANENT_REDIRECT: 308,
|
|
28
|
+
|
|
29
|
+
// ── 4xx Client Errors ────────────────────────
|
|
30
|
+
BAD_REQUEST: 400,
|
|
31
|
+
UNAUTHORIZED: 401,
|
|
32
|
+
FORBIDDEN: 403,
|
|
33
|
+
NOT_FOUND: 404,
|
|
34
|
+
METHOD_NOT_ALLOWED: 405,
|
|
35
|
+
CONFLICT: 409,
|
|
36
|
+
GONE: 410,
|
|
37
|
+
UNPROCESSABLE_ENTITY: 422,
|
|
38
|
+
TOO_MANY_REQUESTS: 429,
|
|
39
|
+
|
|
40
|
+
// ── 5xx Server Errors ────────────────────────
|
|
41
|
+
INTERNAL_SERVER_ERROR: 500,
|
|
42
|
+
NOT_IMPLEMENTED: 501,
|
|
43
|
+
BAD_GATEWAY: 502,
|
|
44
|
+
SERVICE_UNAVAILABLE: 503,
|
|
45
|
+
GATEWAY_TIMEOUT: 504,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
module.exports = httpStatus;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const AppError = require('./AppError');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Middleware that catches requests which did not match any route
|
|
7
|
+
* and forwards a 404 AppError to the error handler.
|
|
8
|
+
*
|
|
9
|
+
* Must be registered **after** all routes and **before** the error handler.
|
|
10
|
+
*
|
|
11
|
+
* @param {object} [options]
|
|
12
|
+
* @param {string} [options.message] - Custom "not found" message
|
|
13
|
+
* @returns {Function} Express middleware
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* const { notFoundHandler, errorHandler } = require('express-pro-toolkit');
|
|
17
|
+
*
|
|
18
|
+
* // After all routes
|
|
19
|
+
* app.use(notFoundHandler());
|
|
20
|
+
* app.use(errorHandler());
|
|
21
|
+
*/
|
|
22
|
+
function notFoundHandler(options) {
|
|
23
|
+
const opts = options && typeof options === 'object' ? options : {};
|
|
24
|
+
|
|
25
|
+
return function notFoundMiddleware(req, res, next) {
|
|
26
|
+
const msg =
|
|
27
|
+
(typeof opts.message === 'string' && opts.message) ||
|
|
28
|
+
`Cannot ${req.method} ${req.originalUrl || req.url}`;
|
|
29
|
+
|
|
30
|
+
next(new AppError(msg, 404, { code: 'ROUTE_NOT_FOUND' }));
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = notFoundHandler;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/* ─── Colour helpers (ANSI 256 — only when stdout is a TTY) ────── */
|
|
4
|
+
|
|
5
|
+
const isTTY = process.stdout.isTTY;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {number} code ANSI colour code
|
|
9
|
+
* @param {string} text
|
|
10
|
+
* @returns {string}
|
|
11
|
+
*/
|
|
12
|
+
function colour(code, text) {
|
|
13
|
+
return isTTY ? `\x1b[${code}m${text}\x1b[0m` : text;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Pick a colour based on HTTP status code.
|
|
18
|
+
* @param {number} status
|
|
19
|
+
* @returns {string}
|
|
20
|
+
*/
|
|
21
|
+
function colourStatus(status) {
|
|
22
|
+
if (status >= 500) return colour(31, String(status)); // red
|
|
23
|
+
if (status >= 400) return colour(33, String(status)); // yellow
|
|
24
|
+
if (status >= 300) return colour(36, String(status)); // cyan
|
|
25
|
+
if (status >= 200) return colour(32, String(status)); // green
|
|
26
|
+
return String(status);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Pick a colour for response time.
|
|
31
|
+
* @param {number} ms
|
|
32
|
+
* @returns {string}
|
|
33
|
+
*/
|
|
34
|
+
function colourTime(ms) {
|
|
35
|
+
const text = ms.toFixed(2) + 'ms';
|
|
36
|
+
if (ms >= 1000) return colour(31, text); // red ≥ 1 s
|
|
37
|
+
if (ms >= 500) return colour(33, text); // yellow ≥ 500 ms
|
|
38
|
+
return colour(32, text); // green
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* ─── Default log function ────────────────────────────────────────── */
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {object} info
|
|
45
|
+
*/
|
|
46
|
+
function defaultLog(info) {
|
|
47
|
+
console.log(
|
|
48
|
+
`[${info.timestamp}] ${colour(1, info.method)} ${info.url} ${colourStatus(info.status)} — ${colourTime(info.duration)}${info.requestId ? ' id=' + info.requestId : ''}`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* ─── Factory ─────────────────────────────────────────────────────── */
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Creates a configurable request-logging middleware.
|
|
56
|
+
*
|
|
57
|
+
* @param {object} [options]
|
|
58
|
+
* @param {Function} [options.log] - Custom log function `(info) => void`.
|
|
59
|
+
* Receives `{ method, url, status, duration, timestamp, requestId }`.
|
|
60
|
+
* @param {Function} [options.skip] - `(req, res) => boolean` — skip logging
|
|
61
|
+
* for certain requests (e.g. health checks).
|
|
62
|
+
* @param {boolean} [options.colorize=true] - Enable ANSI colours (auto-detected)
|
|
63
|
+
* @returns {Function} Express middleware
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* const { createRequestLogger } = require('express-pro-toolkit');
|
|
67
|
+
*
|
|
68
|
+
* // Default
|
|
69
|
+
* app.use(createRequestLogger());
|
|
70
|
+
*
|
|
71
|
+
* // Skip health checks, custom logger
|
|
72
|
+
* app.use(createRequestLogger({
|
|
73
|
+
* skip: (req) => req.url === '/health',
|
|
74
|
+
* log: (info) => pinoLogger.info(info, 'request'),
|
|
75
|
+
* }));
|
|
76
|
+
*/
|
|
77
|
+
function createRequestLogger(options) {
|
|
78
|
+
const opts = options && typeof options === 'object' ? options : {};
|
|
79
|
+
const logFn = typeof opts.log === 'function' ? opts.log : defaultLog;
|
|
80
|
+
const skipFn = typeof opts.skip === 'function' ? opts.skip : null;
|
|
81
|
+
|
|
82
|
+
return function requestLoggerMiddleware(req, res, next) {
|
|
83
|
+
// Use hrtime tuple for high-resolution, Node 12+ compatible timing
|
|
84
|
+
const start = process.hrtime();
|
|
85
|
+
|
|
86
|
+
res.on('finish', function onFinish() {
|
|
87
|
+
// Allow caller to skip specific requests (health, metrics, etc.)
|
|
88
|
+
if (skipFn && skipFn(req, res)) return;
|
|
89
|
+
|
|
90
|
+
const diff = process.hrtime(start);
|
|
91
|
+
const durationMs = diff[0] * 1e3 + diff[1] / 1e6;
|
|
92
|
+
|
|
93
|
+
logFn({
|
|
94
|
+
method: req.method,
|
|
95
|
+
url: req.originalUrl || req.url,
|
|
96
|
+
status: res.statusCode,
|
|
97
|
+
duration: durationMs,
|
|
98
|
+
timestamp: new Date().toISOString(),
|
|
99
|
+
requestId: req.id || null,
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
next();
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/* ─── Backward-compatible default instance ────────────────────────── */
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Pre-configured request logger using default options.
|
|
111
|
+
* Drop-in replacement for the v1 export.
|
|
112
|
+
*
|
|
113
|
+
* @type {Function}
|
|
114
|
+
*/
|
|
115
|
+
const requestLogger = createRequestLogger();
|
|
116
|
+
|
|
117
|
+
module.exports = requestLogger;
|
|
118
|
+
module.exports.requestLogger = requestLogger;
|
|
119
|
+
module.exports.createRequestLogger = createRequestLogger;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Send a consistent JSON **success** response.
|
|
5
|
+
*
|
|
6
|
+
* @param {import('express').Response} res - Express response object
|
|
7
|
+
* @param {*} data - Payload to return under `data`
|
|
8
|
+
* @param {string} [message] - Human-readable message (default: `"Success"`)
|
|
9
|
+
* @param {number} [statusCode=200] - HTTP status code
|
|
10
|
+
* @param {object} [meta] - Optional pagination / extra metadata
|
|
11
|
+
* @returns {void}
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* const { sendSuccess } = require('express-pro-toolkit');
|
|
15
|
+
*
|
|
16
|
+
* sendSuccess(res, users, 'Users fetched', 200, {
|
|
17
|
+
* page: 1, limit: 25, total: 100,
|
|
18
|
+
* });
|
|
19
|
+
*
|
|
20
|
+
* // Response:
|
|
21
|
+
* // {
|
|
22
|
+
* // "success": true,
|
|
23
|
+
* // "message": "Users fetched",
|
|
24
|
+
* // "data": [...],
|
|
25
|
+
* // "meta": { "page": 1, "limit": 25, "total": 100 }
|
|
26
|
+
* // }
|
|
27
|
+
*/
|
|
28
|
+
function sendSuccess(res, data, message, statusCode, meta) {
|
|
29
|
+
const code = typeof statusCode === 'number' ? statusCode : 200;
|
|
30
|
+
const msg = typeof message === 'string' && message.length > 0 ? message : 'Success';
|
|
31
|
+
|
|
32
|
+
const body = {
|
|
33
|
+
success: true,
|
|
34
|
+
message: msg,
|
|
35
|
+
data: data !== undefined ? data : null,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
if (meta !== undefined && meta !== null) {
|
|
39
|
+
body.meta = meta;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
res.status(code).json(body);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Send a consistent JSON **error** response.
|
|
47
|
+
*
|
|
48
|
+
* @param {import('express').Response} res - Express response object
|
|
49
|
+
* @param {string} [message] - Human-readable error message
|
|
50
|
+
* @param {number} [statusCode=500] - HTTP status code
|
|
51
|
+
* @param {Array|null} [errors=null] - Optional array of detailed error objects
|
|
52
|
+
* @param {string} [code] - Machine-readable error code
|
|
53
|
+
* @returns {void}
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* sendError(res, 'Validation failed', 422, [
|
|
57
|
+
* { field: 'email', message: 'Email is required' },
|
|
58
|
+
* ], 'VALIDATION_ERROR');
|
|
59
|
+
*/
|
|
60
|
+
function sendError(res, message, statusCode, errors, code) {
|
|
61
|
+
const httpCode = typeof statusCode === 'number' ? statusCode : 500;
|
|
62
|
+
const msg = typeof message === 'string' && message.length > 0 ? message : 'Internal Server Error';
|
|
63
|
+
|
|
64
|
+
const body = {
|
|
65
|
+
success: false,
|
|
66
|
+
message: msg,
|
|
67
|
+
errors: errors != null ? (Array.isArray(errors) ? errors : [errors]) : null,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
if (typeof code === 'string' && code.length > 0) {
|
|
71
|
+
body.code = code;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
res.status(httpCode).json(body);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Send a paginated JSON success response.
|
|
79
|
+
*
|
|
80
|
+
* @param {import('express').Response} res
|
|
81
|
+
* @param {Array} items
|
|
82
|
+
* @param {object} pagination
|
|
83
|
+
* @param {number} pagination.page
|
|
84
|
+
* @param {number} pagination.limit
|
|
85
|
+
* @param {number} pagination.total
|
|
86
|
+
* @param {string} [message]
|
|
87
|
+
* @returns {void}
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* sendPaginated(res, users, { page: 2, limit: 25, total: 100 });
|
|
91
|
+
*/
|
|
92
|
+
function sendPaginated(res, items, pagination, message) {
|
|
93
|
+
const page = pagination.page || 1;
|
|
94
|
+
const limit = pagination.limit || 25;
|
|
95
|
+
const total = pagination.total || 0;
|
|
96
|
+
const totalPages = Math.ceil(total / limit) || 1;
|
|
97
|
+
|
|
98
|
+
sendSuccess(res, items, message || 'Success', 200, {
|
|
99
|
+
page,
|
|
100
|
+
limit,
|
|
101
|
+
total,
|
|
102
|
+
totalPages,
|
|
103
|
+
hasNextPage: page < totalPages,
|
|
104
|
+
hasPrevPage: page > 1,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = { sendSuccess, sendError, sendPaginated };
|