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 CHANGED
@@ -58,10 +58,115 @@ npx x-openapi-flow init [--flows path] [--force] [--dry-run]
58
58
  npx x-openapi-flow apply [openapi-file] [--flows path] [--out path]
59
59
  npx x-openapi-flow diff [openapi-file] [--flows path] [--format pretty|json]
60
60
  npx x-openapi-flow lint [openapi-file] [--format pretty|json] [--config path]
61
+ npx x-openapi-flow analyze [openapi-file] [--format pretty|json] [--out path] [--merge] [--flows path]
62
+ npx x-openapi-flow generate-sdk [openapi-file] --lang typescript [--output path]
63
+ npx x-openapi-flow export-doc-flows [openapi-file] [--output path] [--format markdown|json]
64
+ npx x-openapi-flow generate-postman [openapi-file] [--output path] [--with-scripts]
65
+ npx x-openapi-flow generate-insomnia [openapi-file] [--output path]
66
+ npx x-openapi-flow generate-redoc [openapi-file] [--output path]
61
67
  npx x-openapi-flow graph <openapi-file> [--format mermaid|json]
62
68
  npx x-openapi-flow doctor [--config path]
63
69
  ```
64
70
 
71
+ ## Output Adapters
72
+
73
+ `x-openapi-flow` now supports modular output adapters that reuse the same internal flow graph:
74
+
75
+ - OpenAPI + `x-openapi-flow` -> parser -> graph builder -> adapters
76
+ - Adapters: docs (`export-doc-flows`), SDK (`generate-sdk`), Postman (`generate-postman`), Insomnia (`generate-insomnia`)
77
+ and Redoc package (`generate-redoc`)
78
+
79
+ ### Redoc/Docs Adapter (`export-doc-flows`)
80
+
81
+ ```bash
82
+ npx x-openapi-flow export-doc-flows openapi.yaml --output ./docs/api-flows.md
83
+ npx x-openapi-flow export-doc-flows openapi.yaml --format json --output ./docs/api-flows.json
84
+ ```
85
+
86
+ Generates a lifecycle page (or JSON model) with:
87
+
88
+ - Flow/Lifecycle panel per resource
89
+ - Mermaid diagram per resource
90
+ - Current state, prerequisites (`prerequisite_operation_ids`), next operations (`next_operation_id`)
91
+
92
+ ### Redoc Package Adapter (`generate-redoc`)
93
+
94
+ ```bash
95
+ npx x-openapi-flow generate-redoc openapi.yaml --output ./redoc-flow
96
+ ```
97
+
98
+ Generates a ready-to-open Redoc bundle with:
99
+
100
+ - `index.html` (Redoc + Flow/Lifecycle panel)
101
+ - `x-openapi-flow-redoc-plugin.js` (DOM enhancer)
102
+ - `flow-model.json` (flow graph model)
103
+ - copied OpenAPI spec (`openapi.yaml`/`openapi.json`)
104
+
105
+ ### Postman Adapter (`generate-postman`)
106
+
107
+ ```bash
108
+ npx x-openapi-flow generate-postman openapi.yaml --output ./x-openapi-flow.postman_collection.json --with-scripts
109
+ ```
110
+
111
+ Generates lifecycle-oriented folders/journeys and optional scripts for:
112
+
113
+ - prerequisite enforcement before request execution
114
+ - propagated operation tracking and ID persistence in collection variables
115
+
116
+ ### Insomnia Adapter (`generate-insomnia`)
117
+
118
+ ```bash
119
+ npx x-openapi-flow generate-insomnia openapi.yaml --output ./x-openapi-flow.insomnia.json
120
+ ```
121
+
122
+ Generates an Insomnia export organized by resource flow groups and ordered requests.
123
+
124
+ ## SDK Generator (`generate-sdk`)
125
+
126
+ Generate a flow-aware SDK from OpenAPI + `x-openapi-flow` metadata.
127
+
128
+ ```bash
129
+ npx x-openapi-flow generate-sdk openapi.yaml --lang typescript --output ./sdk
130
+ ```
131
+
132
+ MVP output (TypeScript):
133
+
134
+ - `src/resources/<Resource>.ts`: resource client + state classes (`PaymentAuthorized`, `PaymentCaptured`, etc.)
135
+ - `src/index.ts`: root `FlowApiClient`
136
+ - `src/http-client.ts`: pluggable HTTP client interface and fetch implementation
137
+ - `src/flow-helpers.ts`: `runFlow("authorize -> capture")`
138
+ - `flow-model.json`: intermediate model `{ resource, operations, prerequisites, nextOperations, states }`
139
+
140
+ SDK layers (resource-centric):
141
+
142
+ - Collection/service layer: `api.payments.create()`, `api.payments.retrieve(id)`, `api.payments.list()`
143
+ - Resource instance/state layer: objects expose valid lifecycle transitions (`payment.capture()`, etc.)
144
+ - Optional lifecycle helper methods at service level (`api.payments.capture(id, params, { autoPrerequisites: true })`)
145
+
146
+ Pipeline used by the generator:
147
+
148
+ - OpenAPI -> parser -> flow graph -> state machine -> templates -> SDK
149
+ - Reuses lifecycle graph logic from the validator to stay consistent with `validate`, `graph`, and `diff`
150
+ - Transition ordering uses `next_operation_id`, `prerequisite_operation_ids`, and state transitions from `x-openapi-flow`
151
+
152
+ ## Flow Analyzer (`analyze`)
153
+
154
+ Use `analyze` to bootstrap a sidecar from OpenAPI paths/operation names.
155
+
156
+ ```bash
157
+ npx x-openapi-flow analyze openapi.yaml --out openapi.x.yaml
158
+ npx x-openapi-flow analyze openapi.yaml --format json
159
+ npx x-openapi-flow analyze openapi.yaml --merge --flows openapi.x.yaml
160
+ ```
161
+
162
+ Notes:
163
+
164
+ - The output is heuristic and intended as a starting point.
165
+ - Inferred states/transitions should be reviewed and adjusted by API/domain owners.
166
+ - Default output format is `pretty`; without `--out`, the suggested sidecar is printed to stdout.
167
+ - `--merge` merges inferred data into an existing sidecar (default path or `--flows`) while preserving existing operation fields.
168
+ - In `json`, inferred transition confidence is available in `analysis.transitionConfidence`.
169
+
65
170
  `diff` now reports field-level changes for operations that already exist in the sidecar.
66
171
  In `pretty` format, this appears under `Changed details` with changed paths per operation (for example, `current_state` or `transitions[0].target_state`).
67
172
  In `json` format, this appears in `diff.changedOperationDetails`:
@@ -299,9 +404,9 @@ Field reference format:
299
404
 
300
405
  ### Swagger UI
301
406
 
302
- - There is no Swagger UI-based automated test in this repo today (tests are CLI-only).
303
- - For UI interpretation of `x-openapi-flow`, use `showExtensions: true` with the plugin at `lib/swagger-ui/x-openapi-flow-plugin.js`.
304
- - A ready HTML example is available at `examples/swagger-ui/index.html`.
407
+ - UI plugin behavior is covered by tests in `tests/plugins/plugin-ui.test.js`.
408
+ - For UI interpretation of `x-openapi-flow`, use `showExtensions: true` with the plugin at `adapters/ui/swagger-ui/x-openapi-flow-plugin.js`.
409
+ - A ready HTML example is available at `../example-project/examples/swagger-ui/index.html`.
305
410
  - The plugin renders a global **Flow Overview** (Mermaid image) near the top of the docs, plus operation-level flow cards.
306
411
 
307
412
  ![Swagger UI integration result](../docs/assets/x-openapi-flow-extension.png)
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const { loadApi } = require("../../lib/validator");
6
+ const { buildIntermediateModel } = require("../../lib/sdk-generator");
7
+ const { toTitleCase, pathToPostmanUrl, buildLifecycleSequences } = require("../shared/helpers");
8
+
9
+ function generateInsomniaWorkspace(options) {
10
+ const apiPath = path.resolve(options.apiPath);
11
+ const outputPath = path.resolve(options.outputPath || path.join(process.cwd(), "x-openapi-flow.insomnia.json"));
12
+
13
+ const api = loadApi(apiPath);
14
+ const model = buildIntermediateModel(api);
15
+
16
+ const workspaceId = "wrk_x_openapi_flow";
17
+ const resources = [
18
+ {
19
+ _id: workspaceId,
20
+ _type: "workspace",
21
+ name: "x-openapi-flow Workspace",
22
+ description: `Generated from ${apiPath}`,
23
+ scope: "collection",
24
+ },
25
+ ];
26
+
27
+ for (const resource of model.resources) {
28
+ const groupId = `fld_${resource.resourcePropertyName}`;
29
+ resources.push({
30
+ _id: groupId,
31
+ _type: "request_group",
32
+ parentId: workspaceId,
33
+ name: `${toTitleCase(resource.resourcePlural || resource.resource)} Flow`,
34
+ });
35
+
36
+ const sequences = buildLifecycleSequences(resource);
37
+ const operations = sequences.length > 0
38
+ ? Array.from(new Map(sequences.flat().map((op) => [op.operationId, op])).values())
39
+ : resource.operations.filter((operation) => operation.hasFlow);
40
+
41
+ operations.forEach((operation, index) => {
42
+ const requestId = `req_${resource.resourcePropertyName}_${index + 1}`;
43
+ resources.push({
44
+ _id: requestId,
45
+ _type: "request",
46
+ parentId: groupId,
47
+ name: operation.operationId,
48
+ method: String(operation.httpMethod || "get").toUpperCase(),
49
+ url: `{{ base_url }}${pathToPostmanUrl(operation.path, resource.resourcePropertyName)}`,
50
+ body: {},
51
+ });
52
+ });
53
+ }
54
+
55
+ const exportPayload = {
56
+ _type: "export",
57
+ __export_format: 4,
58
+ __export_date: new Date().toISOString(),
59
+ __export_source: "x-openapi-flow",
60
+ resources,
61
+ };
62
+
63
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
64
+ fs.writeFileSync(outputPath, `${JSON.stringify(exportPayload, null, 2)}\n`, "utf8");
65
+
66
+ return {
67
+ outputPath,
68
+ resources: model.resources.length,
69
+ flowCount: model.flowCount,
70
+ };
71
+ }
72
+
73
+ module.exports = { generateInsomniaWorkspace };
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const { loadApi } = require("../../lib/validator");
6
+ const { buildIntermediateModel } = require("../../lib/sdk-generator");
7
+ const { toTitleCase, pathToPostmanUrl, buildLifecycleSequences } = require("../shared/helpers");
8
+
9
+ function buildPostmanItem(operation, resource) {
10
+ const rawPath = pathToPostmanUrl(operation.path, resource.resourcePropertyName);
11
+ const urlRaw = `{{baseUrl}}${rawPath}`;
12
+
13
+ const item = {
14
+ name: operation.operationId,
15
+ request: {
16
+ method: String(operation.httpMethod || "get").toUpperCase(),
17
+ header: [
18
+ {
19
+ key: "Content-Type",
20
+ value: "application/json",
21
+ type: "text",
22
+ },
23
+ ],
24
+ url: {
25
+ raw: urlRaw,
26
+ host: ["{{baseUrl}}"],
27
+ path: rawPath.split("/").filter(Boolean),
28
+ },
29
+ },
30
+ response: [],
31
+ };
32
+
33
+ if (["POST", "PUT", "PATCH"].includes(item.request.method)) {
34
+ item.request.body = {
35
+ mode: "raw",
36
+ raw: "{}",
37
+ options: { raw: { language: "json" } },
38
+ };
39
+ }
40
+
41
+ return item;
42
+ }
43
+
44
+ function addPostmanScripts(item, operation, resource) {
45
+ const prereqs = JSON.stringify(operation.prerequisites || []);
46
+ const operationId = operation.operationId;
47
+ const idCandidateKey = `${resource.resourcePropertyName}Id`;
48
+
49
+ item.event = [
50
+ {
51
+ listen: "prerequest",
52
+ script: {
53
+ type: "text/javascript",
54
+ exec: [
55
+ `const required = ${prereqs};`,
56
+ "const executed = JSON.parse(pm.collectionVariables.get('flowExecutedOps') || '[]');",
57
+ "const missing = required.filter((operationId) => !executed.includes(operationId));",
58
+ "if (missing.length > 0) {",
59
+ ` throw new Error('Missing prerequisites for ${operationId}: ' + missing.join(', '));`,
60
+ "}",
61
+ ],
62
+ },
63
+ },
64
+ {
65
+ listen: "test",
66
+ script: {
67
+ type: "text/javascript",
68
+ exec: [
69
+ "const payload = pm.response.json ? pm.response.json() : {};",
70
+ `if (payload && payload.id) pm.collectionVariables.set('${idCandidateKey}', payload.id);`,
71
+ "const executed = JSON.parse(pm.collectionVariables.get('flowExecutedOps') || '[]');",
72
+ `if (!executed.includes('${operationId}')) executed.push('${operationId}');`,
73
+ "pm.collectionVariables.set('flowExecutedOps', JSON.stringify(executed));",
74
+ ],
75
+ },
76
+ },
77
+ ];
78
+ }
79
+
80
+ function generatePostmanCollection(options) {
81
+ const apiPath = path.resolve(options.apiPath);
82
+ const outputPath = path.resolve(options.outputPath || path.join(process.cwd(), "x-openapi-flow.postman_collection.json"));
83
+ const withScripts = options.withScripts !== false;
84
+
85
+ const api = loadApi(apiPath);
86
+ const model = buildIntermediateModel(api);
87
+
88
+ const collection = {
89
+ info: {
90
+ name: "x-openapi-flow Lifecycle Collection",
91
+ schema: "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
92
+ description: `Generated from ${apiPath}`,
93
+ },
94
+ item: [],
95
+ variable: [
96
+ { key: "baseUrl", value: "http://localhost:3000" },
97
+ { key: "flowExecutedOps", value: "[]" },
98
+ ],
99
+ };
100
+
101
+ for (const resource of model.resources) {
102
+ const sequences = buildLifecycleSequences(resource);
103
+ const folder = {
104
+ name: `${toTitleCase(resource.resourcePlural || resource.resource)} Lifecycle`,
105
+ item: [],
106
+ };
107
+
108
+ if (sequences.length === 0) {
109
+ const fallbackItems = resource.operations
110
+ .filter((operation) => operation.hasFlow)
111
+ .map((operation) => {
112
+ const item = buildPostmanItem(operation, resource);
113
+ if (withScripts) addPostmanScripts(item, operation, resource);
114
+ return item;
115
+ });
116
+ folder.item.push(...fallbackItems);
117
+ } else {
118
+ sequences.forEach((sequence, index) => {
119
+ const journey = {
120
+ name: `Journey ${index + 1}`,
121
+ item: sequence.map((operation) => {
122
+ const item = buildPostmanItem(operation, resource);
123
+ if (withScripts) addPostmanScripts(item, operation, resource);
124
+ return item;
125
+ }),
126
+ };
127
+ folder.item.push(journey);
128
+ });
129
+ }
130
+
131
+ collection.item.push(folder);
132
+ }
133
+
134
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
135
+ fs.writeFileSync(outputPath, `${JSON.stringify(collection, null, 2)}\n`, "utf8");
136
+
137
+ return {
138
+ outputPath,
139
+ resources: model.resources.length,
140
+ flowCount: model.flowCount,
141
+ withScripts,
142
+ };
143
+ }
144
+
145
+ module.exports = { generatePostmanCollection };
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const { loadApi } = require("../../lib/validator");
6
+ const { buildIntermediateModel } = require("../../lib/sdk-generator");
7
+ const { toTitleCase, buildLifecycleSequences } = require("../shared/helpers");
8
+
9
+ function buildResourceMermaid(resource) {
10
+ const flowOperations = resource.operations.filter((operation) => operation.hasFlow);
11
+ const lines = ["stateDiagram-v2", " direction LR"];
12
+
13
+ const states = new Set(flowOperations.map((operation) => operation.currentState).filter(Boolean));
14
+ for (const state of [...states].sort()) {
15
+ lines.push(` state ${state}`);
16
+ }
17
+
18
+ const edgeSet = new Set();
19
+ for (const operation of flowOperations) {
20
+ for (const next of operation.nextOperations || []) {
21
+ const targetOperation = flowOperations.find((candidate) => candidate.operationId === next.nextOperationId);
22
+ const targetState = next.targetState || (targetOperation && targetOperation.currentState);
23
+ if (!targetState || !operation.currentState) continue;
24
+
25
+ const label = next.nextOperationId
26
+ ? `${operation.methodName} -> ${next.nextOperationId}`
27
+ : operation.methodName;
28
+ const edgeKey = `${operation.currentState}::${targetState}::${label}`;
29
+ if (edgeSet.has(edgeKey)) continue;
30
+ edgeSet.add(edgeKey);
31
+ lines.push(` ${operation.currentState} --> ${targetState}: ${label}`);
32
+ }
33
+ }
34
+
35
+ return lines.join("\n");
36
+ }
37
+
38
+ function buildDocFlowsMarkdown(model, sourcePath) {
39
+ const lines = [];
40
+ lines.push("# API Flows");
41
+ lines.push("");
42
+ lines.push(`Source: ${sourcePath}`);
43
+ lines.push("");
44
+ lines.push("This page is generated from x-openapi-flow metadata.");
45
+ lines.push("");
46
+
47
+ for (const resource of model.resources) {
48
+ const displayName = toTitleCase(resource.resourcePlural || resource.resource);
49
+ lines.push(`## ${displayName} Lifecycle`);
50
+ lines.push("");
51
+ lines.push("### Flow / Lifecycle");
52
+ lines.push("");
53
+ lines.push("```mermaid");
54
+ lines.push(buildResourceMermaid(resource));
55
+ lines.push("```");
56
+ lines.push("");
57
+
58
+ const sequences = buildLifecycleSequences(resource);
59
+ if (sequences.length > 0) {
60
+ lines.push("### Journeys");
61
+ lines.push("");
62
+ sequences.forEach((sequence, index) => {
63
+ const label = sequence.map((operation) => operation.methodName).join(" -> ");
64
+ lines.push(`- Journey ${index + 1}: ${label}`);
65
+ });
66
+ lines.push("");
67
+ }
68
+
69
+ lines.push("### Operations");
70
+ lines.push("");
71
+ for (const operation of resource.operations.filter((item) => item.hasFlow)) {
72
+ lines.push(`#### ${operation.operationId}`);
73
+ lines.push(`- Endpoint: ${operation.httpMethod.toUpperCase()} ${operation.path}`);
74
+ lines.push(`- Current state: ${operation.currentState || "-"}`);
75
+ const prereqs = operation.prerequisites && operation.prerequisites.length > 0
76
+ ? operation.prerequisites.join(", ")
77
+ : "-";
78
+ lines.push(`- Prerequisites: ${prereqs}`);
79
+
80
+ const nextOps = (operation.nextOperations || [])
81
+ .map((next) => next.nextOperationId)
82
+ .filter(Boolean);
83
+ lines.push(`- Next operations: ${nextOps.length > 0 ? nextOps.join(", ") : "-"}`);
84
+ lines.push("");
85
+ }
86
+ }
87
+
88
+ return `${lines.join("\n").trimEnd()}\n`;
89
+ }
90
+
91
+ function exportDocFlows(options) {
92
+ const apiPath = path.resolve(options.apiPath);
93
+ const outputPath = path.resolve(options.outputPath || path.join(process.cwd(), "api-flows.md"));
94
+ const format = options.format || "markdown";
95
+
96
+ if (!["markdown", "json"].includes(format)) {
97
+ throw new Error(`Unsupported doc flow format '${format}'. Use 'markdown' or 'json'.`);
98
+ }
99
+
100
+ const api = loadApi(apiPath);
101
+ const model = buildIntermediateModel(api);
102
+
103
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
104
+
105
+ if (format === "json") {
106
+ fs.writeFileSync(outputPath, `${JSON.stringify(model, null, 2)}\n`, "utf8");
107
+ } else {
108
+ fs.writeFileSync(outputPath, buildDocFlowsMarkdown(model, apiPath), "utf8");
109
+ }
110
+
111
+ return {
112
+ outputPath,
113
+ format,
114
+ resources: model.resources.length,
115
+ flowCount: model.flowCount,
116
+ };
117
+ }
118
+
119
+ module.exports = { exportDocFlows };
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+
3
+ // Barrel — re-exports all output adapters from their domain modules.
4
+ // Add new adapters to the relevant domain folder and re-export here.
5
+ const { exportDocFlows } = require("./docs/doc-adapter");
6
+ const { generatePostmanCollection } = require("./collections/postman-adapter");
7
+ const { generateInsomniaWorkspace } = require("./collections/insomnia-adapter");
8
+ const { generateRedocPackage } = require("./ui/redoc-adapter");
9
+
10
+ module.exports = {
11
+ exportDocFlows,
12
+ generatePostmanCollection,
13
+ generateInsomniaWorkspace,
14
+ generateRedocPackage,
15
+ };
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+
3
+ function toTitleCase(value) {
4
+ return String(value || "")
5
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
6
+ .replace(/[_-]+/g, " ")
7
+ .trim()
8
+ .split(/\s+/)
9
+ .filter(Boolean)
10
+ .map((word) => word[0].toUpperCase() + word.slice(1).toLowerCase())
11
+ .join(" ");
12
+ }
13
+
14
+ function pathToPostmanUrl(pathTemplate, resourceKey) {
15
+ const variablePrefix = resourceKey || "resource";
16
+ return String(pathTemplate || "")
17
+ .replace(/\{([^}]+)\}/g, (_full, name) => `{{${variablePrefix}${toTitleCase(name).replace(/\s+/g, "")}}}`);
18
+ }
19
+
20
+ function buildLifecycleSequences(resource) {
21
+ const flowOperations = resource.operations.filter((operation) => operation.hasFlow);
22
+ if (flowOperations.length === 0) {
23
+ return [];
24
+ }
25
+
26
+ const byId = new Map(flowOperations.map((operation) => [operation.operationId, operation]));
27
+ const indegree = new Map(flowOperations.map((operation) => [operation.operationId, 0]));
28
+
29
+ for (const operation of flowOperations) {
30
+ for (const next of operation.nextOperations || []) {
31
+ if (next.nextOperationId && indegree.has(next.nextOperationId)) {
32
+ indegree.set(next.nextOperationId, indegree.get(next.nextOperationId) + 1);
33
+ }
34
+ }
35
+ }
36
+
37
+ const starts = flowOperations
38
+ .filter((operation) => indegree.get(operation.operationId) === 0)
39
+ .map((operation) => operation.operationId);
40
+
41
+ const roots = starts.length > 0 ? starts : [flowOperations[0].operationId];
42
+ const sequences = [];
43
+
44
+ function walk(operationId, trail, seen) {
45
+ if (!byId.has(operationId) || seen.has(operationId)) {
46
+ sequences.push(trail.slice());
47
+ return;
48
+ }
49
+
50
+ const current = byId.get(operationId);
51
+ trail.push(current);
52
+
53
+ const nextIds = (current.nextOperations || [])
54
+ .map((next) => next.nextOperationId)
55
+ .filter((nextId) => nextId && byId.has(nextId));
56
+
57
+ if (nextIds.length === 0) {
58
+ sequences.push(trail.slice());
59
+ trail.pop();
60
+ return;
61
+ }
62
+
63
+ const nextSeen = new Set(seen);
64
+ nextSeen.add(operationId);
65
+ for (const nextId of nextIds) {
66
+ walk(nextId, trail, nextSeen);
67
+ }
68
+ trail.pop();
69
+ }
70
+
71
+ for (const root of roots) {
72
+ walk(root, [], new Set());
73
+ }
74
+
75
+ const dedup = new Map();
76
+ for (const sequence of sequences) {
77
+ if (!sequence || sequence.length === 0) continue;
78
+ const key = sequence.map((operation) => operation.operationId).join("->");
79
+ if (!dedup.has(key)) {
80
+ dedup.set(key, sequence);
81
+ }
82
+ }
83
+
84
+ return [...dedup.values()];
85
+ }
86
+
87
+ module.exports = { toTitleCase, pathToPostmanUrl, buildLifecycleSequences };
@@ -0,0 +1,127 @@
1
+ (function () {
2
+ "use strict";
3
+
4
+ function createSectionTitle(text) {
5
+ var title = document.createElement("h3");
6
+ title.textContent = text;
7
+ title.style.margin = "16px 0 8px";
8
+ title.style.fontSize = "14px";
9
+ title.style.fontWeight = "700";
10
+ return title;
11
+ }
12
+
13
+ function listItem(text) {
14
+ var item = document.createElement("li");
15
+ item.textContent = text;
16
+ item.style.marginBottom = "6px";
17
+ return item;
18
+ }
19
+
20
+ function renderMermaidText(resource) {
21
+ var lines = ["stateDiagram-v2", " direction LR"];
22
+
23
+ var states = Array.from(new Set((resource.states || []).filter(Boolean))).sort();
24
+ states.forEach(function (state) {
25
+ lines.push(" state " + state);
26
+ });
27
+
28
+ (resource.operations || []).forEach(function (operation) {
29
+ (operation.nextOperations || []).forEach(function (next) {
30
+ if (!operation.currentState || !next.targetState) return;
31
+ var label = next.nextOperationId || operation.operationId;
32
+ lines.push(" " + operation.currentState + " --> " + next.targetState + ": " + label);
33
+ });
34
+ });
35
+
36
+ return lines.join("\n");
37
+ }
38
+
39
+ function renderResourceBlock(resource) {
40
+ var container = document.createElement("div");
41
+ container.style.border = "1px solid #e5e7eb";
42
+ container.style.borderRadius = "8px";
43
+ container.style.padding = "12px";
44
+ container.style.marginBottom = "14px";
45
+
46
+ var title = document.createElement("h4");
47
+ title.textContent = (resource.resourcePlural || resource.resource || "Resource") + " Lifecycle";
48
+ title.style.margin = "0 0 8px";
49
+ title.style.fontSize = "13px";
50
+ container.appendChild(title);
51
+
52
+ var mermaid = document.createElement("pre");
53
+ mermaid.textContent = renderMermaidText(resource);
54
+ mermaid.style.background = "#f8fafc";
55
+ mermaid.style.border = "1px solid #e2e8f0";
56
+ mermaid.style.padding = "8px";
57
+ mermaid.style.borderRadius = "6px";
58
+ mermaid.style.whiteSpace = "pre-wrap";
59
+ mermaid.style.fontSize = "11px";
60
+ container.appendChild(mermaid);
61
+
62
+ var operationsTitle = createSectionTitle("Operations");
63
+ operationsTitle.style.marginTop = "10px";
64
+ operationsTitle.style.fontSize = "12px";
65
+ container.appendChild(operationsTitle);
66
+
67
+ var opList = document.createElement("ul");
68
+ opList.style.paddingLeft = "16px";
69
+
70
+ (resource.operations || []).forEach(function (operation) {
71
+ if (!operation.hasFlow) return;
72
+ var prerequisites = (operation.prerequisites || []).length > 0
73
+ ? operation.prerequisites.join(", ")
74
+ : "-";
75
+ var nextOps = (operation.nextOperations || [])
76
+ .map(function (next) { return next.nextOperationId; })
77
+ .filter(Boolean);
78
+ var nextText = nextOps.length > 0 ? nextOps.join(", ") : "-";
79
+
80
+ opList.appendChild(
81
+ listItem(
82
+ operation.operationId
83
+ + " | state=" + (operation.currentState || "-")
84
+ + " | prerequisites=" + prerequisites
85
+ + " | next=" + nextText
86
+ )
87
+ );
88
+ });
89
+
90
+ container.appendChild(opList);
91
+ return container;
92
+ }
93
+
94
+ function mount(options) {
95
+ var model = options && options.model;
96
+ if (!model || !Array.isArray(model.resources) || model.resources.length === 0) {
97
+ return;
98
+ }
99
+
100
+ var target = document.querySelector(options.targetSelector || "#x-openapi-flow-panel");
101
+ if (!target) {
102
+ return;
103
+ }
104
+
105
+ target.innerHTML = "";
106
+
107
+ var heading = document.createElement("h2");
108
+ heading.textContent = "Flow / Lifecycle";
109
+ heading.style.margin = "0 0 8px";
110
+ heading.style.fontSize = "18px";
111
+ target.appendChild(heading);
112
+
113
+ var subtitle = document.createElement("p");
114
+ subtitle.textContent = "Generated from x-openapi-flow metadata.";
115
+ subtitle.style.margin = "0 0 12px";
116
+ subtitle.style.color = "#4b5563";
117
+ target.appendChild(subtitle);
118
+
119
+ model.resources.forEach(function (resource) {
120
+ target.appendChild(renderResourceBlock(resource));
121
+ });
122
+ }
123
+
124
+ window.XOpenApiFlowRedocPlugin = {
125
+ mount: mount,
126
+ };
127
+ })();