@noony-serverless/core 0.5.1 → 0.5.2

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,4 +1,28 @@
1
1
  import { BaseMiddleware, Context } from '../core';
2
+ export interface ErrorCategory {
3
+ type: string;
4
+ userMessage: string;
5
+ httpStatus: number;
6
+ retryable: boolean;
7
+ }
8
+ export interface ErrorMatcher {
9
+ /**
10
+ * Function to determine if this category applies to the error.
11
+ * Return true if the error matches this category.
12
+ */
13
+ matches: (error: Error) => boolean;
14
+ /**
15
+ * The category to return if the error matches.
16
+ */
17
+ category: ErrorCategory;
18
+ }
19
+ export interface ErrorHandlerOptions {
20
+ /**
21
+ * Custom error categories to check before default ones.
22
+ * These are evaluated in order.
23
+ */
24
+ customCategories?: ErrorMatcher[];
25
+ }
2
26
  /**
3
27
  * Middleware class for handling errors in the application.
4
28
  * Implements the `BaseMiddleware` interface and provides an asynchronous
@@ -9,12 +33,7 @@ import { BaseMiddleware, Context } from '../core';
9
33
  *
10
34
  * @remarks
11
35
  * This middleware should be registered to catch and process errors that occur
12
- * during request handling.
13
- *
14
- * @method onError
15
- * @param error - The error object that was thrown.
16
- * @param context - The context in which the error occurred.
17
- * @returns A promise that resolves when error handling is complete.
36
+ * during request handling. It supports custom error categorization via options.
18
37
  *
19
38
  * @example
20
39
  * Basic handler with error handling:
@@ -36,24 +55,32 @@ import { BaseMiddleware, Context } from '../core';
36
55
  * ```
37
56
  *
38
57
  * @example
39
- * Google Cloud Functions integration:
58
+ * Usage with custom categories:
40
59
  * ```typescript
41
- * import { http } from '@google-cloud/functions-framework';
42
60
  * import { Handler, ErrorHandlerMiddleware } from '@noony-serverless/core';
43
61
  *
44
- * const orderHandler = new Handler()
45
- * .use(new ErrorHandlerMiddleware())
46
- * .handle(async (request, context) => {
47
- * // Handler logic that might throw errors
48
- * return { success: true, data: processedOrder };
49
- * });
50
- *
51
- * export const processOrder = http('processOrder', (req, res) => {
52
- * return orderHandler.execute(req, res);
53
- * });
62
+ * const customCategories = [{
63
+ * matches: (err) => err.message.includes('payment_failed'),
64
+ * category: {
65
+ * type: 'PAYMENT_ERROR',
66
+ * userMessage: 'Payment processing failed',
67
+ * httpStatus: 402,
68
+ * retryable: false
69
+ * }
70
+ * }];
71
+ *
72
+ * const handler = new Handler()
73
+ * .use(new ErrorHandlerMiddleware({ customCategories }))
74
+ * .handle(...);
54
75
  * ```
55
76
  */
56
77
  export declare class ErrorHandlerMiddleware<TBody = unknown, TUser = unknown> implements BaseMiddleware<TBody, TUser> {
78
+ private options?;
79
+ /**
80
+ * Creates an instance of ErrorHandlerMiddleware.
81
+ * @param {ErrorHandlerOptions} [options] - Configuration options for the middleware, including custom error categories.
82
+ */
83
+ constructor(options?: ErrorHandlerOptions | undefined);
57
84
  onError(error: Error, context: Context<TBody, TUser>): Promise<void>;
58
85
  }
59
86
  /**
@@ -61,6 +88,7 @@ export declare class ErrorHandlerMiddleware<TBody = unknown, TUser = unknown> im
61
88
  *
62
89
  * @template TBody - The type of the request body payload (preserves type chain)
63
90
  * @template TUser - The type of the authenticated user (preserves type chain)
91
+ * @param {ErrorHandlerOptions} [options] - Configuration options for the middleware.
64
92
  * @returns {BaseMiddleware} An object implementing the `onError` method to handle errors.
65
93
  *
66
94
  * @remarks
@@ -75,18 +103,31 @@ export declare class ErrorHandlerMiddleware<TBody = unknown, TUser = unknown> im
75
103
  * const loginHandler = new Handler()
76
104
  * .use(errorHandler())
77
105
  * .handle(async (request, context) => {
78
- * const { username, password } = request.body || {};
79
- *
80
- * if (!username || !password) {
81
- * throw new HttpError(400, 'Credentials required', 'MISSING_CREDENTIALS');
82
- * }
83
- *
84
- * const user = await authenticateUser(username, password);
85
- * return { success: true, data: { token: generateToken(user) } };
106
+ * // ...
86
107
  * });
87
108
  * ```
88
109
  *
89
110
  * @example
111
+ * Usage with custom categories:
112
+ * ```typescript
113
+ * import { Handler, errorHandler } from '@noony-serverless/core';
114
+ *
115
+ * const customCategories = [{
116
+ * matches: (err) => err.message.includes('custom'),
117
+ * category: {
118
+ * type: 'CUSTOM_ERROR',
119
+ * userMessage: 'A custom error occurred',
120
+ * httpStatus: 400,
121
+ * retryable: false
122
+ * }
123
+ * }];
124
+ *
125
+ * const handler = new Handler()
126
+ * .use(errorHandler({ customCategories }))
127
+ * .handle(...)
128
+ * ```
129
+ *
130
+ * @example
90
131
  * Multiple middleware chain:
91
132
  * ```typescript
92
133
  * import { Handler, errorHandler, BodyParserMiddleware } from '@noony-serverless/core';
@@ -101,5 +142,5 @@ export declare class ErrorHandlerMiddleware<TBody = unknown, TUser = unknown> im
101
142
  * });
102
143
  * ```
103
144
  */
104
- export declare const errorHandler: <TBody = unknown, TUser = unknown>() => BaseMiddleware<TBody, TUser>;
145
+ export declare const errorHandler: <TBody = unknown, TUser = unknown>(options?: ErrorHandlerOptions) => BaseMiddleware<TBody, TUser>;
105
146
  //# sourceMappingURL=errorHandlerMiddleware.d.ts.map
@@ -2,6 +2,63 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.errorHandler = exports.ErrorHandlerMiddleware = void 0;
4
4
  const core_1 = require("../core");
5
+ // Optimization: Cache environment variables to avoid process.env access on every request
6
+ const IS_DEVELOPMENT = process.env.NODE_ENV === 'development' || process.env.DEBUG === 'true';
7
+ const DEBUG_API_RESPONSE = process.env.DEBUG_API_RESPONSE === 'true';
8
+ // Optimization: Pre-allocate static error categories to reduce garbage collection pressure
9
+ const DATABASE_ERROR = {
10
+ type: 'DATABASE_ERROR',
11
+ userMessage: 'Database temporarily unavailable. Please try again.',
12
+ httpStatus: 503,
13
+ retryable: true,
14
+ };
15
+ const TIMEOUT_ERROR = {
16
+ type: 'TIMEOUT_ERROR',
17
+ userMessage: 'Request took too long. Please try again.',
18
+ httpStatus: 504,
19
+ retryable: true,
20
+ };
21
+ const EXTERNAL_SERVICE_ERROR = {
22
+ type: 'EXTERNAL_SERVICE_ERROR',
23
+ userMessage: 'External service unavailable. Please try again later.',
24
+ httpStatus: 502,
25
+ retryable: true,
26
+ };
27
+ const INTERNAL_ERROR = {
28
+ type: 'INTERNAL_ERROR',
29
+ userMessage: 'An unexpected error occurred.',
30
+ httpStatus: 500,
31
+ retryable: false,
32
+ };
33
+ const categorizeError = (error, customMatchers) => {
34
+ // Check custom matchers first
35
+ if (customMatchers) {
36
+ for (const matcher of customMatchers) {
37
+ if (matcher.matches(error)) {
38
+ return matcher.category;
39
+ }
40
+ }
41
+ }
42
+ const message = error.message.toLowerCase();
43
+ // Database errors (MongoDB, Mongoose)
44
+ if (message.includes('mongodb') ||
45
+ message.includes('mongoose') ||
46
+ message.includes('buffering timed out')) {
47
+ return DATABASE_ERROR;
48
+ }
49
+ // Timeout errors
50
+ if (message.includes('timeout') || message.includes('timed out')) {
51
+ return TIMEOUT_ERROR;
52
+ }
53
+ // Network/external service errors
54
+ if (message.includes('econnrefused') ||
55
+ message.includes('network') ||
56
+ message.includes('fetch failed')) {
57
+ return EXTERNAL_SERVICE_ERROR;
58
+ }
59
+ // Default: generic server error
60
+ return INTERNAL_ERROR;
61
+ };
5
62
  /**
6
63
  * Handles errors thrown during request processing and sends an appropriate JSON response.
7
64
  *
@@ -15,25 +72,33 @@ const core_1 = require("../core");
15
72
  * @param context - The request context containing request and response objects.
16
73
  * @returns A promise that resolves when the error response has been sent.
17
74
  */
18
- const handleError = async (error, context) => {
19
- const isDevelopment = process.env.NODE_ENV === 'development' || process.env.DEBUG === 'true';
75
+ const handleError = async (error, context, options) => {
76
+ // Safe property collection for logging
77
+ const errorType = error?.constructor?.name;
78
+ const errorMessage = error?.message;
20
79
  core_1.logger.error('Error processing request', {
21
- errorMessage: error?.message,
80
+ errorType,
81
+ errorMessage,
22
82
  errorStack: error?.stack,
83
+ errorCode: error?.code,
84
+ httpStatus: error?.status,
23
85
  requestId: context.requestId,
86
+ url: context.req.url,
87
+ method: context.req.method,
24
88
  userAgent: context.req.headers?.['user-agent'],
25
89
  ip: context.req.ip || 'unknown',
26
90
  });
91
+ const timestamp = new Date().toISOString();
27
92
  if (error instanceof core_1.HttpError) {
28
93
  const responsePayload = {
29
94
  success: false,
30
95
  payload: {
31
96
  error: error.message,
32
97
  },
33
- timestamp: new Date().toISOString(),
98
+ timestamp,
34
99
  };
35
100
  // Only include sensitive details in development
36
- if (isDevelopment && error.details) {
101
+ if (IS_DEVELOPMENT && error.details) {
37
102
  responsePayload.payload.details = error.details;
38
103
  }
39
104
  // Only include error codes for client errors (4xx), not server errors
@@ -41,26 +106,33 @@ const handleError = async (error, context) => {
41
106
  responsePayload.payload.code = error.code;
42
107
  }
43
108
  context.res.status(error.status).json(responsePayload);
109
+ return;
44
110
  }
45
- else {
46
- // For non-HttpError exceptions, provide generic error message in production
47
- const errorMessage = isDevelopment
48
- ? error.message
49
- : 'Internal Server Error';
50
- const responsePayload = {
51
- error: 'Internal Server Error',
52
- success: false,
53
- payload: {
54
- error: errorMessage,
55
- },
56
- timestamp: new Date().toISOString(),
111
+ // Categorize the error for appropriate response
112
+ const category = categorizeError(error, options?.customCategories);
113
+ const responsePayload = {
114
+ error: category.userMessage,
115
+ success: false,
116
+ payload: {
117
+ error: category.userMessage,
118
+ code: category.type,
119
+ },
120
+ timestamp,
121
+ };
122
+ // Add retryable hint for client
123
+ if (category.retryable) {
124
+ responsePayload.payload.retryable = true;
125
+ context.res.header('Retry-After', '5');
126
+ }
127
+ // Include full error details when DEBUG_API_RESPONSE is enabled (dev/local)
128
+ if (DEBUG_API_RESPONSE) {
129
+ responsePayload.payload.debug = {
130
+ originalError: error.message,
131
+ stack: error.stack,
132
+ errorType: error.constructor?.name,
57
133
  };
58
- // Add stack trace in development for non-HTTP errors
59
- if (isDevelopment && error.stack) {
60
- responsePayload.payload.stack = error.stack;
61
- }
62
- context.res.status(500).json(responsePayload);
63
134
  }
135
+ context.res.status(category.httpStatus).json(responsePayload);
64
136
  };
65
137
  /**
66
138
  * Middleware class for handling errors in the application.
@@ -72,12 +144,7 @@ const handleError = async (error, context) => {
72
144
  *
73
145
  * @remarks
74
146
  * This middleware should be registered to catch and process errors that occur
75
- * during request handling.
76
- *
77
- * @method onError
78
- * @param error - The error object that was thrown.
79
- * @param context - The context in which the error occurred.
80
- * @returns A promise that resolves when error handling is complete.
147
+ * during request handling. It supports custom error categorization via options.
81
148
  *
82
149
  * @example
83
150
  * Basic handler with error handling:
@@ -99,26 +166,36 @@ const handleError = async (error, context) => {
99
166
  * ```
100
167
  *
101
168
  * @example
102
- * Google Cloud Functions integration:
169
+ * Usage with custom categories:
103
170
  * ```typescript
104
- * import { http } from '@google-cloud/functions-framework';
105
171
  * import { Handler, ErrorHandlerMiddleware } from '@noony-serverless/core';
106
172
  *
107
- * const orderHandler = new Handler()
108
- * .use(new ErrorHandlerMiddleware())
109
- * .handle(async (request, context) => {
110
- * // Handler logic that might throw errors
111
- * return { success: true, data: processedOrder };
112
- * });
173
+ * const customCategories = [{
174
+ * matches: (err) => err.message.includes('payment_failed'),
175
+ * category: {
176
+ * type: 'PAYMENT_ERROR',
177
+ * userMessage: 'Payment processing failed',
178
+ * httpStatus: 402,
179
+ * retryable: false
180
+ * }
181
+ * }];
113
182
  *
114
- * export const processOrder = http('processOrder', (req, res) => {
115
- * return orderHandler.execute(req, res);
116
- * });
183
+ * const handler = new Handler()
184
+ * .use(new ErrorHandlerMiddleware({ customCategories }))
185
+ * .handle(...);
117
186
  * ```
118
187
  */
119
188
  class ErrorHandlerMiddleware {
189
+ options;
190
+ /**
191
+ * Creates an instance of ErrorHandlerMiddleware.
192
+ * @param {ErrorHandlerOptions} [options] - Configuration options for the middleware, including custom error categories.
193
+ */
194
+ constructor(options) {
195
+ this.options = options;
196
+ }
120
197
  async onError(error, context) {
121
- await handleError(error, context);
198
+ await handleError(error, context, this.options);
122
199
  }
123
200
  }
124
201
  exports.ErrorHandlerMiddleware = ErrorHandlerMiddleware;
@@ -127,6 +204,7 @@ exports.ErrorHandlerMiddleware = ErrorHandlerMiddleware;
127
204
  *
128
205
  * @template TBody - The type of the request body payload (preserves type chain)
129
206
  * @template TUser - The type of the authenticated user (preserves type chain)
207
+ * @param {ErrorHandlerOptions} [options] - Configuration options for the middleware.
130
208
  * @returns {BaseMiddleware} An object implementing the `onError` method to handle errors.
131
209
  *
132
210
  * @remarks
@@ -141,15 +219,28 @@ exports.ErrorHandlerMiddleware = ErrorHandlerMiddleware;
141
219
  * const loginHandler = new Handler()
142
220
  * .use(errorHandler())
143
221
  * .handle(async (request, context) => {
144
- * const { username, password } = request.body || {};
222
+ * // ...
223
+ * });
224
+ * ```
145
225
  *
146
- * if (!username || !password) {
147
- * throw new HttpError(400, 'Credentials required', 'MISSING_CREDENTIALS');
148
- * }
226
+ * @example
227
+ * Usage with custom categories:
228
+ * ```typescript
229
+ * import { Handler, errorHandler } from '@noony-serverless/core';
149
230
  *
150
- * const user = await authenticateUser(username, password);
151
- * return { success: true, data: { token: generateToken(user) } };
152
- * });
231
+ * const customCategories = [{
232
+ * matches: (err) => err.message.includes('custom'),
233
+ * category: {
234
+ * type: 'CUSTOM_ERROR',
235
+ * userMessage: 'A custom error occurred',
236
+ * httpStatus: 400,
237
+ * retryable: false
238
+ * }
239
+ * }];
240
+ *
241
+ * const handler = new Handler()
242
+ * .use(errorHandler({ customCategories }))
243
+ * .handle(...)
153
244
  * ```
154
245
  *
155
246
  * @example
@@ -167,9 +258,9 @@ exports.ErrorHandlerMiddleware = ErrorHandlerMiddleware;
167
258
  * });
168
259
  * ```
169
260
  */
170
- const errorHandler = () => ({
261
+ const errorHandler = (options) => ({
171
262
  onError: async (error, context) => {
172
- await handleError(error, context);
263
+ await handleError(error, context, options);
173
264
  },
174
265
  });
175
266
  exports.errorHandler = errorHandler;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noony-serverless/core",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "description": "A Middy base framework compatible with Firebase and GCP Cloud Functions with TypeScript",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",