request-scope-api 1.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.
Files changed (75) hide show
  1. package/README.md +275 -0
  2. package/dist/cjs/adapters/adapter.interface.js +9 -0
  3. package/dist/cjs/adapters/adapter.interface.js.map +1 -0
  4. package/dist/cjs/adapters/mongo.adapter.js +188 -0
  5. package/dist/cjs/adapters/mongo.adapter.js.map +1 -0
  6. package/dist/cjs/adapters/mysql.adapter.js +243 -0
  7. package/dist/cjs/adapters/mysql.adapter.js.map +1 -0
  8. package/dist/cjs/adapters/pg.adapter.js +334 -0
  9. package/dist/cjs/adapters/pg.adapter.js.map +1 -0
  10. package/dist/cjs/capture.js +310 -0
  11. package/dist/cjs/capture.js.map +1 -0
  12. package/dist/cjs/config.js +122 -0
  13. package/dist/cjs/config.js.map +1 -0
  14. package/dist/cjs/dashboard/api.js +173 -0
  15. package/dist/cjs/dashboard/api.js.map +1 -0
  16. package/dist/cjs/dashboard/router.js +96 -0
  17. package/dist/cjs/dashboard/router.js.map +1 -0
  18. package/dist/cjs/index.js +49 -0
  19. package/dist/cjs/index.js.map +1 -0
  20. package/dist/cjs/masker.js +73 -0
  21. package/dist/cjs/masker.js.map +1 -0
  22. package/dist/cjs/middleware.js +198 -0
  23. package/dist/cjs/middleware.js.map +1 -0
  24. package/dist/cjs/queue.js +114 -0
  25. package/dist/cjs/queue.js.map +1 -0
  26. package/dist/cjs/retention.js +64 -0
  27. package/dist/cjs/retention.js.map +1 -0
  28. package/dist/cjs/types.js +9 -0
  29. package/dist/cjs/types.js.map +1 -0
  30. package/dist/dashboard/assets/index-C0TqFHk6.css +1 -0
  31. package/dist/dashboard/assets/index-MCuAZo4Q.js +67 -0
  32. package/dist/dashboard/index.html +13 -0
  33. package/dist/esm/adapters/adapter.interface.js +8 -0
  34. package/dist/esm/adapters/adapter.interface.js.map +1 -0
  35. package/dist/esm/adapters/mongo.adapter.js +184 -0
  36. package/dist/esm/adapters/mongo.adapter.js.map +1 -0
  37. package/dist/esm/adapters/mysql.adapter.js +236 -0
  38. package/dist/esm/adapters/mysql.adapter.js.map +1 -0
  39. package/dist/esm/adapters/pg.adapter.js +330 -0
  40. package/dist/esm/adapters/pg.adapter.js.map +1 -0
  41. package/dist/esm/capture.js +304 -0
  42. package/dist/esm/capture.js.map +1 -0
  43. package/dist/esm/config.js +117 -0
  44. package/dist/esm/config.js.map +1 -0
  45. package/dist/esm/dashboard/api.js +168 -0
  46. package/dist/esm/dashboard/api.js.map +1 -0
  47. package/dist/esm/dashboard/router.js +90 -0
  48. package/dist/esm/dashboard/router.js.map +1 -0
  49. package/dist/esm/index.js +50 -0
  50. package/dist/esm/index.js.map +1 -0
  51. package/dist/esm/masker.js +70 -0
  52. package/dist/esm/masker.js.map +1 -0
  53. package/dist/esm/middleware.js +193 -0
  54. package/dist/esm/middleware.js.map +1 -0
  55. package/dist/esm/queue.js +110 -0
  56. package/dist/esm/queue.js.map +1 -0
  57. package/dist/esm/retention.js +60 -0
  58. package/dist/esm/retention.js.map +1 -0
  59. package/dist/esm/types.js +8 -0
  60. package/dist/esm/types.js.map +1 -0
  61. package/dist/types/adapters/adapter.interface.d.ts +7 -0
  62. package/dist/types/adapters/mongo.adapter.d.ts +25 -0
  63. package/dist/types/adapters/mysql.adapter.d.ts +24 -0
  64. package/dist/types/adapters/pg.adapter.d.ts +29 -0
  65. package/dist/types/capture.d.ts +88 -0
  66. package/dist/types/config.d.ts +38 -0
  67. package/dist/types/dashboard/api.d.ts +49 -0
  68. package/dist/types/dashboard/router.d.ts +28 -0
  69. package/dist/types/index.d.ts +31 -0
  70. package/dist/types/masker.d.ts +15 -0
  71. package/dist/types/middleware.d.ts +67 -0
  72. package/dist/types/queue.d.ts +49 -0
  73. package/dist/types/retention.d.ts +30 -0
  74. package/dist/types/types.d.ts +101 -0
  75. package/package.json +48 -0
@@ -0,0 +1,310 @@
1
+ "use strict";
2
+ /**
3
+ * RequestScope — Capture layer, request and response sides.
4
+ *
5
+ * Provides:
6
+ * - parseClientIp(req) — extract client IP from headers/socket
7
+ * - bufferRequestBody(req) — consume request stream, buffer ≤50 KB,
8
+ * attempt JSON / URL-encoded parse, return
9
+ * body string + original byte size + client IP.
10
+ * - wrapResponse(res) — monkey-patch res.write/res.end to accumulate
11
+ * response chunks, cap at 100 KB, return wrapper
12
+ * with finish event handler that builds RequestRecord.
13
+ *
14
+ * Uses Node.js built-ins only (no external deps).
15
+ * Compiles under strict: true.
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.ERROR_DATA_SYMBOL = void 0;
19
+ exports.parseClientIp = parseClientIp;
20
+ exports.bufferRequestBody = bufferRequestBody;
21
+ exports.wrapResponse = wrapResponse;
22
+ const node_crypto_1 = require("node:crypto");
23
+ const masker_1 = require("./masker");
24
+ // ---------------------------------------------------------------------------
25
+ // Constants
26
+ // ---------------------------------------------------------------------------
27
+ /** Maximum bytes to retain from a request body before truncation. */
28
+ const MAX_BODY_BYTES = 50000;
29
+ /** Maximum bytes to retain from a response body before truncation. */
30
+ const MAX_RESPONSE_BODY_BYTES = 100000;
31
+ /** Suffix appended to a body that was truncated. */
32
+ const TRUNCATED_SUFFIX = '[truncated]';
33
+ // ---------------------------------------------------------------------------
34
+ // Client IP extraction
35
+ // ---------------------------------------------------------------------------
36
+ /**
37
+ * Extracts the client IP address from the request.
38
+ *
39
+ * Prefers the first entry of the `X-Forwarded-For` header (proxy-aware),
40
+ * falling back to `req.socket.remoteAddress`. Returns `"0.0.0.0"` when
41
+ * neither is available.
42
+ *
43
+ * @param req Node.js IncomingMessage (or Express Request)
44
+ */
45
+ function parseClientIp(req) {
46
+ const xff = req.headers['x-forwarded-for'];
47
+ if (xff) {
48
+ const raw = Array.isArray(xff) ? xff[0] : xff;
49
+ // X-Forwarded-For may be comma-separated: "client, proxy1, proxy2"
50
+ const first = raw.split(',')[0].trim();
51
+ if (first) {
52
+ return first;
53
+ }
54
+ }
55
+ return req.socket?.remoteAddress ?? '0.0.0.0';
56
+ }
57
+ // ---------------------------------------------------------------------------
58
+ // URL-encoded body parsing helper
59
+ // ---------------------------------------------------------------------------
60
+ /**
61
+ * Attempts to parse `raw` as a URL-encoded query string.
62
+ *
63
+ * Returns a JSON string of the key-value pairs on success, or `null` when
64
+ * the string contains no parseable key-value pairs.
65
+ */
66
+ function tryParseUrlEncoded(raw) {
67
+ // A URL-encoded body must contain at least one '=' (key=value separator).
68
+ // Without this guard, URLSearchParams treats any bare string as a key with
69
+ // an empty value, producing false positives for plain text / XML bodies.
70
+ if (!raw.includes('=')) {
71
+ return null;
72
+ }
73
+ try {
74
+ const params = new URLSearchParams(raw);
75
+ const obj = {};
76
+ params.forEach((value, key) => {
77
+ obj[key] = value;
78
+ });
79
+ // Only treat as URL-encoded if we got at least one key-value pair.
80
+ if (Object.keys(obj).length === 0) {
81
+ return null;
82
+ }
83
+ return JSON.stringify(obj);
84
+ }
85
+ catch {
86
+ return null;
87
+ }
88
+ }
89
+ // ---------------------------------------------------------------------------
90
+ // Request body buffering
91
+ // ---------------------------------------------------------------------------
92
+ /**
93
+ * Consumes the `req` stream and buffers up to {@link MAX_BODY_BYTES} bytes.
94
+ *
95
+ * Behaviour:
96
+ * 1. Collects `Buffer` chunks from `data` events; tracks total byte length.
97
+ * 2. On `end`: concatenates chunks, slices to MAX_BODY_BYTES; appends
98
+ * `"[truncated]"` when the original size exceeded the limit.
99
+ * 3. Attempts `JSON.parse` → stores normalised JSON string.
100
+ * 4. On JSON failure, attempts `URLSearchParams` parse → stores JSON map.
101
+ * 5. On both failures, stores the raw string as-is.
102
+ * 6. On stream `error`, resolves with `{ body: '', bodySize: 0, clientIp }`.
103
+ *
104
+ * @param req Node.js IncomingMessage (or Express Request)
105
+ * @returns Resolved promise with body string, original byte size, and IP.
106
+ */
107
+ async function bufferRequestBody(req) {
108
+ const clientIp = parseClientIp(req);
109
+ return new Promise((resolve) => {
110
+ const chunks = [];
111
+ let totalBytes = 0;
112
+ req.on('data', (chunk) => {
113
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
114
+ totalBytes += buf.byteLength;
115
+ chunks.push(buf);
116
+ });
117
+ req.on('end', () => {
118
+ // Concatenate all chunks into one buffer, then slice to the limit.
119
+ const full = Buffer.concat(chunks);
120
+ const truncated = totalBytes > MAX_BODY_BYTES;
121
+ const kept = truncated ? full.subarray(0, MAX_BODY_BYTES) : full;
122
+ let rawString = kept.toString('utf8');
123
+ if (truncated) {
124
+ rawString += TRUNCATED_SUFFIX;
125
+ }
126
+ // Try JSON first.
127
+ try {
128
+ const parsed = JSON.parse(rawString.replace(TRUNCATED_SUFFIX, ''));
129
+ // Store as normalised JSON string (handles whitespace variation).
130
+ const base = JSON.stringify(parsed);
131
+ resolve({
132
+ body: truncated ? base + TRUNCATED_SUFFIX : base,
133
+ bodySize: totalBytes,
134
+ clientIp,
135
+ });
136
+ return;
137
+ }
138
+ catch {
139
+ // JSON parse failed — try URL-encoded next (only on untruncated or
140
+ // the raw portion; URLSearchParams on a truncated string may be
141
+ // structurally invalid, but we try on the raw slice).
142
+ }
143
+ // Try URL-encoded.
144
+ const rawForUrlParse = truncated
145
+ ? full.subarray(0, MAX_BODY_BYTES).toString('utf8')
146
+ : rawString;
147
+ const urlEncoded = tryParseUrlEncoded(rawForUrlParse);
148
+ if (urlEncoded !== null) {
149
+ resolve({
150
+ body: truncated ? urlEncoded + TRUNCATED_SUFFIX : urlEncoded,
151
+ bodySize: totalBytes,
152
+ clientIp,
153
+ });
154
+ return;
155
+ }
156
+ // Fall back to raw string (possibly truncated).
157
+ resolve({ body: rawString, bodySize: totalBytes, clientIp });
158
+ });
159
+ req.on('error', () => {
160
+ resolve({ body: '', bodySize: 0, clientIp });
161
+ });
162
+ });
163
+ }
164
+ // ---------------------------------------------------------------------------
165
+ // Response body wrapping
166
+ // ---------------------------------------------------------------------------
167
+ /**
168
+ * Symbol used to attach error data to the request object for the finish handler.
169
+ */
170
+ exports.ERROR_DATA_SYMBOL = Symbol('requestscope_error_data');
171
+ /**
172
+ * Wraps a ServerResponse to accumulate response chunks.
173
+ *
174
+ * Monkey-patches `res.write` and `res.end` to accumulate response body chunks.
175
+ * Caps the buffer at MAX_RESPONSE_BODY_BYTES; beyond that stores first
176
+ * MAX_RESPONSE_BODY_BYTES bytes + "[truncated]". Records responseBodySize as
177
+ * the original Content-Length or accumulated size.
178
+ *
179
+ * On the `finish` event, builds a RequestRecord with:
180
+ * - UUIDv4 id
181
+ * - process.hrtime-based responseTime
182
+ * - maskObject() applied to request headers and request body
183
+ * - maskObject() applied to response headers
184
+ * - Enqueues the record synchronously via the provided callback
185
+ *
186
+ * If wrapping fails (stream/binary response), sets responseBody = "",
187
+ * reads Content-Length header as responseBodySize, and calls original write/end
188
+ * without disrupting the response.
189
+ *
190
+ * @param res - Node.js ServerResponse (or Express Response)
191
+ * @param req - Node.js IncomingMessage (or Express Request)
192
+ * @param requestBody - Buffered request body string
193
+ * @param requestBodySize - Original request body byte size
194
+ * @param clientIp - Client IP address
195
+ * @param sensitiveFields - Set of sensitive field names to mask
196
+ * @param enqueueRecord - Callback to enqueue the RequestRecord synchronously
197
+ * @returns The wrapped response (same object, with patched methods)
198
+ */
199
+ function wrapResponse(res, req, requestBody, requestBodySize, clientIp, sensitiveFields, enqueueRecord) {
200
+ const chunks = [];
201
+ let totalBytes = 0;
202
+ const startTime = process.hrtime.bigint();
203
+ // Store original methods
204
+ const originalWrite = res.write;
205
+ const originalEnd = res.end;
206
+ // Helper to accumulate chunks
207
+ const accumulateChunk = (chunk, encoding) => {
208
+ try {
209
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding);
210
+ totalBytes += buf.byteLength;
211
+ if (totalBytes <= MAX_RESPONSE_BODY_BYTES) {
212
+ chunks.push(buf);
213
+ }
214
+ }
215
+ catch (err) {
216
+ // Ignore accumulation errors
217
+ }
218
+ };
219
+ // Patch res.write
220
+ res.write = function (chunk, encoding, cb) {
221
+ accumulateChunk(chunk, encoding);
222
+ return originalWrite.call(this, chunk, encoding, cb);
223
+ };
224
+ // Patch res.end
225
+ res.end = function (chunk, encoding, cb) {
226
+ if (chunk !== undefined) {
227
+ accumulateChunk(chunk, encoding);
228
+ }
229
+ originalEnd.call(this, chunk, encoding, cb);
230
+ };
231
+ // Patch Express-specific methods that might bypass res.write/res.end
232
+ const expressRes = res;
233
+ if (expressRes.send) {
234
+ const originalSend = expressRes.send;
235
+ expressRes.send = function (body) {
236
+ // Capture the body before sending
237
+ if (body !== undefined) {
238
+ const str = typeof body === 'string' ? body : JSON.stringify(body);
239
+ accumulateChunk(str, 'utf8');
240
+ }
241
+ return originalSend.call(this, body);
242
+ };
243
+ }
244
+ if (expressRes.json) {
245
+ const originalJson = expressRes.json;
246
+ expressRes.json = function (obj) {
247
+ const str = JSON.stringify(obj);
248
+ accumulateChunk(str, 'utf8');
249
+ return originalJson.call(this, obj);
250
+ };
251
+ }
252
+ // Listen for finish event to build the record
253
+ res.on('finish', () => {
254
+ const endTime = process.hrtime.bigint();
255
+ const responseTime = Number(endTime - startTime) / 1000000; // Convert to milliseconds
256
+ // Build response body
257
+ let responseBody = '';
258
+ let responseBodySize = totalBytes;
259
+ if (chunks.length > 0) {
260
+ const full = Buffer.concat(chunks);
261
+ const truncated = totalBytes > MAX_RESPONSE_BODY_BYTES;
262
+ const kept = truncated ? full.subarray(0, MAX_RESPONSE_BODY_BYTES) : full;
263
+ responseBody = kept.toString('utf8');
264
+ if (truncated) {
265
+ responseBody += TRUNCATED_SUFFIX;
266
+ }
267
+ }
268
+ else {
269
+ // No chunks accumulated - try to get size from Content-Length header
270
+ const contentLength = res.getHeader('Content-Length');
271
+ if (typeof contentLength === 'number') {
272
+ responseBodySize = contentLength;
273
+ }
274
+ else if (typeof contentLength === 'string') {
275
+ responseBodySize = parseInt(contentLength, 10) || 0;
276
+ }
277
+ // Log when no chunks are accumulated for debugging
278
+ process.stderr.write(`[RequestScope] No response chunks accumulated for ${req.method} ${req.url}\n`);
279
+ }
280
+ // Get error data from request if present
281
+ const errorData = req[exports.ERROR_DATA_SYMBOL];
282
+ // Build request record
283
+ const record = {
284
+ id: (0, node_crypto_1.randomUUID)(),
285
+ method: req.method || 'GET',
286
+ url: req.url || '/',
287
+ route: null, // Will be set by Express if available
288
+ queryParams: {}, // Will be populated by Express
289
+ pathParams: {}, // Will be populated by Express
290
+ requestHeaders: (0, masker_1.maskObject)(Object.fromEntries(Object.entries(req.headers).map(([k, v]) => [k, Array.isArray(v) ? v[0] : v ?? ''])), sensitiveFields),
291
+ requestBody,
292
+ requestBodySize,
293
+ clientIp,
294
+ userAgent: req.headers['user-agent'] || '',
295
+ statusCode: res.statusCode,
296
+ responseHeaders: (0, masker_1.maskObject)(Object.fromEntries(Object.entries(res.getHeaders()).map(([k, v]) => [k, Array.isArray(v) ? v[0] : v ?? ''])), sensitiveFields),
297
+ responseBody,
298
+ responseBodySize,
299
+ responseTime,
300
+ errorMessage: errorData?.message ?? null,
301
+ errorStack: errorData?.stack ?? null,
302
+ errorStatusCode: errorData?.statusCode ?? null,
303
+ timestamp: new Date().toISOString(),
304
+ };
305
+ // Enqueue the record synchronously
306
+ enqueueRecord(record);
307
+ });
308
+ return res;
309
+ }
310
+ //# sourceMappingURL=capture.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capture.js","sourceRoot":"","sources":["../../src/capture.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,122 @@
1
+ "use strict";
2
+ /**
3
+ * RequestScope — Configuration validation, defaults, and sensitive-field set.
4
+ *
5
+ * All validation errors are thrown synchronously so that `requestscope()` fails
6
+ * fast at startup before any middleware is registered.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.applyDefaults = applyDefaults;
10
+ exports.validateConfig = validateConfig;
11
+ exports.buildSensitiveFieldSet = buildSensitiveFieldSet;
12
+ // ---------------------------------------------------------------------------
13
+ // Constants
14
+ // ---------------------------------------------------------------------------
15
+ const SUPPORTED_STORAGE_TYPES = ['mongodb', 'mysql', 'postgresql'];
16
+ const SQL_REQUIRED_FIELDS = [
17
+ 'host',
18
+ 'database',
19
+ 'username',
20
+ 'password',
21
+ ];
22
+ /** Built-in sensitive field names (all compared case-insensitively). */
23
+ const DEFAULT_SENSITIVE_FIELDS = [
24
+ 'password',
25
+ 'token',
26
+ 'authorization',
27
+ 'apiKey',
28
+ 'secret',
29
+ ];
30
+ // ---------------------------------------------------------------------------
31
+ // applyDefaults
32
+ // ---------------------------------------------------------------------------
33
+ /**
34
+ * Fills in missing optional fields with their documented default values.
35
+ * Mutates the config object in place and returns it for chaining.
36
+ *
37
+ * Defaults applied:
38
+ * retentionDays → 30
39
+ * ignore → []
40
+ * maskFields → []
41
+ * storage.poolSize → 5
42
+ * storage.ssl → false
43
+ */
44
+ function applyDefaults(config) {
45
+ if (config.retentionDays === undefined) {
46
+ config.retentionDays = 30;
47
+ }
48
+ if (config.ignore === undefined) {
49
+ config.ignore = ['/requestscope/api', '/requestscope/assets'];
50
+ }
51
+ if (config.maskFields === undefined) {
52
+ config.maskFields = [];
53
+ }
54
+ if (config.storage.poolSize === undefined) {
55
+ config.storage.poolSize = 5;
56
+ }
57
+ if (config.storage.ssl === undefined) {
58
+ config.storage.ssl = false;
59
+ }
60
+ return config;
61
+ }
62
+ // ---------------------------------------------------------------------------
63
+ // validateConfig
64
+ // ---------------------------------------------------------------------------
65
+ /**
66
+ * Validates the configuration object and throws a synchronous `Error` for
67
+ * any invalid value.
68
+ *
69
+ * Checks (in order):
70
+ * 1. `storage.type` must be one of the supported values.
71
+ * 2. MongoDB requires `storage.uri`.
72
+ * 3. MySQL / PostgreSQL require `host`, `database`, `username`, `password`.
73
+ * 4. `retentionDays` (when provided) must be a positive integer in [1, 365].
74
+ */
75
+ function validateConfig(config) {
76
+ const { storage, retentionDays } = config;
77
+ // 1. Validate storage.type
78
+ if (!SUPPORTED_STORAGE_TYPES.includes(storage.type)) {
79
+ throw new Error(`Invalid storage type: '${storage.type}'. Supported values are: mongodb, mysql, postgresql`);
80
+ }
81
+ // 2. MongoDB: uri is required
82
+ if (storage.type === 'mongodb') {
83
+ if (!storage.uri) {
84
+ throw new Error("storage.uri is required when storage.type is 'mongodb'");
85
+ }
86
+ }
87
+ // 3. MySQL / PostgreSQL: all SQL required fields must be present
88
+ if (storage.type === 'mysql' || storage.type === 'postgresql') {
89
+ const missing = SQL_REQUIRED_FIELDS.filter((field) => storage[field] === undefined || storage[field] === null);
90
+ if (missing.length > 0) {
91
+ throw new Error(`Missing required storage fields: ${missing.join(', ')}`);
92
+ }
93
+ }
94
+ // 4. retentionDays: must be a positive integer in [1, 365] when provided
95
+ if (retentionDays !== undefined) {
96
+ const isValidInteger = typeof retentionDays === 'number' &&
97
+ Number.isInteger(retentionDays) &&
98
+ retentionDays >= 1 &&
99
+ retentionDays <= 365;
100
+ if (!isValidInteger) {
101
+ throw new Error(`retentionDays must be a positive integer between 1 and 365, received: ${retentionDays}`);
102
+ }
103
+ }
104
+ }
105
+ // ---------------------------------------------------------------------------
106
+ // buildSensitiveFieldSet
107
+ // ---------------------------------------------------------------------------
108
+ /**
109
+ * Constructs the `Set<string>` of sensitive field names by forming the union
110
+ * of the built-in defaults and any user-supplied `maskFields`.
111
+ *
112
+ * All names are lowercased so that lookups from `maskObject()` can compare
113
+ * case-insensitively by lowercasing the inspected key.
114
+ */
115
+ function buildSensitiveFieldSet(config) {
116
+ const names = [
117
+ ...DEFAULT_SENSITIVE_FIELDS,
118
+ ...(config.maskFields ?? []),
119
+ ];
120
+ return new Set(names.map((n) => n.toLowerCase()));
121
+ }
122
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,173 @@
1
+ "use strict";
2
+ /**
3
+ * RequestScope — Dashboard API handlers.
4
+ *
5
+ * Provides Express route handlers for the dashboard:
6
+ * - GET /api/records — List records with filtering, sorting, and pagination
7
+ * - GET /api/records/:id — Get a single record by ID
8
+ *
9
+ * Requirements: 9.1, 9.2, 9.3, 9.4, 9.5, 9.6
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.getRecords = getRecords;
13
+ exports.getRecordById = getRecordById;
14
+ exports.deleteAllRecords = deleteAllRecords;
15
+ // ---------------------------------------------------------------------------
16
+ // Helper functions
17
+ // ---------------------------------------------------------------------------
18
+ /**
19
+ * Parses a query parameter as an integer with a default value.
20
+ */
21
+ function parseIntParam(value, defaultValue, min = 1) {
22
+ if (value === undefined) {
23
+ return defaultValue;
24
+ }
25
+ const parsed = parseInt(value, 10);
26
+ if (isNaN(parsed) || parsed < min) {
27
+ return defaultValue;
28
+ }
29
+ return parsed;
30
+ }
31
+ /**
32
+ * Validates that a value is one of the allowed options.
33
+ */
34
+ function validateOption(value, allowed, defaultValue) {
35
+ if (value === undefined) {
36
+ return defaultValue;
37
+ }
38
+ return allowed.includes(value) ? value : defaultValue;
39
+ }
40
+ // ---------------------------------------------------------------------------
41
+ // Route handlers
42
+ // ---------------------------------------------------------------------------
43
+ /**
44
+ * GET /api/records handler.
45
+ *
46
+ * Query parameters:
47
+ * - search: string (optional) — case-insensitive substring match on url
48
+ * - startDate: string (optional) — ISO 8601 timestamp, inclusive lower bound
49
+ * - endDate: string (optional) — ISO 8601 timestamp, inclusive upper bound
50
+ * - statusCodeGroup: string (optional) — '2xx', '3xx', '4xx', or '5xx'
51
+ * - statusCode: number (optional) — exact status code match
52
+ * - method: string (optional) — exact HTTP method match (case-insensitive)
53
+ * - sortBy: string (optional) — 'timestamp', 'responseTime', or 'statusCode'
54
+ * - sortOrder: string (optional) — 'asc' or 'desc'
55
+ * - page: number (optional) — page number, default 1
56
+ * - pageSize: number (optional) — page size, default 25
57
+ *
58
+ * Returns:
59
+ * - 200 with { records: RequestRecord[], total: number }
60
+ * - 400 for malformed query parameters
61
+ */
62
+ async function getRecords(req, res) {
63
+ try {
64
+ const adapter = req.app.get('requestscopeAdapter');
65
+ if (!adapter) {
66
+ res.status(500).json({ error: 'Storage adapter not configured' });
67
+ return;
68
+ }
69
+ // Parse and validate query parameters
70
+ const page = parseIntParam(req.query.page, 1);
71
+ const pageSize = parseIntParam(req.query.pageSize, 25);
72
+ const sortBy = validateOption(req.query.sortBy, ['timestamp', 'responseTime', 'statusCode'], 'timestamp');
73
+ const sortOrder = validateOption(req.query.sortOrder, ['asc', 'desc'], 'desc');
74
+ // Build QueryFilters
75
+ const filters = {
76
+ search: req.query.search,
77
+ startDate: req.query.startDate,
78
+ endDate: req.query.endDate,
79
+ statusCodeGroup: (() => {
80
+ const value = req.query.statusCodeGroup;
81
+ if (!value)
82
+ return undefined;
83
+ if (['2xx', '3xx', '4xx', '5xx'].includes(value)) {
84
+ return value;
85
+ }
86
+ return undefined;
87
+ })(),
88
+ statusCode: req.query.statusCode
89
+ ? parseIntParam(req.query.statusCode, 0)
90
+ : undefined,
91
+ method: req.query.method,
92
+ sortBy,
93
+ sortOrder,
94
+ page,
95
+ pageSize,
96
+ };
97
+ // Query the adapter
98
+ const result = await adapter.query(filters, { page, pageSize });
99
+ res.status(200).json(result);
100
+ }
101
+ catch (err) {
102
+ const message = err instanceof Error ? err.message : String(err);
103
+ res.status(400).json({ error: message });
104
+ }
105
+ }
106
+ /**
107
+ * GET /api/records/:id handler.
108
+ *
109
+ * Returns:
110
+ * - 200 with the RequestRecord
111
+ * - 404 if the record is not found
112
+ * - 400 for malformed ID
113
+ */
114
+ async function getRecordById(req, res) {
115
+ try {
116
+ const adapter = req.app.get('requestscopeAdapter');
117
+ if (!adapter) {
118
+ res.status(500).json({ error: 'Storage adapter not configured' });
119
+ return;
120
+ }
121
+ const id = req.params.id;
122
+ if (!id) {
123
+ res.status(400).json({ error: 'Record ID is required' });
124
+ return;
125
+ }
126
+ // Query for the specific record using id filter
127
+ const filters = {
128
+ id,
129
+ page: 1,
130
+ pageSize: 1,
131
+ };
132
+ const result = await adapter.query(filters, { page: 1, pageSize: 1 });
133
+ const record = result.records[0];
134
+ if (!record) {
135
+ res.status(404).json({ error: 'Record not found' });
136
+ return;
137
+ }
138
+ res.status(200).json(record);
139
+ }
140
+ catch (err) {
141
+ const message = err instanceof Error ? err.message : String(err);
142
+ res.status(400).json({ error: message });
143
+ }
144
+ }
145
+ /**
146
+ * DELETE /api/records handler.
147
+ *
148
+ * Deletes all records from the database.
149
+ *
150
+ * Returns:
151
+ * - 200 with { deleted: number }
152
+ * - 500 for server errors
153
+ */
154
+ async function deleteAllRecords(req, res) {
155
+ try {
156
+ const adapter = req.app.get('requestscopeAdapter');
157
+ if (!adapter) {
158
+ res.status(500).json({ error: 'Storage adapter not configured' });
159
+ return;
160
+ }
161
+ // Query to get total count before deletion
162
+ const allRecords = await adapter.query({ page: 1, pageSize: 1000000 }, { page: 1, pageSize: 1000000 });
163
+ const totalToDelete = allRecords.total;
164
+ // Delete all records by setting retention to a future date
165
+ await adapter.deleteOlderThan(new Date(Date.now() + 86400000));
166
+ res.status(200).json({ deleted: totalToDelete });
167
+ }
168
+ catch (err) {
169
+ const message = err instanceof Error ? err.message : String(err);
170
+ res.status(500).json({ error: message });
171
+ }
172
+ }
173
+ //# sourceMappingURL=api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["../../../src/dashboard/api.ts"],"names":[],"mappings":"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"}