@noony-serverless/core 0.3.4 → 0.4.1

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 (53) hide show
  1. package/README.md +199 -0
  2. package/build/core/containerPool.d.ts +129 -26
  3. package/build/core/containerPool.js +213 -68
  4. package/build/core/handler.d.ts +2 -2
  5. package/build/core/handler.js +6 -12
  6. package/build/core/index.d.ts +1 -0
  7. package/build/core/index.js +1 -0
  8. package/build/core/logger.d.ts +89 -1
  9. package/build/core/logger.js +136 -5
  10. package/build/core/telemetry/config.d.ts +331 -0
  11. package/build/core/telemetry/config.js +153 -0
  12. package/build/core/telemetry/index.d.ts +22 -0
  13. package/build/core/telemetry/index.js +45 -0
  14. package/build/core/telemetry/provider.d.ts +203 -0
  15. package/build/core/telemetry/provider.js +3 -0
  16. package/build/core/telemetry/providers/console-provider.d.ts +54 -0
  17. package/build/core/telemetry/providers/console-provider.js +124 -0
  18. package/build/core/telemetry/providers/index.d.ts +10 -0
  19. package/build/core/telemetry/providers/index.js +19 -0
  20. package/build/core/telemetry/providers/noop-provider.d.ts +51 -0
  21. package/build/core/telemetry/providers/noop-provider.js +67 -0
  22. package/build/core/telemetry/providers/opentelemetry-provider.d.ts +102 -0
  23. package/build/core/telemetry/providers/opentelemetry-provider.js +342 -0
  24. package/build/middlewares/bodyValidationMiddleware.js +1 -1
  25. package/build/middlewares/dependencyInjectionMiddleware.d.ts +16 -8
  26. package/build/middlewares/dependencyInjectionMiddleware.js +31 -11
  27. package/build/middlewares/guards/adapters/CustomTokenVerificationPortAdapter.d.ts +1 -1
  28. package/build/middlewares/guards/guards/FastAuthGuard.d.ts +5 -5
  29. package/build/middlewares/guards/guards/FastAuthGuard.js +3 -2
  30. package/build/middlewares/guards/guards/PermissionGuardFactory.d.ts +7 -9
  31. package/build/middlewares/guards/resolvers/ExpressionPermissionResolver.d.ts +1 -1
  32. package/build/middlewares/guards/resolvers/ExpressionPermissionResolver.js +1 -1
  33. package/build/middlewares/guards/resolvers/PermissionResolver.d.ts +1 -1
  34. package/build/middlewares/guards/resolvers/PlainPermissionResolver.d.ts +1 -1
  35. package/build/middlewares/guards/resolvers/WildcardPermissionResolver.d.ts +1 -1
  36. package/build/middlewares/guards/services/FastUserContextService.d.ts +11 -32
  37. package/build/middlewares/index.d.ts +1 -0
  38. package/build/middlewares/index.js +1 -0
  39. package/build/middlewares/openTelemetryMiddleware.d.ts +162 -0
  40. package/build/middlewares/openTelemetryMiddleware.js +359 -0
  41. package/build/middlewares/rateLimitingMiddleware.js +16 -5
  42. package/build/utils/container.utils.js +4 -1
  43. package/build/utils/fastify-wrapper.d.ts +74 -0
  44. package/build/utils/fastify-wrapper.js +175 -0
  45. package/build/utils/index.d.ts +4 -0
  46. package/build/utils/index.js +23 -1
  47. package/build/utils/otel.helper.d.ts +122 -0
  48. package/build/utils/otel.helper.js +258 -0
  49. package/build/utils/pubsub-trace.utils.d.ts +102 -0
  50. package/build/utils/pubsub-trace.utils.js +155 -0
  51. package/build/utils/wrapper-utils.d.ts +177 -0
  52. package/build/utils/wrapper-utils.js +236 -0
  53. package/package.json +61 -2
@@ -0,0 +1,102 @@
1
+ import { Context } from '../core';
2
+ /**
3
+ * Google Cloud Pub/Sub message structure
4
+ */
5
+ export interface PubSubMessage {
6
+ message: {
7
+ data: string;
8
+ publishTime?: string;
9
+ messageId?: string;
10
+ attributes?: Record<string, string>;
11
+ };
12
+ }
13
+ /**
14
+ * W3C Trace Context extracted from Pub/Sub message attributes
15
+ */
16
+ export interface TraceContext {
17
+ traceparent?: string;
18
+ tracestate?: string;
19
+ }
20
+ /**
21
+ * Type guard to check if request body is a Pub/Sub message
22
+ *
23
+ * @param body - Request body to check
24
+ * @returns True if body is a Pub/Sub message
25
+ *
26
+ * @example
27
+ * if (isPubSubMessage(context.req.body)) {
28
+ * const traceContext = extractTraceContext(context.req.body);
29
+ * }
30
+ */
31
+ export declare function isPubSubMessage(body: unknown): body is PubSubMessage;
32
+ /**
33
+ * Extract W3C Trace Context from Pub/Sub message attributes
34
+ *
35
+ * Extracts the following headers from message.attributes:
36
+ * - `traceparent`: W3C Trace Context propagation header (required)
37
+ * - `tracestate`: Vendor-specific trace state (optional)
38
+ *
39
+ * @param message - Pub/Sub message
40
+ * @returns Trace context object with traceparent and tracestate
41
+ *
42
+ * @example
43
+ * const traceContext = extractTraceContext(pubsubMessage);
44
+ * if (traceContext.traceparent) {
45
+ * console.log('Parent trace ID:', traceContext.traceparent);
46
+ * }
47
+ */
48
+ export declare function extractTraceContext(message: PubSubMessage): TraceContext;
49
+ /**
50
+ * Inject W3C Trace Context into Pub/Sub message attributes
51
+ *
52
+ * Adds trace context from the current OpenTelemetry span to message attributes.
53
+ * This enables distributed tracing across Pub/Sub publishers and subscribers.
54
+ *
55
+ * The trace context is extracted using OpenTelemetry's propagation API and
56
+ * injected into the message attributes as:
57
+ * - `traceparent`: W3C Trace Context version-traceid-spanid-flags
58
+ * - `tracestate`: Vendor-specific trace state (if present)
59
+ *
60
+ * @param message - Pub/Sub message to inject trace context into
61
+ * @param context - Noony request context (optional, used to get active span)
62
+ * @returns Message with trace context injected into attributes
63
+ *
64
+ * @example
65
+ * // Publishing a message with trace context
66
+ * import { injectTraceContext } from '@noony-serverless/core';
67
+ *
68
+ * const message = {
69
+ * data: Buffer.from(JSON.stringify({ userId: '123' })).toString('base64'),
70
+ * attributes: {
71
+ * type: 'user.created'
72
+ * }
73
+ * };
74
+ *
75
+ * const tracedMessage = injectTraceContext(message, context);
76
+ * await pubsub.topic('users').publish(tracedMessage);
77
+ *
78
+ * @example
79
+ * // Publishing without context (uses active span from context)
80
+ * const tracedMessage = injectTraceContext(message);
81
+ * await pubsub.topic('users').publish(tracedMessage);
82
+ */
83
+ export declare function injectTraceContext(message: {
84
+ data: string;
85
+ attributes?: Record<string, string>;
86
+ }, context?: Context<unknown, unknown>): {
87
+ data: string;
88
+ attributes: Record<string, string>;
89
+ };
90
+ /**
91
+ * Extract trace context and create parent context for OpenTelemetry
92
+ *
93
+ * This is a lower-level utility used internally by OpenTelemetryMiddleware.
94
+ * Most users should use the middleware's automatic trace propagation instead.
95
+ *
96
+ * @param traceContext - Extracted W3C trace context
97
+ * @returns OpenTelemetry context with extracted trace context
98
+ *
99
+ * @internal
100
+ */
101
+ export declare function createParentContext(traceContext: TraceContext): Record<string, string>;
102
+ //# sourceMappingURL=pubsub-trace.utils.d.ts.map
@@ -0,0 +1,155 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isPubSubMessage = isPubSubMessage;
4
+ exports.extractTraceContext = extractTraceContext;
5
+ exports.injectTraceContext = injectTraceContext;
6
+ exports.createParentContext = createParentContext;
7
+ /**
8
+ * Type guard to check if request body is a Pub/Sub message
9
+ *
10
+ * @param body - Request body to check
11
+ * @returns True if body is a Pub/Sub message
12
+ *
13
+ * @example
14
+ * if (isPubSubMessage(context.req.body)) {
15
+ * const traceContext = extractTraceContext(context.req.body);
16
+ * }
17
+ */
18
+ function isPubSubMessage(body) {
19
+ return (!!body &&
20
+ typeof body === 'object' &&
21
+ 'message' in body &&
22
+ typeof body.message === 'object' &&
23
+ 'data' in body.message);
24
+ }
25
+ /**
26
+ * Extract W3C Trace Context from Pub/Sub message attributes
27
+ *
28
+ * Extracts the following headers from message.attributes:
29
+ * - `traceparent`: W3C Trace Context propagation header (required)
30
+ * - `tracestate`: Vendor-specific trace state (optional)
31
+ *
32
+ * @param message - Pub/Sub message
33
+ * @returns Trace context object with traceparent and tracestate
34
+ *
35
+ * @example
36
+ * const traceContext = extractTraceContext(pubsubMessage);
37
+ * if (traceContext.traceparent) {
38
+ * console.log('Parent trace ID:', traceContext.traceparent);
39
+ * }
40
+ */
41
+ function extractTraceContext(message) {
42
+ const attributes = message.message.attributes || {};
43
+ return {
44
+ traceparent: attributes.traceparent,
45
+ tracestate: attributes.tracestate,
46
+ };
47
+ }
48
+ /**
49
+ * Inject W3C Trace Context into Pub/Sub message attributes
50
+ *
51
+ * Adds trace context from the current OpenTelemetry span to message attributes.
52
+ * This enables distributed tracing across Pub/Sub publishers and subscribers.
53
+ *
54
+ * The trace context is extracted using OpenTelemetry's propagation API and
55
+ * injected into the message attributes as:
56
+ * - `traceparent`: W3C Trace Context version-traceid-spanid-flags
57
+ * - `tracestate`: Vendor-specific trace state (if present)
58
+ *
59
+ * @param message - Pub/Sub message to inject trace context into
60
+ * @param context - Noony request context (optional, used to get active span)
61
+ * @returns Message with trace context injected into attributes
62
+ *
63
+ * @example
64
+ * // Publishing a message with trace context
65
+ * import { injectTraceContext } from '@noony-serverless/core';
66
+ *
67
+ * const message = {
68
+ * data: Buffer.from(JSON.stringify({ userId: '123' })).toString('base64'),
69
+ * attributes: {
70
+ * type: 'user.created'
71
+ * }
72
+ * };
73
+ *
74
+ * const tracedMessage = injectTraceContext(message, context);
75
+ * await pubsub.topic('users').publish(tracedMessage);
76
+ *
77
+ * @example
78
+ * // Publishing without context (uses active span from context)
79
+ * const tracedMessage = injectTraceContext(message);
80
+ * await pubsub.topic('users').publish(tracedMessage);
81
+ */
82
+ function injectTraceContext(message, context) {
83
+ // Initialize attributes if not present
84
+ const attributes = message.attributes || {};
85
+ try {
86
+ // Try to use OpenTelemetry API if available
87
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
88
+ const otelApi = require('@opentelemetry/api');
89
+ const { trace, propagation, context: otelContext } = otelApi;
90
+ // Get current context (either from provided context or active context)
91
+ let activeContext = otelContext.active();
92
+ // If Noony context provided, try to get span from businessData
93
+ if (context) {
94
+ const span = context.businessData.get('otel_span');
95
+ if (span && typeof span === 'object' && 'spanContext' in span) {
96
+ // Create context with span
97
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
98
+ activeContext = trace.setSpan(activeContext, span);
99
+ }
100
+ }
101
+ // Get active span if no span found in businessData
102
+ const activeSpan = trace.getSpan(activeContext);
103
+ if (!activeSpan) {
104
+ // No active span, return message without trace context
105
+ return {
106
+ data: message.data,
107
+ attributes,
108
+ };
109
+ }
110
+ // Inject trace context into a carrier object
111
+ const carrier = {};
112
+ propagation.inject(activeContext, carrier);
113
+ // Merge trace context into message attributes
114
+ return {
115
+ data: message.data,
116
+ attributes: {
117
+ ...attributes,
118
+ ...carrier, // This adds traceparent and tracestate
119
+ },
120
+ };
121
+ }
122
+ catch (error) {
123
+ // OpenTelemetry not available or error during injection
124
+ // Return message without trace context (fail gracefully)
125
+ console.warn('[Telemetry] Failed to inject trace context:', error);
126
+ return {
127
+ data: message.data,
128
+ attributes,
129
+ };
130
+ }
131
+ }
132
+ /**
133
+ * Extract trace context and create parent context for OpenTelemetry
134
+ *
135
+ * This is a lower-level utility used internally by OpenTelemetryMiddleware.
136
+ * Most users should use the middleware's automatic trace propagation instead.
137
+ *
138
+ * @param traceContext - Extracted W3C trace context
139
+ * @returns OpenTelemetry context with extracted trace context
140
+ *
141
+ * @internal
142
+ */
143
+ function createParentContext(traceContext) {
144
+ if (!traceContext.traceparent) {
145
+ return {};
146
+ }
147
+ const carrier = {
148
+ traceparent: traceContext.traceparent,
149
+ };
150
+ if (traceContext.tracestate) {
151
+ carrier.tracestate = traceContext.tracestate;
152
+ }
153
+ return carrier;
154
+ }
155
+ //# sourceMappingURL=pubsub-trace.utils.js.map
@@ -0,0 +1,177 @@
1
+ import type { HttpFunction } from '@google-cloud/functions-framework';
2
+ import type { Request, Response } from 'express';
3
+ import { Handler } from '../core/handler';
4
+ /**
5
+ * Create an HttpFunction wrapper for a Noony handler
6
+ *
7
+ * Wraps a Noony handler into a Google Cloud Functions `HttpFunction` for production deployment.
8
+ * This pattern ensures proper initialization, error handling, and prevents double responses.
9
+ *
10
+ * @param noonyHandler - The Noony handler to wrap (contains middleware chain and controller)
11
+ * @param functionName - Name for error logging purposes
12
+ * @param initializeDependencies - Async function that initializes dependencies (database, services, etc.)
13
+ * Uses singleton pattern to prevent re-initialization on warm starts
14
+ * @returns HttpFunction compatible with `@google-cloud/functions-framework`
15
+ *
16
+ * @remarks
17
+ * This function ensures:
18
+ * - Dependencies are initialized before handler execution (optimized for cold/warm starts)
19
+ * - Errors are caught and returned as proper HTTP responses
20
+ * - Response is not sent twice (`headersSent` check)
21
+ * - `RESPONSE_SENT` errors are ignored (response already sent by middleware)
22
+ * - Real errors return 500 with generic message for security
23
+ *
24
+ * @example
25
+ * Creating and registering Cloud Functions:
26
+ * ```typescript
27
+ * import { http } from '@google-cloud/functions-framework';
28
+ * import { createHttpFunction } from '@noony-serverless/core';
29
+ * import { loginHandler } from './handlers/auth.handlers';
30
+ *
31
+ * // Initialize dependencies once per cold start
32
+ * let initialized = false;
33
+ * async function initializeDependencies(): Promise<void> {
34
+ * if (initialized) return;
35
+ * const db = await databaseService.connect();
36
+ * await initializeServices(db);
37
+ * initialized = true;
38
+ * }
39
+ *
40
+ * // Create and register function
41
+ * const loginFunction = createHttpFunction(
42
+ * loginHandler,
43
+ * 'login',
44
+ * initializeDependencies
45
+ * );
46
+ * http('login', loginFunction);
47
+ * export const login = loginFunction;
48
+ * ```
49
+ *
50
+ * @example
51
+ * Execution flow:
52
+ * ```
53
+ * HTTP Request → createHttpFunction wrapper
54
+ * │
55
+ * ▼
56
+ * initializeDependencies() (only on cold start)
57
+ * │
58
+ * ▼
59
+ * noonyHandler.execute(req, res)
60
+ * │
61
+ * ├─── errorHandler()
62
+ * ├─── authMiddleware()
63
+ * ├─── requirePermission()
64
+ * ├─── bodyValidator()
65
+ * ├─── ResponseWrapperMiddleware
66
+ * └─── Controller function
67
+ * │
68
+ * ▼
69
+ * HTTP Response
70
+ * ```
71
+ *
72
+ * @see {@link wrapNoonyHandler} for Express integration (local development)
73
+ */
74
+ export declare function createHttpFunction(noonyHandler: Handler<unknown>, functionName: string, initializeDependencies: () => Promise<void>): HttpFunction;
75
+ /**
76
+ * Wrap a Noony handler for use with Express routing
77
+ *
78
+ * Wraps a Noony handler into an Express route handler for local development environments.
79
+ * This pattern enables running all endpoints through a single Express app with standard
80
+ * Express routing, middleware, and error handling.
81
+ *
82
+ * @param noonyHandler - The Noony handler to wrap (contains middleware chain and controller)
83
+ * @param functionName - Name for error logging purposes
84
+ * @param initializeDependencies - Async function that initializes dependencies (database, services, etc.)
85
+ * Uses singleton pattern to prevent re-initialization across requests
86
+ * @returns Express route handler compatible with Express Router: `(req: Request, res: Response) => Promise<void>`
87
+ *
88
+ * @remarks
89
+ * This wrapper ensures:
90
+ * - Dependencies are initialized before handler execution (singleton pattern for efficiency)
91
+ * - Noony handlers work seamlessly with Express routing and middleware
92
+ * - Errors are caught and returned as proper HTTP responses
93
+ * - Response is not sent twice (`headersSent` check)
94
+ * - `RESPONSE_SENT` errors are ignored (response already sent by middleware)
95
+ * - Real errors return 500 with generic message for security
96
+ *
97
+ * **Differences from createHttpFunction:**
98
+ *
99
+ * | Aspect | createHttpFunction | wrapNoonyHandler |
100
+ * |--------|-------------------|------------------|
101
+ * | **Use case** | Production deployment | Local development |
102
+ * | **Framework** | Cloud Functions Framework | Express |
103
+ * | **Return type** | `HttpFunction` | Express handler |
104
+ * | **Registration** | `http('name', fn)` | `app.get('/path', fn)` |
105
+ * | **Deployment** | Individual functions | Single Express app |
106
+ *
107
+ * @example
108
+ * Creating Express app with multiple routes:
109
+ * ```typescript
110
+ * import express, { Express } from 'express';
111
+ * import { wrapNoonyHandler } from '@noony-serverless/core';
112
+ * import { loginHandler, logoutHandler, getConfigHandler } from './handlers';
113
+ *
114
+ * // Initialize dependencies once per app startup
115
+ * let initialized = false;
116
+ * async function initializeDependencies(): Promise<void> {
117
+ * if (initialized) return;
118
+ * const db = await databaseService.connect();
119
+ * await initializeServices(db);
120
+ * initialized = true;
121
+ * }
122
+ *
123
+ * function createExpressApp(): Express {
124
+ * const app = express();
125
+ *
126
+ * // Global Express middleware
127
+ * app.use(cors());
128
+ * app.use(express.json());
129
+ *
130
+ * // Health check (no DB required)
131
+ * app.get('/health', (_req, res) => {
132
+ * res.json({ success: true, data: { status: 'healthy' } });
133
+ * });
134
+ *
135
+ * // Auth routes
136
+ * app.post('/api/auth/login', wrapNoonyHandler(loginHandler, 'login', initializeDependencies));
137
+ * app.post('/api/auth/logout', wrapNoonyHandler(logoutHandler, 'logout', initializeDependencies));
138
+ *
139
+ * // Config routes
140
+ * app.get('/api/config', wrapNoonyHandler(getConfigHandler, 'getConfig', initializeDependencies));
141
+ *
142
+ * // 404 handler
143
+ * app.use((_req, res) => {
144
+ * res.status(404).json({
145
+ * success: false,
146
+ * error: { code: 'NOT_FOUND', message: 'Endpoint not found' }
147
+ * });
148
+ * });
149
+ *
150
+ * return app;
151
+ * }
152
+ *
153
+ * // Start server
154
+ * const app = createExpressApp();
155
+ * const PORT = process.env.PORT || 3000;
156
+ * app.listen(PORT, () => {
157
+ * console.log(`Server running on port ${PORT}`);
158
+ * });
159
+ * ```
160
+ *
161
+ * @example
162
+ * Express routing with path parameters:
163
+ * ```typescript
164
+ * const app = express();
165
+ *
166
+ * // Routes with path parameters work seamlessly
167
+ * app.get('/api/users/:userId', wrapNoonyHandler(getUserHandler, 'getUser', initializeDependencies));
168
+ * app.patch('/api/config/sections/:sectionId', wrapNoonyHandler(updateSectionHandler, 'updateSection', initializeDependencies));
169
+ * app.delete('/api/config/sections/:sectionId', wrapNoonyHandler(deleteSectionHandler, 'deleteSection', initializeDependencies));
170
+ *
171
+ * // Path parameters available in Noony handler via context.req.params
172
+ * ```
173
+ *
174
+ * @see {@link createHttpFunction} for Cloud Functions Framework integration (production deployment)
175
+ */
176
+ export declare function wrapNoonyHandler(noonyHandler: Handler<unknown>, functionName: string, initializeDependencies: () => Promise<void>): (req: Request, res: Response) => Promise<void>;
177
+ //# sourceMappingURL=wrapper-utils.d.ts.map
@@ -0,0 +1,236 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createHttpFunction = createHttpFunction;
4
+ exports.wrapNoonyHandler = wrapNoonyHandler;
5
+ const logger_1 = require("../core/logger");
6
+ /**
7
+ * Create an HttpFunction wrapper for a Noony handler
8
+ *
9
+ * Wraps a Noony handler into a Google Cloud Functions `HttpFunction` for production deployment.
10
+ * This pattern ensures proper initialization, error handling, and prevents double responses.
11
+ *
12
+ * @param noonyHandler - The Noony handler to wrap (contains middleware chain and controller)
13
+ * @param functionName - Name for error logging purposes
14
+ * @param initializeDependencies - Async function that initializes dependencies (database, services, etc.)
15
+ * Uses singleton pattern to prevent re-initialization on warm starts
16
+ * @returns HttpFunction compatible with `@google-cloud/functions-framework`
17
+ *
18
+ * @remarks
19
+ * This function ensures:
20
+ * - Dependencies are initialized before handler execution (optimized for cold/warm starts)
21
+ * - Errors are caught and returned as proper HTTP responses
22
+ * - Response is not sent twice (`headersSent` check)
23
+ * - `RESPONSE_SENT` errors are ignored (response already sent by middleware)
24
+ * - Real errors return 500 with generic message for security
25
+ *
26
+ * @example
27
+ * Creating and registering Cloud Functions:
28
+ * ```typescript
29
+ * import { http } from '@google-cloud/functions-framework';
30
+ * import { createHttpFunction } from '@noony-serverless/core';
31
+ * import { loginHandler } from './handlers/auth.handlers';
32
+ *
33
+ * // Initialize dependencies once per cold start
34
+ * let initialized = false;
35
+ * async function initializeDependencies(): Promise<void> {
36
+ * if (initialized) return;
37
+ * const db = await databaseService.connect();
38
+ * await initializeServices(db);
39
+ * initialized = true;
40
+ * }
41
+ *
42
+ * // Create and register function
43
+ * const loginFunction = createHttpFunction(
44
+ * loginHandler,
45
+ * 'login',
46
+ * initializeDependencies
47
+ * );
48
+ * http('login', loginFunction);
49
+ * export const login = loginFunction;
50
+ * ```
51
+ *
52
+ * @example
53
+ * Execution flow:
54
+ * ```
55
+ * HTTP Request → createHttpFunction wrapper
56
+ * │
57
+ * ▼
58
+ * initializeDependencies() (only on cold start)
59
+ * │
60
+ * ▼
61
+ * noonyHandler.execute(req, res)
62
+ * │
63
+ * ├─── errorHandler()
64
+ * ├─── authMiddleware()
65
+ * ├─── requirePermission()
66
+ * ├─── bodyValidator()
67
+ * ├─── ResponseWrapperMiddleware
68
+ * └─── Controller function
69
+ * │
70
+ * ▼
71
+ * HTTP Response
72
+ * ```
73
+ *
74
+ * @see {@link wrapNoonyHandler} for Express integration (local development)
75
+ */
76
+ function createHttpFunction(noonyHandler, functionName, initializeDependencies) {
77
+ return async (req, res) => {
78
+ try {
79
+ // Ensure dependencies are initialized
80
+ await initializeDependencies();
81
+ // Execute Noony handler (runs middleware chain + controller)
82
+ // Cast is safe because Handler.execute internally adapts GCP Request/Response
83
+ await noonyHandler.execute(req, res);
84
+ }
85
+ catch (error) {
86
+ // Only handle errors if they're real errors (not RESPONSE_SENT markers)
87
+ if (error instanceof Error && error.message === 'RESPONSE_SENT') {
88
+ return;
89
+ }
90
+ logger_1.logger.error(`${functionName} function error`, {
91
+ error: error instanceof Error ? error.message : 'Unknown error',
92
+ stack: error instanceof Error ? error.stack : undefined,
93
+ });
94
+ // Graceful error handling - only send if headers not already sent
95
+ if (!res.headersSent) {
96
+ res.status(500).json({
97
+ success: false,
98
+ error: {
99
+ code: 'INTERNAL_SERVER_ERROR',
100
+ message: 'An unexpected error occurred',
101
+ },
102
+ });
103
+ }
104
+ }
105
+ };
106
+ }
107
+ /**
108
+ * Wrap a Noony handler for use with Express routing
109
+ *
110
+ * Wraps a Noony handler into an Express route handler for local development environments.
111
+ * This pattern enables running all endpoints through a single Express app with standard
112
+ * Express routing, middleware, and error handling.
113
+ *
114
+ * @param noonyHandler - The Noony handler to wrap (contains middleware chain and controller)
115
+ * @param functionName - Name for error logging purposes
116
+ * @param initializeDependencies - Async function that initializes dependencies (database, services, etc.)
117
+ * Uses singleton pattern to prevent re-initialization across requests
118
+ * @returns Express route handler compatible with Express Router: `(req: Request, res: Response) => Promise<void>`
119
+ *
120
+ * @remarks
121
+ * This wrapper ensures:
122
+ * - Dependencies are initialized before handler execution (singleton pattern for efficiency)
123
+ * - Noony handlers work seamlessly with Express routing and middleware
124
+ * - Errors are caught and returned as proper HTTP responses
125
+ * - Response is not sent twice (`headersSent` check)
126
+ * - `RESPONSE_SENT` errors are ignored (response already sent by middleware)
127
+ * - Real errors return 500 with generic message for security
128
+ *
129
+ * **Differences from createHttpFunction:**
130
+ *
131
+ * | Aspect | createHttpFunction | wrapNoonyHandler |
132
+ * |--------|-------------------|------------------|
133
+ * | **Use case** | Production deployment | Local development |
134
+ * | **Framework** | Cloud Functions Framework | Express |
135
+ * | **Return type** | `HttpFunction` | Express handler |
136
+ * | **Registration** | `http('name', fn)` | `app.get('/path', fn)` |
137
+ * | **Deployment** | Individual functions | Single Express app |
138
+ *
139
+ * @example
140
+ * Creating Express app with multiple routes:
141
+ * ```typescript
142
+ * import express, { Express } from 'express';
143
+ * import { wrapNoonyHandler } from '@noony-serverless/core';
144
+ * import { loginHandler, logoutHandler, getConfigHandler } from './handlers';
145
+ *
146
+ * // Initialize dependencies once per app startup
147
+ * let initialized = false;
148
+ * async function initializeDependencies(): Promise<void> {
149
+ * if (initialized) return;
150
+ * const db = await databaseService.connect();
151
+ * await initializeServices(db);
152
+ * initialized = true;
153
+ * }
154
+ *
155
+ * function createExpressApp(): Express {
156
+ * const app = express();
157
+ *
158
+ * // Global Express middleware
159
+ * app.use(cors());
160
+ * app.use(express.json());
161
+ *
162
+ * // Health check (no DB required)
163
+ * app.get('/health', (_req, res) => {
164
+ * res.json({ success: true, data: { status: 'healthy' } });
165
+ * });
166
+ *
167
+ * // Auth routes
168
+ * app.post('/api/auth/login', wrapNoonyHandler(loginHandler, 'login', initializeDependencies));
169
+ * app.post('/api/auth/logout', wrapNoonyHandler(logoutHandler, 'logout', initializeDependencies));
170
+ *
171
+ * // Config routes
172
+ * app.get('/api/config', wrapNoonyHandler(getConfigHandler, 'getConfig', initializeDependencies));
173
+ *
174
+ * // 404 handler
175
+ * app.use((_req, res) => {
176
+ * res.status(404).json({
177
+ * success: false,
178
+ * error: { code: 'NOT_FOUND', message: 'Endpoint not found' }
179
+ * });
180
+ * });
181
+ *
182
+ * return app;
183
+ * }
184
+ *
185
+ * // Start server
186
+ * const app = createExpressApp();
187
+ * const PORT = process.env.PORT || 3000;
188
+ * app.listen(PORT, () => {
189
+ * console.log(`Server running on port ${PORT}`);
190
+ * });
191
+ * ```
192
+ *
193
+ * @example
194
+ * Express routing with path parameters:
195
+ * ```typescript
196
+ * const app = express();
197
+ *
198
+ * // Routes with path parameters work seamlessly
199
+ * app.get('/api/users/:userId', wrapNoonyHandler(getUserHandler, 'getUser', initializeDependencies));
200
+ * app.patch('/api/config/sections/:sectionId', wrapNoonyHandler(updateSectionHandler, 'updateSection', initializeDependencies));
201
+ * app.delete('/api/config/sections/:sectionId', wrapNoonyHandler(deleteSectionHandler, 'deleteSection', initializeDependencies));
202
+ *
203
+ * // Path parameters available in Noony handler via context.req.params
204
+ * ```
205
+ *
206
+ * @see {@link createHttpFunction} for Cloud Functions Framework integration (production deployment)
207
+ */
208
+ function wrapNoonyHandler(noonyHandler, functionName, initializeDependencies) {
209
+ return async (req, res) => {
210
+ try {
211
+ // Ensure dependencies are initialized
212
+ await initializeDependencies();
213
+ // Execute Noony handler with Express req/res (cast to generic types)
214
+ await noonyHandler.executeGeneric(req, res);
215
+ }
216
+ catch (error) {
217
+ if (error instanceof Error && error.message === 'RESPONSE_SENT') {
218
+ return;
219
+ }
220
+ logger_1.logger.error(`${functionName} handler error`, {
221
+ error: error instanceof Error ? error.message : 'Unknown error',
222
+ stack: error instanceof Error ? error.stack : undefined,
223
+ });
224
+ if (!res.headersSent) {
225
+ res.status(500).json({
226
+ success: false,
227
+ error: {
228
+ code: 'INTERNAL_SERVER_ERROR',
229
+ message: 'An unexpected error occurred',
230
+ },
231
+ });
232
+ }
233
+ }
234
+ };
235
+ }
236
+ //# sourceMappingURL=wrapper-utils.js.map