@avtechno/sfr 2.1.0 → 2.1.2

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/dist/index.mjs CHANGED
@@ -58,16 +58,26 @@ async function write_combined_openapi_index(cfg, documents) {
58
58
  const combined = {
59
59
  openapi: "3.1.0",
60
60
  info: {},
61
- paths: {}
61
+ paths: {},
62
+ components: {
63
+ schemas: {}
64
+ }
62
65
  };
63
66
  for (const [protocol, documentation] of Object.entries(documents)) {
64
67
  switch (protocol) {
65
68
  case "REST":
66
69
  {
67
70
  const rest_doc = documentation;
68
- // Merge REST OpenAPI info and paths into combined doc
71
+ // Merge REST OpenAPI info, paths, and components into combined doc
69
72
  combined.info = rest_doc.info;
70
73
  combined.paths = rest_doc.paths;
74
+ // Merge component schemas if they exist
75
+ if (rest_doc.components?.schemas) {
76
+ combined.components.schemas = {
77
+ ...combined.components.schemas,
78
+ ...rest_doc.components.schemas
79
+ };
80
+ }
71
81
  }
72
82
  break;
73
83
  }
@@ -509,7 +509,10 @@ export class SFRPipeline {
509
509
  const spec = {
510
510
  openapi: "3.0.0",
511
511
  info: Object.assign({}, this.oas_cfg),
512
- paths: {}
512
+ paths: {},
513
+ components: {
514
+ schemas: {}
515
+ }
513
516
  };
514
517
  // Iterate through each protocol (e.g., REST, WS, MQ)
515
518
  Object.entries(declaration).forEach(([namespace, module]) => {
@@ -623,9 +626,30 @@ export class SFRPipeline {
623
626
  document.description = (document.description || "") + rate_limit_note;
624
627
  }
625
628
  }
629
+ // Generate schema name from namespace and endpoint
630
+ const schema_name = this.generate_schema_name(namespace, endpoint, method);
626
631
  //Insert either a parameter or a requestBody according to the method type
627
632
  if (validator_type === "joi") {
628
- document[method === "GET" ? "parameters" : "requestBody"] = j2s(validator).swagger;
633
+ const swagger_schema = j2s(validator).swagger;
634
+ if (method === "get") {
635
+ // For GET requests, convert schema properties to query parameters
636
+ document.parameters = this.convert_schema_to_query_parameters(swagger_schema, schema_name, spec);
637
+ }
638
+ else {
639
+ // For POST/PUT/PATCH/DELETE, use requestBody with application/json
640
+ // Store schema in components
641
+ spec.components.schemas[schema_name] = swagger_schema;
642
+ document.requestBody = {
643
+ required: true,
644
+ content: {
645
+ "application/json": {
646
+ schema: {
647
+ $ref: `#/components/schemas/${schema_name}`
648
+ }
649
+ }
650
+ }
651
+ };
652
+ }
629
653
  }
630
654
  // Convert Multer validators to multipart/form-data OpenAPI schema
631
655
  if (validator_type === "multer") {
@@ -644,6 +668,71 @@ export class SFRPipeline {
644
668
  spec.info.meta = Object.fromEntries(Object.entries(spec.info.meta).map(([k, v]) => [`x-${k}`, v]));
645
669
  return spec;
646
670
  }
671
+ /**
672
+ * Generates a unique schema name from namespace, endpoint, and method.
673
+ */
674
+ generate_schema_name(namespace, endpoint, method) {
675
+ // Convert to PascalCase and create a descriptive schema name
676
+ const pascal_case = (str) => str
677
+ .split(/[-_]/)
678
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
679
+ .join('');
680
+ const method_prefix = method.charAt(0).toUpperCase() + method.slice(1).toLowerCase();
681
+ const namespace_part = pascal_case(namespace);
682
+ const endpoint_part = pascal_case(endpoint);
683
+ return `${method_prefix}${namespace_part}${endpoint_part}Request`;
684
+ }
685
+ /**
686
+ * Converts a JSON schema to OpenAPI query parameters array.
687
+ * Stores complex schemas in components and references them.
688
+ */
689
+ convert_schema_to_query_parameters(schema, base_name, spec) {
690
+ const parameters = [];
691
+ if (!schema || !schema.properties) {
692
+ return parameters;
693
+ }
694
+ const required_fields = schema.required || [];
695
+ for (const [prop_name, prop_schema] of Object.entries(schema.properties)) {
696
+ const param_schema = prop_schema;
697
+ const is_required = required_fields.includes(prop_name);
698
+ // Check if the property is a complex type (object or array of objects)
699
+ const is_complex = param_schema.type === 'object' ||
700
+ (param_schema.type === 'array' && param_schema.items?.type === 'object');
701
+ if (is_complex) {
702
+ // Store complex schemas in components and reference them
703
+ const component_name = `${base_name}${prop_name.charAt(0).toUpperCase() + prop_name.slice(1)}`;
704
+ spec.components.schemas[component_name] = param_schema;
705
+ parameters.push({
706
+ name: prop_name,
707
+ in: "query",
708
+ required: is_required,
709
+ description: param_schema.description || "",
710
+ content: {
711
+ "application/json": {
712
+ schema: {
713
+ $ref: `#/components/schemas/${component_name}`
714
+ }
715
+ }
716
+ }
717
+ });
718
+ }
719
+ else {
720
+ // Simple types can be defined inline
721
+ const param = {
722
+ name: prop_name,
723
+ in: "query",
724
+ required: is_required,
725
+ schema: { ...param_schema }
726
+ };
727
+ if (param_schema.description) {
728
+ param.description = param_schema.description;
729
+ delete param.schema.description;
730
+ }
731
+ parameters.push(param);
732
+ }
733
+ }
734
+ return parameters;
735
+ }
647
736
  // Method to generate an AsyncAPI document from a WSNamespaceDeclaration
648
737
  generate_ws_async_api_document(declaration) {
649
738
  const spec = {
@@ -68,6 +68,15 @@ export declare class SFRPipeline {
68
68
  private bind_mq_fns;
69
69
  private spec_generation;
70
70
  private generate_open_api_document;
71
+ /**
72
+ * Generates a unique schema name from namespace, endpoint, and method.
73
+ */
74
+ private generate_schema_name;
75
+ /**
76
+ * Converts a JSON schema to OpenAPI query parameters array.
77
+ * Stores complex schemas in components and references them.
78
+ */
79
+ private convert_schema_to_query_parameters;
71
80
  private generate_ws_async_api_document;
72
81
  private generate_mq_async_api_document;
73
82
  private print_to_console;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@avtechno/sfr",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
4
4
  "description": "An opinionated way of writing services using ExpressJS.",
5
5
  "type": "module",
6
6
  "files": [
package/src/index.mts CHANGED
@@ -68,16 +68,27 @@ async function write_combined_openapi_index(cfg: ParserCFG, documents: ServiceDo
68
68
  const combined: any = {
69
69
  openapi: "3.1.0",
70
70
  info: {},
71
- paths: {}
71
+ paths: {},
72
+ components: {
73
+ schemas: {}
74
+ }
72
75
  };
73
76
 
74
77
  for (const [protocol, documentation] of Object.entries(documents)) {
75
78
  switch (protocol) {
76
79
  case "REST": {
77
80
  const rest_doc = documentation as OAPI_Document;
78
- // Merge REST OpenAPI info and paths into combined doc
81
+ // Merge REST OpenAPI info, paths, and components into combined doc
79
82
  combined.info = rest_doc.info;
80
83
  combined.paths = rest_doc.paths;
84
+
85
+ // Merge component schemas if they exist
86
+ if (rest_doc.components?.schemas) {
87
+ combined.components.schemas = {
88
+ ...combined.components.schemas,
89
+ ...rest_doc.components.schemas
90
+ };
91
+ }
81
92
  } break;
82
93
  }
83
94
  }
@@ -178,6 +189,8 @@ export {
178
189
  type LoggerConfig
179
190
  } from "./observability/logger.mjs";
180
191
 
192
+ export { sfr_rest_telemetry } from "./observability/middleware/rest.mjs";
193
+
181
194
  export {
182
195
  REST,
183
196
  WS,
@@ -9,23 +9,23 @@ import { trace } from "@opentelemetry/api";
9
9
 
10
10
  // Define log levels matching standard syslog levels
11
11
  const levels = {
12
- error: 0,
13
- warn: 1,
14
- info: 2,
15
- http: 3,
16
- verbose: 4,
17
- debug: 5,
18
- silly: 6
12
+ error : 0,
13
+ warn : 1,
14
+ info : 2,
15
+ http : 3,
16
+ verbose : 4,
17
+ debug : 5,
18
+ silly : 6
19
19
  };
20
20
 
21
21
  const colors = {
22
- error: "red",
23
- warn: "yellow",
24
- info: "green",
25
- http: "magenta",
26
- verbose: "cyan",
27
- debug: "blue",
28
- silly: "gray"
22
+ error : "red",
23
+ warn : "yellow",
24
+ info : "green",
25
+ http : "magenta",
26
+ verbose : "cyan",
27
+ debug : "blue",
28
+ silly : "gray"
29
29
  };
30
30
 
31
31
  winston.addColors(colors);
@@ -74,14 +74,10 @@ const pretty_format = winston.format.combine(
74
74
  winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss.SSS" }),
75
75
  winston.format.colorize({ all: true }),
76
76
  winston.format.printf((info) => {
77
- const trace_suffix = info.trace_id
78
- ? ` [${String(info.trace_id).slice(0, 8)}]`
79
- : "";
77
+ const trace_suffix = info.trace_id ? ` [${String(info.trace_id).slice(0, 8)}]` : "";
80
78
 
81
79
  let meta_str = "";
82
- const meta_keys = Object.keys(info).filter(
83
- (k) => !["level", "message", "timestamp", "trace_id", "span_id", "trace_flags", "service"].includes(k)
84
- );
80
+ const meta_keys = Object.keys(info).filter((k) => !["level", "message", "timestamp", "trace_id", "span_id", "trace_flags", "service"].includes(k));
85
81
 
86
82
  if (meta_keys.length > 0) {
87
83
  const meta_obj: Record<string, any> = {};
@@ -89,7 +85,7 @@ const pretty_format = winston.format.combine(
89
85
  meta_str = ` ${JSON.stringify(meta_obj)}`;
90
86
  }
91
87
 
92
- return `${info.timestamp} ${info.level}${trace_suffix}: ${info.message}${meta_str}`;
88
+ return `[${info.timestamp}][${info.level}]${trace_suffix}: ${info.message} > ${meta_str}`;
93
89
  })
94
90
  );
95
91
 
@@ -150,13 +146,13 @@ export function create_child_logger(meta: Record<string, any>): winston.Logger {
150
146
  * Automatically includes trace context when available.
151
147
  */
152
148
  export const sfr_logger = {
153
- error: (message: string, meta?: Record<string, any>) => get_logger().error(message, meta),
154
- warn: (message: string, meta?: Record<string, any>) => get_logger().warn(message, meta),
155
- info: (message: string, meta?: Record<string, any>) => get_logger().info(message, meta),
156
- http: (message: string, meta?: Record<string, any>) => get_logger().http(message, meta),
157
- verbose: (message: string, meta?: Record<string, any>) => get_logger().verbose(message, meta),
158
- debug: (message: string, meta?: Record<string, any>) => get_logger().debug(message, meta),
159
- silly: (message: string, meta?: Record<string, any>) => get_logger().silly(message, meta),
149
+ error : (message: string, meta?: Record<string, any>) => get_logger().error(message, meta),
150
+ warn : (message: string, meta?: Record<string, any>) => get_logger().warn(message, meta),
151
+ info : (message: string, meta?: Record<string, any>) => get_logger().info(message, meta),
152
+ http : (message: string, meta?: Record<string, any>) => get_logger().http(message, meta),
153
+ verbose : (message: string, meta?: Record<string, any>) => get_logger().verbose(message, meta),
154
+ debug : (message: string, meta?: Record<string, any>) => get_logger().debug(message, meta),
155
+ silly : (message: string, meta?: Record<string, any>) => get_logger().silly(message, meta),
160
156
 
161
157
  /**
162
158
  * Creates a child logger with additional context.
@@ -20,26 +20,26 @@ const METER_VERSION = "1.0.0";
20
20
  */
21
21
  export interface SFRMetrics {
22
22
  // REST Metrics
23
- rest_requests_total: Counter;
24
- rest_request_duration: Histogram;
25
- rest_errors_total: Counter;
26
- rest_request_size: Histogram;
27
- rest_response_size: Histogram;
23
+ rest_requests_total : Counter;
24
+ rest_request_duration : Histogram;
25
+ rest_errors_total : Counter;
26
+ rest_request_size : Histogram;
27
+ rest_response_size : Histogram;
28
28
 
29
29
  // WebSocket Metrics
30
- ws_connections_active: UpDownCounter;
31
- ws_connections_total: Counter;
32
- ws_events_total: Counter;
33
- ws_event_duration: Histogram;
34
- ws_errors_total: Counter;
30
+ ws_connections_active : UpDownCounter;
31
+ ws_connections_total : Counter;
32
+ ws_events_total : Counter;
33
+ ws_event_duration : Histogram;
34
+ ws_errors_total : Counter;
35
35
 
36
36
  // MQ Metrics
37
- mq_messages_received: Counter;
38
- mq_messages_published: Counter;
39
- mq_processing_duration: Histogram;
40
- mq_errors_total: Counter;
41
- mq_messages_rejected: Counter;
42
- mq_messages_acked: Counter;
37
+ mq_messages_received : Counter;
38
+ mq_messages_published : Counter;
39
+ mq_processing_duration : Histogram;
40
+ mq_errors_total : Counter;
41
+ mq_messages_rejected : Counter;
42
+ mq_messages_acked : Counter;
43
43
  }
44
44
 
45
45
  let sfr_metrics: SFRMetrics | null = null;
@@ -53,9 +53,7 @@ export function wrap_mq_handler(
53
53
  const ctx = trace.setSpan(parent_context, span);
54
54
 
55
55
  // Record received metric
56
- if (metrics) {
57
- metrics.mq_messages_received.add(1, { queue, pattern });
58
- }
56
+ if (metrics) metrics.mq_messages_received.add(1, { queue, pattern });
59
57
 
60
58
  try {
61
59
  await context.with(ctx, async () => {
@@ -48,7 +48,7 @@ export function sfr_rest_telemetry() {
48
48
  "http.user_agent": req.get("user-agent") ?? "unknown",
49
49
  "http.request_content_length": req.get("content-length") ?? 0,
50
50
  "sfr.protocol": "REST",
51
- "sfr.route": route
51
+ "sfr.route": route,
52
52
  }
53
53
  },
54
54
  parent_context
@@ -88,6 +88,7 @@ export class SFRPipeline {
88
88
 
89
89
  async init(base_url?: string): Promise<ServiceDocuments> {
90
90
  this.base_url = base_url;
91
+ const observability_enabled = is_observability_enabled();
91
92
 
92
93
  // Initialize observability with service config from oas_cfg
93
94
  if (SFRPipeline.observability_options.enabled !== false) {
@@ -95,12 +96,12 @@ export class SFRPipeline {
95
96
  }
96
97
 
97
98
  // Add REST telemetry middleware if enabled
98
- if (this.comms["REST"] && is_observability_enabled()) {
99
+ if (this.comms["REST"] && observability_enabled) {
99
100
  this.comms["REST"].use(sfr_rest_telemetry());
100
101
  }
101
102
 
102
103
  // Instrument Socket.IO if enabled
103
- if (this.comms["WS"] && is_observability_enabled()) {
104
+ if (this.comms["WS"] && observability_enabled) {
104
105
  instrument_socket_io(this.comms["WS"]);
105
106
  }
106
107
 
@@ -562,7 +563,10 @@ export class SFRPipeline {
562
563
  const spec: OAPI_Document = {
563
564
  openapi: "3.0.0",
564
565
  info : Object.assign({}, this.oas_cfg),
565
- paths: {}
566
+ paths: {},
567
+ components: {
568
+ schemas: {}
569
+ }
566
570
  };
567
571
 
568
572
  // Iterate through each protocol (e.g., REST, WS, MQ)
@@ -686,9 +690,32 @@ export class SFRPipeline {
686
690
  }
687
691
  }
688
692
 
693
+ // Generate schema name from namespace and endpoint
694
+ const schema_name = this.generate_schema_name(namespace, endpoint, method);
695
+
689
696
  //Insert either a parameter or a requestBody according to the method type
690
- if(validator_type === "joi"){
691
- document[method === "GET" ? "parameters" : "requestBody"] = j2s(validator).swagger;
697
+ if (validator_type === "joi") {
698
+ const swagger_schema = j2s(validator).swagger;
699
+
700
+ if (method === "get") {
701
+ // For GET requests, convert schema properties to query parameters
702
+ document.parameters = this.convert_schema_to_query_parameters(swagger_schema, schema_name, spec);
703
+ } else {
704
+ // For POST/PUT/PATCH/DELETE, use requestBody with application/json
705
+ // Store schema in components
706
+ spec.components.schemas[schema_name] = swagger_schema;
707
+
708
+ document.requestBody = {
709
+ required: true,
710
+ content: {
711
+ "application/json": {
712
+ schema: {
713
+ $ref: `#/components/schemas/${schema_name}`
714
+ }
715
+ }
716
+ }
717
+ };
718
+ }
692
719
  }
693
720
 
694
721
  // Convert Multer validators to multipart/form-data OpenAPI schema
@@ -710,6 +737,83 @@ export class SFRPipeline {
710
737
  return spec;
711
738
  }
712
739
 
740
+ /**
741
+ * Generates a unique schema name from namespace, endpoint, and method.
742
+ */
743
+ private generate_schema_name(namespace: string, endpoint: string, method: string): string {
744
+ // Convert to PascalCase and create a descriptive schema name
745
+ const pascal_case = (str: string) => str
746
+ .split(/[-_]/)
747
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
748
+ .join('');
749
+
750
+ const method_prefix = method.charAt(0).toUpperCase() + method.slice(1).toLowerCase();
751
+ const namespace_part = pascal_case(namespace);
752
+ const endpoint_part = pascal_case(endpoint);
753
+
754
+ return `${method_prefix}${namespace_part}${endpoint_part}Request`;
755
+ }
756
+
757
+ /**
758
+ * Converts a JSON schema to OpenAPI query parameters array.
759
+ * Stores complex schemas in components and references them.
760
+ */
761
+ private convert_schema_to_query_parameters(schema: any, base_name: string, spec: OAPI_Document): any[] {
762
+ const parameters: any[] = [];
763
+
764
+ if (!schema || !schema.properties) {
765
+ return parameters;
766
+ }
767
+
768
+ const required_fields = schema.required || [];
769
+
770
+ for (const [prop_name, prop_schema] of Object.entries(schema.properties)) {
771
+ const param_schema: any = prop_schema;
772
+ const is_required = required_fields.includes(prop_name);
773
+
774
+ // Check if the property is a complex type (object or array of objects)
775
+ const is_complex = param_schema.type === 'object' ||
776
+ (param_schema.type === 'array' && param_schema.items?.type === 'object');
777
+
778
+ if (is_complex) {
779
+ // Store complex schemas in components and reference them
780
+ const component_name = `${base_name}${prop_name.charAt(0).toUpperCase() + prop_name.slice(1)}`;
781
+ spec.components.schemas[component_name] = param_schema;
782
+
783
+ parameters.push({
784
+ name: prop_name,
785
+ in: "query",
786
+ required: is_required,
787
+ description: param_schema.description || "",
788
+ content: {
789
+ "application/json": {
790
+ schema: {
791
+ $ref: `#/components/schemas/${component_name}`
792
+ }
793
+ }
794
+ }
795
+ });
796
+ } else {
797
+ // Simple types can be defined inline
798
+ const param: any = {
799
+ name: prop_name,
800
+ in: "query",
801
+ required: is_required,
802
+ schema: { ...param_schema }
803
+ };
804
+
805
+ if (param_schema.description) {
806
+ param.description = param_schema.description;
807
+ delete param.schema.description;
808
+ }
809
+
810
+ parameters.push(param);
811
+ }
812
+ }
813
+
814
+ return parameters;
815
+ }
816
+
713
817
  // Method to generate an AsyncAPI document from a WSNamespaceDeclaration
714
818
  private generate_ws_async_api_document(declaration: WSNamespaceDeclaration): AsyncAPIDocument {
715
819
  const spec: any = {