@revenexx/studio-integrations 1.4.1 → 2.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/CHANGELOG.md +297 -0
- package/README.md +2 -1
- package/dist/module.json +1 -1
- package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +6 -6
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +134 -131
- package/dist/runtime/components/Integrations/CredentialLogo.vue +1 -1
- package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +1 -1
- package/dist/runtime/components/Integrations/CredentialTypePicker.vue +6 -6
- package/dist/runtime/components/Integrations/ExpressionInput.d.vue.ts +2 -0
- package/dist/runtime/components/Integrations/ExpressionInput.vue +90 -11
- package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +2 -0
- package/dist/runtime/components/Integrations/GroupFrameView.vue +121 -121
- package/dist/runtime/components/Integrations/NodeConfigField.vue +20 -5
- package/dist/runtime/components/Integrations/NodeInspector.vue +44 -41
- package/dist/runtime/components/Integrations/NodePalette.vue +52 -37
- package/dist/runtime/components/Integrations/RunActionsMenu.vue +6 -5
- package/dist/runtime/components/Integrations/RunDetailPanel.d.vue.ts +7 -0
- package/dist/runtime/components/Integrations/RunDetailPanel.vue +14 -2
- package/dist/runtime/components/Integrations/RunDetailPanel.vue.d.ts +7 -0
- package/dist/runtime/components/Integrations/RunsTable.vue +72 -70
- package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +20 -6
- package/dist/runtime/components/Integrations/StickyNoteView.vue +1 -0
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +10 -6
- package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +0 -34
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +226 -277
- package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +0 -34
- package/dist/runtime/components/Integrations/WorkflowExportDialog.d.vue.ts +6 -0
- package/dist/runtime/components/Integrations/WorkflowExportDialog.vue +71 -0
- package/dist/runtime/components/Integrations/WorkflowExportDialog.vue.d.ts +6 -0
- package/dist/runtime/components/Integrations/WorkflowImportFileStep.d.vue.ts +8 -0
- package/dist/runtime/components/Integrations/WorkflowImportFileStep.vue +95 -0
- package/dist/runtime/components/Integrations/WorkflowImportFileStep.vue.d.ts +8 -0
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue +5 -4
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +0 -7
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +2 -2
- package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +2 -3
- package/dist/runtime/components/Integrations/WorkflowsTable.vue +14 -8
- package/dist/runtime/composables/useIntegrationsNodes.js +4 -1
- package/dist/runtime/composables/useIntegrationsWorkflows.d.ts +3 -1
- package/dist/runtime/composables/useIntegrationsWorkflows.js +5 -22
- package/dist/runtime/composables/useNow.d.ts +17 -0
- package/dist/runtime/composables/useNow.js +16 -0
- package/dist/runtime/composables/useWorkflowFile.d.ts +21 -0
- package/dist/runtime/composables/useWorkflowFile.js +63 -0
- package/dist/runtime/data/expressionEditor.d.ts +11 -1
- package/dist/runtime/data/expressionEditor.js +2 -1
- package/dist/runtime/data/nodeIcons.d.ts +14 -0
- package/dist/runtime/data/nodeIcons.js +57 -14
- package/dist/runtime/data/nodeRunData.js +1 -1
- package/dist/runtime/data/payloadForm.js +1 -1
- package/dist/runtime/i18n/messages/de.json +47 -11
- package/dist/runtime/i18n/messages/en.json +47 -11
- package/dist/runtime/pages/integrations/audit.vue +3 -3
- package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +2 -2
- package/dist/runtime/pages/integrations/credentials.vue +17 -5
- package/dist/runtime/pages/integrations/index.vue +5 -10
- package/dist/runtime/pages/integrations/schedules.vue +24 -13
- package/dist/runtime/pages/integrations/templates/index.vue +40 -40
- package/dist/runtime/pages/integrations/workflows/[id].vue +324 -324
- package/dist/runtime/types/integrations.d.ts +13 -0
- package/dist/runtime/utils/cockpitSources.d.ts +68 -1
- package/dist/runtime/utils/credentialState.d.ts +15 -7
- package/dist/runtime/utils/credentialState.js +4 -12
- package/dist/runtime/utils/localize.d.ts +12 -0
- package/dist/runtime/utils/localize.js +1 -1
- package/dist/runtime/utils/paletteOrder.d.ts +29 -0
- package/dist/runtime/utils/paletteOrder.js +46 -0
- package/dist/runtime/utils/relativeTime.d.ts +29 -0
- package/dist/runtime/utils/relativeTime.js +23 -0
- package/dist/runtime/utils/runFeedQuery.d.ts +12 -6
- package/dist/runtime/utils/runFeedQuery.js +3 -18
- package/dist/runtime/utils/settingValue.d.ts +33 -0
- package/dist/runtime/utils/settingValue.js +8 -0
- package/dist/runtime/utils/statusStyle.d.ts +8 -8
- package/dist/runtime/utils/statusStyle.js +4 -1
- package/dist/runtime/utils/workflowFile.d.ts +119 -0
- package/dist/runtime/utils/workflowFile.js +192 -0
- package/package.json +10 -5
- package/dist/runtime/utils/errorPathAdvice.d.ts +0 -60
- package/dist/runtime/utils/errorPathAdvice.js +0 -19
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { deepClone } from "./deepClone.js";
|
|
2
|
+
export const WORKFLOW_FILE_KIND = "revenexx.workflow";
|
|
3
|
+
export const WORKFLOW_FILE_VERSION = 1;
|
|
4
|
+
const BLOB_VERSION = "v0-draft";
|
|
5
|
+
export function withoutCredentials(blob, fieldsFor, credentialIds = /* @__PURE__ */ new Set()) {
|
|
6
|
+
const out = deepClone(blob);
|
|
7
|
+
for (const node of out.nodes ?? []) {
|
|
8
|
+
if (!node.config || typeof node.config !== "object") continue;
|
|
9
|
+
const fields = fieldsFor(node);
|
|
10
|
+
if (fields) stripDeclared(fields, node.config);
|
|
11
|
+
if (credentialIds.size) node.config = stripValues(node.config, credentialIds);
|
|
12
|
+
}
|
|
13
|
+
return out;
|
|
14
|
+
}
|
|
15
|
+
function stripDeclared(fields, values) {
|
|
16
|
+
for (const field of fields) {
|
|
17
|
+
if (!field?.key || !(field.key in values)) continue;
|
|
18
|
+
const value = values[field.key];
|
|
19
|
+
if (field.type === "credentials-ref") {
|
|
20
|
+
delete values[field.key];
|
|
21
|
+
} else if (field.type === "object" && field.properties && isRecord(value)) {
|
|
22
|
+
stripDeclared(field.properties, value);
|
|
23
|
+
} else if (field.type === "array" && field.items && Array.isArray(value)) {
|
|
24
|
+
const items = field.items;
|
|
25
|
+
if (items.type === "credentials-ref") {
|
|
26
|
+
values[field.key] = [];
|
|
27
|
+
} else if (items.type === "object" && items.properties) {
|
|
28
|
+
for (const row of value) if (isRecord(row)) stripDeclared(items.properties, row);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function stripValues(value, ids) {
|
|
34
|
+
if (Array.isArray(value)) {
|
|
35
|
+
return value.filter((v) => !(typeof v === "string" && ids.has(v))).map((v) => stripValues(v, ids));
|
|
36
|
+
}
|
|
37
|
+
if (isRecord(value)) {
|
|
38
|
+
const out = {};
|
|
39
|
+
for (const [k, v] of Object.entries(value)) {
|
|
40
|
+
if (typeof v === "string" && ids.has(v)) continue;
|
|
41
|
+
out[k] = stripValues(v, ids);
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
function isRecord(v) {
|
|
48
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
49
|
+
}
|
|
50
|
+
const ENVIRONMENT_TRIGGER_KEYS = ["inbound", "secretRef"];
|
|
51
|
+
function withoutEnvironment(trigger) {
|
|
52
|
+
const config = { ...trigger.config ?? {} };
|
|
53
|
+
for (const key of ENVIRONMENT_TRIGGER_KEYS) delete config[key];
|
|
54
|
+
return { ...trigger, config };
|
|
55
|
+
}
|
|
56
|
+
export function toWorkflowFile(wf, fieldsFor, credentialIds) {
|
|
57
|
+
const blob = withoutCredentials(wf.blob, fieldsFor, credentialIds);
|
|
58
|
+
blob.triggers = (blob.triggers ?? []).map(withoutEnvironment);
|
|
59
|
+
return {
|
|
60
|
+
kind: WORKFLOW_FILE_KIND,
|
|
61
|
+
version: WORKFLOW_FILE_VERSION,
|
|
62
|
+
workflow: {
|
|
63
|
+
name: wf.name,
|
|
64
|
+
description: wf.description ?? null,
|
|
65
|
+
executionMode: wf.execution_mode ?? "async_only",
|
|
66
|
+
blobDefinitionVersion: wf.blob_definition_version ?? BLOB_VERSION,
|
|
67
|
+
blob
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
export function hasVariableValues(blob) {
|
|
72
|
+
return Object.values(blob.variables ?? {}).some((v) => v?.default !== void 0 && v.default !== null && v.default !== "");
|
|
73
|
+
}
|
|
74
|
+
export function workflowFileName(name) {
|
|
75
|
+
const slug = name.normalize("NFKD").replace(/\p{Mn}/gu, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
76
|
+
return `${slug || "workflow"}.workflow.json`;
|
|
77
|
+
}
|
|
78
|
+
export function parseWorkflowFile(text) {
|
|
79
|
+
let raw;
|
|
80
|
+
try {
|
|
81
|
+
raw = JSON.parse(text);
|
|
82
|
+
} catch {
|
|
83
|
+
return { ok: false, reason: "unreadable" };
|
|
84
|
+
}
|
|
85
|
+
if (!isRecord(raw)) return { ok: false, reason: "unreadable" };
|
|
86
|
+
if (raw.kind !== WORKFLOW_FILE_KIND) return { ok: false, reason: "kind" };
|
|
87
|
+
if (raw.version !== WORKFLOW_FILE_VERSION) return { ok: false, reason: "version" };
|
|
88
|
+
const wf = raw.workflow;
|
|
89
|
+
if (!isRecord(wf) || !isRecord(wf.blob)) return { ok: false, reason: "unreadable" };
|
|
90
|
+
if ((wf.blobDefinitionVersion ?? BLOB_VERSION) !== BLOB_VERSION) return { ok: false, reason: "definition" };
|
|
91
|
+
const blob = wf.blob;
|
|
92
|
+
const nodes = blob.nodes;
|
|
93
|
+
const edges = blob.edges ?? [];
|
|
94
|
+
const triggers = blob.triggers ?? [];
|
|
95
|
+
if (!Array.isArray(nodes) || !nodes.every(isNode)) return { ok: false, reason: "unreadable" };
|
|
96
|
+
if (!Array.isArray(edges) || !edges.every(isEdge)) return { ok: false, reason: "unreadable" };
|
|
97
|
+
if (!Array.isArray(triggers) || !triggers.every(isTrigger)) return { ok: false, reason: "unreadable" };
|
|
98
|
+
if (hasDuplicates([...nodes.map((n) => n.id), ...triggers.map((tr) => tr.id)])) return { ok: false, reason: "unreadable" };
|
|
99
|
+
return {
|
|
100
|
+
ok: true,
|
|
101
|
+
file: {
|
|
102
|
+
kind: WORKFLOW_FILE_KIND,
|
|
103
|
+
version: WORKFLOW_FILE_VERSION,
|
|
104
|
+
workflow: {
|
|
105
|
+
name: typeof wf.name === "string" ? wf.name : "",
|
|
106
|
+
description: typeof wf.description === "string" ? wf.description : null,
|
|
107
|
+
executionMode: isExecutionMode(wf.executionMode) ? wf.executionMode : "async_only",
|
|
108
|
+
blobDefinitionVersion: BLOB_VERSION,
|
|
109
|
+
blob: {
|
|
110
|
+
...blob,
|
|
111
|
+
nodes: nodes.map((n) => ({ ...n, config: isRecord(n.config) ? n.config : {} })),
|
|
112
|
+
edges,
|
|
113
|
+
triggers: triggers.map((tr) => ({ ...tr, config: isRecord(tr.config) ? tr.config : {} }))
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function hasDuplicates(ids) {
|
|
120
|
+
return new Set(ids).size !== ids.length;
|
|
121
|
+
}
|
|
122
|
+
function isNode(v) {
|
|
123
|
+
return isRecord(v) && typeof v.id === "string" && typeof v.nodeSlug === "string";
|
|
124
|
+
}
|
|
125
|
+
function isEdge(v) {
|
|
126
|
+
return isRecord(v) && isRecord(v.from) && typeof v.from.nodeId === "string" && isRecord(v.to) && typeof v.to.nodeId === "string";
|
|
127
|
+
}
|
|
128
|
+
function isTrigger(v) {
|
|
129
|
+
return isRecord(v) && typeof v.id === "string" && typeof v.type === "string";
|
|
130
|
+
}
|
|
131
|
+
function isExecutionMode(v) {
|
|
132
|
+
return v === "async_only" || v === "sync_only" || v === "caller_decides";
|
|
133
|
+
}
|
|
134
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
135
|
+
export function assignTriggerHandles(blob, taken, keep) {
|
|
136
|
+
const out = deepClone(blob);
|
|
137
|
+
const handleMap = /* @__PURE__ */ new Map();
|
|
138
|
+
const used = /* @__PURE__ */ new Set();
|
|
139
|
+
for (const trigger of out.triggers ?? []) {
|
|
140
|
+
const keepable = UUID.test(trigger.id) && !taken.has(trigger.id) && !used.has(trigger.id);
|
|
141
|
+
if (!keepable && !(keep?.(trigger) && !used.has(trigger.id))) {
|
|
142
|
+
const fresh = crypto.randomUUID();
|
|
143
|
+
handleMap.set(trigger.id, fresh);
|
|
144
|
+
trigger.id = fresh;
|
|
145
|
+
}
|
|
146
|
+
used.add(trigger.id);
|
|
147
|
+
}
|
|
148
|
+
if (!handleMap.size) return out;
|
|
149
|
+
for (const edge of out.edges ?? []) {
|
|
150
|
+
const mapped = handleMap.get(edge.from.nodeId);
|
|
151
|
+
if (mapped) edge.from.nodeId = mapped;
|
|
152
|
+
}
|
|
153
|
+
if (out.triggerPositions) {
|
|
154
|
+
const rekeyed = {};
|
|
155
|
+
for (const [handle, pos] of Object.entries(out.triggerPositions)) rekeyed[handleMap.get(handle) ?? handle] = pos;
|
|
156
|
+
out.triggerPositions = rekeyed;
|
|
157
|
+
}
|
|
158
|
+
for (const group of out.groups ?? []) {
|
|
159
|
+
group.nodeIds = (group.nodeIds ?? []).map((id) => handleMap.get(id) ?? id);
|
|
160
|
+
}
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
export function pausedTriggers(blob) {
|
|
164
|
+
return { ...blob, triggers: (blob.triggers ?? []).map((t) => ({ ...t, active: false })) };
|
|
165
|
+
}
|
|
166
|
+
export function blobForNewWorkflow(file, fieldsFor, tenantHandles, credentialIds = /* @__PURE__ */ new Set()) {
|
|
167
|
+
const clean = withoutCredentials(file.workflow.blob, fieldsFor, credentialIds);
|
|
168
|
+
clean.triggers = (clean.triggers ?? []).map(withoutEnvironment);
|
|
169
|
+
return pausedTriggers(assignTriggerHandles(clean, tenantHandles));
|
|
170
|
+
}
|
|
171
|
+
export function overwritePlan(target, file, fieldsFor, otherHandles, credentialIds = /* @__PURE__ */ new Set()) {
|
|
172
|
+
const clean = withoutCredentials(file.workflow.blob, fieldsFor, credentialIds);
|
|
173
|
+
const existing = new Map((target.triggers ?? []).map((t) => [t.id, t]));
|
|
174
|
+
const same = (t) => existing.get(t.id)?.type === t.type;
|
|
175
|
+
clean.triggers = (clean.triggers ?? []).map((t) => {
|
|
176
|
+
const mine = same(t) ? existing.get(t.id) : null;
|
|
177
|
+
const incoming = withoutEnvironment(t);
|
|
178
|
+
if (!mine) return { ...incoming, active: false };
|
|
179
|
+
const config = { ...incoming.config };
|
|
180
|
+
for (const key of ENVIRONMENT_TRIGGER_KEYS) {
|
|
181
|
+
if (mine.config?.[key] !== void 0) config[key] = mine.config[key];
|
|
182
|
+
}
|
|
183
|
+
return { ...incoming, config, active: mine.active };
|
|
184
|
+
});
|
|
185
|
+
const taken = /* @__PURE__ */ new Set([...otherHandles, ...[...existing.values()].map((t) => t.id)]);
|
|
186
|
+
const blob = assignTriggerHandles(clean, taken, same);
|
|
187
|
+
return {
|
|
188
|
+
blob,
|
|
189
|
+
replaced: { nodes: target.nodes?.length ?? 0, triggers: target.triggers?.length ?? 0 },
|
|
190
|
+
incoming: { nodes: blob.nodes.length, triggers: blob.triggers?.length ?? 0 }
|
|
191
|
+
};
|
|
192
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@revenexx/studio-integrations",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Integration Studio (core: workflows, node editor, runs, schedules, credentials, secrets, audit) as a standalone, publishable Nuxt module.",
|
|
6
6
|
"repository": {
|
|
@@ -26,13 +26,14 @@
|
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@nuxt/kit": "^4.0.0",
|
|
29
|
-
"@revenexx/studio-shared": "^0.3.
|
|
29
|
+
"@revenexx/studio-shared": "^0.3.2",
|
|
30
30
|
"@solar-icons/nuxt": "^1.2.0",
|
|
31
|
+
"@solar-icons/vue": "^1.2.0",
|
|
31
32
|
"marked": "^18.0.5"
|
|
32
33
|
},
|
|
33
34
|
"peerDependencies": {
|
|
34
35
|
"@dagrejs/dagre": "^3.0.0",
|
|
35
|
-
"@revenexx/studio": "
|
|
36
|
+
"@revenexx/studio": "^0.2.3",
|
|
36
37
|
"@tiptap/core": "^3.28.0",
|
|
37
38
|
"@tiptap/extension-document": "^3.28.0",
|
|
38
39
|
"@tiptap/extension-history": "^3.28.0",
|
|
@@ -52,12 +53,15 @@
|
|
|
52
53
|
},
|
|
53
54
|
"devDependencies": {
|
|
54
55
|
"@changesets/cli": "^2.31.0",
|
|
56
|
+
"@nuxt/eslint-config": "^1.17.0",
|
|
57
|
+
"@nuxt/kit": "^4.0.0",
|
|
55
58
|
"@nuxt/module-builder": "^1.0.2",
|
|
59
|
+
"@nuxt/schema": "^4.0.0",
|
|
56
60
|
"@playwright/test": "^1.57.0",
|
|
57
|
-
"@nuxt/kit": "^4.0.0",
|
|
58
61
|
"@revenexx/talkback-js": "^0.3.0",
|
|
59
|
-
"
|
|
62
|
+
"eslint": "^10.11.0",
|
|
60
63
|
"nuxt": "^4.2.2",
|
|
64
|
+
"typescript": "5.9.3",
|
|
61
65
|
"vitest": "^3.2.0",
|
|
62
66
|
"vue": "^3.5.26",
|
|
63
67
|
"vue-tsc": "^2.2.0"
|
|
@@ -68,6 +72,7 @@
|
|
|
68
72
|
"test": "vitest run",
|
|
69
73
|
"test:e2e": "playwright test",
|
|
70
74
|
"typecheck": "node scripts/typecheck.mjs",
|
|
75
|
+
"lint": "eslint . --max-warnings=0",
|
|
71
76
|
"spec:check": "node scripts/spec-check.mjs",
|
|
72
77
|
"changeset": "changeset",
|
|
73
78
|
"version-packages": "changeset version"
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import type { WorkflowBlob } from '../types/integrations.js';
|
|
2
|
-
/**
|
|
3
|
-
* Nodes that route failures to an error output nothing is connected to
|
|
4
|
-
* (specs/workflow-advice.md).
|
|
5
|
-
*
|
|
6
|
-
* A step that fails on a path its author drew is handled. A step that fails on a
|
|
7
|
-
* path nobody drew is not: the engine ends the run there
|
|
8
|
-
* (`specs/node-failure-handling.md` AC-8 in the service repository), and before
|
|
9
|
-
* it did, the failure vanished and the run reported success. Either way the
|
|
10
|
-
* author never chose it — they simply never saw the port.
|
|
11
|
-
*
|
|
12
|
-
* ## Why this is advice and not a finding
|
|
13
|
-
*
|
|
14
|
-
* The backend's validation findings feed the activation gate AND an
|
|
15
|
-
* unconditional refusal to start a run. A finding here would therefore stop
|
|
16
|
-
* every workflow with an unwired error port from running at all — turning
|
|
17
|
-
* "delivers nothing quietly" into "does not start", for authors who never asked
|
|
18
|
-
* for either. Computing it in the editor makes that impossible rather than
|
|
19
|
-
* merely unlikely: there is no value for a blocking consumer to read.
|
|
20
|
-
*
|
|
21
|
-
* What the client cannot do is be authoritative — the catalogue may not be
|
|
22
|
-
* loaded, and a workflow built through the API never passes here at all. For
|
|
23
|
-
* advice that is the right trade; for a gate it would not be, which is why the
|
|
24
|
-
* gate stays where the manifests are.
|
|
25
|
-
*
|
|
26
|
-
* ## Deliberately not in `validationFindings.ts`
|
|
27
|
-
*
|
|
28
|
-
* Everything there is a backend finding addressed by blob path, and the node
|
|
29
|
-
* card counts those as *incomplete settings*. An unwired port is neither. Sent
|
|
30
|
-
* through `groupFindingsByNode` it would type-check perfectly and quietly
|
|
31
|
-
* corrupt two counts — the banner's and the card badge's.
|
|
32
|
-
*/
|
|
33
|
-
export interface ErrorPathAdvice {
|
|
34
|
-
/** Blob node id — not an index, so it survives a node being added or removed. */
|
|
35
|
-
nodeId: string;
|
|
36
|
-
/** The error output with nothing attached. */
|
|
37
|
-
port: string;
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Every unwired error output in the blob, in node order.
|
|
41
|
-
*
|
|
42
|
-
* Runs against the blob the author is *editing*, not the saved one, so wiring a
|
|
43
|
-
* port clears its line in the same tick — no save, and no second copy of this
|
|
44
|
-
* rule to decide when the advice went stale.
|
|
45
|
-
*
|
|
46
|
-
* **Resolved against whichever manifest version the catalogue has.**
|
|
47
|
-
* `findManifest` falls back to another version of the same slug when the pinned
|
|
48
|
-
* one is absent, and that fallback is accepted here: a node pinned to a version
|
|
49
|
-
* nobody serves any more is better described by a sibling version than by
|
|
50
|
-
* silence. The cost is real though — where the versions disagree about their
|
|
51
|
-
* ports, the advice describes the sibling's, so a port could be named that the
|
|
52
|
-
* author's own node does not have.
|
|
53
|
-
*
|
|
54
|
-
* **Reads the manifest's static outputs only.** A node whose ports are named at
|
|
55
|
-
* author time (`sourceFromConfig` / `resolveOutputs`) cannot be judged from the
|
|
56
|
-
* declaration alone, so nothing is claimed about it — the same forbearance the
|
|
57
|
-
* engine shows. No shipped node has a dynamic error port today; if one appears,
|
|
58
|
-
* this is where it would silently escape notice.
|
|
59
|
-
*/
|
|
60
|
-
export declare function unwiredErrorPorts(blob: Pick<WorkflowBlob, 'nodes' | 'edges'>): ErrorPathAdvice[];
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { findManifest } from "../data/integrationNodeCatalog.js";
|
|
2
|
-
export function unwiredErrorPorts(blob) {
|
|
3
|
-
const advice = [];
|
|
4
|
-
for (const node of blob.nodes) {
|
|
5
|
-
const manifest = findManifest(node.nodeSlug, node.nodeVersion);
|
|
6
|
-
if (!manifest) continue;
|
|
7
|
-
const errorPorts = (manifest.outputs ?? []).filter((o) => o.kind === "error" && o.name).map((o) => o.name);
|
|
8
|
-
if (!errorPorts.length) continue;
|
|
9
|
-
if (!isReachable(node, blob, errorPorts)) continue;
|
|
10
|
-
for (const port of errorPorts) {
|
|
11
|
-
const wired = blob.edges.some((e) => e.from.nodeId === node.id && e.from.output === port);
|
|
12
|
-
if (!wired) advice.push({ nodeId: node.id, port });
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
return advice;
|
|
16
|
-
}
|
|
17
|
-
function isReachable(node, blob, errorPorts) {
|
|
18
|
-
return blob.edges.some((e) => e.to.nodeId === node.id) || blob.edges.some((e) => e.from.nodeId === node.id && !errorPorts.includes(e.from.output));
|
|
19
|
-
}
|