x-openapi-flow 1.1.2 → 1.1.3
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 +11 -0
- package/bin/x-openapi-flow.js +47 -12
- package/examples/order-api.yaml +8 -0
- package/lib/validator.js +57 -1
- package/package.json +1 -1
- package/schema/flow-schema.json +11 -0
package/README.md
CHANGED
|
@@ -56,12 +56,23 @@ Create `x-openapi-flow.config.json` in your project directory:
|
|
|
56
56
|
- OpenAPI input in `.yaml`, `.yml`, and `.json`
|
|
57
57
|
- Validation processes OAS content with the `x-openapi-flow` extension
|
|
58
58
|
|
|
59
|
+
### Optional Transition Guidance Fields
|
|
60
|
+
|
|
61
|
+
- `next_operation_id`: operationId usually called for the next state transition
|
|
62
|
+
- `prerequisite_operation_ids`: operationIds expected before a transition
|
|
63
|
+
|
|
59
64
|
## Swagger UI
|
|
60
65
|
|
|
61
66
|
- There is no Swagger UI-based automated test in this repo today (tests are CLI-only).
|
|
62
67
|
- For UI interpretation of `x-openapi-flow`, use `showExtensions: true` plus the example plugin at `examples/swagger-ui/x-openapi-flow-plugin.js`.
|
|
63
68
|
- A ready HTML example is available at `examples/swagger-ui/index.html`.
|
|
64
69
|
|
|
70
|
+
## Graph Output Example
|
|
71
|
+
|
|
72
|
+
`x-openapi-flow graph` includes transition guidance labels in Mermaid output when present (`next_operation_id`, `prerequisite_operation_ids`).
|
|
73
|
+
|
|
74
|
+

|
|
75
|
+
|
|
65
76
|
## Repository and Full Documentation
|
|
66
77
|
|
|
67
78
|
- Repository: https://github.com/tiago-marques/x-openapi-flow
|
package/bin/x-openapi-flow.js
CHANGED
|
@@ -8,7 +8,6 @@ const {
|
|
|
8
8
|
run,
|
|
9
9
|
loadApi,
|
|
10
10
|
extractFlows,
|
|
11
|
-
buildStateGraph,
|
|
12
11
|
} = require("../lib/validator");
|
|
13
12
|
|
|
14
13
|
const DEFAULT_CONFIG_NAME = "x-openapi-flow.config.json";
|
|
@@ -702,25 +701,61 @@ function runDoctor(parsed) {
|
|
|
702
701
|
function buildMermaidGraph(filePath) {
|
|
703
702
|
const api = loadApi(filePath);
|
|
704
703
|
const flows = extractFlows(api);
|
|
705
|
-
const graph = buildStateGraph(flows);
|
|
706
704
|
const lines = ["stateDiagram-v2"];
|
|
705
|
+
const nodes = new Set();
|
|
706
|
+
const edges = [];
|
|
707
|
+
const edgeSeen = new Set();
|
|
708
|
+
|
|
709
|
+
for (const { flow } of flows) {
|
|
710
|
+
nodes.add(flow.current_state);
|
|
711
|
+
|
|
712
|
+
const transitions = flow.transitions || [];
|
|
713
|
+
for (const transition of transitions) {
|
|
714
|
+
const from = flow.current_state;
|
|
715
|
+
const to = transition.target_state;
|
|
716
|
+
if (!to) {
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
707
719
|
|
|
708
|
-
|
|
709
|
-
lines.push(` state ${state}`);
|
|
710
|
-
}
|
|
720
|
+
nodes.add(to);
|
|
711
721
|
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
722
|
+
const labelParts = [];
|
|
723
|
+
if (transition.next_operation_id) {
|
|
724
|
+
labelParts.push(`next:${transition.next_operation_id}`);
|
|
725
|
+
}
|
|
726
|
+
if (
|
|
727
|
+
Array.isArray(transition.prerequisite_operation_ids) &&
|
|
728
|
+
transition.prerequisite_operation_ids.length > 0
|
|
729
|
+
) {
|
|
730
|
+
labelParts.push(`requires:${transition.prerequisite_operation_ids.join(",")}`);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const label = labelParts.join(" | ");
|
|
734
|
+
const edgeKey = `${from}::${to}::${label}`;
|
|
735
|
+
if (edgeSeen.has(edgeKey)) {
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
edgeSeen.add(edgeKey);
|
|
740
|
+
edges.push({
|
|
741
|
+
from,
|
|
742
|
+
to,
|
|
743
|
+
next_operation_id: transition.next_operation_id,
|
|
744
|
+
prerequisite_operation_ids: transition.prerequisite_operation_ids || [],
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
lines.push(` ${from} --> ${to}${label ? `: ${label}` : ""}`);
|
|
715
748
|
}
|
|
716
749
|
}
|
|
717
750
|
|
|
751
|
+
for (const state of nodes) {
|
|
752
|
+
lines.splice(1, 0, ` state ${state}`);
|
|
753
|
+
}
|
|
754
|
+
|
|
718
755
|
return {
|
|
719
756
|
flowCount: flows.length,
|
|
720
|
-
nodes: [...
|
|
721
|
-
edges
|
|
722
|
-
[...targets].map((to) => ({ from, to }))
|
|
723
|
-
),
|
|
757
|
+
nodes: [...nodes],
|
|
758
|
+
edges,
|
|
724
759
|
mermaid: lines.join("\n"),
|
|
725
760
|
};
|
|
726
761
|
}
|
package/examples/order-api.yaml
CHANGED
|
@@ -19,9 +19,11 @@ paths:
|
|
|
19
19
|
- target_state: CONFIRMED
|
|
20
20
|
condition: Stock and payment checks pass.
|
|
21
21
|
trigger_type: synchronous
|
|
22
|
+
next_operation_id: confirmOrder
|
|
22
23
|
- target_state: CANCELLED
|
|
23
24
|
condition: Validation fails before confirmation.
|
|
24
25
|
trigger_type: synchronous
|
|
26
|
+
next_operation_id: cancelOrder
|
|
25
27
|
responses:
|
|
26
28
|
"201":
|
|
27
29
|
description: Order created successfully.
|
|
@@ -39,6 +41,9 @@ paths:
|
|
|
39
41
|
- target_state: SHIPPED
|
|
40
42
|
condition: Warehouse dispatches package.
|
|
41
43
|
trigger_type: webhook
|
|
44
|
+
next_operation_id: shipOrder
|
|
45
|
+
prerequisite_operation_ids:
|
|
46
|
+
- createOrder
|
|
42
47
|
parameters:
|
|
43
48
|
- name: id
|
|
44
49
|
in: path
|
|
@@ -62,6 +67,9 @@ paths:
|
|
|
62
67
|
- target_state: DELIVERED
|
|
63
68
|
condition: Carrier confirms delivery.
|
|
64
69
|
trigger_type: webhook
|
|
70
|
+
next_operation_id: deliverOrder
|
|
71
|
+
prerequisite_operation_ids:
|
|
72
|
+
- confirmOrder
|
|
65
73
|
parameters:
|
|
66
74
|
- name: id
|
|
67
75
|
in: path
|
package/lib/validator.js
CHANGED
|
@@ -40,7 +40,7 @@ function loadApi(filePath) {
|
|
|
40
40
|
/**
|
|
41
41
|
* Extract every x-openapi-flow object found in the `paths` section of an OAS document.
|
|
42
42
|
* @param {object} api - Parsed OAS document.
|
|
43
|
-
* @returns {{ endpoint: string, flow: object }[]}
|
|
43
|
+
* @returns {{ endpoint: string, operation_id?: string, flow: object }[]}
|
|
44
44
|
*/
|
|
45
45
|
function extractFlows(api) {
|
|
46
46
|
const entries = [];
|
|
@@ -62,6 +62,7 @@ function extractFlows(api) {
|
|
|
62
62
|
if (operation && operation["x-openapi-flow"]) {
|
|
63
63
|
entries.push({
|
|
64
64
|
endpoint: `${method.toUpperCase()} ${pathKey}`,
|
|
65
|
+
operation_id: operation.operationId,
|
|
65
66
|
flow: operation["x-openapi-flow"],
|
|
66
67
|
});
|
|
67
68
|
}
|
|
@@ -147,11 +148,55 @@ function defaultResult(pathValue, ok = true) {
|
|
|
147
148
|
multiple_initial_states: [],
|
|
148
149
|
duplicate_transitions: [],
|
|
149
150
|
non_terminating_states: [],
|
|
151
|
+
invalid_operation_references: [],
|
|
150
152
|
warnings: [],
|
|
151
153
|
},
|
|
152
154
|
};
|
|
153
155
|
}
|
|
154
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Detect invalid operationId references declared in transitions.
|
|
159
|
+
* @param {{ endpoint: string, operation_id?: string, flow: object }[]} flows
|
|
160
|
+
* @returns {{ type: string, operation_id: string, declared_in: string }[]}
|
|
161
|
+
*/
|
|
162
|
+
function detectInvalidOperationReferences(flows) {
|
|
163
|
+
const knownOperationIds = new Set(
|
|
164
|
+
flows.map(({ operation_id }) => operation_id).filter(Boolean)
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const invalidReferences = [];
|
|
168
|
+
|
|
169
|
+
for (const { endpoint, flow } of flows) {
|
|
170
|
+
const transitions = flow.transitions || [];
|
|
171
|
+
|
|
172
|
+
for (const transition of transitions) {
|
|
173
|
+
if (transition.next_operation_id && !knownOperationIds.has(transition.next_operation_id)) {
|
|
174
|
+
invalidReferences.push({
|
|
175
|
+
type: "next_operation_id",
|
|
176
|
+
operation_id: transition.next_operation_id,
|
|
177
|
+
declared_in: endpoint,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const prerequisites = Array.isArray(transition.prerequisite_operation_ids)
|
|
182
|
+
? transition.prerequisite_operation_ids
|
|
183
|
+
: [];
|
|
184
|
+
|
|
185
|
+
for (const prerequisiteOperationId of prerequisites) {
|
|
186
|
+
if (!knownOperationIds.has(prerequisiteOperationId)) {
|
|
187
|
+
invalidReferences.push({
|
|
188
|
+
type: "prerequisite_operation_ids",
|
|
189
|
+
operation_id: prerequisiteOperationId,
|
|
190
|
+
declared_in: endpoint,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return invalidReferences;
|
|
198
|
+
}
|
|
199
|
+
|
|
155
200
|
/**
|
|
156
201
|
* Verify that every target_state referenced in transitions corresponds to a
|
|
157
202
|
* current_state defined in at least one endpoint of the same API.
|
|
@@ -554,6 +599,7 @@ function run(apiPath, options = {}) {
|
|
|
554
599
|
const cycle = detectCycle(graph);
|
|
555
600
|
const duplicateTransitions = detectDuplicateTransitions(flows);
|
|
556
601
|
const terminalCoverage = detectTerminalCoverage(graph);
|
|
602
|
+
const invalidOperationReferences = detectInvalidOperationReferences(flows);
|
|
557
603
|
const multipleInitialStates = initialStates.length > 1 ? initialStates : [];
|
|
558
604
|
|
|
559
605
|
if (profileConfig.runAdvanced) {
|
|
@@ -590,6 +636,15 @@ function run(apiPath, options = {}) {
|
|
|
590
636
|
);
|
|
591
637
|
}
|
|
592
638
|
|
|
639
|
+
if (profileConfig.runQuality && invalidOperationReferences.length > 0) {
|
|
640
|
+
const invalidOperationIds = [
|
|
641
|
+
...new Set(invalidOperationReferences.map((item) => item.operation_id)),
|
|
642
|
+
];
|
|
643
|
+
qualityWarnings.push(
|
|
644
|
+
`Transition operation references not found: ${invalidOperationIds.join(", ")}`
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
|
|
593
648
|
if (strictQuality && qualityWarnings.length > 0) {
|
|
594
649
|
hasErrors = true;
|
|
595
650
|
}
|
|
@@ -678,6 +733,7 @@ function run(apiPath, options = {}) {
|
|
|
678
733
|
multiple_initial_states: multipleInitialStates,
|
|
679
734
|
duplicate_transitions: duplicateTransitions,
|
|
680
735
|
non_terminating_states: terminalCoverage.non_terminating_states,
|
|
736
|
+
invalid_operation_references: invalidOperationReferences,
|
|
681
737
|
warnings: qualityWarnings,
|
|
682
738
|
},
|
|
683
739
|
};
|
package/package.json
CHANGED
package/schema/flow-schema.json
CHANGED
|
@@ -58,6 +58,17 @@
|
|
|
58
58
|
"type": "string",
|
|
59
59
|
"enum": ["synchronous", "webhook", "polling"],
|
|
60
60
|
"description": "Mechanism that triggers the state transition."
|
|
61
|
+
},
|
|
62
|
+
"next_operation_id": {
|
|
63
|
+
"type": "string",
|
|
64
|
+
"description": "operationId usually called to apply the next state transition."
|
|
65
|
+
},
|
|
66
|
+
"prerequisite_operation_ids": {
|
|
67
|
+
"type": "array",
|
|
68
|
+
"description": "operationIds that are expected to have happened before this transition.",
|
|
69
|
+
"items": {
|
|
70
|
+
"type": "string"
|
|
71
|
+
}
|
|
61
72
|
}
|
|
62
73
|
},
|
|
63
74
|
"additionalProperties": false
|