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 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
+ ![Guided graph example](../docs/assets/graph-order-guided.svg)
75
+
65
76
  ## Repository and Full Documentation
66
77
 
67
78
  - Repository: https://github.com/tiago-marques/x-openapi-flow
@@ -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
- for (const state of graph.nodes) {
709
- lines.push(` state ${state}`);
710
- }
720
+ nodes.add(to);
711
721
 
712
- for (const [from, targets] of graph.adjacency.entries()) {
713
- for (const to of targets) {
714
- lines.push(` ${from} --> ${to}`);
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: [...graph.nodes],
721
- edges: [...graph.adjacency.entries()].flatMap(([from, targets]) =>
722
- [...targets].map((to) => ({ from, to }))
723
- ),
757
+ nodes: [...nodes],
758
+ edges,
724
759
  mermaid: lines.join("\n"),
725
760
  };
726
761
  }
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "x-openapi-flow",
3
- "version": "1.1.2",
3
+ "version": "1.1.3",
4
4
  "description": "OpenAPI extension for resource workflow and lifecycle management",
5
5
  "main": "lib/validator.js",
6
6
  "repository": {
@@ -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