@noony-serverless/core 0.3.4 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/build/core/containerPool.d.ts +129 -26
  2. package/build/core/containerPool.js +213 -68
  3. package/build/core/handler.d.ts +2 -2
  4. package/build/core/handler.js +6 -12
  5. package/build/core/index.d.ts +1 -0
  6. package/build/core/index.js +1 -0
  7. package/build/core/logger.d.ts +89 -1
  8. package/build/core/logger.js +136 -5
  9. package/build/core/telemetry/config.d.ts +331 -0
  10. package/build/core/telemetry/config.js +153 -0
  11. package/build/core/telemetry/index.d.ts +22 -0
  12. package/build/core/telemetry/index.js +45 -0
  13. package/build/core/telemetry/provider.d.ts +203 -0
  14. package/build/core/telemetry/provider.js +3 -0
  15. package/build/core/telemetry/providers/console-provider.d.ts +54 -0
  16. package/build/core/telemetry/providers/console-provider.js +124 -0
  17. package/build/core/telemetry/providers/index.d.ts +10 -0
  18. package/build/core/telemetry/providers/index.js +19 -0
  19. package/build/core/telemetry/providers/noop-provider.d.ts +51 -0
  20. package/build/core/telemetry/providers/noop-provider.js +67 -0
  21. package/build/core/telemetry/providers/opentelemetry-provider.d.ts +102 -0
  22. package/build/core/telemetry/providers/opentelemetry-provider.js +342 -0
  23. package/build/middlewares/dependencyInjectionMiddleware.d.ts +16 -8
  24. package/build/middlewares/dependencyInjectionMiddleware.js +31 -11
  25. package/build/middlewares/guards/adapters/CustomTokenVerificationPortAdapter.d.ts +1 -1
  26. package/build/middlewares/guards/guards/FastAuthGuard.d.ts +5 -5
  27. package/build/middlewares/guards/guards/FastAuthGuard.js +3 -2
  28. package/build/middlewares/guards/guards/PermissionGuardFactory.d.ts +7 -9
  29. package/build/middlewares/guards/resolvers/ExpressionPermissionResolver.d.ts +1 -1
  30. package/build/middlewares/guards/resolvers/ExpressionPermissionResolver.js +1 -1
  31. package/build/middlewares/guards/resolvers/PermissionResolver.d.ts +1 -1
  32. package/build/middlewares/guards/resolvers/PlainPermissionResolver.d.ts +1 -1
  33. package/build/middlewares/guards/resolvers/WildcardPermissionResolver.d.ts +1 -1
  34. package/build/middlewares/guards/services/FastUserContextService.d.ts +11 -32
  35. package/build/middlewares/index.d.ts +1 -0
  36. package/build/middlewares/index.js +1 -0
  37. package/build/middlewares/openTelemetryMiddleware.d.ts +162 -0
  38. package/build/middlewares/openTelemetryMiddleware.js +359 -0
  39. package/build/middlewares/rateLimitingMiddleware.js +16 -5
  40. package/build/utils/container.utils.js +4 -1
  41. package/build/utils/fastify-wrapper.d.ts +74 -0
  42. package/build/utils/fastify-wrapper.js +175 -0
  43. package/build/utils/index.d.ts +4 -0
  44. package/build/utils/index.js +23 -1
  45. package/build/utils/otel.helper.d.ts +122 -0
  46. package/build/utils/otel.helper.js +258 -0
  47. package/build/utils/pubsub-trace.utils.d.ts +102 -0
  48. package/build/utils/pubsub-trace.utils.js +155 -0
  49. package/build/utils/wrapper-utils.d.ts +177 -0
  50. package/build/utils/wrapper-utils.js +236 -0
  51. package/package.json +61 -2
@@ -0,0 +1,359 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.openTelemetry = exports.OpenTelemetryMiddleware = void 0;
4
+ const providers_1 = require("../core/telemetry/providers");
5
+ const pubsub_trace_utils_1 = require("../utils/pubsub-trace.utils");
6
+ /**
7
+ * OpenTelemetry Middleware
8
+ *
9
+ * Provides distributed tracing and metrics collection with:
10
+ * - Auto-detection of telemetry provider from environment
11
+ * - Graceful degradation when configuration is missing
12
+ * - Zero-configuration local development support
13
+ * - Type-safe generics to preserve middleware chain
14
+ *
15
+ * Provider Auto-Detection Priority:
16
+ * 1. Explicit provider via options.provider
17
+ * 2. New Relic (if NEW_RELIC_LICENSE_KEY set)
18
+ * 3. Datadog (if DD_API_KEY or DD_SERVICE set)
19
+ * 4. Standard OTEL (if OTEL_EXPORTER_OTLP_ENDPOINT set)
20
+ * 5. Console (if NODE_ENV=development and no OTEL endpoint)
21
+ * 6. Noop (if NODE_ENV=test or no configuration)
22
+ *
23
+ * @template TBody - Request body type
24
+ * @template TUser - Authenticated user type
25
+ *
26
+ * @example
27
+ * // Zero configuration (auto-detects provider)
28
+ * const handler = new Handler()
29
+ * .use(new OpenTelemetryMiddleware())
30
+ * .handle(async (context) => {
31
+ * // Your business logic
32
+ * });
33
+ *
34
+ * @example
35
+ * // With custom filtering
36
+ * const handler = new Handler()
37
+ * .use(new OpenTelemetryMiddleware({
38
+ * shouldTrace: (context) => context.req.path !== '/health'
39
+ * }))
40
+ * .handle(async (context) => {
41
+ * // Your business logic
42
+ * });
43
+ */
44
+ class OpenTelemetryMiddleware {
45
+ provider;
46
+ enabled;
47
+ failSilently;
48
+ propagatePubSubTraces;
49
+ extractAttributes;
50
+ shouldTrace;
51
+ customErrorHandler;
52
+ initialized = false;
53
+ constructor(options = {}) {
54
+ this.enabled = options.enabled ?? process.env.NODE_ENV !== 'test';
55
+ this.failSilently = options.failSilently ?? true;
56
+ this.propagatePubSubTraces = options.propagatePubSubTraces ?? true;
57
+ this.extractAttributes =
58
+ options.extractAttributes || this.defaultExtractAttributes;
59
+ this.shouldTrace = options.shouldTrace || (() => true);
60
+ this.customErrorHandler = options.onError || this.defaultOnError;
61
+ // Use NoopProvider if disabled
62
+ if (!this.enabled) {
63
+ this.provider = new providers_1.NoopProvider();
64
+ return;
65
+ }
66
+ // Use provided provider or auto-detect
67
+ this.provider = options.provider || this.autoDetectProvider();
68
+ }
69
+ /**
70
+ * Auto-detect telemetry provider based on environment
71
+ */
72
+ autoDetectProvider() {
73
+ // Priority 1: New Relic (check for license key and package)
74
+ if (process.env.NEW_RELIC_LICENSE_KEY) {
75
+ try {
76
+ require.resolve('newrelic');
77
+ console.log('[Telemetry] Detected New Relic configuration');
78
+ // Note: NewRelicProvider would be imported here when implemented
79
+ // const { NewRelicProvider } = require('../core/telemetry/providers/newrelic-provider');
80
+ // return new NewRelicProvider();
81
+ }
82
+ catch {
83
+ console.warn('[Telemetry] NEW_RELIC_LICENSE_KEY set but newrelic package not installed');
84
+ }
85
+ }
86
+ // Priority 2: Datadog (check for API key or service name and package)
87
+ if (process.env.DD_API_KEY || process.env.DD_SERVICE) {
88
+ try {
89
+ require.resolve('dd-trace');
90
+ console.log('[Telemetry] Detected Datadog configuration');
91
+ // Note: DatadogProvider would be imported here when implemented
92
+ // const { DatadogProvider } = require('../core/telemetry/providers/datadog-provider');
93
+ // return new DatadogProvider();
94
+ }
95
+ catch {
96
+ console.warn('[Telemetry] Datadog config detected but dd-trace package not installed');
97
+ }
98
+ }
99
+ // Priority 3: Standard OTEL (check for OTLP endpoint)
100
+ if (process.env.OTEL_EXPORTER_OTLP_ENDPOINT) {
101
+ console.log('[Telemetry] Using standard OpenTelemetry provider');
102
+ return new providers_1.OpenTelemetryProvider();
103
+ }
104
+ // Priority 4: Console (development mode without OTEL endpoint)
105
+ if (process.env.NODE_ENV === 'development' &&
106
+ !process.env.OTEL_EXPORTER_OTLP_ENDPOINT) {
107
+ console.log('[Telemetry] Using console provider for local development');
108
+ return new providers_1.ConsoleProvider();
109
+ }
110
+ // Priority 5: Noop (no configuration found)
111
+ console.log('[Telemetry] No telemetry configuration detected, using Noop provider');
112
+ return new providers_1.NoopProvider();
113
+ }
114
+ /**
115
+ * Initialize provider with configuration
116
+ *
117
+ * This should be called once at application startup.
118
+ * If not called explicitly, it will be initialized on first request.
119
+ *
120
+ * @param config Telemetry configuration
121
+ */
122
+ async initialize(config) {
123
+ if (this.initialized)
124
+ return;
125
+ if (!this.enabled) {
126
+ this.initialized = true;
127
+ return;
128
+ }
129
+ try {
130
+ // Validate provider before initialization
131
+ const validation = await this.provider.validate();
132
+ if (!validation.valid) {
133
+ console.warn(`[Telemetry] Provider '${this.provider.name}' validation failed: ${validation.reason}`);
134
+ console.warn('[Telemetry] Falling back to Noop provider');
135
+ this.provider = new providers_1.NoopProvider();
136
+ this.initialized = true;
137
+ return;
138
+ }
139
+ // Initialize provider
140
+ await this.provider.initialize(config);
141
+ // Check if provider is ready
142
+ if (!this.provider.isReady()) {
143
+ console.warn(`[Telemetry] Provider '${this.provider.name}' initialization failed, falling back to Noop`);
144
+ this.provider = new providers_1.NoopProvider();
145
+ }
146
+ else {
147
+ console.log(`[Telemetry] Provider '${this.provider.name}' initialized successfully`);
148
+ }
149
+ this.initialized = true;
150
+ }
151
+ catch (error) {
152
+ console.error('[Telemetry] Failed to initialize provider:', error);
153
+ this.provider = new providers_1.NoopProvider();
154
+ this.initialized = true;
155
+ }
156
+ }
157
+ /**
158
+ * Before hook - Create span and store in context
159
+ *
160
+ * If propagatePubSubTraces is enabled and the request is a Pub/Sub message:
161
+ * 1. Extracts W3C Trace Context from message attributes
162
+ * 2. Creates a child span linked to the publisher's trace
163
+ * 3. Enables end-to-end distributed tracing across Pub/Sub
164
+ */
165
+ async before(context) {
166
+ if (!this.enabled)
167
+ return;
168
+ // Auto-initialize with minimal config if not initialized
169
+ if (!this.initialized) {
170
+ await this.initialize({
171
+ serviceName: process.env.SERVICE_NAME || 'noony-service',
172
+ serviceVersion: process.env.SERVICE_VERSION || '1.0.0',
173
+ environment: process.env.NODE_ENV || 'production',
174
+ });
175
+ }
176
+ // Check if should trace
177
+ if (!this.shouldTrace(context)) {
178
+ return;
179
+ }
180
+ try {
181
+ // Extract trace context from Pub/Sub message if enabled
182
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
183
+ let parentContext = undefined;
184
+ if (this.propagatePubSubTraces && (0, pubsub_trace_utils_1.isPubSubMessage)(context.req.body)) {
185
+ const traceContext = (0, pubsub_trace_utils_1.extractTraceContext)(context.req.body);
186
+ if (traceContext.traceparent) {
187
+ // Store trace context for span creation
188
+ const carrier = (0, pubsub_trace_utils_1.createParentContext)(traceContext);
189
+ // Try to extract parent context using OpenTelemetry API
190
+ try {
191
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
192
+ const otelApi = require('@opentelemetry/api');
193
+ const { propagation, context: otelContext } = otelApi;
194
+ // Extract parent context from carrier
195
+ parentContext = propagation.extract(otelContext.active(), carrier);
196
+ console.log('[Telemetry] Extracted Pub/Sub trace context:', {
197
+ traceparent: traceContext.traceparent,
198
+ tracestate: traceContext.tracestate,
199
+ });
200
+ }
201
+ catch (err) {
202
+ // OpenTelemetry API not available, continue without parent context
203
+ console.warn('[Telemetry] Failed to extract Pub/Sub trace context:', err);
204
+ }
205
+ }
206
+ }
207
+ // Create span (with parent context if available)
208
+ const span = this.provider.createSpan(context);
209
+ if (!span)
210
+ return;
211
+ // If we have a parent context from Pub/Sub, link the span
212
+ if (parentContext) {
213
+ span.setAttributes({
214
+ 'messaging.system': 'pubsub',
215
+ 'messaging.operation': 'process',
216
+ 'pubsub.message_id':
217
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
218
+ context.req.body?.message?.messageId || 'unknown',
219
+ });
220
+ }
221
+ // Add custom attributes
222
+ const customAttributes = this.extractAttributes(context);
223
+ span.setAttributes(customAttributes);
224
+ // Store span and provider name in businessData
225
+ context.businessData.set('otel_span', span);
226
+ context.businessData.set('otel_provider', this.provider.name);
227
+ }
228
+ catch (error) {
229
+ if (!this.failSilently)
230
+ throw error;
231
+ console.error('[Telemetry] Error in before hook:', error);
232
+ }
233
+ }
234
+ /**
235
+ * After hook - End span with success status and add X-Trace-Id header
236
+ */
237
+ async after(context) {
238
+ if (!this.enabled)
239
+ return;
240
+ try {
241
+ const span = context.businessData.get('otel_span');
242
+ if (span) {
243
+ // Add response attributes
244
+ span.setAttributes({
245
+ 'http.status_code': context.res.statusCode || 200,
246
+ 'request.duration_ms': Date.now() - context.startTime,
247
+ });
248
+ // Add X-Trace-Id header with clean trace ID
249
+ try {
250
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
251
+ const otelApi = require('@opentelemetry/api');
252
+ const { context: otelContext, trace } = otelApi;
253
+ // Get span from active context
254
+ const activeContext = otelContext.active();
255
+ const activeSpan = trace.getSpan(activeContext);
256
+ if (activeSpan) {
257
+ const spanContext = activeSpan.spanContext();
258
+ if (spanContext.traceId) {
259
+ // Add custom header with clean trace ID (32 hex chars)
260
+ context.res.header('X-Trace-Id', spanContext.traceId);
261
+ }
262
+ }
263
+ }
264
+ catch (headerError) {
265
+ // Non-critical error - don't fail the request
266
+ console.warn('[Telemetry] Failed to add X-Trace-Id header:', headerError);
267
+ }
268
+ // Set success status (code 0 = OK in OTEL)
269
+ span.setStatus({ code: 0 });
270
+ // End span
271
+ span.end();
272
+ }
273
+ }
274
+ catch (error) {
275
+ if (!this.failSilently)
276
+ throw error;
277
+ console.error('[Telemetry] Error in after hook:', error);
278
+ }
279
+ }
280
+ /**
281
+ * Error hook - Record exception and end span
282
+ */
283
+ async onError(error, context) {
284
+ if (!this.enabled)
285
+ return;
286
+ try {
287
+ const span = context.businessData.get('otel_span');
288
+ if (span) {
289
+ // Record exception
290
+ span.recordException(error);
291
+ // Set error status (code 1 = ERROR in OTEL, code 2 = ERROR in some systems)
292
+ span.setStatus({
293
+ code: 1,
294
+ message: error.message,
295
+ });
296
+ // End span
297
+ span.end();
298
+ }
299
+ // Call custom error handler
300
+ this.customErrorHandler(error, context);
301
+ }
302
+ catch (err) {
303
+ if (!this.failSilently)
304
+ throw err;
305
+ console.error('[Telemetry] Error in onError hook:', err);
306
+ }
307
+ }
308
+ /**
309
+ * Default attribute extractor
310
+ */
311
+ defaultExtractAttributes(context) {
312
+ return {
313
+ 'http.method': context.req.method,
314
+ 'http.url': context.req.url || context.req.path,
315
+ 'http.target': context.req.path || '/',
316
+ 'request.id': context.requestId,
317
+ 'http.user_agent': context.req.headers?.['user-agent'] || '',
318
+ };
319
+ }
320
+ /**
321
+ * Default error handler
322
+ */
323
+ defaultOnError(error, _context) {
324
+ console.error('[Telemetry] Request error:', {
325
+ name: error.name,
326
+ message: error.message,
327
+ });
328
+ }
329
+ /**
330
+ * Get current provider (useful for testing)
331
+ */
332
+ getProvider() {
333
+ return this.provider;
334
+ }
335
+ /**
336
+ * Shutdown telemetry provider
337
+ *
338
+ * Should be called during application shutdown to flush pending data.
339
+ */
340
+ async shutdown() {
341
+ if (this.provider) {
342
+ await this.provider.shutdown();
343
+ }
344
+ }
345
+ }
346
+ exports.OpenTelemetryMiddleware = OpenTelemetryMiddleware;
347
+ /**
348
+ * Factory function for OpenTelemetry middleware
349
+ *
350
+ * @example
351
+ * const handler = new Handler()
352
+ * .use(openTelemetry({ shouldTrace: ctx => ctx.req.path !== '/health' }))
353
+ * .handle(async (context) => { });
354
+ */
355
+ const openTelemetry = (options = {}) => {
356
+ return new OpenTelemetryMiddleware(options);
357
+ };
358
+ exports.openTelemetry = openTelemetry;
359
+ //# sourceMappingURL=openTelemetryMiddleware.js.map
@@ -618,6 +618,7 @@ exports.RateLimitPresets = {
618
618
  keyGenerator: (context) => {
619
619
  // Rate limit per IP + email combination for better security
620
620
  const ip = context.req.ip || 'unknown';
621
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
621
622
  const email = context.req.parsedBody?.email;
622
623
  return email ? `auth:${email}:${ip}` : `auth:${ip}`;
623
624
  },
@@ -670,28 +671,38 @@ exports.RateLimitPresets = {
670
671
  free: {
671
672
  maxRequests: 100,
672
673
  windowMs: 60000,
673
- matcher: (context) => !context.user || context.user?.plan === 'free',
674
+ matcher: (context) =>
675
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
676
+ !context.user || context.user?.plan === 'free',
674
677
  },
675
678
  premium: {
676
679
  maxRequests: 1000,
677
680
  windowMs: 60000,
678
- matcher: (context) => context.user?.plan === 'premium',
681
+ matcher: (context) =>
682
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
683
+ context.user?.plan === 'premium',
679
684
  },
680
685
  enterprise: {
681
686
  maxRequests: 5000,
682
687
  windowMs: 60000,
683
- matcher: (context) => context.user?.plan === 'enterprise',
688
+ matcher: (context) =>
689
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
690
+ context.user?.plan === 'enterprise',
684
691
  },
685
692
  admin: {
686
693
  maxRequests: 10000,
687
694
  windowMs: 60000,
688
- matcher: (context) => context.user?.role === 'admin',
695
+ matcher: (context) =>
696
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
697
+ context.user?.role === 'admin',
689
698
  },
690
699
  },
691
700
  keyGenerator: (context) => {
692
701
  // Use user ID for authenticated, IP for anonymous
702
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
693
703
  return context.user?.id
694
- ? `user:${context.user.id}`
704
+ ? // eslint-disable-next-line @typescript-eslint/no-explicit-any
705
+ `user:${context.user.id}`
695
706
  : `ip:${context.req.ip}`;
696
707
  },
697
708
  },
@@ -39,10 +39,13 @@ exports.getService = getService;
39
39
  * }
40
40
  * ```
41
41
  */
42
- function getService(context, serviceIdentifier) {
42
+ function getService(context,
43
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
44
+ serviceIdentifier) {
43
45
  if (!context.container) {
44
46
  throw new Error('Container not initialized. Did you forget to add DependencyInjectionMiddleware?');
45
47
  }
48
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
46
49
  return context.container.get(serviceIdentifier);
47
50
  }
48
51
  //# sourceMappingURL=container.utils.js.map
@@ -0,0 +1,74 @@
1
+ import type { FastifyRequest, FastifyReply } from 'fastify';
2
+ import { Handler } from '../core/handler';
3
+ /**
4
+ * Create a Fastify route handler wrapper for a Noony handler
5
+ *
6
+ * Wraps a Noony handler into a Fastify route handler for use with Fastify server.
7
+ * This pattern enables running Noony handlers with Fastify's high-performance HTTP framework.
8
+ *
9
+ * @param noonyHandler - The Noony handler to wrap (contains middleware chain and controller)
10
+ * @param functionName - Name for error logging purposes
11
+ * @param initializeDependencies - Async function that initializes dependencies (database, services, etc.)
12
+ * Uses singleton pattern to prevent re-initialization across requests
13
+ * @returns Fastify route handler: `(req: FastifyRequest, reply: FastifyReply) => Promise<void>`
14
+ *
15
+ * @remarks
16
+ * This wrapper ensures:
17
+ * - Dependencies are initialized before handler execution (singleton pattern for efficiency)
18
+ * - Noony handlers work seamlessly with Fastify routing
19
+ * - Errors are caught and returned as proper HTTP responses
20
+ * - Response is not sent twice (`reply.sent` 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 Fastify app with multiple routes:
26
+ * ```typescript
27
+ * import Fastify from 'fastify';
28
+ * import { createFastifyHandler } from '@noony-serverless/core';
29
+ * import { loginHandler, getConfigHandler } from './handlers';
30
+ *
31
+ * // Initialize dependencies once per app startup
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
+ * const server = Fastify({ logger: true });
41
+ *
42
+ * // Helper shorthand
43
+ * const adapt = (handler, name) => createFastifyHandler(handler, name, initializeDependencies);
44
+ *
45
+ * // Auth routes
46
+ * server.post('/api/auth/login', adapt(loginHandler, 'login'));
47
+ *
48
+ * // Config routes
49
+ * server.get('/api/config', adapt(getConfigHandler, 'getConfig'));
50
+ *
51
+ * // Start server
52
+ * server.listen({ port: 3000 }, (err) => {
53
+ * if (err) throw err;
54
+ * console.log('Server running on port 3000');
55
+ * });
56
+ * ```
57
+ *
58
+ * @example
59
+ * Fastify routing with path parameters:
60
+ * ```typescript
61
+ * const server = Fastify();
62
+ *
63
+ * // Routes with path parameters work seamlessly
64
+ * server.get('/api/users/:userId', adapt(getUserHandler, 'getUser'));
65
+ * server.patch('/api/config/sections/:sectionId', adapt(updateSectionHandler, 'updateSection'));
66
+ *
67
+ * // Path parameters available in Noony handler via context.req.params
68
+ * ```
69
+ *
70
+ * @see {@link createHttpFunction} for Cloud Functions Framework integration (production deployment)
71
+ * @see {@link wrapNoonyHandler} for Express integration
72
+ */
73
+ export declare function createFastifyHandler(noonyHandler: Handler<unknown>, functionName: string, initializeDependencies: () => Promise<void>): (req: FastifyRequest, reply: FastifyReply) => Promise<void>;
74
+ //# sourceMappingURL=fastify-wrapper.d.ts.map
@@ -0,0 +1,175 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createFastifyHandler = createFastifyHandler;
4
+ const logger_1 = require("../core/logger");
5
+ /**
6
+ * Adapt Fastify Request to GenericRequest for Noony handlers
7
+ *
8
+ * @internal
9
+ */
10
+ function adaptFastifyRequest(req) {
11
+ return {
12
+ method: req.method,
13
+ url: req.url,
14
+ path: req.routeOptions?.url || req.url,
15
+ headers: req.headers,
16
+ query: (req.query || {}),
17
+ params: (req.params || {}),
18
+ body: req.body,
19
+ // Fastify already parses the body, so set parsedBody for BodyValidationMiddleware
20
+ parsedBody: req.body,
21
+ ip: req.ip,
22
+ userAgent: req.headers['user-agent'],
23
+ };
24
+ }
25
+ /**
26
+ * Adapt Fastify Reply to GenericResponse for Noony handlers
27
+ *
28
+ * @internal
29
+ */
30
+ function adaptFastifyResponse(reply) {
31
+ let statusCode = 200;
32
+ let headersSent = false;
33
+ const response = {
34
+ status(code) {
35
+ statusCode = code;
36
+ reply.code(code);
37
+ return response;
38
+ },
39
+ json(data) {
40
+ headersSent = true;
41
+ reply.send(data);
42
+ return response;
43
+ },
44
+ send(data) {
45
+ headersSent = true;
46
+ reply.send(data);
47
+ return response;
48
+ },
49
+ header(name, value) {
50
+ reply.header(name, value);
51
+ return response;
52
+ },
53
+ headers(headers) {
54
+ Object.entries(headers).forEach(([key, value]) => {
55
+ reply.header(key, value);
56
+ });
57
+ return response;
58
+ },
59
+ end() {
60
+ headersSent = true;
61
+ reply.send();
62
+ },
63
+ get statusCode() {
64
+ return statusCode;
65
+ },
66
+ get headersSent() {
67
+ return headersSent || reply.sent;
68
+ },
69
+ };
70
+ return response;
71
+ }
72
+ /**
73
+ * Create a Fastify route handler wrapper for a Noony handler
74
+ *
75
+ * Wraps a Noony handler into a Fastify route handler for use with Fastify server.
76
+ * This pattern enables running Noony handlers with Fastify's high-performance HTTP framework.
77
+ *
78
+ * @param noonyHandler - The Noony handler to wrap (contains middleware chain and controller)
79
+ * @param functionName - Name for error logging purposes
80
+ * @param initializeDependencies - Async function that initializes dependencies (database, services, etc.)
81
+ * Uses singleton pattern to prevent re-initialization across requests
82
+ * @returns Fastify route handler: `(req: FastifyRequest, reply: FastifyReply) => Promise<void>`
83
+ *
84
+ * @remarks
85
+ * This wrapper ensures:
86
+ * - Dependencies are initialized before handler execution (singleton pattern for efficiency)
87
+ * - Noony handlers work seamlessly with Fastify routing
88
+ * - Errors are caught and returned as proper HTTP responses
89
+ * - Response is not sent twice (`reply.sent` check)
90
+ * - `RESPONSE_SENT` errors are ignored (response already sent by middleware)
91
+ * - Real errors return 500 with generic message for security
92
+ *
93
+ * @example
94
+ * Creating Fastify app with multiple routes:
95
+ * ```typescript
96
+ * import Fastify from 'fastify';
97
+ * import { createFastifyHandler } from '@noony-serverless/core';
98
+ * import { loginHandler, getConfigHandler } from './handlers';
99
+ *
100
+ * // Initialize dependencies once per app startup
101
+ * let initialized = false;
102
+ * async function initializeDependencies(): Promise<void> {
103
+ * if (initialized) return;
104
+ * const db = await databaseService.connect();
105
+ * await initializeServices(db);
106
+ * initialized = true;
107
+ * }
108
+ *
109
+ * const server = Fastify({ logger: true });
110
+ *
111
+ * // Helper shorthand
112
+ * const adapt = (handler, name) => createFastifyHandler(handler, name, initializeDependencies);
113
+ *
114
+ * // Auth routes
115
+ * server.post('/api/auth/login', adapt(loginHandler, 'login'));
116
+ *
117
+ * // Config routes
118
+ * server.get('/api/config', adapt(getConfigHandler, 'getConfig'));
119
+ *
120
+ * // Start server
121
+ * server.listen({ port: 3000 }, (err) => {
122
+ * if (err) throw err;
123
+ * console.log('Server running on port 3000');
124
+ * });
125
+ * ```
126
+ *
127
+ * @example
128
+ * Fastify routing with path parameters:
129
+ * ```typescript
130
+ * const server = Fastify();
131
+ *
132
+ * // Routes with path parameters work seamlessly
133
+ * server.get('/api/users/:userId', adapt(getUserHandler, 'getUser'));
134
+ * server.patch('/api/config/sections/:sectionId', adapt(updateSectionHandler, 'updateSection'));
135
+ *
136
+ * // Path parameters available in Noony handler via context.req.params
137
+ * ```
138
+ *
139
+ * @see {@link createHttpFunction} for Cloud Functions Framework integration (production deployment)
140
+ * @see {@link wrapNoonyHandler} for Express integration
141
+ */
142
+ function createFastifyHandler(noonyHandler, functionName, initializeDependencies) {
143
+ return async (req, reply) => {
144
+ try {
145
+ // Ensure dependencies are initialized
146
+ await initializeDependencies();
147
+ // Adapt Fastify req/reply to GenericRequest/GenericResponse
148
+ const genericReq = adaptFastifyRequest(req);
149
+ const genericRes = adaptFastifyResponse(reply);
150
+ // Execute Noony handler with adapted request/response
151
+ await noonyHandler.executeGeneric(genericReq, genericRes);
152
+ }
153
+ catch (error) {
154
+ // Ignore RESPONSE_SENT markers (response already sent by middleware)
155
+ if (error instanceof Error && error.message === 'RESPONSE_SENT') {
156
+ return;
157
+ }
158
+ logger_1.logger.error(`${functionName} handler error`, {
159
+ error: error instanceof Error ? error.message : 'Unknown error',
160
+ stack: error instanceof Error ? error.stack : undefined,
161
+ });
162
+ // Graceful error handling - only send if response not already sent
163
+ if (!reply.sent) {
164
+ reply.code(500).send({
165
+ success: false,
166
+ error: {
167
+ code: 'INTERNAL_SERVER_ERROR',
168
+ message: 'An unexpected error occurred',
169
+ },
170
+ });
171
+ }
172
+ }
173
+ };
174
+ }
175
+ //# sourceMappingURL=fastify-wrapper.js.map
@@ -3,4 +3,8 @@
3
3
  */
4
4
  export { getService } from './container.utils';
5
5
  export { asString, asStringArray, asNumber, asBoolean, } from './query-param.utils';
6
+ export { isPubSubMessage, extractTraceContext, injectTraceContext, createParentContext, type PubSubMessage, type TraceContext, } from './pubsub-trace.utils';
7
+ export { createOTELMixin, getOTELContext, getOTELContextFromSpan, getOTELContextFromContext, formatTraceIdForCloudLogging, createCloudLoggingEntry, isOTELActive, isOTELInstalled, type OTELLogContext, } from './otel.helper';
8
+ export { createHttpFunction, wrapNoonyHandler } from './wrapper-utils';
9
+ export { createFastifyHandler } from './fastify-wrapper';
6
10
  //# sourceMappingURL=index.d.ts.map