@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.
- 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/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,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) =>
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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
|
-
?
|
|
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,
|
|
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
|
package/build/utils/index.d.ts
CHANGED
|
@@ -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
|