@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.
- package/README.md +199 -0
- package/build/core/containerPool.d.ts +129 -26
- package/build/core/containerPool.js +213 -68
- package/build/core/handler.d.ts +2 -2
- package/build/core/handler.js +6 -12
- package/build/core/index.d.ts +1 -0
- package/build/core/index.js +1 -0
- package/build/core/logger.d.ts +89 -1
- package/build/core/logger.js +136 -5
- package/build/core/telemetry/config.d.ts +331 -0
- package/build/core/telemetry/config.js +153 -0
- package/build/core/telemetry/index.d.ts +22 -0
- package/build/core/telemetry/index.js +45 -0
- package/build/core/telemetry/provider.d.ts +203 -0
- package/build/core/telemetry/provider.js +3 -0
- package/build/core/telemetry/providers/console-provider.d.ts +54 -0
- package/build/core/telemetry/providers/console-provider.js +124 -0
- package/build/core/telemetry/providers/index.d.ts +10 -0
- package/build/core/telemetry/providers/index.js +19 -0
- package/build/core/telemetry/providers/noop-provider.d.ts +51 -0
- package/build/core/telemetry/providers/noop-provider.js +67 -0
- package/build/core/telemetry/providers/opentelemetry-provider.d.ts +102 -0
- package/build/core/telemetry/providers/opentelemetry-provider.js +342 -0
- package/build/middlewares/bodyValidationMiddleware.js +1 -1
- package/build/middlewares/dependencyInjectionMiddleware.d.ts +16 -8
- package/build/middlewares/dependencyInjectionMiddleware.js +31 -11
- package/build/middlewares/guards/adapters/CustomTokenVerificationPortAdapter.d.ts +1 -1
- package/build/middlewares/guards/guards/FastAuthGuard.d.ts +5 -5
- package/build/middlewares/guards/guards/FastAuthGuard.js +3 -2
- package/build/middlewares/guards/guards/PermissionGuardFactory.d.ts +7 -9
- package/build/middlewares/guards/resolvers/ExpressionPermissionResolver.d.ts +1 -1
- package/build/middlewares/guards/resolvers/ExpressionPermissionResolver.js +1 -1
- package/build/middlewares/guards/resolvers/PermissionResolver.d.ts +1 -1
- package/build/middlewares/guards/resolvers/PlainPermissionResolver.d.ts +1 -1
- package/build/middlewares/guards/resolvers/WildcardPermissionResolver.d.ts +1 -1
- package/build/middlewares/guards/services/FastUserContextService.d.ts +11 -32
- package/build/middlewares/index.d.ts +1 -0
- package/build/middlewares/index.js +1 -0
- package/build/middlewares/openTelemetryMiddleware.d.ts +162 -0
- package/build/middlewares/openTelemetryMiddleware.js +359 -0
- package/build/middlewares/rateLimitingMiddleware.js +16 -5
- package/build/utils/container.utils.js +4 -1
- package/build/utils/fastify-wrapper.d.ts +74 -0
- package/build/utils/fastify-wrapper.js +175 -0
- package/build/utils/index.d.ts +4 -0
- package/build/utils/index.js +23 -1
- package/build/utils/otel.helper.d.ts +122 -0
- package/build/utils/otel.helper.js +258 -0
- package/build/utils/pubsub-trace.utils.d.ts +102 -0
- package/build/utils/pubsub-trace.utils.js +155 -0
- package/build/utils/wrapper-utils.d.ts +177 -0
- package/build/utils/wrapper-utils.js +236 -0
- 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
|