x-openapi-flow 1.2.3 → 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 +319 -20
- package/adapters/collections/insomnia-adapter.js +73 -0
- package/adapters/collections/postman-adapter.js +145 -0
- package/adapters/docs/doc-adapter.js +119 -0
- package/adapters/flow-output-adapters.js +15 -0
- package/adapters/shared/helpers.js +87 -0
- package/adapters/ui/redoc/x-openapi-flow-redoc-plugin.js +127 -0
- package/adapters/ui/redoc-adapter.js +75 -0
- package/adapters/ui/swagger-ui/x-openapi-flow-plugin.js +856 -0
- package/bin/x-openapi-flow.js +1502 -64
- package/lib/sdk-generator.js +673 -0
- package/lib/validator.js +36 -3
- package/package.json +9 -3
- package/schema/flow-schema.json +2 -2
- package/templates/go/README.md +3 -0
- package/templates/kotlin/README.md +3 -0
- package/templates/python/README.md +3 -0
- package/templates/typescript/flow-helpers.hbs +26 -0
- package/templates/typescript/http-client.hbs +37 -0
- package/templates/typescript/index.hbs +16 -0
- package/templates/typescript/resource.hbs +24 -0
- package/examples/swagger-ui/index.html +0 -33
- package/lib/swagger-ui/x-openapi-flow-plugin.js +0 -455
|
@@ -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
|
+
})();
|
|
@@ -0,0 +1,75 @@
|
|
|
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
|
+
|
|
8
|
+
function buildRedocHtml(model, specFileName) {
|
|
9
|
+
const modelPayload = JSON.stringify(model);
|
|
10
|
+
return `<!doctype html>
|
|
11
|
+
<html lang="en">
|
|
12
|
+
<head>
|
|
13
|
+
<meta charset="utf-8" />
|
|
14
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
15
|
+
<title>x-openapi-flow Redoc</title>
|
|
16
|
+
<style>
|
|
17
|
+
body { margin: 0; background: #ffffff; color: #111827; font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif; }
|
|
18
|
+
#x-openapi-flow-panel { max-width: 1200px; margin: 0 auto; padding: 16px; border-bottom: 1px solid #e5e7eb; }
|
|
19
|
+
</style>
|
|
20
|
+
</head>
|
|
21
|
+
<body>
|
|
22
|
+
<div id="x-openapi-flow-panel"></div>
|
|
23
|
+
<redoc spec-url="./${specFileName}"></redoc>
|
|
24
|
+
<script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"></script>
|
|
25
|
+
<script src="./x-openapi-flow-redoc-plugin.js"></script>
|
|
26
|
+
<script>
|
|
27
|
+
window.XOpenApiFlowRedocPlugin.mount({
|
|
28
|
+
model: ${modelPayload},
|
|
29
|
+
targetSelector: "#x-openapi-flow-panel"
|
|
30
|
+
});
|
|
31
|
+
</script>
|
|
32
|
+
</body>
|
|
33
|
+
</html>
|
|
34
|
+
`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function generateRedocPackage(options) {
|
|
38
|
+
const apiPath = path.resolve(options.apiPath);
|
|
39
|
+
const outputDir = path.resolve(options.outputDir || path.join(process.cwd(), "redoc-flow"));
|
|
40
|
+
|
|
41
|
+
const api = loadApi(apiPath);
|
|
42
|
+
const model = buildIntermediateModel(api);
|
|
43
|
+
const specFileName = path.extname(apiPath).toLowerCase() === ".json" ? "openapi.json" : "openapi.yaml";
|
|
44
|
+
|
|
45
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
46
|
+
|
|
47
|
+
const sourceSpec = fs.readFileSync(apiPath, "utf8");
|
|
48
|
+
fs.writeFileSync(path.join(outputDir, specFileName), sourceSpec, "utf8");
|
|
49
|
+
|
|
50
|
+
// Plugin lives at adapters/ui/redoc/ — __dirname is adapters/ui
|
|
51
|
+
const pluginSourcePath = path.join(__dirname, "redoc", "x-openapi-flow-redoc-plugin.js");
|
|
52
|
+
const pluginTargetPath = path.join(outputDir, "x-openapi-flow-redoc-plugin.js");
|
|
53
|
+
fs.copyFileSync(pluginSourcePath, pluginTargetPath);
|
|
54
|
+
|
|
55
|
+
fs.writeFileSync(
|
|
56
|
+
path.join(outputDir, "flow-model.json"),
|
|
57
|
+
`${JSON.stringify(model, null, 2)}\n`,
|
|
58
|
+
"utf8"
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
fs.writeFileSync(
|
|
62
|
+
path.join(outputDir, "index.html"),
|
|
63
|
+
buildRedocHtml(model, specFileName),
|
|
64
|
+
"utf8"
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
outputDir,
|
|
69
|
+
indexPath: path.join(outputDir, "index.html"),
|
|
70
|
+
resources: model.resources.length,
|
|
71
|
+
flowCount: model.flowCount,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = { generateRedocPackage };
|