x-openapi-flow 1.3.0 → 1.3.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/README.md +108 -3
- package/adapters/collections/insomnia-adapter.js +73 -0
- package/adapters/collections/postman-adapter.js +145 -0
- package/adapters/docs/doc-adapter.js +119 -0
- package/adapters/flow-output-adapters.js +15 -0
- package/adapters/shared/helpers.js +87 -0
- package/adapters/ui/redoc/x-openapi-flow-redoc-plugin.js +127 -0
- package/adapters/ui/redoc-adapter.js +75 -0
- package/bin/x-openapi-flow.js +766 -0
- package/lib/sdk-generator.js +673 -0
- package/package.json +9 -4
- package/templates/go/README.md +3 -0
- package/templates/kotlin/README.md +3 -0
- package/templates/python/README.md +3 -0
- package/templates/typescript/flow-helpers.hbs +26 -0
- package/templates/typescript/http-client.hbs +37 -0
- package/templates/typescript/index.hbs +16 -0
- package/templates/typescript/resource.hbs +24 -0
- package/examples/swagger-ui/index.html +0 -33
- /package/{lib → adapters/ui}/swagger-ui/x-openapi-flow-plugin.js +0 -0
package/bin/x-openapi-flow.js
CHANGED
|
@@ -13,6 +13,13 @@ const {
|
|
|
13
13
|
detectInvalidOperationReferences,
|
|
14
14
|
detectTerminalCoverage,
|
|
15
15
|
} = require("../lib/validator");
|
|
16
|
+
const { generateSdk } = require("../lib/sdk-generator");
|
|
17
|
+
const {
|
|
18
|
+
exportDocFlows,
|
|
19
|
+
generatePostmanCollection,
|
|
20
|
+
generateInsomniaWorkspace,
|
|
21
|
+
generateRedocPackage,
|
|
22
|
+
} = require("../adapters/flow-output-adapters");
|
|
16
23
|
|
|
17
24
|
const DEFAULT_CONFIG_NAME = "x-openapi-flow.config.json";
|
|
18
25
|
const DEFAULT_FLOWS_FILE = "x-openapi-flow.flows.yaml";
|
|
@@ -51,6 +58,12 @@ Usage:
|
|
|
51
58
|
x-openapi-flow apply [openapi-file] [--flows path] [--out path] [--in-place]
|
|
52
59
|
x-openapi-flow diff [openapi-file] [--flows path] [--format pretty|json]
|
|
53
60
|
x-openapi-flow lint [openapi-file] [--format pretty|json] [--config path]
|
|
61
|
+
x-openapi-flow analyze [openapi-file] [--format pretty|json] [--out path] [--merge] [--flows path]
|
|
62
|
+
x-openapi-flow generate-sdk [openapi-file] --lang typescript [--output path]
|
|
63
|
+
x-openapi-flow export-doc-flows [openapi-file] [--output path] [--format markdown|json]
|
|
64
|
+
x-openapi-flow generate-postman [openapi-file] [--output path] [--with-scripts]
|
|
65
|
+
x-openapi-flow generate-insomnia [openapi-file] [--output path]
|
|
66
|
+
x-openapi-flow generate-redoc [openapi-file] [--output path]
|
|
54
67
|
x-openapi-flow graph <openapi-file> [--format mermaid|json]
|
|
55
68
|
x-openapi-flow doctor [--config path]
|
|
56
69
|
x-openapi-flow --help
|
|
@@ -70,6 +83,15 @@ Examples:
|
|
|
70
83
|
x-openapi-flow diff openapi.yaml --format json
|
|
71
84
|
x-openapi-flow lint openapi.yaml
|
|
72
85
|
x-openapi-flow lint openapi.yaml --format json
|
|
86
|
+
x-openapi-flow analyze openapi.yaml
|
|
87
|
+
x-openapi-flow analyze openapi.yaml --out openapi.x.yaml
|
|
88
|
+
x-openapi-flow analyze openapi.yaml --format json
|
|
89
|
+
x-openapi-flow analyze openapi.yaml --merge --flows openapi.x.yaml
|
|
90
|
+
x-openapi-flow generate-sdk openapi.yaml --lang typescript --output ./sdk
|
|
91
|
+
x-openapi-flow export-doc-flows openapi.yaml --output ./docs/api-flows.md
|
|
92
|
+
x-openapi-flow generate-postman openapi.yaml --output ./x-openapi-flow.postman_collection.json --with-scripts
|
|
93
|
+
x-openapi-flow generate-insomnia openapi.yaml --output ./x-openapi-flow.insomnia.json
|
|
94
|
+
x-openapi-flow generate-redoc openapi.yaml --output ./redoc-flow
|
|
73
95
|
x-openapi-flow graph examples/order-api.yaml
|
|
74
96
|
x-openapi-flow doctor
|
|
75
97
|
`);
|
|
@@ -567,6 +589,58 @@ function parseGraphArgs(args) {
|
|
|
567
589
|
return { filePath: path.resolve(positional[0]), format };
|
|
568
590
|
}
|
|
569
591
|
|
|
592
|
+
function parseAnalyzeArgs(args) {
|
|
593
|
+
const unknown = findUnknownOptions(args, ["--format", "--out", "--flows"], ["--merge"]);
|
|
594
|
+
if (unknown) {
|
|
595
|
+
return { error: `Unknown option: ${unknown}` };
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const formatOpt = getOptionValue(args, "--format");
|
|
599
|
+
if (formatOpt.error) {
|
|
600
|
+
return { error: `${formatOpt.error} Use 'pretty' or 'json'.` };
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const outOpt = getOptionValue(args, "--out");
|
|
604
|
+
if (outOpt.error) {
|
|
605
|
+
return { error: outOpt.error };
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const flowsOpt = getOptionValue(args, "--flows");
|
|
609
|
+
if (flowsOpt.error) {
|
|
610
|
+
return { error: flowsOpt.error };
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const format = formatOpt.found ? formatOpt.value : "pretty";
|
|
614
|
+
if (!["pretty", "json"].includes(format)) {
|
|
615
|
+
return { error: `Invalid --format '${format}'. Use 'pretty' or 'json'.` };
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const positional = args.filter((token, index) => {
|
|
619
|
+
if (token === "--format" || token === "--out" || token === "--flows" || token === "--merge") {
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
622
|
+
if (
|
|
623
|
+
index > 0
|
|
624
|
+
&& (args[index - 1] === "--format" || args[index - 1] === "--out" || args[index - 1] === "--flows")
|
|
625
|
+
) {
|
|
626
|
+
return false;
|
|
627
|
+
}
|
|
628
|
+
return !token.startsWith("--");
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
if (positional.length > 1) {
|
|
632
|
+
return { error: `Unexpected argument: ${positional[1]}` };
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
return {
|
|
636
|
+
openApiFile: positional[0] ? path.resolve(positional[0]) : undefined,
|
|
637
|
+
format,
|
|
638
|
+
outPath: outOpt.found ? path.resolve(outOpt.value) : undefined,
|
|
639
|
+
merge: args.includes("--merge"),
|
|
640
|
+
flowsPath: flowsOpt.found ? path.resolve(flowsOpt.value) : undefined,
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
|
|
570
644
|
function parseDoctorArgs(args) {
|
|
571
645
|
const unknown = findUnknownOptions(args, ["--config"], []);
|
|
572
646
|
if (unknown) {
|
|
@@ -583,6 +657,176 @@ function parseDoctorArgs(args) {
|
|
|
583
657
|
};
|
|
584
658
|
}
|
|
585
659
|
|
|
660
|
+
function parseGenerateSdkArgs(args) {
|
|
661
|
+
const unknown = findUnknownOptions(args, ["--lang", "--output"], []);
|
|
662
|
+
if (unknown) {
|
|
663
|
+
return { error: `Unknown option: ${unknown}` };
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const langOpt = getOptionValue(args, "--lang");
|
|
667
|
+
if (langOpt.error) {
|
|
668
|
+
return { error: `${langOpt.error} Use 'typescript'.` };
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const outputOpt = getOptionValue(args, "--output");
|
|
672
|
+
if (outputOpt.error) {
|
|
673
|
+
return { error: outputOpt.error };
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
if (!langOpt.found) {
|
|
677
|
+
return { error: "Missing --lang option. Usage: x-openapi-flow generate-sdk [openapi-file] --lang typescript [--output path]" };
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const language = langOpt.value;
|
|
681
|
+
if (language !== "typescript") {
|
|
682
|
+
return { error: `Unsupported --lang '${language}'. MVP currently supports only 'typescript'.` };
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const positional = args.filter((token, index) => {
|
|
686
|
+
if (token === "--lang" || token === "--output") {
|
|
687
|
+
return false;
|
|
688
|
+
}
|
|
689
|
+
if (index > 0 && (args[index - 1] === "--lang" || args[index - 1] === "--output")) {
|
|
690
|
+
return false;
|
|
691
|
+
}
|
|
692
|
+
return !token.startsWith("--");
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
if (positional.length > 1) {
|
|
696
|
+
return { error: `Unexpected argument: ${positional[1]}` };
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
return {
|
|
700
|
+
openApiFile: positional[0] ? path.resolve(positional[0]) : undefined,
|
|
701
|
+
language,
|
|
702
|
+
outputPath: outputOpt.found ? path.resolve(outputOpt.value) : path.resolve(process.cwd(), "sdk"),
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function parseExportDocFlowsArgs(args) {
|
|
707
|
+
const unknown = findUnknownOptions(args, ["--output", "--format"], []);
|
|
708
|
+
if (unknown) {
|
|
709
|
+
return { error: `Unknown option: ${unknown}` };
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const outputOpt = getOptionValue(args, "--output");
|
|
713
|
+
if (outputOpt.error) {
|
|
714
|
+
return { error: outputOpt.error };
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
const formatOpt = getOptionValue(args, "--format");
|
|
718
|
+
if (formatOpt.error) {
|
|
719
|
+
return { error: `${formatOpt.error} Use 'markdown' or 'json'.` };
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const format = formatOpt.found ? formatOpt.value : "markdown";
|
|
723
|
+
if (!["markdown", "json"].includes(format)) {
|
|
724
|
+
return { error: `Invalid --format '${format}'. Use 'markdown' or 'json'.` };
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const positional = args.filter((token, index) => {
|
|
728
|
+
if (token === "--output" || token === "--format") return false;
|
|
729
|
+
if (index > 0 && (args[index - 1] === "--output" || args[index - 1] === "--format")) return false;
|
|
730
|
+
return !token.startsWith("--");
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
if (positional.length > 1) {
|
|
734
|
+
return { error: `Unexpected argument: ${positional[1]}` };
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
return {
|
|
738
|
+
openApiFile: positional[0] ? path.resolve(positional[0]) : undefined,
|
|
739
|
+
outputPath: outputOpt.found ? path.resolve(outputOpt.value) : path.resolve(process.cwd(), "api-flows.md"),
|
|
740
|
+
format,
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function parseGeneratePostmanArgs(args) {
|
|
745
|
+
const unknown = findUnknownOptions(args, ["--output"], ["--with-scripts"]);
|
|
746
|
+
if (unknown) {
|
|
747
|
+
return { error: `Unknown option: ${unknown}` };
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
const outputOpt = getOptionValue(args, "--output");
|
|
751
|
+
if (outputOpt.error) {
|
|
752
|
+
return { error: outputOpt.error };
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
const positional = args.filter((token, index) => {
|
|
756
|
+
if (token === "--output" || token === "--with-scripts") return false;
|
|
757
|
+
if (index > 0 && args[index - 1] === "--output") return false;
|
|
758
|
+
return !token.startsWith("--");
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
if (positional.length > 1) {
|
|
762
|
+
return { error: `Unexpected argument: ${positional[1]}` };
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
return {
|
|
766
|
+
openApiFile: positional[0] ? path.resolve(positional[0]) : undefined,
|
|
767
|
+
outputPath: outputOpt.found
|
|
768
|
+
? path.resolve(outputOpt.value)
|
|
769
|
+
: path.resolve(process.cwd(), "x-openapi-flow.postman_collection.json"),
|
|
770
|
+
withScripts: args.includes("--with-scripts"),
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function parseGenerateInsomniaArgs(args) {
|
|
775
|
+
const unknown = findUnknownOptions(args, ["--output"], []);
|
|
776
|
+
if (unknown) {
|
|
777
|
+
return { error: `Unknown option: ${unknown}` };
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
const outputOpt = getOptionValue(args, "--output");
|
|
781
|
+
if (outputOpt.error) {
|
|
782
|
+
return { error: outputOpt.error };
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
const positional = args.filter((token, index) => {
|
|
786
|
+
if (token === "--output") return false;
|
|
787
|
+
if (index > 0 && args[index - 1] === "--output") return false;
|
|
788
|
+
return !token.startsWith("--");
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
if (positional.length > 1) {
|
|
792
|
+
return { error: `Unexpected argument: ${positional[1]}` };
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
return {
|
|
796
|
+
openApiFile: positional[0] ? path.resolve(positional[0]) : undefined,
|
|
797
|
+
outputPath: outputOpt.found
|
|
798
|
+
? path.resolve(outputOpt.value)
|
|
799
|
+
: path.resolve(process.cwd(), "x-openapi-flow.insomnia.json"),
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function parseGenerateRedocArgs(args) {
|
|
804
|
+
const unknown = findUnknownOptions(args, ["--output"], []);
|
|
805
|
+
if (unknown) {
|
|
806
|
+
return { error: `Unknown option: ${unknown}` };
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
const outputOpt = getOptionValue(args, "--output");
|
|
810
|
+
if (outputOpt.error) {
|
|
811
|
+
return { error: outputOpt.error };
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
const positional = args.filter((token, index) => {
|
|
815
|
+
if (token === "--output") return false;
|
|
816
|
+
if (index > 0 && args[index - 1] === "--output") return false;
|
|
817
|
+
return !token.startsWith("--");
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
if (positional.length > 1) {
|
|
821
|
+
return { error: `Unexpected argument: ${positional[1]}` };
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
return {
|
|
825
|
+
openApiFile: positional[0] ? path.resolve(positional[0]) : undefined,
|
|
826
|
+
outputPath: outputOpt.found ? path.resolve(outputOpt.value) : path.resolve(process.cwd(), "redoc-flow"),
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
|
|
586
830
|
function parseArgs(argv) {
|
|
587
831
|
const args = argv.slice(2);
|
|
588
832
|
const command = args[0];
|
|
@@ -611,6 +855,11 @@ function parseArgs(argv) {
|
|
|
611
855
|
return parsed.error ? parsed : { command, ...parsed };
|
|
612
856
|
}
|
|
613
857
|
|
|
858
|
+
if (command === "analyze") {
|
|
859
|
+
const parsed = parseAnalyzeArgs(commandArgs);
|
|
860
|
+
return parsed.error ? parsed : { command, ...parsed };
|
|
861
|
+
}
|
|
862
|
+
|
|
614
863
|
if (command === "apply") {
|
|
615
864
|
const parsed = parseApplyArgs(commandArgs);
|
|
616
865
|
return parsed.error ? parsed : { command, ...parsed };
|
|
@@ -631,6 +880,31 @@ function parseArgs(argv) {
|
|
|
631
880
|
return parsed.error ? parsed : { command, ...parsed };
|
|
632
881
|
}
|
|
633
882
|
|
|
883
|
+
if (command === "generate-sdk") {
|
|
884
|
+
const parsed = parseGenerateSdkArgs(commandArgs);
|
|
885
|
+
return parsed.error ? parsed : { command, ...parsed };
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
if (command === "export-doc-flows") {
|
|
889
|
+
const parsed = parseExportDocFlowsArgs(commandArgs);
|
|
890
|
+
return parsed.error ? parsed : { command, ...parsed };
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
if (command === "generate-postman") {
|
|
894
|
+
const parsed = parseGeneratePostmanArgs(commandArgs);
|
|
895
|
+
return parsed.error ? parsed : { command, ...parsed };
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
if (command === "generate-insomnia") {
|
|
899
|
+
const parsed = parseGenerateInsomniaArgs(commandArgs);
|
|
900
|
+
return parsed.error ? parsed : { command, ...parsed };
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
if (command === "generate-redoc") {
|
|
904
|
+
const parsed = parseGenerateRedocArgs(commandArgs);
|
|
905
|
+
return parsed.error ? parsed : { command, ...parsed };
|
|
906
|
+
}
|
|
907
|
+
|
|
634
908
|
return { error: `Unknown command: ${command}` };
|
|
635
909
|
}
|
|
636
910
|
|
|
@@ -1507,6 +1781,474 @@ function runGraph(parsed) {
|
|
|
1507
1781
|
}
|
|
1508
1782
|
}
|
|
1509
1783
|
|
|
1784
|
+
function toKebabCase(value) {
|
|
1785
|
+
if (!value) {
|
|
1786
|
+
return "operation";
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
const normalized = String(value)
|
|
1790
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
1791
|
+
.replace(/[^a-zA-Z0-9]+/g, "-")
|
|
1792
|
+
.replace(/-+/g, "-")
|
|
1793
|
+
.replace(/^-|-$/g, "")
|
|
1794
|
+
.toLowerCase();
|
|
1795
|
+
|
|
1796
|
+
return normalized || "operation";
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
function deriveResourceKey(pathKey) {
|
|
1800
|
+
const tokens = String(pathKey || "")
|
|
1801
|
+
.split("/")
|
|
1802
|
+
.filter((token) => token && !token.startsWith("{") && !token.endsWith("}"));
|
|
1803
|
+
|
|
1804
|
+
return tokens[0] || "root";
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
function inferCurrentState(entry) {
|
|
1808
|
+
const fingerprint = [entry.method, entry.path, entry.resolvedOperationId]
|
|
1809
|
+
.filter(Boolean)
|
|
1810
|
+
.join(" ")
|
|
1811
|
+
.toLowerCase();
|
|
1812
|
+
|
|
1813
|
+
const keywordToState = [
|
|
1814
|
+
{ match: /cancel|void/, state: "CANCELED" },
|
|
1815
|
+
{ match: /refund/, state: "REFUNDED" },
|
|
1816
|
+
{ match: /fail|error|decline|reject/, state: "FAILED" },
|
|
1817
|
+
{ match: /deliver|received/, state: "DELIVERED" },
|
|
1818
|
+
{ match: /ship|dispatch/, state: "SHIPPED" },
|
|
1819
|
+
{ match: /complete|done|close|finish/, state: "COMPLETED" },
|
|
1820
|
+
{ match: /pay|charge|collect/, state: "PAID" },
|
|
1821
|
+
{ match: /confirm|approve|accept/, state: "CONFIRMED" },
|
|
1822
|
+
{ match: /process|fulfill/, state: "PROCESSING" },
|
|
1823
|
+
{ match: /create|submit|register/, state: "CREATED" },
|
|
1824
|
+
{ match: /get|list|find|fetch|read/, state: "OBSERVED" },
|
|
1825
|
+
];
|
|
1826
|
+
|
|
1827
|
+
for (const rule of keywordToState) {
|
|
1828
|
+
if (rule.match.test(fingerprint)) {
|
|
1829
|
+
return rule.state;
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
if (entry.method === "post") {
|
|
1834
|
+
return "CREATED";
|
|
1835
|
+
}
|
|
1836
|
+
if (entry.method === "delete") {
|
|
1837
|
+
return "CANCELED";
|
|
1838
|
+
}
|
|
1839
|
+
if (entry.method === "patch" || entry.method === "put") {
|
|
1840
|
+
return "PROCESSING";
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
return "OBSERVED";
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
function inferTriggerType(fromEntry, toEntry) {
|
|
1847
|
+
const toFingerprint = `${toEntry.method} ${toEntry.path} ${toEntry.resolvedOperationId}`.toLowerCase();
|
|
1848
|
+
if (/(webhook|callback|event)/.test(toFingerprint)) {
|
|
1849
|
+
return "webhook";
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
if (fromEntry.method === "get" || fromEntry.method === "head") {
|
|
1853
|
+
return "polling";
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
return "synchronous";
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
function buildAnalyzedFlowsDoc(api) {
|
|
1860
|
+
const stateOrder = {
|
|
1861
|
+
CREATED: 10,
|
|
1862
|
+
CONFIRMED: 20,
|
|
1863
|
+
PAID: 30,
|
|
1864
|
+
PROCESSING: 40,
|
|
1865
|
+
SHIPPED: 50,
|
|
1866
|
+
DELIVERED: 60,
|
|
1867
|
+
COMPLETED: 70,
|
|
1868
|
+
REFUNDED: 80,
|
|
1869
|
+
CANCELED: 90,
|
|
1870
|
+
FAILED: 100,
|
|
1871
|
+
OBSERVED: 1000,
|
|
1872
|
+
};
|
|
1873
|
+
|
|
1874
|
+
const terminalStates = new Set(["COMPLETED", "DELIVERED", "REFUNDED", "CANCELED", "FAILED"]);
|
|
1875
|
+
const entries = extractOperationEntries(api);
|
|
1876
|
+
|
|
1877
|
+
const inferred = entries.map((entry) => ({
|
|
1878
|
+
...entry,
|
|
1879
|
+
resourceKey: deriveResourceKey(entry.path),
|
|
1880
|
+
inferredState: inferCurrentState(entry),
|
|
1881
|
+
inferredRank: stateOrder[inferCurrentState(entry)] || 1000,
|
|
1882
|
+
}));
|
|
1883
|
+
|
|
1884
|
+
const transitionInsights = [];
|
|
1885
|
+
|
|
1886
|
+
const operations = inferred.map((entry) => {
|
|
1887
|
+
const sameResource = inferred.filter((candidate) => candidate.resourceKey === entry.resourceKey);
|
|
1888
|
+
const prioritized = sameResource.length > 1 ? sameResource : inferred;
|
|
1889
|
+
|
|
1890
|
+
let transition = null;
|
|
1891
|
+
if (!terminalStates.has(entry.inferredState)) {
|
|
1892
|
+
const nextCandidates = prioritized
|
|
1893
|
+
.filter((candidate) => candidate.resolvedOperationId !== entry.resolvedOperationId)
|
|
1894
|
+
.filter((candidate) => candidate.inferredRank > entry.inferredRank)
|
|
1895
|
+
.sort((left, right) => {
|
|
1896
|
+
const rankDelta = (left.inferredRank - entry.inferredRank) - (right.inferredRank - entry.inferredRank);
|
|
1897
|
+
if (rankDelta !== 0) {
|
|
1898
|
+
return rankDelta;
|
|
1899
|
+
}
|
|
1900
|
+
return left.resolvedOperationId.localeCompare(right.resolvedOperationId);
|
|
1901
|
+
});
|
|
1902
|
+
|
|
1903
|
+
const next = nextCandidates[0];
|
|
1904
|
+
if (next) {
|
|
1905
|
+
const confidence = Math.min(
|
|
1906
|
+
0.95,
|
|
1907
|
+
0.55
|
|
1908
|
+
+ (sameResource.length > 1 ? 0.25 : 0)
|
|
1909
|
+
+ (entry.inferredRank < 1000 && next.inferredRank < 1000 ? 0.1 : 0)
|
|
1910
|
+
+ (nextCandidates.length === 1 ? 0.1 : 0)
|
|
1911
|
+
);
|
|
1912
|
+
|
|
1913
|
+
const confidenceReasons = [];
|
|
1914
|
+
if (sameResource.length > 1) {
|
|
1915
|
+
confidenceReasons.push("same_resource");
|
|
1916
|
+
}
|
|
1917
|
+
if (entry.inferredRank < 1000 && next.inferredRank < 1000) {
|
|
1918
|
+
confidenceReasons.push("known_state_progression");
|
|
1919
|
+
}
|
|
1920
|
+
if (nextCandidates.length === 1) {
|
|
1921
|
+
confidenceReasons.push("single_candidate");
|
|
1922
|
+
}
|
|
1923
|
+
if (confidenceReasons.length === 0) {
|
|
1924
|
+
confidenceReasons.push("fallback_ordering");
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
transition = {
|
|
1928
|
+
target_state: next.inferredState,
|
|
1929
|
+
trigger_type: inferTriggerType(entry, next),
|
|
1930
|
+
next_operation_id: next.resolvedOperationId,
|
|
1931
|
+
};
|
|
1932
|
+
|
|
1933
|
+
transitionInsights.push({
|
|
1934
|
+
from_operation_id: entry.resolvedOperationId,
|
|
1935
|
+
to_operation_id: next.resolvedOperationId,
|
|
1936
|
+
from_state: entry.inferredState,
|
|
1937
|
+
target_state: next.inferredState,
|
|
1938
|
+
trigger_type: transition.trigger_type,
|
|
1939
|
+
confidence: Number(confidence.toFixed(2)),
|
|
1940
|
+
confidence_reasons: confidenceReasons,
|
|
1941
|
+
});
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
return {
|
|
1946
|
+
operationId: entry.resolvedOperationId,
|
|
1947
|
+
"x-openapi-flow": {
|
|
1948
|
+
version: "1.0",
|
|
1949
|
+
id: toKebabCase(entry.resolvedOperationId),
|
|
1950
|
+
current_state: entry.inferredState,
|
|
1951
|
+
description: "Auto-generated by x-openapi-flow analyze",
|
|
1952
|
+
transitions: transition ? [transition] : [],
|
|
1953
|
+
},
|
|
1954
|
+
};
|
|
1955
|
+
});
|
|
1956
|
+
|
|
1957
|
+
return {
|
|
1958
|
+
version: "1.0",
|
|
1959
|
+
operations,
|
|
1960
|
+
analysis: {
|
|
1961
|
+
operationCount: inferred.length,
|
|
1962
|
+
uniqueStates: Array.from(new Set(inferred.map((entry) => entry.inferredState))).sort(),
|
|
1963
|
+
inferredTransitions: operations.reduce((total, operation) => {
|
|
1964
|
+
const transitions = operation["x-openapi-flow"].transitions || [];
|
|
1965
|
+
return total + transitions.length;
|
|
1966
|
+
}, 0),
|
|
1967
|
+
transitionConfidence: transitionInsights,
|
|
1968
|
+
},
|
|
1969
|
+
};
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
function mergeSidecarOperations(existingDoc, inferredDoc) {
|
|
1973
|
+
const existingOps = Array.isArray(existingDoc && existingDoc.operations)
|
|
1974
|
+
? existingDoc.operations
|
|
1975
|
+
: [];
|
|
1976
|
+
const inferredOps = Array.isArray(inferredDoc && inferredDoc.operations)
|
|
1977
|
+
? inferredDoc.operations
|
|
1978
|
+
: [];
|
|
1979
|
+
|
|
1980
|
+
const existingByOperationId = new Map();
|
|
1981
|
+
for (const operationEntry of existingOps) {
|
|
1982
|
+
if (operationEntry && operationEntry.operationId) {
|
|
1983
|
+
existingByOperationId.set(operationEntry.operationId, operationEntry);
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
const mergedOps = [];
|
|
1988
|
+
const consumedExisting = new Set();
|
|
1989
|
+
|
|
1990
|
+
for (const inferredEntry of inferredOps) {
|
|
1991
|
+
const existingEntry = existingByOperationId.get(inferredEntry.operationId);
|
|
1992
|
+
if (!existingEntry) {
|
|
1993
|
+
mergedOps.push(inferredEntry);
|
|
1994
|
+
continue;
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
consumedExisting.add(inferredEntry.operationId);
|
|
1998
|
+
|
|
1999
|
+
const existingFlow = existingEntry["x-openapi-flow"] || {};
|
|
2000
|
+
const inferredFlow = inferredEntry["x-openapi-flow"] || {};
|
|
2001
|
+
const existingTransitions = Array.isArray(existingFlow.transitions) ? existingFlow.transitions : [];
|
|
2002
|
+
|
|
2003
|
+
mergedOps.push({
|
|
2004
|
+
operationId: inferredEntry.operationId,
|
|
2005
|
+
"x-openapi-flow": {
|
|
2006
|
+
...inferredFlow,
|
|
2007
|
+
...existingFlow,
|
|
2008
|
+
transitions: existingTransitions.length > 0
|
|
2009
|
+
? existingTransitions
|
|
2010
|
+
: (Array.isArray(inferredFlow.transitions) ? inferredFlow.transitions : []),
|
|
2011
|
+
},
|
|
2012
|
+
});
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
for (const existingEntry of existingOps) {
|
|
2016
|
+
if (!existingEntry || !existingEntry.operationId) {
|
|
2017
|
+
continue;
|
|
2018
|
+
}
|
|
2019
|
+
if (!consumedExisting.has(existingEntry.operationId)) {
|
|
2020
|
+
mergedOps.push(existingEntry);
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
return {
|
|
2025
|
+
version: "1.0",
|
|
2026
|
+
operations: mergedOps,
|
|
2027
|
+
mergeStats: {
|
|
2028
|
+
existingOperations: existingOps.length,
|
|
2029
|
+
inferredOperations: inferredOps.length,
|
|
2030
|
+
mergedOperations: mergedOps.length,
|
|
2031
|
+
preservedExistingOnly: Math.max(0, mergedOps.length - inferredOps.length),
|
|
2032
|
+
},
|
|
2033
|
+
};
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
function runAnalyze(parsed) {
|
|
2037
|
+
const targetOpenApiFile = parsed.openApiFile || findOpenApiFile(process.cwd());
|
|
2038
|
+
if (!targetOpenApiFile) {
|
|
2039
|
+
console.error("ERROR: Could not find an existing OpenAPI file in this repository.");
|
|
2040
|
+
console.error("Expected one of: openapi.yaml|yml|json, swagger.yaml|yml|json");
|
|
2041
|
+
return 1;
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
let api;
|
|
2045
|
+
try {
|
|
2046
|
+
api = loadApi(targetOpenApiFile);
|
|
2047
|
+
} catch (err) {
|
|
2048
|
+
console.error(`ERROR: Could not parse OpenAPI file — ${err.message}`);
|
|
2049
|
+
return 1;
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
const analysisResult = buildAnalyzedFlowsDoc(api);
|
|
2053
|
+
let sidecarDoc = {
|
|
2054
|
+
version: analysisResult.version,
|
|
2055
|
+
operations: analysisResult.operations,
|
|
2056
|
+
};
|
|
2057
|
+
let mergeStats = null;
|
|
2058
|
+
|
|
2059
|
+
if (parsed.merge) {
|
|
2060
|
+
const mergeFlowsPath = resolveFlowsPath(targetOpenApiFile, parsed.flowsPath);
|
|
2061
|
+
let existingFlows = { version: "1.0", operations: [] };
|
|
2062
|
+
if (fs.existsSync(mergeFlowsPath)) {
|
|
2063
|
+
try {
|
|
2064
|
+
existingFlows = readFlowsFile(mergeFlowsPath);
|
|
2065
|
+
} catch (err) {
|
|
2066
|
+
console.error(`ERROR: Could not parse flows file for merge — ${err.message}`);
|
|
2067
|
+
return 1;
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
const merged = mergeSidecarOperations(existingFlows, sidecarDoc);
|
|
2072
|
+
sidecarDoc = {
|
|
2073
|
+
version: merged.version,
|
|
2074
|
+
operations: merged.operations,
|
|
2075
|
+
};
|
|
2076
|
+
mergeStats = {
|
|
2077
|
+
enabled: true,
|
|
2078
|
+
flowsPath: mergeFlowsPath,
|
|
2079
|
+
...merged.mergeStats,
|
|
2080
|
+
};
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
if (parsed.outPath) {
|
|
2084
|
+
writeFlowsFile(parsed.outPath, sidecarDoc);
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
if (parsed.format === "json") {
|
|
2088
|
+
console.log(JSON.stringify({
|
|
2089
|
+
openApiFile: targetOpenApiFile,
|
|
2090
|
+
outputPath: parsed.outPath || null,
|
|
2091
|
+
merge: mergeStats || { enabled: false },
|
|
2092
|
+
analysis: analysisResult.analysis,
|
|
2093
|
+
sidecar: sidecarDoc,
|
|
2094
|
+
}, null, 2));
|
|
2095
|
+
return 0;
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
console.log(`Analyzed OpenAPI source: ${targetOpenApiFile}`);
|
|
2099
|
+
console.log(`Inferred operations: ${analysisResult.analysis.operationCount}`);
|
|
2100
|
+
console.log(`Inferred transitions: ${analysisResult.analysis.inferredTransitions}`);
|
|
2101
|
+
console.log(`States: ${analysisResult.analysis.uniqueStates.join(", ") || "-"}`);
|
|
2102
|
+
if (mergeStats && mergeStats.enabled) {
|
|
2103
|
+
console.log(`Merged with sidecar: ${mergeStats.flowsPath}`);
|
|
2104
|
+
console.log(`Merged operations total: ${mergeStats.mergedOperations}`);
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
if (parsed.outPath) {
|
|
2108
|
+
console.log(`Suggested sidecar written to: ${parsed.outPath}`);
|
|
2109
|
+
return 0;
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
console.log("---");
|
|
2113
|
+
console.log(yaml.dump(sidecarDoc, { noRefs: true, lineWidth: -1 }).trimEnd());
|
|
2114
|
+
return 0;
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
function runGenerateSdk(parsed) {
|
|
2118
|
+
const targetOpenApiFile = parsed.openApiFile || findOpenApiFile(process.cwd());
|
|
2119
|
+
if (!targetOpenApiFile) {
|
|
2120
|
+
console.error("ERROR: Could not find an existing OpenAPI file in this repository.");
|
|
2121
|
+
console.error("Expected one of: openapi.yaml|yml|json, swagger.yaml|yml|json");
|
|
2122
|
+
return 1;
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
try {
|
|
2126
|
+
const result = generateSdk({
|
|
2127
|
+
apiPath: targetOpenApiFile,
|
|
2128
|
+
language: parsed.language,
|
|
2129
|
+
outputDir: parsed.outputPath,
|
|
2130
|
+
});
|
|
2131
|
+
|
|
2132
|
+
console.log(`OpenAPI source: ${targetOpenApiFile}`);
|
|
2133
|
+
console.log(`SDK language: ${result.language}`);
|
|
2134
|
+
console.log(`Output directory: ${result.outputDir}`);
|
|
2135
|
+
console.log(`Flow definitions processed: ${result.flowCount}`);
|
|
2136
|
+
console.log(`Resources generated: ${result.resourceCount}`);
|
|
2137
|
+
for (const resource of result.resources) {
|
|
2138
|
+
console.log(`- ${resource.name}: operations=${resource.operations}, states=${resource.states}, initial=[${resource.initialStates.join(", ")}]`);
|
|
2139
|
+
}
|
|
2140
|
+
return 0;
|
|
2141
|
+
} catch (err) {
|
|
2142
|
+
console.error(`ERROR: Could not generate SDK — ${err.message}`);
|
|
2143
|
+
return 1;
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
function runExportDocFlows(parsed) {
|
|
2148
|
+
const targetOpenApiFile = parsed.openApiFile || findOpenApiFile(process.cwd());
|
|
2149
|
+
if (!targetOpenApiFile) {
|
|
2150
|
+
console.error("ERROR: Could not find an existing OpenAPI file in this repository.");
|
|
2151
|
+
console.error("Expected one of: openapi.yaml|yml|json, swagger.yaml|yml|json");
|
|
2152
|
+
return 1;
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
try {
|
|
2156
|
+
const result = exportDocFlows({
|
|
2157
|
+
apiPath: targetOpenApiFile,
|
|
2158
|
+
outputPath: parsed.outputPath,
|
|
2159
|
+
format: parsed.format,
|
|
2160
|
+
});
|
|
2161
|
+
|
|
2162
|
+
console.log(`OpenAPI source: ${targetOpenApiFile}`);
|
|
2163
|
+
console.log(`Output: ${result.outputPath}`);
|
|
2164
|
+
console.log(`Format: ${result.format}`);
|
|
2165
|
+
console.log(`Resources: ${result.resources}`);
|
|
2166
|
+
console.log(`Flow definitions: ${result.flowCount}`);
|
|
2167
|
+
return 0;
|
|
2168
|
+
} catch (err) {
|
|
2169
|
+
console.error(`ERROR: Could not export doc flows — ${err.message}`);
|
|
2170
|
+
return 1;
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
function runGeneratePostman(parsed) {
|
|
2175
|
+
const targetOpenApiFile = parsed.openApiFile || findOpenApiFile(process.cwd());
|
|
2176
|
+
if (!targetOpenApiFile) {
|
|
2177
|
+
console.error("ERROR: Could not find an existing OpenAPI file in this repository.");
|
|
2178
|
+
console.error("Expected one of: openapi.yaml|yml|json, swagger.yaml|yml|json");
|
|
2179
|
+
return 1;
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
try {
|
|
2183
|
+
const result = generatePostmanCollection({
|
|
2184
|
+
apiPath: targetOpenApiFile,
|
|
2185
|
+
outputPath: parsed.outputPath,
|
|
2186
|
+
withScripts: parsed.withScripts,
|
|
2187
|
+
});
|
|
2188
|
+
|
|
2189
|
+
console.log(`OpenAPI source: ${targetOpenApiFile}`);
|
|
2190
|
+
console.log(`Output: ${result.outputPath}`);
|
|
2191
|
+
console.log(`Resources: ${result.resources}`);
|
|
2192
|
+
console.log(`Flow definitions: ${result.flowCount}`);
|
|
2193
|
+
console.log(`Scripts enabled: ${result.withScripts}`);
|
|
2194
|
+
return 0;
|
|
2195
|
+
} catch (err) {
|
|
2196
|
+
console.error(`ERROR: Could not generate Postman collection — ${err.message}`);
|
|
2197
|
+
return 1;
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
function runGenerateInsomnia(parsed) {
|
|
2202
|
+
const targetOpenApiFile = parsed.openApiFile || findOpenApiFile(process.cwd());
|
|
2203
|
+
if (!targetOpenApiFile) {
|
|
2204
|
+
console.error("ERROR: Could not find an existing OpenAPI file in this repository.");
|
|
2205
|
+
console.error("Expected one of: openapi.yaml|yml|json, swagger.yaml|yml|json");
|
|
2206
|
+
return 1;
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
try {
|
|
2210
|
+
const result = generateInsomniaWorkspace({
|
|
2211
|
+
apiPath: targetOpenApiFile,
|
|
2212
|
+
outputPath: parsed.outputPath,
|
|
2213
|
+
});
|
|
2214
|
+
|
|
2215
|
+
console.log(`OpenAPI source: ${targetOpenApiFile}`);
|
|
2216
|
+
console.log(`Output: ${result.outputPath}`);
|
|
2217
|
+
console.log(`Resources: ${result.resources}`);
|
|
2218
|
+
console.log(`Flow definitions: ${result.flowCount}`);
|
|
2219
|
+
return 0;
|
|
2220
|
+
} catch (err) {
|
|
2221
|
+
console.error(`ERROR: Could not generate Insomnia workspace — ${err.message}`);
|
|
2222
|
+
return 1;
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
function runGenerateRedoc(parsed) {
|
|
2227
|
+
const targetOpenApiFile = parsed.openApiFile || findOpenApiFile(process.cwd());
|
|
2228
|
+
if (!targetOpenApiFile) {
|
|
2229
|
+
console.error("ERROR: Could not find an existing OpenAPI file in this repository.");
|
|
2230
|
+
console.error("Expected one of: openapi.yaml|yml|json, swagger.yaml|yml|json");
|
|
2231
|
+
return 1;
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
try {
|
|
2235
|
+
const result = generateRedocPackage({
|
|
2236
|
+
apiPath: targetOpenApiFile,
|
|
2237
|
+
outputDir: parsed.outputPath,
|
|
2238
|
+
});
|
|
2239
|
+
|
|
2240
|
+
console.log(`OpenAPI source: ${targetOpenApiFile}`);
|
|
2241
|
+
console.log(`Output directory: ${result.outputDir}`);
|
|
2242
|
+
console.log(`Redoc index: ${result.indexPath}`);
|
|
2243
|
+
console.log(`Resources: ${result.resources}`);
|
|
2244
|
+
console.log(`Flow definitions: ${result.flowCount}`);
|
|
2245
|
+
return 0;
|
|
2246
|
+
} catch (err) {
|
|
2247
|
+
console.error(`ERROR: Could not generate Redoc package — ${err.message}`);
|
|
2248
|
+
return 1;
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
|
|
1510
2252
|
function main() {
|
|
1511
2253
|
const parsed = parseArgs(process.argv);
|
|
1512
2254
|
|
|
@@ -1534,6 +2276,30 @@ function main() {
|
|
|
1534
2276
|
process.exit(runGraph(parsed));
|
|
1535
2277
|
}
|
|
1536
2278
|
|
|
2279
|
+
if (parsed.command === "analyze") {
|
|
2280
|
+
process.exit(runAnalyze(parsed));
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
if (parsed.command === "generate-sdk") {
|
|
2284
|
+
process.exit(runGenerateSdk(parsed));
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
if (parsed.command === "export-doc-flows") {
|
|
2288
|
+
process.exit(runExportDocFlows(parsed));
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
if (parsed.command === "generate-postman") {
|
|
2292
|
+
process.exit(runGeneratePostman(parsed));
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
if (parsed.command === "generate-insomnia") {
|
|
2296
|
+
process.exit(runGenerateInsomnia(parsed));
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
if (parsed.command === "generate-redoc") {
|
|
2300
|
+
process.exit(runGenerateRedoc(parsed));
|
|
2301
|
+
}
|
|
2302
|
+
|
|
1537
2303
|
if (parsed.command === "apply") {
|
|
1538
2304
|
process.exit(runApply(parsed));
|
|
1539
2305
|
}
|