x-openapi-flow 1.2.2 → 1.3.0
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 +212 -18
- package/bin/x-openapi-flow.js +709 -22
- package/examples/swagger-ui/index.html +1 -1
- package/lib/swagger-ui/x-openapi-flow-plugin.js +856 -0
- package/lib/validator.js +36 -3
- package/package.json +3 -2
- package/schema/flow-schema.json +2 -2
- package/examples/swagger-ui/x-openapi-flow-plugin.js +0 -446
package/lib/validator.js
CHANGED
|
@@ -169,6 +169,8 @@ function getOperationsById(api) {
|
|
|
169
169
|
|
|
170
170
|
operationsById.set(operation.operationId, {
|
|
171
171
|
operation,
|
|
172
|
+
path_item: pathItem,
|
|
173
|
+
path_key: pathKey,
|
|
172
174
|
endpoint: `${method.toUpperCase()} ${pathKey}`,
|
|
173
175
|
});
|
|
174
176
|
}
|
|
@@ -224,15 +226,15 @@ function parseFieldReference(refValue) {
|
|
|
224
226
|
return null;
|
|
225
227
|
}
|
|
226
228
|
|
|
227
|
-
const match = refValue.match(/^([^:]+):(request\.body|response\.(\d{3}|default)\.body)\.(.+)$/);
|
|
229
|
+
const match = refValue.match(/^([^:]+):(request\.(body|path)|response\.(\d{3}|default)\.body)\.(.+)$/);
|
|
228
230
|
if (!match) {
|
|
229
231
|
return null;
|
|
230
232
|
}
|
|
231
233
|
|
|
232
234
|
const operationId = match[1];
|
|
233
235
|
const scope = match[2];
|
|
234
|
-
const responseCode = match[
|
|
235
|
-
const fieldPath = match[
|
|
236
|
+
const responseCode = match[4];
|
|
237
|
+
const fieldPath = match[5];
|
|
236
238
|
|
|
237
239
|
return {
|
|
238
240
|
operation_id: operationId,
|
|
@@ -332,6 +334,34 @@ function resolveFieldReferenceSchema(api, operationsById, parsedRef) {
|
|
|
332
334
|
return { schema: requestSchema };
|
|
333
335
|
}
|
|
334
336
|
|
|
337
|
+
if (parsedRef.scope === "request.path") {
|
|
338
|
+
const pathLevelParams = Array.isArray(operationInfo.path_item && operationInfo.path_item.parameters)
|
|
339
|
+
? operationInfo.path_item.parameters
|
|
340
|
+
: [];
|
|
341
|
+
const operationLevelParams = Array.isArray(operation.parameters)
|
|
342
|
+
? operation.parameters
|
|
343
|
+
: [];
|
|
344
|
+
|
|
345
|
+
const allParams = [...pathLevelParams, ...operationLevelParams].filter(
|
|
346
|
+
(param) => param && typeof param === "object" && param.in === "path" && param.name
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
if (!allParams.length) {
|
|
350
|
+
return { error: "path_parameters_not_found" };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const pathSchema = {
|
|
354
|
+
type: "object",
|
|
355
|
+
properties: {},
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
for (const param of allParams) {
|
|
359
|
+
pathSchema.properties[param.name] = param.schema || { type: "string" };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return { schema: pathSchema };
|
|
363
|
+
}
|
|
364
|
+
|
|
335
365
|
const responseCode = parsedRef.response_code;
|
|
336
366
|
const responseSchema = operation.responses
|
|
337
367
|
&& operation.responses[responseCode]
|
|
@@ -985,5 +1015,8 @@ module.exports = {
|
|
|
985
1015
|
validateFlows,
|
|
986
1016
|
detectOrphanStates,
|
|
987
1017
|
buildStateGraph,
|
|
1018
|
+
detectDuplicateTransitions,
|
|
1019
|
+
detectInvalidOperationReferences,
|
|
1020
|
+
detectTerminalCoverage,
|
|
988
1021
|
run,
|
|
989
1022
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "x-openapi-flow",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "OpenAPI extension for resource workflow and lifecycle management",
|
|
5
5
|
"main": "lib/validator.js",
|
|
6
6
|
"repository": {
|
|
@@ -29,8 +29,9 @@
|
|
|
29
29
|
"x-openapi-flow": "bin/x-openapi-flow.js"
|
|
30
30
|
},
|
|
31
31
|
"scripts": {
|
|
32
|
-
"test": "npm run test:cli && npm run test:smoke",
|
|
32
|
+
"test": "npm run test:cli && npm run test:ui && npm run test:smoke",
|
|
33
33
|
"test:cli": "node --test tests/cli.test.js",
|
|
34
|
+
"test:ui": "node --test tests/plugin-ui.test.js",
|
|
34
35
|
"test:smoke": "node bin/x-openapi-flow.js validate examples/payment-api.yaml --profile strict"
|
|
35
36
|
},
|
|
36
37
|
"keywords": [
|
package/schema/flow-schema.json
CHANGED
|
@@ -72,14 +72,14 @@
|
|
|
72
72
|
},
|
|
73
73
|
"prerequisite_field_refs": {
|
|
74
74
|
"type": "array",
|
|
75
|
-
"description": "Field references required before this transition. Format: operationId:request.body.field or operationId:response.<status>.body.field",
|
|
75
|
+
"description": "Field references required before this transition. Format: operationId:request.body.field, operationId:request.path.paramName, or operationId:response.<status>.body.field",
|
|
76
76
|
"items": {
|
|
77
77
|
"type": "string"
|
|
78
78
|
}
|
|
79
79
|
},
|
|
80
80
|
"propagated_field_refs": {
|
|
81
81
|
"type": "array",
|
|
82
|
-
"description": "Field references produced/propagated for downstream flows. Format: operationId:request.body.field or operationId:response.<status>.body.field",
|
|
82
|
+
"description": "Field references produced/propagated for downstream flows. Format: operationId:request.body.field, operationId:request.path.paramName, or operationId:response.<status>.body.field",
|
|
83
83
|
"items": {
|
|
84
84
|
"type": "string"
|
|
85
85
|
}
|
|
@@ -1,446 +0,0 @@
|
|
|
1
|
-
window.XOpenApiFlowPlugin = function () {
|
|
2
|
-
const h = React.createElement;
|
|
3
|
-
|
|
4
|
-
function toPlain(value) {
|
|
5
|
-
if (!value) return value;
|
|
6
|
-
return value.toJS ? value.toJS() : value;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
function text(value) {
|
|
10
|
-
if (value === null || value === undefined || value === "") return "-";
|
|
11
|
-
if (Array.isArray(value)) return value.length ? value.join(", ") : "-";
|
|
12
|
-
return String(value);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function transitionsList(currentState, transitions) {
|
|
16
|
-
if (!Array.isArray(transitions) || transitions.length === 0) {
|
|
17
|
-
return h("div", { style: { opacity: 0.85, fontStyle: "italic" } }, "No transitions (terminal state)");
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
return h(
|
|
21
|
-
"ul",
|
|
22
|
-
{ style: { margin: "6px 0 0 18px", padding: 0 } },
|
|
23
|
-
transitions.map((transition, index) =>
|
|
24
|
-
h(
|
|
25
|
-
"li",
|
|
26
|
-
{ key: `${currentState}-${index}`, style: { marginBottom: "4px", lineHeight: 1.45 } },
|
|
27
|
-
h("strong", null, text(transition.trigger_type)),
|
|
28
|
-
" → ",
|
|
29
|
-
h("strong", null, text(transition.target_state)),
|
|
30
|
-
transition.condition ? ` — ${text(transition.condition)}` : "",
|
|
31
|
-
transition.next_operation_id ? ` (next: ${text(transition.next_operation_id)})` : ""
|
|
32
|
-
)
|
|
33
|
-
)
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function miniGraph(currentState, transitions) {
|
|
38
|
-
if (!Array.isArray(transitions) || transitions.length === 0) {
|
|
39
|
-
return [h("div", { key: "terminal", style: { fontFamily: "monospace" } }, `${text(currentState)} [terminal]`)];
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return transitions.map((transition, index) =>
|
|
43
|
-
h(
|
|
44
|
-
"div",
|
|
45
|
-
{ key: `edge-${index}`, style: { fontFamily: "monospace", lineHeight: 1.45 } },
|
|
46
|
-
`${text(currentState)} --> ${text(transition.target_state)} [${text(transition.trigger_type)}]`
|
|
47
|
-
)
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return {
|
|
52
|
-
wrapComponents: {
|
|
53
|
-
OperationSummary: (Original) => (props) => {
|
|
54
|
-
const operation = props.operation;
|
|
55
|
-
const flow = operation && operation.get && operation.get("x-openapi-flow");
|
|
56
|
-
|
|
57
|
-
if (!flow) {
|
|
58
|
-
return h(Original, props);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const flowObject = toPlain(flow) || {};
|
|
62
|
-
const currentState = flowObject.current_state;
|
|
63
|
-
const transitions = Array.isArray(flowObject.transitions) ? flowObject.transitions : [];
|
|
64
|
-
const graphImageUrl = flowObject.graph_image_url || window.XOpenApiFlowGraphImageUrl;
|
|
65
|
-
|
|
66
|
-
const metadataGrid = h(
|
|
67
|
-
"div",
|
|
68
|
-
{
|
|
69
|
-
style: {
|
|
70
|
-
display: "grid",
|
|
71
|
-
gridTemplateColumns: "140px 1fr",
|
|
72
|
-
gap: "4px 10px",
|
|
73
|
-
fontSize: "12px",
|
|
74
|
-
marginTop: "6px",
|
|
75
|
-
},
|
|
76
|
-
},
|
|
77
|
-
h("div", { style: { opacity: 0.85 } }, "version"),
|
|
78
|
-
h("div", null, text(flowObject.version)),
|
|
79
|
-
h("div", { style: { opacity: 0.85 } }, "id"),
|
|
80
|
-
h("div", null, text(flowObject.id)),
|
|
81
|
-
h("div", { style: { opacity: 0.85 } }, "current_state"),
|
|
82
|
-
h("div", null, text(currentState))
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
const graphImageNode = graphImageUrl
|
|
86
|
-
? h(
|
|
87
|
-
"div",
|
|
88
|
-
{ style: { marginTop: "10px" } },
|
|
89
|
-
h("div", { style: { fontWeight: 700, marginBottom: "6px" } }, "Flow graph image"),
|
|
90
|
-
h("img", {
|
|
91
|
-
src: graphImageUrl,
|
|
92
|
-
alt: "x-openapi-flow graph",
|
|
93
|
-
style: {
|
|
94
|
-
width: "100%",
|
|
95
|
-
maxWidth: "560px",
|
|
96
|
-
border: "1px solid rgba(255,255,255,0.3)",
|
|
97
|
-
borderRadius: "6px",
|
|
98
|
-
},
|
|
99
|
-
})
|
|
100
|
-
)
|
|
101
|
-
: null;
|
|
102
|
-
|
|
103
|
-
return h(
|
|
104
|
-
"div",
|
|
105
|
-
null,
|
|
106
|
-
h(Original, props),
|
|
107
|
-
h(
|
|
108
|
-
"div",
|
|
109
|
-
{
|
|
110
|
-
style: {
|
|
111
|
-
marginTop: "8px",
|
|
112
|
-
padding: "10px",
|
|
113
|
-
border: "1px solid rgba(255,255,255,0.28)",
|
|
114
|
-
borderRadius: "8px",
|
|
115
|
-
background: "rgba(0,0,0,0.12)",
|
|
116
|
-
fontSize: "12px",
|
|
117
|
-
},
|
|
118
|
-
},
|
|
119
|
-
h("div", { style: { fontWeight: 700 } }, "x-openapi-flow"),
|
|
120
|
-
metadataGrid,
|
|
121
|
-
h("div", { style: { marginTop: "10px", fontWeight: 700 } }, "Transitions"),
|
|
122
|
-
transitionsList(currentState, transitions),
|
|
123
|
-
h("div", { style: { marginTop: "10px", fontWeight: 700 } }, "Flow graph (operation-level)"),
|
|
124
|
-
h(
|
|
125
|
-
"div",
|
|
126
|
-
{
|
|
127
|
-
style: {
|
|
128
|
-
marginTop: "6px",
|
|
129
|
-
border: "1px dashed rgba(255,255,255,0.32)",
|
|
130
|
-
borderRadius: "6px",
|
|
131
|
-
padding: "8px",
|
|
132
|
-
},
|
|
133
|
-
},
|
|
134
|
-
...miniGraph(currentState, transitions)
|
|
135
|
-
),
|
|
136
|
-
graphImageNode
|
|
137
|
-
)
|
|
138
|
-
);
|
|
139
|
-
},
|
|
140
|
-
},
|
|
141
|
-
};
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
(function () {
|
|
145
|
-
const styleId = 'x-openapi-flow-ui-style';
|
|
146
|
-
|
|
147
|
-
function injectStyles() {
|
|
148
|
-
if (document.getElementById(styleId)) return;
|
|
149
|
-
|
|
150
|
-
const style = document.createElement('style');
|
|
151
|
-
style.id = styleId;
|
|
152
|
-
style.textContent = `
|
|
153
|
-
.xof-card { border: 1px solid rgba(255,255,255,0.28); border-radius: 8px; padding: 10px; background: rgba(0,0,0,0.12); }
|
|
154
|
-
.xof-title { font-weight: 700; margin-bottom: 8px; }
|
|
155
|
-
.xof-meta { display: grid; grid-template-columns: 140px 1fr; gap: 4px 10px; font-size: 12px; margin-bottom: 10px; }
|
|
156
|
-
.xof-meta-label { opacity: 0.85; }
|
|
157
|
-
.xof-list { margin: 0; padding-left: 18px; }
|
|
158
|
-
.xof-list li { margin: 4px 0; }
|
|
159
|
-
.xof-graph { margin-top: 10px; padding: 8px; border: 1px dashed rgba(255,255,255,0.32); border-radius: 6px; }
|
|
160
|
-
.xof-graph-title { font-size: 12px; font-weight: 700; margin-bottom: 6px; }
|
|
161
|
-
.xof-edge { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', monospace; font-size: 12px; line-height: 1.45; white-space: pre-wrap; }
|
|
162
|
-
.xof-empty { opacity: 0.85; font-style: italic; }
|
|
163
|
-
.xof-overview { margin: 10px 0 16px; }
|
|
164
|
-
.xof-overview img { width: 100%; max-width: 760px; border: 1px solid rgba(255,255,255,0.3); border-radius: 6px; background: #fff; }
|
|
165
|
-
.xof-overview-code { margin-top: 8px; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', monospace; font-size: 11px; opacity: 0.9; white-space: pre-wrap; }
|
|
166
|
-
`;
|
|
167
|
-
|
|
168
|
-
document.head.appendChild(style);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function text(value) {
|
|
172
|
-
if (value === null || value === undefined || value === '') return '-';
|
|
173
|
-
if (Array.isArray(value)) return value.length ? value.join(', ') : '-';
|
|
174
|
-
return String(value);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function renderTransitions(currentState, transitions) {
|
|
178
|
-
if (!Array.isArray(transitions) || transitions.length === 0) {
|
|
179
|
-
return '<div class="xof-empty">No transitions (terminal state)</div>';
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return `<ul class="xof-list">${transitions
|
|
183
|
-
.map((transition) => {
|
|
184
|
-
const condition = transition.condition ? ` — ${text(transition.condition)}` : '';
|
|
185
|
-
const nextOperation = transition.next_operation_id ? ` (next: ${text(transition.next_operation_id)})` : '';
|
|
186
|
-
return `<li><strong>${text(transition.trigger_type)}</strong> → <strong>${text(transition.target_state)}</strong>${condition}${nextOperation}</li>`;
|
|
187
|
-
})
|
|
188
|
-
.join('')}</ul>`;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function renderGraph(currentState, transitions) {
|
|
192
|
-
if (!Array.isArray(transitions) || transitions.length === 0) {
|
|
193
|
-
return `<div class="xof-edge">${text(currentState)} [terminal]</div>`;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
return transitions
|
|
197
|
-
.map((transition) => `<div class="xof-edge">${text(currentState)} --> ${text(transition.target_state)} [${text(transition.trigger_type)}]</div>`)
|
|
198
|
-
.join('');
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function renderCard(flow) {
|
|
202
|
-
const transitions = Array.isArray(flow.transitions) ? flow.transitions : [];
|
|
203
|
-
return `
|
|
204
|
-
<div class="xof-card">
|
|
205
|
-
<div class="xof-title">x-openapi-flow</div>
|
|
206
|
-
<div class="xof-meta">
|
|
207
|
-
<div class="xof-meta-label">version</div><div>${text(flow.version)}</div>
|
|
208
|
-
<div class="xof-meta-label">id</div><div>${text(flow.id)}</div>
|
|
209
|
-
<div class="xof-meta-label">current_state</div><div>${text(flow.current_state)}</div>
|
|
210
|
-
</div>
|
|
211
|
-
<div><strong>Transitions</strong></div>
|
|
212
|
-
${renderTransitions(flow.current_state, transitions)}
|
|
213
|
-
<div class="xof-graph">
|
|
214
|
-
<div class="xof-graph-title">Flow graph (operation-level)</div>
|
|
215
|
-
${renderGraph(flow.current_state, transitions)}
|
|
216
|
-
</div>
|
|
217
|
-
</div>
|
|
218
|
-
`;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function getSpecFromUi() {
|
|
222
|
-
try {
|
|
223
|
-
if (!window.ui || !window.ui.specSelectors || !window.ui.specSelectors.specJson) {
|
|
224
|
-
return null;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
const spec = window.ui.specSelectors.specJson();
|
|
228
|
-
return spec && spec.toJS ? spec.toJS() : spec;
|
|
229
|
-
} catch (_error) {
|
|
230
|
-
return null;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function extractFlowsFromSpec(spec) {
|
|
235
|
-
const result = [];
|
|
236
|
-
const paths = (spec && spec.paths) || {};
|
|
237
|
-
const methods = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'];
|
|
238
|
-
|
|
239
|
-
Object.entries(paths).forEach(([pathKey, pathItem]) => {
|
|
240
|
-
if (!pathItem || typeof pathItem !== 'object') return;
|
|
241
|
-
|
|
242
|
-
methods.forEach((method) => {
|
|
243
|
-
const operation = pathItem[method];
|
|
244
|
-
if (!operation || typeof operation !== 'object') return;
|
|
245
|
-
|
|
246
|
-
const flow = operation['x-openapi-flow'];
|
|
247
|
-
if (!flow || typeof flow !== 'object' || !flow.current_state) return;
|
|
248
|
-
|
|
249
|
-
result.push({
|
|
250
|
-
operationId: operation.operationId || `${method}_${pathKey}`,
|
|
251
|
-
flow,
|
|
252
|
-
});
|
|
253
|
-
});
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
return result;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function buildOverviewMermaid(flows) {
|
|
260
|
-
const lines = ['stateDiagram-v2'];
|
|
261
|
-
const states = new Set();
|
|
262
|
-
const seen = new Set();
|
|
263
|
-
|
|
264
|
-
flows.forEach(({ flow }) => {
|
|
265
|
-
const current = flow.current_state;
|
|
266
|
-
if (!current) return;
|
|
267
|
-
|
|
268
|
-
states.add(current);
|
|
269
|
-
const transitions = Array.isArray(flow.transitions) ? flow.transitions : [];
|
|
270
|
-
transitions.forEach((transition) => {
|
|
271
|
-
const target = transition.target_state;
|
|
272
|
-
if (!target) return;
|
|
273
|
-
states.add(target);
|
|
274
|
-
|
|
275
|
-
const labelParts = [];
|
|
276
|
-
if (transition.next_operation_id) {
|
|
277
|
-
labelParts.push(`next:${text(transition.next_operation_id)}`);
|
|
278
|
-
}
|
|
279
|
-
if (Array.isArray(transition.prerequisite_operation_ids) && transition.prerequisite_operation_ids.length) {
|
|
280
|
-
labelParts.push(`requires:${transition.prerequisite_operation_ids.join(',')}`);
|
|
281
|
-
}
|
|
282
|
-
const label = labelParts.join(' | ');
|
|
283
|
-
const key = `${current}::${target}::${label}`;
|
|
284
|
-
if (seen.has(key)) return;
|
|
285
|
-
seen.add(key);
|
|
286
|
-
lines.push(` ${current} --> ${target}${label ? `: ${label}` : ''}`);
|
|
287
|
-
});
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
Array.from(states)
|
|
291
|
-
.sort()
|
|
292
|
-
.forEach((state) => {
|
|
293
|
-
lines.splice(1, 0, ` state ${state}`);
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
return lines.join('\n');
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
let mermaidLoaderPromise = null;
|
|
300
|
-
function ensureMermaid() {
|
|
301
|
-
if (window.mermaid) {
|
|
302
|
-
return Promise.resolve(window.mermaid);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
if (mermaidLoaderPromise) {
|
|
306
|
-
return mermaidLoaderPromise;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
mermaidLoaderPromise = new Promise((resolve, reject) => {
|
|
310
|
-
const script = document.createElement('script');
|
|
311
|
-
script.src = 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js';
|
|
312
|
-
script.async = true;
|
|
313
|
-
script.onload = () => {
|
|
314
|
-
if (window.mermaid) {
|
|
315
|
-
window.mermaid.initialize({ startOnLoad: false, securityLevel: 'loose' });
|
|
316
|
-
resolve(window.mermaid);
|
|
317
|
-
} else {
|
|
318
|
-
reject(new Error('Mermaid library not available after load'));
|
|
319
|
-
}
|
|
320
|
-
};
|
|
321
|
-
script.onerror = () => reject(new Error('Could not load Mermaid library'));
|
|
322
|
-
document.head.appendChild(script);
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
return mermaidLoaderPromise;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
function svgToDataUri(svg) {
|
|
329
|
-
const encoded = window.btoa(unescape(encodeURIComponent(svg)));
|
|
330
|
-
return `data:image/svg+xml;base64,${encoded}`;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
let overviewRenderedHash = null;
|
|
334
|
-
let overviewRenderInProgress = false;
|
|
335
|
-
let overviewPendingHash = null;
|
|
336
|
-
async function renderOverview() {
|
|
337
|
-
const spec = getSpecFromUi();
|
|
338
|
-
const flows = extractFlowsFromSpec(spec);
|
|
339
|
-
if (!flows.length) return;
|
|
340
|
-
|
|
341
|
-
const mermaid = buildOverviewMermaid(flows);
|
|
342
|
-
const currentHash = `${flows.length}:${mermaid}`;
|
|
343
|
-
if (overviewRenderedHash === currentHash) return;
|
|
344
|
-
if (overviewRenderInProgress && overviewPendingHash === currentHash) return;
|
|
345
|
-
|
|
346
|
-
const infoContainer = document.querySelector('.swagger-ui .information-container');
|
|
347
|
-
if (!infoContainer) return;
|
|
348
|
-
|
|
349
|
-
let holder = document.getElementById('xof-overview-holder');
|
|
350
|
-
if (!holder) {
|
|
351
|
-
holder = document.createElement('div');
|
|
352
|
-
holder.id = 'xof-overview-holder';
|
|
353
|
-
holder.className = 'xof-overview xof-card';
|
|
354
|
-
infoContainer.parentNode.insertBefore(holder, infoContainer.nextSibling);
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
holder.innerHTML = '<div class="xof-title">x-openapi-flow — Flow Overview</div><div class="xof-empty">Rendering Mermaid graph...</div>';
|
|
358
|
-
overviewRenderInProgress = true;
|
|
359
|
-
overviewPendingHash = currentHash;
|
|
360
|
-
|
|
361
|
-
try {
|
|
362
|
-
const mermaidLib = await ensureMermaid();
|
|
363
|
-
const renderId = `xof-overview-${Date.now()}`;
|
|
364
|
-
const renderResult = await mermaidLib.render(renderId, mermaid);
|
|
365
|
-
const svg = renderResult && renderResult.svg ? renderResult.svg : renderResult;
|
|
366
|
-
const dataUri = svgToDataUri(svg);
|
|
367
|
-
|
|
368
|
-
holder.innerHTML = `
|
|
369
|
-
<div class="xof-title">x-openapi-flow — Flow Overview</div>
|
|
370
|
-
<img src="${dataUri}" alt="x-openapi-flow overview graph" />
|
|
371
|
-
<details style="margin-top:8px;">
|
|
372
|
-
<summary style="cursor:pointer;">Mermaid source</summary>
|
|
373
|
-
<div class="xof-overview-code">${mermaid.replace(/</g, '<').replace(/>/g, '>')}</div>
|
|
374
|
-
</details>
|
|
375
|
-
`;
|
|
376
|
-
} catch (_error) {
|
|
377
|
-
holder.innerHTML = `
|
|
378
|
-
<div class="xof-title">x-openapi-flow — Flow Overview</div>
|
|
379
|
-
<div class="xof-empty">Could not render Mermaid image in this environment.</div>
|
|
380
|
-
<div class="xof-overview-code">${mermaid.replace(/</g, '<').replace(/>/g, '>')}</div>
|
|
381
|
-
`;
|
|
382
|
-
} finally {
|
|
383
|
-
overviewRenderInProgress = false;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
overviewRenderedHash = currentHash;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
function findXOpenApiFlowValueCell(opblock) {
|
|
390
|
-
const rows = opblock.querySelectorAll('tr');
|
|
391
|
-
for (const row of rows) {
|
|
392
|
-
const cells = row.querySelectorAll('td');
|
|
393
|
-
if (cells.length < 2) continue;
|
|
394
|
-
if (cells[0].innerText.trim() === 'x-openapi-flow') {
|
|
395
|
-
return cells[1];
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
return null;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
function enhanceOperation(opblock) {
|
|
402
|
-
const valueCell = findXOpenApiFlowValueCell(opblock);
|
|
403
|
-
if (!valueCell || valueCell.dataset.xofEnhanced === '1') return;
|
|
404
|
-
|
|
405
|
-
const raw = valueCell.innerText.trim();
|
|
406
|
-
if (!raw) return;
|
|
407
|
-
|
|
408
|
-
let flow;
|
|
409
|
-
try {
|
|
410
|
-
flow = JSON.parse(raw);
|
|
411
|
-
} catch (_error) {
|
|
412
|
-
return;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
valueCell.innerHTML = renderCard(flow);
|
|
416
|
-
valueCell.dataset.xofEnhanced = '1';
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
function enhanceAll() {
|
|
420
|
-
injectStyles();
|
|
421
|
-
const opblocks = document.querySelectorAll('.opblock');
|
|
422
|
-
opblocks.forEach((opblock) => enhanceOperation(opblock));
|
|
423
|
-
renderOverview().catch(() => {
|
|
424
|
-
// keep plugin resilient in environments where async rendering fails
|
|
425
|
-
});
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
let enhanceScheduled = false;
|
|
429
|
-
function scheduleEnhance() {
|
|
430
|
-
if (enhanceScheduled) return;
|
|
431
|
-
enhanceScheduled = true;
|
|
432
|
-
window.requestAnimationFrame(() => {
|
|
433
|
-
enhanceScheduled = false;
|
|
434
|
-
enhanceAll();
|
|
435
|
-
});
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
const observer = new MutationObserver(() => {
|
|
439
|
-
scheduleEnhance();
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
window.addEventListener('load', () => {
|
|
443
|
-
scheduleEnhance();
|
|
444
|
-
observer.observe(document.body, { childList: true, subtree: true });
|
|
445
|
-
});
|
|
446
|
-
})();
|