@valentine-efagene/qshelter-common 2.0.148 → 2.0.150

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.
@@ -1,6 +1,6 @@
1
1
  import { Request, Response, NextFunction } from 'express';
2
2
  /**
3
3
  * Global error handler middleware for Express applications.
4
- * Handles ZodError, AppError, and generic errors with appropriate responses.
4
+ * Handles ZodError, AppError, Prisma errors, and generic errors with appropriate responses.
5
5
  */
6
6
  export declare function errorHandler(err: Error, req: Request, res: Response, next: NextFunction): Response<any, Record<string, any>>;
@@ -1,10 +1,83 @@
1
1
  import { ZodError } from 'zod';
2
2
  import { AppError } from '../utils/errors';
3
+ /**
4
+ * Extract a user-friendly field name from Prisma's target array.
5
+ * Handles composite unique constraints like `tenantId_cacNumber`.
6
+ */
7
+ function extractFieldName(target) {
8
+ // Filter out common fields that are not user-facing
9
+ const userFacingFields = target.filter((f) => !['tenantId', 'id', 'createdAt', 'updatedAt'].includes(f));
10
+ if (userFacingFields.length === 0) {
11
+ // Fallback: use the last target field and make it readable
12
+ const lastField = target[target.length - 1];
13
+ return lastField.replace(/([A-Z])/g, ' $1').trim().toLowerCase();
14
+ }
15
+ // Convert camelCase to readable format
16
+ return userFacingFields
17
+ .map((f) => f.replace(/([A-Z])/g, ' $1').trim().toLowerCase())
18
+ .join(', ');
19
+ }
20
+ /**
21
+ * Handle Prisma-specific errors with user-friendly messages.
22
+ */
23
+ function handlePrismaError(err) {
24
+ // Check if this is a Prisma error (has code property)
25
+ if (!err.code || typeof err.code !== 'string' || !err.code.startsWith('P')) {
26
+ return null;
27
+ }
28
+ switch (err.code) {
29
+ case 'P2002': {
30
+ // Unique constraint violation
31
+ const target = err.meta?.target;
32
+ const modelName = err.meta?.modelName;
33
+ if (target && target.length > 0) {
34
+ const fieldName = extractFieldName(target);
35
+ const entity = modelName || 'Record';
36
+ return {
37
+ statusCode: 409,
38
+ message: `A ${entity.toLowerCase()} with this ${fieldName} already exists`,
39
+ };
40
+ }
41
+ return {
42
+ statusCode: 409,
43
+ message: 'A record with these values already exists',
44
+ };
45
+ }
46
+ case 'P2003': {
47
+ // Foreign key constraint violation
48
+ const field = err.meta?.field_name;
49
+ return {
50
+ statusCode: 400,
51
+ message: field
52
+ ? `Referenced ${field.replace(/([A-Z])/g, ' $1').trim().toLowerCase()} does not exist`
53
+ : 'Referenced record does not exist',
54
+ };
55
+ }
56
+ case 'P2025': {
57
+ // Record not found for update/delete
58
+ return {
59
+ statusCode: 404,
60
+ message: 'Record not found',
61
+ };
62
+ }
63
+ case 'P2014': {
64
+ // Required relation violation
65
+ return {
66
+ statusCode: 400,
67
+ message: 'This operation would violate a required relation',
68
+ };
69
+ }
70
+ default:
71
+ // Unknown Prisma error - log it but return generic message
72
+ return null;
73
+ }
74
+ }
3
75
  /**
4
76
  * Global error handler middleware for Express applications.
5
- * Handles ZodError, AppError, and generic errors with appropriate responses.
77
+ * Handles ZodError, AppError, Prisma errors, and generic errors with appropriate responses.
6
78
  */
7
79
  export function errorHandler(err, req, res, next) {
80
+ // Handle Zod validation errors
8
81
  if (err instanceof ZodError) {
9
82
  return res.status(400).json({
10
83
  success: false,
@@ -13,6 +86,7 @@ export function errorHandler(err, req, res, next) {
13
86
  details: err.issues,
14
87
  });
15
88
  }
89
+ // Handle custom application errors
16
90
  if (err instanceof AppError) {
17
91
  return res.status(err.statusCode).json({
18
92
  success: false,
@@ -20,6 +94,16 @@ export function errorHandler(err, req, res, next) {
20
94
  error: err.message,
21
95
  });
22
96
  }
97
+ // Handle Prisma errors
98
+ const prismaError = handlePrismaError(err);
99
+ if (prismaError) {
100
+ return res.status(prismaError.statusCode).json({
101
+ success: false,
102
+ message: prismaError.message,
103
+ error: prismaError.message,
104
+ });
105
+ }
106
+ // Unhandled error - log and return generic message
23
107
  console.error('Unhandled error:', err);
24
108
  return res.status(500).json({
25
109
  success: false,
@@ -1,8 +1,12 @@
1
1
  import { Request, Response, NextFunction } from 'express';
2
2
  /**
3
- * Request logging middleware that logs method, path, status code, and duration.
4
- * Logs in JSON format for easy parsing by log aggregation tools.
3
+ * Request logging middleware that logs:
4
+ * - HTTP method, path, status code, and duration
5
+ * - Request body (with sensitive fields redacted)
6
+ * - Caller info (userId, tenantId, email, roles)
7
+ * - Response body (with sensitive fields redacted)
8
+ * - Unique request ID for correlation
5
9
  *
6
- * In debug mode, also logs the authorizer context from API Gateway.
10
+ * Logs in JSON format for easy parsing by CloudWatch/log aggregation tools.
7
11
  */
8
12
  export declare function requestLogger(req: Request, res: Response, next: NextFunction): void;
@@ -8,51 +8,161 @@ catch {
8
8
  // Package not available
9
9
  }
10
10
  /**
11
- * Request logging middleware that logs method, path, status code, and duration.
12
- * Logs in JSON format for easy parsing by log aggregation tools.
11
+ * Fields to redact from request/response logging for security.
12
+ */
13
+ const SENSITIVE_FIELDS = ['password', 'token', 'accessToken', 'refreshToken', 'secret', 'apiKey', 'credential'];
14
+ /**
15
+ * Redact sensitive fields from an object for safe logging.
16
+ */
17
+ function redactSensitive(obj) {
18
+ if (!obj || typeof obj !== 'object')
19
+ return obj;
20
+ if (Array.isArray(obj))
21
+ return obj.map(redactSensitive);
22
+ const redacted = {};
23
+ for (const [key, value] of Object.entries(obj)) {
24
+ if (SENSITIVE_FIELDS.some(f => key.toLowerCase().includes(f.toLowerCase()))) {
25
+ redacted[key] = '[REDACTED]';
26
+ }
27
+ else if (typeof value === 'object' && value !== null) {
28
+ redacted[key] = redactSensitive(value);
29
+ }
30
+ else {
31
+ redacted[key] = value;
32
+ }
33
+ }
34
+ return redacted;
35
+ }
36
+ /**
37
+ * Extract caller context from the request (authorizer or headers).
38
+ */
39
+ function extractCaller(req) {
40
+ let authorizer = null;
41
+ // Try getCurrentInvoke first
42
+ if (getCurrentInvoke) {
43
+ const { event } = getCurrentInvoke();
44
+ if (event?.requestContext?.authorizer) {
45
+ const auth = event.requestContext.authorizer;
46
+ authorizer = auth.lambda || auth;
47
+ }
48
+ }
49
+ // Fallback to req.apiGateway
50
+ if (!authorizer) {
51
+ const lambdaReq = req;
52
+ if (lambdaReq.apiGateway?.event?.requestContext?.authorizer) {
53
+ const auth = lambdaReq.apiGateway.event.requestContext.authorizer;
54
+ authorizer = auth.lambda || auth;
55
+ }
56
+ }
57
+ // Fallback to headers (for tests/local dev)
58
+ if (!authorizer) {
59
+ return {
60
+ userId: req.headers['x-user-id'],
61
+ tenantId: req.headers['x-tenant-id'],
62
+ email: req.headers['x-user-email'],
63
+ roles: req.headers['x-user-roles'] ? req.headers['x-user-roles'].split(',') : undefined,
64
+ };
65
+ }
66
+ return {
67
+ userId: authorizer.userId,
68
+ tenantId: authorizer.tenantId,
69
+ email: authorizer.email,
70
+ roles: authorizer.roles ? (typeof authorizer.roles === 'string' ? authorizer.roles.split(',') : authorizer.roles) : undefined,
71
+ };
72
+ }
73
+ /**
74
+ * Generate a unique request ID for correlation.
75
+ */
76
+ function generateRequestId() {
77
+ return `req_${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 9)}`;
78
+ }
79
+ /**
80
+ * Request logging middleware that logs:
81
+ * - HTTP method, path, status code, and duration
82
+ * - Request body (with sensitive fields redacted)
83
+ * - Caller info (userId, tenantId, email, roles)
84
+ * - Response body (with sensitive fields redacted)
85
+ * - Unique request ID for correlation
13
86
  *
14
- * In debug mode, also logs the authorizer context from API Gateway.
87
+ * Logs in JSON format for easy parsing by CloudWatch/log aggregation tools.
15
88
  */
16
89
  export function requestLogger(req, res, next) {
17
90
  const start = Date.now();
18
- // Debug: Log authorizer context structure
19
- if (process.env.DEBUG_AUTH === 'true' || process.env.NODE_ENV !== 'production') {
20
- let authorizer = null;
21
- let source = 'none';
22
- // Try getCurrentInvoke first (preferred for @codegenie/serverless-express)
23
- if (getCurrentInvoke) {
24
- const { event } = getCurrentInvoke();
25
- if (event?.requestContext?.authorizer) {
26
- authorizer = event.requestContext.authorizer;
27
- source = 'getCurrentInvoke';
28
- }
91
+ const requestId = generateRequestId();
92
+ // Attach requestId to request for downstream use
93
+ req.requestId = requestId;
94
+ // Extract caller info
95
+ const caller = extractCaller(req);
96
+ // Capture request body (redacted)
97
+ const requestBody = req.body && Object.keys(req.body).length > 0
98
+ ? redactSensitive(req.body)
99
+ : undefined;
100
+ // Log incoming request
101
+ console.log(JSON.stringify({
102
+ type: 'http_request',
103
+ requestId,
104
+ method: req.method,
105
+ path: req.path,
106
+ query: Object.keys(req.query).length > 0 ? req.query : undefined,
107
+ caller: caller.userId ? caller : undefined,
108
+ body: requestBody,
109
+ timestamp: new Date().toISOString(),
110
+ }));
111
+ // Capture response body by intercepting res.json and res.send
112
+ let responseBody = undefined;
113
+ const originalJson = res.json.bind(res);
114
+ const originalSend = res.send.bind(res);
115
+ res.json = function (body) {
116
+ responseBody = body;
117
+ return originalJson(body);
118
+ };
119
+ res.send = function (body) {
120
+ // Only capture if it looks like JSON
121
+ if (typeof body === 'object') {
122
+ responseBody = body;
29
123
  }
30
- // Fallback to req.apiGateway (for other packages)
31
- if (!authorizer) {
32
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
33
- const lambdaReq = req;
34
- if (lambdaReq.apiGateway?.event?.requestContext?.authorizer) {
35
- authorizer = lambdaReq.apiGateway.event.requestContext.authorizer;
36
- source = 'req.apiGateway';
124
+ else if (typeof body === 'string') {
125
+ try {
126
+ responseBody = JSON.parse(body);
127
+ }
128
+ catch {
129
+ // Not JSON, don't capture
37
130
  }
38
131
  }
39
- console.log(JSON.stringify({
40
- type: 'auth_debug',
41
- path: req.path,
42
- source,
43
- hasAuthorizer: !!authorizer,
44
- authorizerKeys: authorizer ? Object.keys(authorizer) : [],
45
- authorizer: authorizer,
46
- }));
47
- }
132
+ return originalSend(body);
133
+ };
48
134
  res.on('finish', () => {
49
135
  const duration = Date.now() - start;
50
- console.log(JSON.stringify({
136
+ // Determine log level based on status code
137
+ const isError = res.statusCode >= 400;
138
+ // Build response log
139
+ const logEntry = {
140
+ type: 'http_response',
141
+ requestId,
51
142
  method: req.method,
52
143
  path: req.path,
53
144
  statusCode: res.statusCode,
54
145
  duration,
55
- }));
146
+ caller: caller.userId ? { userId: caller.userId, tenantId: caller.tenantId } : undefined,
147
+ };
148
+ // Include response body for errors or when DEBUG_RESPONSE is set
149
+ if (isError || process.env.DEBUG_RESPONSE === 'true') {
150
+ logEntry.response = responseBody ? redactSensitive(responseBody) : undefined;
151
+ // Also include request body for errors to aid debugging
152
+ if (isError) {
153
+ logEntry.request = requestBody;
154
+ }
155
+ }
156
+ // Log with appropriate level
157
+ if (res.statusCode >= 500) {
158
+ console.error(JSON.stringify(logEntry));
159
+ }
160
+ else if (res.statusCode >= 400) {
161
+ console.warn(JSON.stringify(logEntry));
162
+ }
163
+ else {
164
+ console.log(JSON.stringify(logEntry));
165
+ }
56
166
  });
57
167
  next();
58
168
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@valentine-efagene/qshelter-common",
3
- "version": "2.0.148",
3
+ "version": "2.0.150",
4
4
  "description": "Shared database schemas and utilities for QShelter services",
5
5
  "main": "dist/src/index.js",
6
6
  "types": "dist/src/index.d.ts",