@avtechno/sfr 2.0.9 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -22,6 +22,8 @@ async function write_service_discovery(cfg, documents) {
22
22
  //Setup files in case they do not exist.
23
23
  //Setup output folder
24
24
  await fs.mkdir(path.join(cwd, cfg.out), { recursive: true });
25
+ // Write combined OpenAPI index file containing all protocols
26
+ await write_combined_openapi_index(cfg, documents);
25
27
  // Loop over each protocol
26
28
  for (let [protocol, documentation] of Object.entries(documents)) {
27
29
  //Strictly await for setup to create dirs for each protocol.
@@ -51,6 +53,38 @@ async function write_service_discovery(cfg, documents) {
51
53
  }
52
54
  }
53
55
  }
56
+ // Writes a combined openapi.yml index file containing all generated documentation
57
+ async function write_combined_openapi_index(cfg, documents) {
58
+ const combined = {
59
+ openapi: "3.1.0",
60
+ info: {},
61
+ paths: {},
62
+ components: {
63
+ schemas: {}
64
+ }
65
+ };
66
+ for (const [protocol, documentation] of Object.entries(documents)) {
67
+ switch (protocol) {
68
+ case "REST":
69
+ {
70
+ const rest_doc = documentation;
71
+ // Merge REST OpenAPI info, paths, and components into combined doc
72
+ combined.info = rest_doc.info;
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
+ }
81
+ }
82
+ break;
83
+ }
84
+ }
85
+ // Write the combined index file
86
+ await fs.writeFile(path.join(cwd, cfg.out, "openapi.yml"), yaml.dump(combined, { lineWidth: -1, indent: 2, noRefs: true }), { flag: "w+" });
87
+ }
54
88
  //Tasked with recursively creating directories and spec files.
55
89
  async function write_to_file(cfg, dir, data) {
56
90
  //Path is a slash(/) delimited string, with the end delimiter indicating the filename to be used.
@@ -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.0.9",
3
+ "version": "2.1.1",
4
4
  "description": "An opinionated way of writing services using ExpressJS.",
5
5
  "type": "module",
6
6
  "files": [
package/src/index.mts CHANGED
@@ -31,6 +31,9 @@ async function write_service_discovery(cfg: ParserCFG, documents: ServiceDocumen
31
31
  //Setup output folder
32
32
  await fs.mkdir(path.join(cwd, cfg.out), { recursive: true });
33
33
 
34
+ // Write combined OpenAPI index file containing all protocols
35
+ await write_combined_openapi_index(cfg, documents);
36
+
34
37
  // Loop over each protocol
35
38
  for (let [protocol, documentation] of Object.entries(documents)) {
36
39
  //Strictly await for setup to create dirs for each protocol.
@@ -60,6 +63,44 @@ async function write_service_discovery(cfg: ParserCFG, documents: ServiceDocumen
60
63
  }
61
64
  }
62
65
 
66
+ // Writes a combined openapi.yml index file containing all generated documentation
67
+ async function write_combined_openapi_index(cfg: ParserCFG, documents: ServiceDocuments) {
68
+ const combined: any = {
69
+ openapi: "3.1.0",
70
+ info: {},
71
+ paths: {},
72
+ components: {
73
+ schemas: {}
74
+ }
75
+ };
76
+
77
+ for (const [protocol, documentation] of Object.entries(documents)) {
78
+ switch (protocol) {
79
+ case "REST": {
80
+ const rest_doc = documentation as OAPI_Document;
81
+ // Merge REST OpenAPI info, paths, and components into combined doc
82
+ combined.info = rest_doc.info;
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
+ }
92
+ } break;
93
+ }
94
+ }
95
+
96
+ // Write the combined index file
97
+ await fs.writeFile(
98
+ path.join(cwd, cfg.out, "openapi.yml"),
99
+ yaml.dump(combined, { lineWidth: -1, indent: 2, noRefs: true }),
100
+ { flag: "w+" }
101
+ );
102
+ }
103
+
63
104
  //Tasked with recursively creating directories and spec files.
64
105
  async function write_to_file(cfg: ParserCFG, dir: string, data: object) {
65
106
  //Path is a slash(/) delimited string, with the end delimiter indicating the filename to be used.
@@ -562,7 +562,10 @@ export class SFRPipeline {
562
562
  const spec: OAPI_Document = {
563
563
  openapi: "3.0.0",
564
564
  info : Object.assign({}, this.oas_cfg),
565
- paths: {}
565
+ paths: {},
566
+ components: {
567
+ schemas: {}
568
+ }
566
569
  };
567
570
 
568
571
  // Iterate through each protocol (e.g., REST, WS, MQ)
@@ -686,9 +689,32 @@ export class SFRPipeline {
686
689
  }
687
690
  }
688
691
 
692
+ // Generate schema name from namespace and endpoint
693
+ const schema_name = this.generate_schema_name(namespace, endpoint, method);
694
+
689
695
  //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;
696
+ if (validator_type === "joi") {
697
+ const swagger_schema = j2s(validator).swagger;
698
+
699
+ if (method === "get") {
700
+ // For GET requests, convert schema properties to query parameters
701
+ document.parameters = this.convert_schema_to_query_parameters(swagger_schema, schema_name, spec);
702
+ } else {
703
+ // For POST/PUT/PATCH/DELETE, use requestBody with application/json
704
+ // Store schema in components
705
+ spec.components.schemas[schema_name] = swagger_schema;
706
+
707
+ document.requestBody = {
708
+ required: true,
709
+ content: {
710
+ "application/json": {
711
+ schema: {
712
+ $ref: `#/components/schemas/${schema_name}`
713
+ }
714
+ }
715
+ }
716
+ };
717
+ }
692
718
  }
693
719
 
694
720
  // Convert Multer validators to multipart/form-data OpenAPI schema
@@ -710,6 +736,83 @@ export class SFRPipeline {
710
736
  return spec;
711
737
  }
712
738
 
739
+ /**
740
+ * Generates a unique schema name from namespace, endpoint, and method.
741
+ */
742
+ private generate_schema_name(namespace: string, endpoint: string, method: string): string {
743
+ // Convert to PascalCase and create a descriptive schema name
744
+ const pascal_case = (str: string) => str
745
+ .split(/[-_]/)
746
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
747
+ .join('');
748
+
749
+ const method_prefix = method.charAt(0).toUpperCase() + method.slice(1).toLowerCase();
750
+ const namespace_part = pascal_case(namespace);
751
+ const endpoint_part = pascal_case(endpoint);
752
+
753
+ return `${method_prefix}${namespace_part}${endpoint_part}Request`;
754
+ }
755
+
756
+ /**
757
+ * Converts a JSON schema to OpenAPI query parameters array.
758
+ * Stores complex schemas in components and references them.
759
+ */
760
+ private convert_schema_to_query_parameters(schema: any, base_name: string, spec: OAPI_Document): any[] {
761
+ const parameters: any[] = [];
762
+
763
+ if (!schema || !schema.properties) {
764
+ return parameters;
765
+ }
766
+
767
+ const required_fields = schema.required || [];
768
+
769
+ for (const [prop_name, prop_schema] of Object.entries(schema.properties)) {
770
+ const param_schema: any = prop_schema;
771
+ const is_required = required_fields.includes(prop_name);
772
+
773
+ // Check if the property is a complex type (object or array of objects)
774
+ const is_complex = param_schema.type === 'object' ||
775
+ (param_schema.type === 'array' && param_schema.items?.type === 'object');
776
+
777
+ if (is_complex) {
778
+ // Store complex schemas in components and reference them
779
+ const component_name = `${base_name}${prop_name.charAt(0).toUpperCase() + prop_name.slice(1)}`;
780
+ spec.components.schemas[component_name] = param_schema;
781
+
782
+ parameters.push({
783
+ name: prop_name,
784
+ in: "query",
785
+ required: is_required,
786
+ description: param_schema.description || "",
787
+ content: {
788
+ "application/json": {
789
+ schema: {
790
+ $ref: `#/components/schemas/${component_name}`
791
+ }
792
+ }
793
+ }
794
+ });
795
+ } else {
796
+ // Simple types can be defined inline
797
+ const param: any = {
798
+ name: prop_name,
799
+ in: "query",
800
+ required: is_required,
801
+ schema: { ...param_schema }
802
+ };
803
+
804
+ if (param_schema.description) {
805
+ param.description = param_schema.description;
806
+ delete param.schema.description;
807
+ }
808
+
809
+ parameters.push(param);
810
+ }
811
+ }
812
+
813
+ return parameters;
814
+ }
815
+
713
816
  // Method to generate an AsyncAPI document from a WSNamespaceDeclaration
714
817
  private generate_ws_async_api_document(declaration: WSNamespaceDeclaration): AsyncAPIDocument {
715
818
  const spec: any = {