rapidoc 11.0.1 → 12.1.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 +171 -54
- package/dist/rapidoc-flow-min.js +580 -0
- package/dist/rapidoc-flow-min.js.LICENSE.txt +171 -0
- package/dist/rapidoc-min.js +645 -202
- package/dist/rapidoc-min.js.LICENSE.txt +381 -0
- package/package.json +57 -42
- package/src/README.md +150 -0
- package/src/components/README.md +91 -0
- package/src/components/api-request.ts +912 -0
- package/src/components/api-response.ts +514 -0
- package/src/components/json-tree.ts +171 -0
- package/src/components/schema-base-element.ts +329 -0
- package/src/components/schema-table.ts +475 -0
- package/src/components/schema-tree.ts +393 -0
- package/src/components/tag-input.ts +180 -0
- package/src/flow.js +3 -0
- package/src/index.js +6 -0
- package/src/oauth-receiver.ts +29 -0
- package/src/rapi-flow.ts +1178 -0
- package/src/rapidoc-mini.ts +603 -0
- package/src/rapidoc.ts +1854 -0
- package/src/styles/README.md +96 -0
- package/src/styles/auth-styles.js +318 -0
- package/src/styles/border-styles.js +24 -0
- package/src/styles/css-bundle.js +129 -0
- package/src/styles/custom-styles.js +8 -0
- package/src/styles/dialog-box-styles.js +102 -0
- package/src/styles/endpoint-styles.js +564 -0
- package/src/styles/flex-styles.js +16 -0
- package/src/styles/flow-styles.js +248 -0
- package/src/styles/font-styles.js +432 -0
- package/src/styles/info-styles.js +36 -0
- package/src/styles/input-styles.js +555 -0
- package/src/styles/layout-styles.js +396 -0
- package/src/styles/microlighter-styles.js +99 -0
- package/src/styles/nav-styles.js +633 -0
- package/src/styles/param-list-styles.js +261 -0
- package/src/styles/schema-styles.js +296 -0
- package/src/styles/tab-styles.js +106 -0
- package/src/styles/theme-tokens.js +582 -0
- package/src/templates/README.md +89 -0
- package/src/templates/advanced-search-dialog-template.js +94 -0
- package/src/templates/api-response-template.js +223 -0
- package/src/templates/callback-template.js +86 -0
- package/src/templates/code-samples-template.js +52 -0
- package/src/templates/components-template.js +86 -0
- package/src/templates/downloads-template.js +70 -0
- package/src/templates/endpoint-template.js +377 -0
- package/src/templates/expanded-endpoint-template.js +270 -0
- package/src/templates/focused-endpoint-template.js +146 -0
- package/src/templates/header-template.js +92 -0
- package/src/templates/logo-template.js +21 -0
- package/src/templates/main-body-template.js +102 -0
- package/src/templates/navbar-template.js +498 -0
- package/src/templates/overview-template.js +104 -0
- package/src/templates/request-body-template.js +673 -0
- package/src/templates/request-params-template.js +485 -0
- package/src/templates/security-scheme-template.js +1272 -0
- package/src/templates/server-template.js +153 -0
- package/src/utils/README.md +146 -0
- package/src/utils/arazzo/README.md +119 -0
- package/src/utils/arazzo/components.js +15 -0
- package/src/utils/arazzo/graph.js +269 -0
- package/src/utils/arazzo/index.js +52 -0
- package/src/utils/arazzo/operations.js +106 -0
- package/src/utils/arazzo/parse.js +196 -0
- package/src/utils/arazzo/refs.js +40 -0
- package/src/utils/arazzo/svg.js +707 -0
- package/src/utils/auto-color-scheme.js +99 -0
- package/src/utils/bind-navbar.js +175 -0
- package/src/utils/color-utils.js +59 -0
- package/src/utils/common-utils.js +191 -0
- package/src/utils/curl-utils.js +126 -0
- package/src/utils/highlighter.js +419 -0
- package/src/utils/markdown-utils.js +110 -0
- package/src/utils/mcp/README.md +155 -0
- package/src/utils/mcp/common.js +166 -0
- package/src/utils/mcp/manifest.js +382 -0
- package/src/utils/mcp/project/server.ts +53 -0
- package/src/utils/mcp/project.js +88 -0
- package/src/utils/mcp/runtime.js +289 -0
- package/src/utils/mcp/schema.js +98 -0
- package/src/utils/mcp/workflow-runtime.js +374 -0
- package/src/utils/mcp/workflows.js +240 -0
- package/src/utils/mcp/zip.js +102 -0
- package/src/utils/mcp-generator.js +47 -0
- package/src/utils/mock-interceptor.js +557 -0
- package/src/utils/oauth-utils.js +87 -0
- package/src/utils/request-utils.js +264 -0
- package/src/utils/response-utils.js +203 -0
- package/src/utils/sanitize.js +57 -0
- package/src/utils/schema-utils.js +1954 -0
- package/src/utils/slugger.js +187 -0
- package/src/utils/spec-parser.js +941 -0
- package/src/utils/url-utils.js +246 -0
- package/src/utils/xml-utils.js +61 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turns one Arazzo workflow into the graph the diagram draws: steps in order, the values passed
|
|
3
|
+
* between them, and each step's success and failure actions.
|
|
4
|
+
*/
|
|
5
|
+
import { collectRefs } from "./refs.js";
|
|
6
|
+
import { resolveComponentRef } from "./parse.js";
|
|
7
|
+
import { resolveOperation } from "./operations.js";
|
|
8
|
+
|
|
9
|
+
function normalizeCriteria(list) {
|
|
10
|
+
return (Array.isArray(list) ? list : [])
|
|
11
|
+
.filter((c) => c && typeof c.condition === "string")
|
|
12
|
+
.map((c) => ({
|
|
13
|
+
condition: c.condition,
|
|
14
|
+
context: c.context || "",
|
|
15
|
+
type: typeof c.type === "string" ? c.type : c.type?.type || "simple",
|
|
16
|
+
}));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function normalizeActions(doc, list, kind, outcome, inherited) {
|
|
20
|
+
return (Array.isArray(list) ? list : [])
|
|
21
|
+
.map((raw) => resolveComponentRef(doc, raw, kind))
|
|
22
|
+
.filter((a) => a && ["end", "goto", "retry"].includes(a.type))
|
|
23
|
+
.map((a) => {
|
|
24
|
+
const criteria = normalizeCriteria(a.criteria);
|
|
25
|
+
return {
|
|
26
|
+
outcome,
|
|
27
|
+
type: a.type,
|
|
28
|
+
name: a.name || "",
|
|
29
|
+
stepId: a.stepId || "",
|
|
30
|
+
workflowId: a.workflowId || "",
|
|
31
|
+
retryAfter: a.retryAfter,
|
|
32
|
+
retryLimit: a.retryLimit,
|
|
33
|
+
criteria,
|
|
34
|
+
conditional: criteria.length > 0,
|
|
35
|
+
inherited,
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A step's own actions, then the workflow's, minus those the step overrides by name. A runner
|
|
42
|
+
* takes the first action whose criteria match, so every action after one without criteria is
|
|
43
|
+
* marked `shadowed`: it can never run.
|
|
44
|
+
*/
|
|
45
|
+
function mergeActions(own, inherited) {
|
|
46
|
+
const names = new Set(own.map((a) => a.name).filter(Boolean));
|
|
47
|
+
const merged = [...own, ...inherited.filter((a) => !a.name || !names.has(a.name))];
|
|
48
|
+
const first = merged.findIndex((a) => !a.conditional);
|
|
49
|
+
return merged.map((a, i) => ({ ...a, shadowed: first >= 0 && i > first }));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The workflow's `inputs` JSON Schema (or a `$ref` to `#/components/inputs/<name>`) as rows. */
|
|
53
|
+
function describeInputs(doc, schema) {
|
|
54
|
+
const ref =
|
|
55
|
+
typeof schema?.$ref === "string" && schema.$ref.match(/^#\/components\/inputs\/(.+)$/);
|
|
56
|
+
const resolved = ref ? doc?.components?.inputs?.[ref[1]] : schema;
|
|
57
|
+
const required = new Set(Array.isArray(resolved?.required) ? resolved.required : []);
|
|
58
|
+
return Object.entries(resolved?.properties || {}).map(([name, prop]) => ({
|
|
59
|
+
name,
|
|
60
|
+
type: prop?.type ? [].concat(prop.type).join(" | ") : prop?.enum ? "enum" : "",
|
|
61
|
+
format: prop?.format || "",
|
|
62
|
+
required: required.has(name),
|
|
63
|
+
default: prop?.default,
|
|
64
|
+
enum: Array.isArray(prop?.enum) ? prop.enum : undefined,
|
|
65
|
+
description: prop?.description || "",
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function describeParameters(doc, list) {
|
|
70
|
+
return (Array.isArray(list) ? list : [])
|
|
71
|
+
.map((raw) => resolveComponentRef(doc, raw, "parameters"))
|
|
72
|
+
.filter((p) => p && p.name)
|
|
73
|
+
.map((p) => ({ name: p.name, in: p.in || "", value: p.value }));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Where a value comes from, for one line in a step box: a workflow input, a step output, another
|
|
78
|
+
* runtime expression, or a fixed value.
|
|
79
|
+
*/
|
|
80
|
+
function describeSource(value) {
|
|
81
|
+
if (typeof value === "string") {
|
|
82
|
+
const text = value.trim();
|
|
83
|
+
let m = text.match(/^\$inputs\.([A-Za-z0-9_-]+)$/);
|
|
84
|
+
if (m) return { kind: "input", name: m[1], text: m[1] };
|
|
85
|
+
m = text.match(/^\$steps\.([A-Za-z0-9_-]+)\.outputs\.([A-Za-z0-9_-]+)$/);
|
|
86
|
+
if (m) return { kind: "step", stepId: m[1], name: m[2], text: `${m[1]}.${m[2]}` };
|
|
87
|
+
if (text.includes("$")) return { kind: "expression", text };
|
|
88
|
+
return { kind: "value", text };
|
|
89
|
+
}
|
|
90
|
+
return { kind: "value", text: value === undefined ? "" : JSON.stringify(value) };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function flattenPayload(obj, prefix, rows) {
|
|
94
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
95
|
+
const name = `${prefix}.${key}`;
|
|
96
|
+
if (value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length) {
|
|
97
|
+
flattenPayload(value, name, rows);
|
|
98
|
+
} else rows.push({ name, in: "body", value });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** A step's parameters and request body fields, one row per value it sends. */
|
|
103
|
+
function describeBindings(parameters, requestBody) {
|
|
104
|
+
const rows = parameters.map((p) => ({ name: p.name, in: p.in, value: p.value }));
|
|
105
|
+
const payload = requestBody?.payload;
|
|
106
|
+
if (payload && typeof payload === "object" && !Array.isArray(payload)) {
|
|
107
|
+
flattenPayload(payload, "body", rows);
|
|
108
|
+
} else if (payload !== undefined && payload !== null) {
|
|
109
|
+
rows.push({ name: "body", in: "body", value: payload });
|
|
110
|
+
}
|
|
111
|
+
for (const r of requestBody?.replacements || []) {
|
|
112
|
+
if (r?.target) rows.push({ name: `body ${r.target}`, in: "body", value: r.value });
|
|
113
|
+
}
|
|
114
|
+
return rows.map((r) => {
|
|
115
|
+
const stepRefs = collectRefs(r.value).filter((x) => x.kind === "step");
|
|
116
|
+
return {
|
|
117
|
+
...r,
|
|
118
|
+
source: describeSource(r.value),
|
|
119
|
+
fromSteps: [...new Set(stepRefs.map((x) => x.stepId))],
|
|
120
|
+
fromOutputs: stepRefs.filter((x) => x.name).map((x) => ({ stepId: x.stepId, name: x.name })),
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const listOf = (names) =>
|
|
126
|
+
names.length > 1 ? `${names.slice(0, -1).join(", ")} and ${names.at(-1)}` : names[0];
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* One warning per step and outcome whose actions include some that can never run, because an
|
|
130
|
+
* earlier action without criteria always matches first.
|
|
131
|
+
*/
|
|
132
|
+
function shadowWarnings(steps, workflowIndex) {
|
|
133
|
+
const warnings = [];
|
|
134
|
+
steps.forEach((step, si) => {
|
|
135
|
+
for (const outcome of ["success", "failure"]) {
|
|
136
|
+
const actions = step.actions.filter((a) => a.outcome === outcome);
|
|
137
|
+
const never = actions.filter((a) => a.shadowed);
|
|
138
|
+
if (!never.length) continue;
|
|
139
|
+
const catchAll = actions.find((a) => !a.conditional);
|
|
140
|
+
const names = never.map((a) => a.name || `an unnamed ${a.type}`);
|
|
141
|
+
const whose = never.every((a) => a.inherited) ? "the workflow's " : "";
|
|
142
|
+
const verb = never.length > 1 ? "never run" : "never runs";
|
|
143
|
+
warnings.push({
|
|
144
|
+
level: "warning",
|
|
145
|
+
path: `workflows[${workflowIndex}].steps[${si}].${outcome === "failure" ? "onFailure" : "onSuccess"}`,
|
|
146
|
+
message: `${step.stepId}: ${whose}${listOf(names)} ${verb}, because ${catchAll?.name || "an action"} has no criteria and always matches first`,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
return warnings;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function pushEdge(edges, from, to, label) {
|
|
154
|
+
const edge = edges.find((e) => e.from === from && e.to === to);
|
|
155
|
+
if (!edge) edges.push({ from, to, labels: [label] });
|
|
156
|
+
else if (!edge.labels.includes(label)) edge.labels.push(label);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* @param {object} doc Parsed Arazzo document
|
|
161
|
+
* @param {string} [workflowId] Defaults to the first workflow
|
|
162
|
+
* @param {object} [options]
|
|
163
|
+
* @param {Map<string, {byId: Map, byPath: Map}>} [options.sources] operation indexes by source name
|
|
164
|
+
* Each step's `bindings` list the values it sends (`{ name, in, value, source, fromSteps }`, with
|
|
165
|
+
* body fields named `body.<path>`; `fromOutputs` the `{ stepId, name }` step outputs they read);
|
|
166
|
+
* `usedOutputs` are the step's own outputs that a later step or the workflow reads; `source` is `{ kind: 'input'|'step'|'expression'|'value', text }`.
|
|
167
|
+
*
|
|
168
|
+
* @returns {object|null} `{ workflowId, summary, description, inputs, inputDetails, workflowActions,
|
|
169
|
+
* operationsKnown, outputs, steps, dataEdges, warnings }`,
|
|
170
|
+
* or null when there is no such workflow. Data edges go from a step id to a later step id or
|
|
171
|
+
* to "outputs"; `labels` are the output names passed.
|
|
172
|
+
*/
|
|
173
|
+
export function buildFlowGraph(doc, workflowId, options = {}) {
|
|
174
|
+
const workflows = Array.isArray(doc?.workflows) ? doc.workflows : [];
|
|
175
|
+
const workflow = workflowId ? workflows.find((w) => w?.workflowId === workflowId) : workflows[0];
|
|
176
|
+
if (!workflow) return null;
|
|
177
|
+
|
|
178
|
+
const rawSteps = Array.isArray(workflow.steps) ? workflow.steps : [];
|
|
179
|
+
const stepIds = new Set(rawSteps.map((s) => s?.stepId).filter(Boolean));
|
|
180
|
+
const workflowSuccess = normalizeActions(
|
|
181
|
+
doc,
|
|
182
|
+
workflow.successActions,
|
|
183
|
+
"successActions",
|
|
184
|
+
"success",
|
|
185
|
+
true,
|
|
186
|
+
);
|
|
187
|
+
const workflowFailure = normalizeActions(
|
|
188
|
+
doc,
|
|
189
|
+
workflow.failureActions,
|
|
190
|
+
"failureActions",
|
|
191
|
+
"failure",
|
|
192
|
+
true,
|
|
193
|
+
);
|
|
194
|
+
const dataEdges = [];
|
|
195
|
+
|
|
196
|
+
const steps = rawSteps.map((s) => {
|
|
197
|
+
const onSuccess = normalizeActions(doc, s.onSuccess, "successActions", "success", false);
|
|
198
|
+
const onFailure = normalizeActions(doc, s.onFailure, "failureActions", "failure", false);
|
|
199
|
+
const operation = s.workflowId ? null : resolveOperation(s, options.sources);
|
|
200
|
+
const refs = collectRefs([s.parameters, s.requestBody]);
|
|
201
|
+
for (const ref of refs) {
|
|
202
|
+
if (ref.kind === "step" && ref.name && ref.stepId !== s.stepId && stepIds.has(ref.stepId)) {
|
|
203
|
+
pushEdge(dataEdges, ref.stepId, s.stepId, ref.name);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const criteria = normalizeCriteria(s.successCriteria);
|
|
207
|
+
const parameters = describeParameters(doc, s.parameters);
|
|
208
|
+
return {
|
|
209
|
+
stepId: s.stepId || "",
|
|
210
|
+
description: s.description || "",
|
|
211
|
+
kind: s.workflowId ? "workflow" : "operation",
|
|
212
|
+
workflowId: s.workflowId || "",
|
|
213
|
+
operationRef: s.operationId || s.operationPath || "",
|
|
214
|
+
operation,
|
|
215
|
+
inputs: refs.filter((r) => r.kind === "input").map((r) => r.name),
|
|
216
|
+
outputs: Object.keys(s.outputs || {}),
|
|
217
|
+
outputExprs: Object.entries(s.outputs || {}).map(([name, expr]) => ({ name, expr })),
|
|
218
|
+
parameters,
|
|
219
|
+
requestBody: s.requestBody || null,
|
|
220
|
+
bindings: describeBindings(parameters, s.requestBody),
|
|
221
|
+
criteria,
|
|
222
|
+
successCriteria: criteria.length,
|
|
223
|
+
actions: [
|
|
224
|
+
...mergeActions(onSuccess, workflowSuccess),
|
|
225
|
+
...mergeActions(onFailure, workflowFailure),
|
|
226
|
+
],
|
|
227
|
+
};
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const inputDetails = describeInputs(doc, workflow.inputs);
|
|
231
|
+
const outputs = Object.entries(workflow.outputs || {}).map(([name, expr]) => {
|
|
232
|
+
const ref = collectRefs(expr).find((r) => r.kind === "step");
|
|
233
|
+
if (ref && stepIds.has(ref.stepId)) pushEdge(dataEdges, ref.stepId, "outputs", name);
|
|
234
|
+
return {
|
|
235
|
+
name,
|
|
236
|
+
stepId: ref?.stepId || "",
|
|
237
|
+
output: ref?.name || "",
|
|
238
|
+
source: describeSource(expr),
|
|
239
|
+
expr: typeof expr === "string" ? expr : JSON.stringify(expr),
|
|
240
|
+
};
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Each step output something else reads: later steps' values and the workflow's outputs
|
|
244
|
+
const used = new Map();
|
|
245
|
+
const markUsed = (stepId, name) => {
|
|
246
|
+
if (stepId && name) used.set(stepId, (used.get(stepId) || new Set()).add(name));
|
|
247
|
+
};
|
|
248
|
+
for (const s of steps)
|
|
249
|
+
for (const b of s.bindings) for (const r of b.fromOutputs) markUsed(r.stepId, r.name);
|
|
250
|
+
for (const o of outputs) markUsed(o.stepId, o.output);
|
|
251
|
+
for (const s of steps) {
|
|
252
|
+
const names = used.get(s.stepId) || new Set();
|
|
253
|
+
s.usedOutputs = s.outputExprs.filter((o) => names.has(o.name));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
workflowId: workflow.workflowId || "",
|
|
258
|
+
summary: workflow.summary || "",
|
|
259
|
+
description: workflow.description || "",
|
|
260
|
+
inputs: inputDetails.map((i) => i.name),
|
|
261
|
+
inputDetails,
|
|
262
|
+
workflowActions: [...workflowSuccess, ...workflowFailure],
|
|
263
|
+
operationsKnown: Boolean(options.sources?.size),
|
|
264
|
+
outputs,
|
|
265
|
+
steps,
|
|
266
|
+
dataEdges,
|
|
267
|
+
warnings: shadowWarnings(steps, workflows.indexOf(workflow)),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Arazzo support shared by <rapi-flow> and the build-time renderer (@rapidoc/prerender).
|
|
3
|
+
*/
|
|
4
|
+
import { parseDocument, validateArazzo } from "./parse.js";
|
|
5
|
+
import { indexOperations } from "./operations.js";
|
|
6
|
+
import { buildFlowGraph } from "./graph.js";
|
|
7
|
+
import { renderFlowDiagram } from "./svg.js";
|
|
8
|
+
|
|
9
|
+
export { parseDocument, validateArazzo, resolveComponentRef } from "./parse.js";
|
|
10
|
+
export { collectRefs } from "./refs.js";
|
|
11
|
+
export { indexOperations, indexResolvedSpec, resolveOperation } from "./operations.js";
|
|
12
|
+
export { buildFlowGraph } from "./graph.js";
|
|
13
|
+
export {
|
|
14
|
+
renderFlowDiagram,
|
|
15
|
+
renderFlowSvg,
|
|
16
|
+
escapeXml,
|
|
17
|
+
actionLabel,
|
|
18
|
+
actionSentence,
|
|
19
|
+
describeAction,
|
|
20
|
+
} from "./svg.js";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* One call from documents to SVG.
|
|
24
|
+
*
|
|
25
|
+
* @param {string|object} arazzo Arazzo text or object
|
|
26
|
+
* @param {object} [options]
|
|
27
|
+
* @param {string} [options.workflowId] defaults to the first workflow
|
|
28
|
+
* @param {Record<string, string|object>} [options.sources] OpenAPI documents (text or object) by
|
|
29
|
+
* source description name; operations are looked up in them
|
|
30
|
+
* @param {'expanded'|'compact'} [options.view] defaults to expanded
|
|
31
|
+
* @param {string} [options.idPrefix]
|
|
32
|
+
* @param {boolean} [options.interactive]
|
|
33
|
+
* @returns {{ svg: string, legend: string[], graph: object|null, problems: object[],
|
|
34
|
+
* workflowIds: string[] }} `legend` names the kinds of lines drawn (see renderFlowDiagram)
|
|
35
|
+
*/
|
|
36
|
+
export function renderArazzoWorkflow(arazzo, options = {}) {
|
|
37
|
+
const doc = parseDocument(arazzo);
|
|
38
|
+
const problems = validateArazzo(doc);
|
|
39
|
+
const sources = new Map();
|
|
40
|
+
for (const [name, openapi] of Object.entries(options.sources || {})) {
|
|
41
|
+
sources.set(name, indexOperations(parseDocument(openapi)));
|
|
42
|
+
}
|
|
43
|
+
const graph = buildFlowGraph(doc, options.workflowId, { sources });
|
|
44
|
+
const { svg, legend } = renderFlowDiagram(graph, options);
|
|
45
|
+
return {
|
|
46
|
+
svg,
|
|
47
|
+
legend,
|
|
48
|
+
graph,
|
|
49
|
+
problems,
|
|
50
|
+
workflowIds: (doc.workflows || []).map((w) => w?.workflowId).filter(Boolean),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Looks up the OpenAPI operation an Arazzo step calls, without dereferencing the OpenAPI document.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const METHODS = ["get", "put", "post", "delete", "options", "head", "patch", "trace", "query"];
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Indexes an OpenAPI document's operations by operationId and by "<method> <path>".
|
|
9
|
+
*
|
|
10
|
+
* @param {object} openapi Raw OpenAPI 2.0, 3.x object
|
|
11
|
+
* @returns {{ byId: Map<string, object>, byPath: Map<string, object> }} values are
|
|
12
|
+
* `{ operationId, method, path, summary, security }`, method lower-case; `security` is the
|
|
13
|
+
* operation's requirement, else the document's
|
|
14
|
+
*/
|
|
15
|
+
export function indexOperations(openapi) {
|
|
16
|
+
const byId = new Map();
|
|
17
|
+
const byPath = new Map();
|
|
18
|
+
for (const [path, item] of Object.entries(openapi?.paths || {})) {
|
|
19
|
+
if (!item || typeof item !== "object") continue;
|
|
20
|
+
for (const method of METHODS) {
|
|
21
|
+
const op = item[method];
|
|
22
|
+
if (!op || typeof op !== "object") continue;
|
|
23
|
+
const entry = {
|
|
24
|
+
operationId: op.operationId || "",
|
|
25
|
+
method,
|
|
26
|
+
path,
|
|
27
|
+
summary: op.summary || "",
|
|
28
|
+
};
|
|
29
|
+
const security = op.security ?? openapi.security;
|
|
30
|
+
if (security !== undefined) entry.security = security;
|
|
31
|
+
if (op.operationId) byId.set(op.operationId, entry);
|
|
32
|
+
byPath.set(`${method} ${path}`, entry);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return { byId, byPath };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The same index from a spec <rapi-doc> has already processed (its `resolvedSpec`), whose
|
|
40
|
+
* operations are listed under `tags[].paths[]`. Webhooks are skipped. An operation without an
|
|
41
|
+
* operationId in the document gets one generated by <rapi-doc>, which no Arazzo step can name,
|
|
42
|
+
* so it only matters for lookup by method and path.
|
|
43
|
+
*
|
|
44
|
+
* @param {{ tags?: { paths?: object[] }[] }} resolvedSpec
|
|
45
|
+
* @returns {{ byId: Map<string, object>, byPath: Map<string, object> }}
|
|
46
|
+
*/
|
|
47
|
+
export function indexResolvedSpec(resolvedSpec) {
|
|
48
|
+
const byId = new Map();
|
|
49
|
+
const byPath = new Map();
|
|
50
|
+
for (const tag of resolvedSpec?.tags || []) {
|
|
51
|
+
for (const p of tag?.paths || []) {
|
|
52
|
+
if (!p?.method || !p.path || p.isWebhook) continue;
|
|
53
|
+
const method = p.method.toLowerCase();
|
|
54
|
+
const entry = {
|
|
55
|
+
operationId: p.operationId || "",
|
|
56
|
+
method,
|
|
57
|
+
path: p.path,
|
|
58
|
+
summary: p.summary || "",
|
|
59
|
+
};
|
|
60
|
+
if (p.security !== undefined) entry.security = p.security;
|
|
61
|
+
// An operation with several tags is listed once per tag; the first entry is kept
|
|
62
|
+
const key = `${method} ${p.path}`;
|
|
63
|
+
if (byPath.has(key)) continue;
|
|
64
|
+
if (p.operationId) byId.set(p.operationId, entry);
|
|
65
|
+
byPath.set(key, entry);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return { byId, byPath };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function unescapePointer(segment) {
|
|
72
|
+
return decodeURIComponent(segment).replace(/~1/g, "/").replace(/~0/g, "~");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Resolves a step's operationId or operationPath.
|
|
77
|
+
*
|
|
78
|
+
* @param {object} step Arazzo step
|
|
79
|
+
* @param {Map<string, {byId: Map, byPath: Map}>} sources indexes by source description name
|
|
80
|
+
* @returns {{ source: string, operationId: string, method: string, path: string, summary: string } | null}
|
|
81
|
+
*/
|
|
82
|
+
export function resolveOperation(step, sources) {
|
|
83
|
+
if (!sources?.size) return null;
|
|
84
|
+
if (step.operationId) {
|
|
85
|
+
let source = "";
|
|
86
|
+
let id = step.operationId;
|
|
87
|
+
const qualified = id.match(/^\$sourceDescriptions\.([A-Za-z0-9_-]+)\.(.+)$/);
|
|
88
|
+
if (qualified) [, source, id] = qualified;
|
|
89
|
+
for (const [name, index] of sources) {
|
|
90
|
+
if (source && name !== source) continue;
|
|
91
|
+
const op = index.byId.get(id);
|
|
92
|
+
if (op) return { source: name, ...op };
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
if (step.operationPath) {
|
|
97
|
+
const m = step.operationPath.match(/^\{\$sourceDescriptions\.([A-Za-z0-9_-]+)\.url\}#(.*)$/);
|
|
98
|
+
if (!m) return null;
|
|
99
|
+
const [, source, pointer] = m;
|
|
100
|
+
const parts = pointer.split("/").slice(1).map(unescapePointer);
|
|
101
|
+
if (parts.length !== 3 || parts[0] !== "paths") return null;
|
|
102
|
+
const op = sources.get(source)?.byPath.get(`${parts[2].toLowerCase()} ${parts[1]}`);
|
|
103
|
+
return op ? { source, ...op } : null;
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Arazzo document parsing and structural validation.
|
|
3
|
+
*/
|
|
4
|
+
import { parse as parseYaml } from "yaml";
|
|
5
|
+
import { collectRefs } from "./refs.js";
|
|
6
|
+
import { resolveComponentRef } from "./components.js";
|
|
7
|
+
|
|
8
|
+
export { resolveComponentRef };
|
|
9
|
+
|
|
10
|
+
const ACTION_TYPES = new Set(["end", "goto", "retry"]);
|
|
11
|
+
const SOURCE_TYPES = new Set(["openapi", "arazzo", "asyncapi"]);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Parses Arazzo (or OpenAPI) text as JSON or YAML. An object is returned as is.
|
|
15
|
+
*
|
|
16
|
+
* @param {string|object} input
|
|
17
|
+
* @returns {object}
|
|
18
|
+
*/
|
|
19
|
+
export function parseDocument(input) {
|
|
20
|
+
if (input && typeof input === "object") return input;
|
|
21
|
+
const text = String(input ?? "").trim();
|
|
22
|
+
if (!text) throw new Error("The document is empty");
|
|
23
|
+
const doc = text.startsWith("{") ? JSON.parse(text) : parseYaml(text);
|
|
24
|
+
if (!doc || typeof doc !== "object") throw new Error("The document is not an object");
|
|
25
|
+
return doc;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function problem(level, path, message) {
|
|
29
|
+
return { level, path, message };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function checkActions(doc, list, kind, path, stepIds, workflowIds, problems) {
|
|
33
|
+
if (list === undefined) return;
|
|
34
|
+
if (!Array.isArray(list)) {
|
|
35
|
+
problems.push(problem("error", path, "must be a list of actions"));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
list.forEach((raw, i) => {
|
|
39
|
+
const p = `${path}[${i}]`;
|
|
40
|
+
const action = resolveComponentRef(doc, raw, kind);
|
|
41
|
+
if (!action) {
|
|
42
|
+
problems.push(problem("error", p, `unresolved reference ${raw?.reference}`));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (!ACTION_TYPES.has(action.type)) {
|
|
46
|
+
problems.push(problem("error", `${p}.type`, "must be end, goto or retry"));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (action.type === "retry" && kind === "successActions") {
|
|
50
|
+
problems.push(problem("error", `${p}.type`, "retry is only allowed on failure"));
|
|
51
|
+
}
|
|
52
|
+
if (action.stepId && action.workflowId) {
|
|
53
|
+
problems.push(problem("error", p, "has both stepId and workflowId"));
|
|
54
|
+
}
|
|
55
|
+
if (action.type === "goto" && !action.stepId && !action.workflowId) {
|
|
56
|
+
problems.push(problem("error", p, "goto needs a stepId or a workflowId"));
|
|
57
|
+
}
|
|
58
|
+
if (action.stepId && !stepIds.has(action.stepId)) {
|
|
59
|
+
problems.push(problem("error", `${p}.stepId`, `no step "${action.stepId}"`));
|
|
60
|
+
}
|
|
61
|
+
if (
|
|
62
|
+
action.workflowId &&
|
|
63
|
+
!action.workflowId.startsWith("$") &&
|
|
64
|
+
!workflowIds.has(action.workflowId)
|
|
65
|
+
) {
|
|
66
|
+
problems.push(problem("error", `${p}.workflowId`, `no workflow "${action.workflowId}"`));
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Checks the structure the visualizer relies on. Returns problems; an empty list means valid.
|
|
73
|
+
* Expression syntax and JSON Schema of `inputs` are not checked.
|
|
74
|
+
*
|
|
75
|
+
* @param {object} doc
|
|
76
|
+
* @returns {{ level: 'error'|'warning', path: string, message: string }[]}
|
|
77
|
+
*/
|
|
78
|
+
export function validateArazzo(doc) {
|
|
79
|
+
const problems = [];
|
|
80
|
+
if (!doc || typeof doc !== "object") return [problem("error", "", "not an object")];
|
|
81
|
+
|
|
82
|
+
if (typeof doc.arazzo !== "string" || !/^1\.\d+(\.\d+)?/.test(doc.arazzo)) {
|
|
83
|
+
problems.push(problem("error", "arazzo", "must be an Arazzo 1.x version string"));
|
|
84
|
+
}
|
|
85
|
+
if (!doc.info?.title) problems.push(problem("error", "info.title", "is required"));
|
|
86
|
+
if (!doc.info?.version) problems.push(problem("error", "info.version", "is required"));
|
|
87
|
+
|
|
88
|
+
const sources = Array.isArray(doc.sourceDescriptions) ? doc.sourceDescriptions : [];
|
|
89
|
+
if (!sources.length) {
|
|
90
|
+
problems.push(problem("error", "sourceDescriptions", "needs at least one source"));
|
|
91
|
+
}
|
|
92
|
+
const sourceNames = new Set();
|
|
93
|
+
sources.forEach((s, i) => {
|
|
94
|
+
const p = `sourceDescriptions[${i}]`;
|
|
95
|
+
if (!s?.name) problems.push(problem("error", `${p}.name`, "is required"));
|
|
96
|
+
else if (sourceNames.has(s.name))
|
|
97
|
+
problems.push(problem("error", `${p}.name`, "is a duplicate"));
|
|
98
|
+
else sourceNames.add(s.name);
|
|
99
|
+
if (!s?.url) problems.push(problem("error", `${p}.url`, "is required"));
|
|
100
|
+
if (s?.type && !SOURCE_TYPES.has(s.type)) {
|
|
101
|
+
problems.push(problem("warning", `${p}.type`, `unknown type "${s.type}"`));
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const workflows = Array.isArray(doc.workflows) ? doc.workflows : [];
|
|
106
|
+
if (!workflows.length)
|
|
107
|
+
problems.push(problem("error", "workflows", "needs at least one workflow"));
|
|
108
|
+
const workflowIds = new Set();
|
|
109
|
+
workflows.forEach((w, i) => {
|
|
110
|
+
if (!w?.workflowId)
|
|
111
|
+
problems.push(problem("error", `workflows[${i}].workflowId`, "is required"));
|
|
112
|
+
else if (workflowIds.has(w.workflowId)) {
|
|
113
|
+
problems.push(problem("error", `workflows[${i}].workflowId`, "is a duplicate"));
|
|
114
|
+
} else workflowIds.add(w.workflowId);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
workflows.forEach((w, wi) => {
|
|
118
|
+
const wp = `workflows[${wi}]`;
|
|
119
|
+
const steps = Array.isArray(w?.steps) ? w.steps : [];
|
|
120
|
+
if (!steps.length) problems.push(problem("error", `${wp}.steps`, "needs at least one step"));
|
|
121
|
+
const stepIds = new Set();
|
|
122
|
+
steps.forEach((s, si) => {
|
|
123
|
+
const sp = `${wp}.steps[${si}]`;
|
|
124
|
+
if (!s?.stepId) problems.push(problem("error", `${sp}.stepId`, "is required"));
|
|
125
|
+
else if (stepIds.has(s.stepId))
|
|
126
|
+
problems.push(problem("error", `${sp}.stepId`, "is a duplicate"));
|
|
127
|
+
else stepIds.add(s.stepId);
|
|
128
|
+
const targets = ["operationId", "operationPath", "workflowId"].filter((k) => s?.[k]);
|
|
129
|
+
if (targets.length !== 1) {
|
|
130
|
+
problems.push(
|
|
131
|
+
problem("error", sp, "needs exactly one of operationId, operationPath or workflowId"),
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
if (s?.workflowId && !s.workflowId.startsWith("$") && !workflowIds.has(s.workflowId)) {
|
|
135
|
+
problems.push(problem("error", `${sp}.workflowId`, `no workflow "${s.workflowId}"`));
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
(w?.dependsOn || []).forEach((id, di) => {
|
|
139
|
+
if (!String(id).startsWith("$") && !workflowIds.has(id)) {
|
|
140
|
+
problems.push(problem("error", `${wp}.dependsOn[${di}]`, `no workflow "${id}"`));
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
checkActions(
|
|
145
|
+
doc,
|
|
146
|
+
w?.successActions,
|
|
147
|
+
"successActions",
|
|
148
|
+
`${wp}.successActions`,
|
|
149
|
+
stepIds,
|
|
150
|
+
workflowIds,
|
|
151
|
+
problems,
|
|
152
|
+
);
|
|
153
|
+
checkActions(
|
|
154
|
+
doc,
|
|
155
|
+
w?.failureActions,
|
|
156
|
+
"failureActions",
|
|
157
|
+
`${wp}.failureActions`,
|
|
158
|
+
stepIds,
|
|
159
|
+
workflowIds,
|
|
160
|
+
problems,
|
|
161
|
+
);
|
|
162
|
+
steps.forEach((s, si) => {
|
|
163
|
+
const sp = `${wp}.steps[${si}]`;
|
|
164
|
+
checkActions(
|
|
165
|
+
doc,
|
|
166
|
+
s?.onSuccess,
|
|
167
|
+
"successActions",
|
|
168
|
+
`${sp}.onSuccess`,
|
|
169
|
+
stepIds,
|
|
170
|
+
workflowIds,
|
|
171
|
+
problems,
|
|
172
|
+
);
|
|
173
|
+
checkActions(
|
|
174
|
+
doc,
|
|
175
|
+
s?.onFailure,
|
|
176
|
+
"failureActions",
|
|
177
|
+
`${sp}.onFailure`,
|
|
178
|
+
stepIds,
|
|
179
|
+
workflowIds,
|
|
180
|
+
problems,
|
|
181
|
+
);
|
|
182
|
+
for (const ref of collectRefs([s?.parameters, s?.requestBody])) {
|
|
183
|
+
if (ref.kind === "step" && !stepIds.has(ref.stepId)) {
|
|
184
|
+
problems.push(problem("error", sp, `refers to unknown step "${ref.stepId}"`));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
for (const ref of collectRefs(w?.outputs)) {
|
|
189
|
+
if (ref.kind === "step" && !stepIds.has(ref.stepId)) {
|
|
190
|
+
problems.push(problem("error", `${wp}.outputs`, `refers to unknown step "${ref.stepId}"`));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
return problems;
|
|
196
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finds Arazzo runtime expressions ($inputs, $steps, $workflows) inside any value.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const EXPRESSION =
|
|
6
|
+
/\$(inputs|steps|workflows)\.([A-Za-z0-9_-]+)(?:\.([A-Za-z0-9_-]+)(?:\.([A-Za-z0-9_-]+))?)?/g;
|
|
7
|
+
|
|
8
|
+
function scan(value, out, seen) {
|
|
9
|
+
if (typeof value === "string") {
|
|
10
|
+
for (const m of value.matchAll(EXPRESSION)) {
|
|
11
|
+
const [expr, root, a, b, c] = m;
|
|
12
|
+
let ref;
|
|
13
|
+
if (root === "inputs") ref = { kind: "input", name: a, expr };
|
|
14
|
+
else if (root === "steps" && b === "outputs" && c)
|
|
15
|
+
ref = { kind: "step", stepId: a, name: c, expr };
|
|
16
|
+
else if (root === "steps") ref = { kind: "step", stepId: a, name: "", expr };
|
|
17
|
+
else if (b === "outputs" && c) ref = { kind: "workflow", workflowId: a, name: c, expr };
|
|
18
|
+
else ref = { kind: "workflow", workflowId: a, name: "", expr };
|
|
19
|
+
if (!seen.has(expr)) {
|
|
20
|
+
seen.add(expr);
|
|
21
|
+
out.push(ref);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
} else if (Array.isArray(value)) {
|
|
25
|
+
for (const v of value) scan(v, out, seen);
|
|
26
|
+
} else if (value && typeof value === "object") {
|
|
27
|
+
for (const v of Object.values(value)) scan(v, out, seen);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {*} value Any JSON value: parameters, a request body, outputs
|
|
33
|
+
* @returns {{ kind: 'input'|'step'|'workflow', name: string, stepId?: string, workflowId?: string, expr: string }[]}
|
|
34
|
+
* distinct references in the order they first appear
|
|
35
|
+
*/
|
|
36
|
+
export function collectRefs(value) {
|
|
37
|
+
const out = [];
|
|
38
|
+
scan(value, out, new Set());
|
|
39
|
+
return out;
|
|
40
|
+
}
|