@avtechno/sfr 1.0.18 → 2.0.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 (42) hide show
  1. package/README.md +890 -45
  2. package/dist/index.mjs +20 -2
  3. package/dist/logger.mjs +9 -37
  4. package/dist/mq.mjs +46 -0
  5. package/dist/observability/index.mjs +143 -0
  6. package/dist/observability/logger.mjs +128 -0
  7. package/dist/observability/metrics.mjs +177 -0
  8. package/dist/observability/middleware/mq.mjs +156 -0
  9. package/dist/observability/middleware/rest.mjs +120 -0
  10. package/dist/observability/middleware/ws.mjs +135 -0
  11. package/dist/observability/tracer.mjs +163 -0
  12. package/dist/sfr-pipeline.mjs +412 -12
  13. package/dist/templates.mjs +8 -1
  14. package/dist/types/index.d.mts +8 -1
  15. package/dist/types/logger.d.mts +9 -3
  16. package/dist/types/mq.d.mts +19 -0
  17. package/dist/types/observability/index.d.mts +45 -0
  18. package/dist/types/observability/logger.d.mts +54 -0
  19. package/dist/types/observability/metrics.d.mts +74 -0
  20. package/dist/types/observability/middleware/mq.d.mts +46 -0
  21. package/dist/types/observability/middleware/rest.d.mts +33 -0
  22. package/dist/types/observability/middleware/ws.d.mts +35 -0
  23. package/dist/types/observability/tracer.d.mts +90 -0
  24. package/dist/types/sfr-pipeline.d.mts +42 -1
  25. package/dist/types/templates.d.mts +1 -6
  26. package/package.json +29 -4
  27. package/src/index.mts +66 -3
  28. package/src/logger.mts +16 -51
  29. package/src/mq.mts +49 -0
  30. package/src/observability/index.mts +184 -0
  31. package/src/observability/logger.mts +169 -0
  32. package/src/observability/metrics.mts +266 -0
  33. package/src/observability/middleware/mq.mts +187 -0
  34. package/src/observability/middleware/rest.mts +143 -0
  35. package/src/observability/middleware/ws.mts +162 -0
  36. package/src/observability/tracer.mts +205 -0
  37. package/src/sfr-pipeline.mts +468 -18
  38. package/src/templates.mts +14 -5
  39. package/src/types/index.d.ts +240 -16
  40. package/dist/example.mjs +0 -33
  41. package/dist/types/example.d.mts +0 -11
  42. package/src/example.mts +0 -35
@@ -3,8 +3,19 @@ import fs from "fs/promises";
3
3
  import { template } from "./util.mjs";
4
4
  import { BroadcastMQ, TargetedMQ } from "./mq.mjs";
5
5
  import j2s from "joi-to-swagger";
6
-
7
- import {logger} from "./logger.mjs";
6
+ import rateLimit from "express-rate-limit";
7
+
8
+ // Observability imports
9
+ import {
10
+ init_observability,
11
+ is_observability_enabled,
12
+ type ObservabilityOptions
13
+ } from "./observability/index.mjs";
14
+ import { sfr_logger as logger, create_child_logger } from "./observability/logger.mjs";
15
+ import { sfr_rest_telemetry } from "./observability/middleware/rest.mjs";
16
+ import { instrument_socket_io, wrap_ws_handler } from "./observability/middleware/ws.mjs";
17
+ import { wrap_mq_handler, record_mq_reject } from "./observability/middleware/mq.mjs";
18
+ import { with_span, SpanKind, add_span_attributes } from "./observability/tracer.mjs";
8
19
 
9
20
  const CWD = process.cwd();
10
21
 
@@ -28,16 +39,71 @@ export class SFRPipeline {
28
39
  handlers: {},
29
40
  controllers: {}
30
41
  },
42
+ ws: {
43
+ handlers: {},
44
+ controllers: {}
45
+ },
31
46
  mq: {
32
47
  handlers: {},
33
48
  controllers: {}
34
49
  },
35
50
  }
36
51
 
52
+ /* Authentication middleware configuration */
53
+ static auth_config: AuthConfig = {};
54
+
55
+ /* Rate limit configuration */
56
+ static rate_limit_config: RateLimitGlobalConfig = {};
57
+
58
+ /* Observability configuration */
59
+ static observability_options: ObservabilityOptions = {};
60
+
61
+ /**
62
+ * Sets the authentication middleware for protected routes.
63
+ * @param middleware - The authentication middleware function
64
+ */
65
+ static set_auth_middleware(middleware: AuthMiddleware) {
66
+ SFRPipeline.auth_config.middleware = middleware;
67
+ }
68
+
69
+ /**
70
+ * Configures global rate limiting options for all routes.
71
+ * Can be overridden per-route using cfg.rate_limit in SFR files.
72
+ * @param config - Rate limit configuration options
73
+ */
74
+ static set_rate_limit_config(config: RateLimitGlobalConfig) {
75
+ SFRPipeline.rate_limit_config = config;
76
+ }
77
+
78
+ /**
79
+ * Configures observability options for tracing, metrics, and logging.
80
+ * Must be called before SFR initialization to take effect.
81
+ * @param options - Observability configuration options
82
+ */
83
+ static set_observability_options(options: ObservabilityOptions) {
84
+ SFRPipeline.observability_options = options;
85
+ }
86
+
37
87
  constructor(private cfg: ParserCFG, private oas_cfg: OASConfig, private comms: SFRProtocols) { }
38
88
 
39
89
  async init(base_url?: string): Promise<ServiceDocuments> {
40
90
  this.base_url = base_url;
91
+
92
+ // Initialize observability with service config from oas_cfg
93
+ if (SFRPipeline.observability_options.enabled !== false) {
94
+ init_observability(this.oas_cfg, SFRPipeline.observability_options);
95
+ }
96
+
97
+ // Add REST telemetry middleware if enabled
98
+ if (this.comms["REST"] && is_observability_enabled()) {
99
+ this.comms["REST"].use(sfr_rest_telemetry());
100
+ }
101
+
102
+ // Instrument Socket.IO if enabled
103
+ if (this.comms["WS"] && is_observability_enabled()) {
104
+ instrument_socket_io(this.comms["WS"]);
105
+ }
106
+
41
107
  if(this.comms["MQ"]){
42
108
  //Create channels for each type of Communication Pattern
43
109
  const channels = await Promise.all(PATTERNS.map(async (v) => {
@@ -58,6 +124,19 @@ export class SFRPipeline {
58
124
  ...SFRPipeline.injections.mq.handlers,
59
125
  mq: this.pattern_channels
60
126
  }
127
+
128
+ SFRPipeline.injections.ws.handlers = {
129
+ ...SFRPipeline.injections.ws.handlers,
130
+ mq: this.pattern_channels
131
+ }
132
+ }
133
+
134
+ // Inject Socket.IO server into WS handlers
135
+ if (this.comms["WS"]) {
136
+ SFRPipeline.injections.ws.handlers = {
137
+ ...SFRPipeline.injections.ws.handlers,
138
+ io: this.comms["WS"]
139
+ }
61
140
  }
62
141
 
63
142
  //Declarations for each protocol in key/value pairs.
@@ -118,6 +197,91 @@ export class SFRPipeline {
118
197
  }
119
198
  });
120
199
  }
200
+ /**
201
+ * Generates OpenAPI multipart/form-data schema for Multer validators.
202
+ * Since Multer middleware functions can't be easily inspected at runtime,
203
+ * this generates a generic file upload schema.
204
+ */
205
+ private generate_multer_openapi_schema(validator: RequestHandler): any {
206
+ // Multer middleware is a function, so we generate a generic multipart schema
207
+ // In the future, this could be enhanced to parse Multer configuration
208
+ return {
209
+ content: {
210
+ "multipart/form-data": {
211
+ schema: {
212
+ type: "object",
213
+ properties: {
214
+ file: {
215
+ type: "string",
216
+ format: "binary",
217
+ description: "File to upload"
218
+ }
219
+ }
220
+ },
221
+ encoding: {
222
+ file: {
223
+ contentType: "*/*"
224
+ }
225
+ }
226
+ }
227
+ }
228
+ };
229
+ }
230
+
231
+ /**
232
+ * Gets rate limit configuration for a route, merging global defaults with route-specific config.
233
+ */
234
+ private get_rate_limit_config_for_route(cfg?: SFRConfig): RateLimitConfig | null {
235
+ if (cfg?.rate_limit === false) {
236
+ return null; // Explicitly disabled
237
+ }
238
+
239
+ const global_default = SFRPipeline.rate_limit_config.default;
240
+ const route_config = cfg?.rate_limit;
241
+
242
+ if (!global_default && !route_config) {
243
+ return null; // No rate limiting configured
244
+ }
245
+
246
+ return {
247
+ max: 100,
248
+ windowMs: 60000,
249
+ ...global_default,
250
+ ...route_config
251
+ };
252
+ }
253
+
254
+ /**
255
+ * Creates a rate limiter middleware based on configuration.
256
+ * Merges route-specific config with global defaults.
257
+ */
258
+ private create_rate_limiter(route_config?: RateLimitConfig | false): RequestHandler | null {
259
+ // If explicitly disabled, return null
260
+ if (route_config === false) {
261
+ return null;
262
+ }
263
+
264
+ // Merge route config with global default
265
+ const global_default = SFRPipeline.rate_limit_config.default;
266
+ const config: RateLimitConfig = {
267
+ max: 100,
268
+ windowMs: 60000,
269
+ message: "Too many requests, please try again later.",
270
+ statusCode: 429,
271
+ standardHeaders: true,
272
+ legacyHeaders: false,
273
+ ...global_default,
274
+ ...route_config
275
+ };
276
+
277
+ // Only create rate limiter if max is greater than 0
278
+ if (config.max && config.max > 0) {
279
+ return rateLimit(config);
280
+ }
281
+
282
+ return null;
283
+ }
284
+
121
285
  /* Fn Binding */
122
286
  private async bind_rest_fns(declaration: RESTNamespaceDeclaration) {
123
287
  const comms = this.comms["REST"];
@@ -146,14 +310,27 @@ export class SFRPipeline {
146
310
  const validator = validators[name];
147
311
  const validator_type = typeof validator !== "function" ? "joi" : "multer";
148
312
 
313
+ // Apply rate limiting middleware if configured
314
+ const rate_limiter = this.create_rate_limiter(cfg.rate_limit);
315
+ if (rate_limiter) {
316
+ comms[method.toLowerCase() as RESTRequestType](dir, rate_limiter);
317
+ }
318
+
149
319
  /* bind validators */
150
320
  switch (validator_type) {
151
321
  case "joi": {
152
- comms[method.toLowerCase() as RESTRequestType](dir, (req, res, next) => {
322
+ comms[method.toLowerCase() as RESTRequestType](dir, async (req, res, next) => {
153
323
  let error: string | boolean = true;
154
324
 
155
- if (is_public) {
156
- //if(!req.session.user)return res.status(401).json({error : "Session not found."});
325
+ // Authentication check for protected routes
326
+ if (!is_public && SFRPipeline.auth_config.middleware) {
327
+ const auth_result = await SFRPipeline.auth_config.middleware(req, res);
328
+ if (auth_result === false) {
329
+ return res.status(401).json({ error: "Unauthorized" });
330
+ }
331
+ if (typeof auth_result === "object" && auth_result.error) {
332
+ return res.status(auth_result.status || 401).json({ error: auth_result.error });
333
+ }
157
334
  }
158
335
 
159
336
  const validator_keys = Object.keys(validator);
@@ -186,7 +363,101 @@ export class SFRPipeline {
186
363
  });
187
364
  }
188
365
  private async bind_ws_fns(declaration: WSNamespaceDeclaration) {
366
+ const io = this.comms["WS"];
367
+ if (!io) throw new Error(`FN Binding failed: \nWS protocol lacks its associated SFRProtocols`);
368
+
369
+ Object.entries(declaration).forEach(([namespace, module]) => {
370
+ const { cfg, validators, handlers } = module.content;
371
+
372
+ // Determine the Socket.IO namespace to use
373
+ // Priority: cfg.namespace -> cfg.base_dir -> "/"
374
+ let ws_namespace = "/";
375
+ if (cfg.namespace) ws_namespace = cfg.namespace.startsWith("/") ? cfg.namespace : `/${cfg.namespace}`;
376
+ else if (cfg.base_dir) ws_namespace = cfg.base_dir.startsWith("/") ? cfg.base_dir : `/${cfg.base_dir}`;
377
+
378
+ logger.info(`[SFR WS ${module.name}] namespace: ${ws_namespace}`);
379
+
380
+ // Get or create the namespace
381
+ const nsp = io.of(ws_namespace);
382
+
383
+ // Handle connections to this namespace
384
+ nsp.on("connection", (socket) => {
385
+ logger.info(`[SFR WS] Client connected to ${ws_namespace}: ${socket.id}`);
386
+
387
+ // Register event handlers for this connection
388
+ Object.entries(handlers).forEach(([event, handler]) => {
389
+ // Skip if no matching validator
390
+ const validator = validators[event];
391
+ if (!validator) return;
392
+
393
+ socket.on(event, async (data: any, ack?: (...args: any[]) => void) => {
394
+ // Validate incoming data if validator is a Joi schema (not Multer)
395
+ const validator_type = typeof validator !== "function" ? "joi" : "custom";
396
+
397
+ if (validator_type === "joi") {
398
+ const validator_keys = Object.keys(validator);
399
+ if (validator_keys.length) {
400
+ const error = template(validator, data || {});
401
+ if (error) {
402
+ // Send validation error back via acknowledgement or emit
403
+ if (ack) {
404
+ ack({ error });
405
+ } else {
406
+ socket.emit(`${event}:error`, { error });
407
+ }
408
+ return;
409
+ }
410
+ }
411
+ }
189
412
 
413
+ // Build context for handler
414
+ const ctx: WSRequestCtx = {
415
+ io,
416
+ socket,
417
+ data: data || {},
418
+ ack
419
+ };
420
+
421
+ // Create the handler execution function
422
+ const execute_handler = async () => {
423
+ await handler.fn.call({
424
+ ...module.content.controllers,
425
+ ...SFRPipeline.injections.ws.handlers,
426
+ io,
427
+ socket
428
+ }, ctx);
429
+ };
430
+
431
+ try {
432
+ // Wrap with telemetry if observability is enabled
433
+ if (is_observability_enabled()) {
434
+ const instrumented = wrap_ws_handler(ws_namespace, event, execute_handler);
435
+ await instrumented();
436
+ } else {
437
+ await execute_handler();
438
+ }
439
+ } catch (err) {
440
+ logger.error(`[SFR WS] Error in handler ${event}`, {
441
+ error: err instanceof Error ? err.message : String(err),
442
+ event,
443
+ namespace: ws_namespace
444
+ });
445
+ if (ack) {
446
+ ack({ error: err instanceof Error ? err.message : "Unknown error" });
447
+ } else {
448
+ socket.emit(`${event}:error`, { error: err instanceof Error ? err.message : "Unknown error" });
449
+ }
450
+ }
451
+ });
452
+
453
+ this.mount_data.ws++;
454
+ });
455
+
456
+ socket.on("disconnect", (reason) => {
457
+ logger.info(`[SFR WS] Client disconnected from ${ws_namespace}: ${socket.id} (${reason})`);
458
+ });
459
+ });
460
+ });
190
461
  }
191
462
  private async bind_mq_fns(declaration: MQNamespaceDeclaration) {
192
463
  const comms = this.comms["MQ"];
@@ -214,31 +485,41 @@ export class SFRPipeline {
214
485
  const validator = validators[name];
215
486
  //const validator_type = typeof validator !== "function" ? "joi" : "multer"; // TODO: Dynamically update parameter content based on validator type.
216
487
 
217
- let validator_fn = function (msg: ConsumeMessage) {
488
+ // Create the base handler with validation
489
+ const base_handler = function (msg: ParsedMessage) {
218
490
  const validator_keys = Object.keys(validator);
219
491
 
220
492
  if(validator_keys.length){
221
493
  const error = template(validator, msg.content);
222
494
 
223
495
  if (error) {
496
+ // Record rejection metrics
497
+ if (is_observability_enabled()) {
498
+ record_mq_reject(dir, pattern, false);
499
+ }
500
+
224
501
  if (mq instanceof TargetedMQ) {
225
502
  if (error) {
226
503
  switch (pattern) {
227
- case "Request-Reply": mq.reply(msg, { error }); break;
228
- default: mq.channel.reject(msg, false);
504
+ case "Request-Reply": mq.reply(msg as any, { error }); break;
505
+ default: mq.channel.reject(msg as any, false);
229
506
  }
230
507
  }
231
508
  }
232
509
 
233
- if (mq instanceof BroadcastMQ)mq.channel.reject(msg, false);
510
+ if (mq instanceof BroadcastMQ) mq.channel.reject(msg as any, false);
234
511
 
235
512
  return; //Return immediately to avoid executing handler fn (which may contain reply or ack calls.)
236
513
  }
237
514
  }
238
515
 
239
-
240
516
  handler.fn(msg);
241
- }
517
+ };
518
+
519
+ // Wrap with telemetry if observability is enabled
520
+ const validator_fn = is_observability_enabled()
521
+ ? wrap_mq_handler(dir, pattern, base_handler)
522
+ : base_handler;
242
523
 
243
524
  /* bind validators */
244
525
  if (mq instanceof TargetedMQ) {
@@ -267,8 +548,8 @@ export class SFRPipeline {
267
548
  const documents = Object.entries(protocols).map(([protocol, declaration]) => {
268
549
  switch (protocol) {
269
550
  case "REST": return [protocol, this.generate_open_api_document(declaration as RESTNamespaceDeclaration)];
270
- case "WS": return [protocol, this.generate_async_api_document(declaration as WSNamespaceDeclaration)];
271
- case "MQ": return [protocol, this.generate_async_api_document(declaration as MQNamespaceDeclaration)];
551
+ case "WS": return [protocol, this.generate_ws_async_api_document(declaration as WSNamespaceDeclaration)];
552
+ case "MQ": return [protocol, this.generate_mq_async_api_document(declaration as MQNamespaceDeclaration)];
272
553
  default: throw new Error(`Failed to generate SFR Spec: ${protocol} protocol is unknown.`);
273
554
  }
274
555
  });
@@ -313,12 +594,15 @@ export class SFRPipeline {
313
594
  if (body.tags && Array.isArray(body.tags)) default_tags.push(...body.tags);
314
595
 
315
596
  const operation_id = `${base_dir}${namespace}/${endpoint}`;
597
+ const is_public = Boolean(cfg.public);
598
+ const rate_limit_config = this.get_rate_limit_config_for_route(cfg);
599
+
316
600
  const document: any = {
317
601
  operationId : `${method.toUpperCase()}:${operation_id}`,
318
602
  summary : body.summary || "",
319
603
  description : body.description || "",
320
604
  tags : default_tags,
321
- public : Boolean(cfg.public),
605
+ public : is_public,
322
606
 
323
607
  responses: {
324
608
  "200": {
@@ -326,19 +610,90 @@ export class SFRPipeline {
326
610
  content: { "application/json": { schema: { type: "object" } } }
327
611
  },
328
612
  "400": {
329
- description: "An error has occured",
330
- content: { "application/json": { schema: { type: "object" } } }
613
+ description: "Bad request - validation error",
614
+ content: { "application/json": { schema: { type: "object", properties: { error: { type: "string" } } } } }
331
615
  }
332
616
  }
333
617
  };
334
618
 
619
+ // Add authentication-related responses for protected routes
620
+ if (!is_public) {
621
+ document.responses["401"] = {
622
+ description: "Unauthorized - authentication required",
623
+ content: { "application/json": { schema: { type: "object", properties: { error: { type: "string" } } } } }
624
+ };
625
+ document.responses["403"] = {
626
+ description: "Forbidden - insufficient permissions",
627
+ content: { "application/json": { schema: { type: "object", properties: { error: { type: "string" } } } } }
628
+ };
629
+ }
630
+
631
+ // Add rate limiting response and headers if rate limiting is configured
632
+ if (rate_limit_config && rate_limit_config.max && rate_limit_config.max > 0) {
633
+ document.responses["429"] = {
634
+ description: "Too many requests - rate limit exceeded",
635
+ content: {
636
+ "application/json": {
637
+ schema: {
638
+ type: "object",
639
+ properties: {
640
+ error: { type: "string" },
641
+ message: { type: "string" }
642
+ }
643
+ }
644
+ }
645
+ },
646
+ headers: {}
647
+ };
648
+
649
+ // Add rate limit headers if standardHeaders is enabled
650
+ if (rate_limit_config.standardHeaders !== false) {
651
+ document.responses["429"].headers["RateLimit-Limit"] = {
652
+ description: "The maximum number of requests allowed in the current window",
653
+ schema: { type: "integer", example: rate_limit_config.max }
654
+ };
655
+ document.responses["429"].headers["RateLimit-Remaining"] = {
656
+ description: "The number of requests remaining in the current window",
657
+ schema: { type: "integer" }
658
+ };
659
+ document.responses["429"].headers["RateLimit-Reset"] = {
660
+ description: "The time at which the current rate limit window resets (Unix timestamp)",
661
+ schema: { type: "integer" }
662
+ };
663
+ }
664
+
665
+ // Add legacy headers if enabled
666
+ if (rate_limit_config.legacyHeaders) {
667
+ document.responses["429"].headers["X-RateLimit-Limit"] = {
668
+ description: "The maximum number of requests allowed in the current window (legacy)",
669
+ schema: { type: "integer", example: rate_limit_config.max }
670
+ };
671
+ document.responses["429"].headers["X-RateLimit-Remaining"] = {
672
+ description: "The number of requests remaining in the current window (legacy)",
673
+ schema: { type: "integer" }
674
+ };
675
+ document.responses["429"].headers["X-RateLimit-Reset"] = {
676
+ description: "The time at which the current rate limit window resets (legacy)",
677
+ schema: { type: "integer" }
678
+ };
679
+ }
680
+
681
+ // Add rate limit metadata to operation description
682
+ if (rate_limit_config.windowMs) {
683
+ const window_seconds = Math.floor(rate_limit_config.windowMs / 1000);
684
+ const rate_limit_note = `\n\n**Rate Limit:** ${rate_limit_config.max} requests per ${window_seconds} second${window_seconds !== 1 ? 's' : ''}`;
685
+ document.description = (document.description || "") + rate_limit_note;
686
+ }
687
+ }
688
+
335
689
  //Insert either a parameter or a requestBody according to the method type
336
690
  if(validator_type === "joi"){
337
691
  document[method === "GET" ? "parameters" : "requestBody"] = j2s(validator).swagger;
338
692
  }
339
693
 
340
- //Haven't found a library for converting multer validators to swagger doc
694
+ // Convert Multer validators to multipart/form-data OpenAPI schema
341
695
  if(validator_type === "multer"){
696
+ document.requestBody = this.generate_multer_openapi_schema(validator as RequestHandler);
342
697
  }
343
698
 
344
699
  //Create path if it does not exist.
@@ -355,8 +710,103 @@ export class SFRPipeline {
355
710
  return spec;
356
711
  }
357
712
 
713
+ // Method to generate an AsyncAPI document from a WSNamespaceDeclaration
714
+ private generate_ws_async_api_document(declaration: WSNamespaceDeclaration): AsyncAPIDocument {
715
+ const spec: any = {
716
+ asyncapi: '3.0.0',
717
+ info: Object.assign({}, this.oas_cfg),
718
+ servers: {
719
+ websocket: {
720
+ host: `localhost:${this.oas_cfg.meta?.port || 3000}`,
721
+ protocol: 'ws',
722
+ description: 'WebSocket server'
723
+ }
724
+ },
725
+ channels: {},
726
+ operations: {},
727
+ components: {
728
+ messages: {},
729
+ },
730
+ };
731
+
732
+ // Conform to AsyncAPI standards by appending "x-property" on each field under "meta"
733
+ if (spec.info.meta) {
734
+ spec.info.meta = Object.fromEntries(Object.entries(spec.info.meta).map(([k, v]) => [`x-${k}`, v]));
735
+ }
736
+
737
+ Object.entries(declaration).forEach(([namespace, module]) => {
738
+ const { cfg, validators, handlers } = module.content;
739
+
740
+ // Determine namespace path
741
+ let ws_namespace = "/";
742
+ if (cfg.namespace) ws_namespace = cfg.namespace.startsWith("/") ? cfg.namespace : `/${cfg.namespace}`;
743
+ else if (cfg.base_dir) ws_namespace = cfg.base_dir.startsWith("/") ? cfg.base_dir : `/${cfg.base_dir}`;
744
+
745
+ // Loop over all event handlers
746
+ Object.entries(handlers).forEach(([event, handler]) => {
747
+ const validator: any = validators[event];
748
+ if (!validator) return;
749
+
750
+ const validator_type = typeof validator !== "function" ? "joi" : "custom";
751
+ const channel_id = `${ws_namespace === "/" ? "" : ws_namespace}/${event}`.replace(/^\/+/, "").replace(/\//g, "-") || event;
752
+
753
+ // Default tags for service classification
754
+ const default_tags = [
755
+ { name: `sfr-namespace:${namespace}` },
756
+ { name: `sfr-service:${this.oas_cfg.title || "unspecified"}` }
757
+ ];
758
+
759
+ if (handler.tags && Array.isArray(handler.tags)) {
760
+ default_tags.push(...handler.tags.map(t => ({ name: t })));
761
+ }
762
+
763
+ const channel_document: any = {
764
+ address: ws_namespace === "/" ? `/${event}` : `${ws_namespace}/${event}`,
765
+ messages: {}
766
+ };
767
+
768
+ const operation_document: any = {
769
+ action: "receive",
770
+ summary: handler.summary || `Handle ${event} event`,
771
+ description: handler.description || "",
772
+ channel: { $ref: `#/channels/${channel_id}` },
773
+ tags: default_tags
774
+ };
775
+
776
+ // Generate message schema from Joi validator
777
+ if (validator_type === "joi") {
778
+ const message_id = `${channel_id}-message`;
779
+ channel_document.messages[message_id] = {
780
+ $ref: `#/components/messages/${message_id}`
781
+ };
782
+
783
+ spec.components.messages[message_id] = {
784
+ name: message_id,
785
+ contentType: "application/json",
786
+ payload: j2s(validator).swagger
787
+ };
788
+ }
789
+
790
+ spec.channels[channel_id] = channel_document;
791
+ spec.operations[`${channel_id}-receive`] = operation_document;
792
+
793
+ // Add send operation for events that can emit responses
794
+ const send_operation: any = {
795
+ action: "send",
796
+ summary: `Send ${event} response`,
797
+ description: `Response for ${event} event`,
798
+ channel: { $ref: `#/channels/${channel_id}` },
799
+ tags: default_tags
800
+ };
801
+ spec.operations[`${channel_id}-send`] = send_operation;
802
+ });
803
+ });
804
+
805
+ return spec;
806
+ }
807
+
358
808
  // Method to generate an AsyncAPI document from an MQNamespaceDeclaration
359
- private generate_async_api_document(declaration: MQNamespaceDeclaration): AsyncAPIDocument {
809
+ private generate_mq_async_api_document(declaration: MQNamespaceDeclaration): AsyncAPIDocument {
360
810
  // This will hold the final AsyncAPI Document.
361
811
  const spec = {
362
812
  asyncapi: '3.0.0',
package/src/templates.mts CHANGED
@@ -22,14 +22,23 @@ export function REST<V, H, C>(struct : RESTHandlerDescriptor<V, H, C>) : H & C {
22
22
  return { validators, controllers, handlers, cfg } as H & C
23
23
  }
24
24
 
25
- export function WS<V, H, C>(struct : WSHandlerDescriptor<V, H, C>){
25
+ export function WS<V, H, C>(struct : WSHandlerDescriptor<V, H, C>): H & C {
26
26
  const validators : RequestValidators = struct.validators || {};
27
27
  const controllers : RequestControllers = struct.controllers || {};
28
28
  const handlers : WSRequestHandlers = struct.handlers || {};
29
- const cfg : SFRConfig = struct.cfg || {};
30
-
31
- //Dependency injection happens here...npm
32
- return { validators, controllers, handlers, cfg }
29
+ const cfg : WSConfig = struct.cfg || {};
30
+
31
+ // Bind controller injections to each controller
32
+ Object.entries(controllers).map(([k, v]) => controllers[k] = v.bind({...SFRPipeline.injections.ws.controllers}) as RequestController);
33
+
34
+ // Bind handler injections and SFR controllers into each handler
35
+ Object.entries(handlers).forEach(([event, handler]) => {
36
+ /* @ts-ignore */
37
+ handlers[event] = { ...handler, fn: handler.fn.bind({ ...controllers, ...SFRPipeline.injections.ws.handlers }) };
38
+ });
39
+
40
+ /* @ts-ignore */
41
+ return { validators, controllers, handlers, cfg } as H & C;
33
42
  }
34
43
 
35
44
  export function MQ<V, H, C>(struct : MQHandlerDescriptor<V, H, C>): H & C{