@pipefy/pipefy-process-coder 0.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/.agents/skills/ppc-pipefy-flow-authoring/SKILL.md +262 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/01-main-2-danfe-consulta.json +247 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/01-main-2-webhook-retorno-consulta.json +589 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/01-main-4-recebimento-barramento.json +1391 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/02-subflow-2-danfe-retorno.json +623 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/02-subflow-4-criacao-operacao.json +636 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/03-subflow-4-criacao-titulo.json +3642 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/04-subflow-4-criacao-cedente.json +863 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/04-subflow-4-criacao-sacado.json +799 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/README.md +42 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-01-subflow-1-titulo-acompanhamento-cobranca.json +581 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-01-subflow-1-titulo-nfe-monitoramento.json +503 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-01-subflow-1-titulo-retorno-bancario.json +562 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-02-subflow-2-retorno-consulta-cedente.json +557 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-02-subflow-2-retorno-consulta-sacado.json +609 -0
- package/.agents/skills/ppc-pipefy-pipe-authoring/SKILL.md +146 -0
- package/.agents/skills/ppc-pipefy-process-design/SKILL.md +127 -0
- package/.agents/skills/ppc-pipefy-workspace/SKILL.md +91 -0
- package/AGENTS.md +456 -0
- package/README.md +808 -0
- package/bin/pipe.js +13 -0
- package/package.json +35 -0
- package/src/apply/adopt.ts +150 -0
- package/src/apply/agentops.ts +71 -0
- package/src/apply/compile.ts +875 -0
- package/src/apply/execute.ts +336 -0
- package/src/apply/flowops.ts +399 -0
- package/src/apply/idmap.ts +241 -0
- package/src/apply/mutations.ts +955 -0
- package/src/apply/registry.ts +430 -0
- package/src/apply/types.ts +134 -0
- package/src/cli/args.ts +88 -0
- package/src/cli.ts +211 -0
- package/src/codec/flow.ts +199 -0
- package/src/codec/pack.ts +103 -0
- package/src/codec/roundtrip.ts +94 -0
- package/src/codec/unpack.ts +198 -0
- package/src/commands/agents.ts +184 -0
- package/src/commands/apply.ts +1144 -0
- package/src/commands/context.ts +119 -0
- package/src/commands/create.ts +79 -0
- package/src/commands/diff.ts +314 -0
- package/src/commands/flows.ts +414 -0
- package/src/commands/misc.ts +644 -0
- package/src/commands/plan.ts +331 -0
- package/src/commands/pull.ts +567 -0
- package/src/commands/runs.ts +83 -0
- package/src/commands/skills.ts +137 -0
- package/src/commands/verify.ts +253 -0
- package/src/config.ts +168 -0
- package/src/diff/agents.ts +122 -0
- package/src/diff/diff.ts +1130 -0
- package/src/diff/flow.ts +318 -0
- package/src/diff/html.ts +322 -0
- package/src/diff/render.ts +101 -0
- package/src/model/payload.ts +154 -0
- package/src/model/tree.ts +99 -0
- package/src/model/volatile.ts +55 -0
- package/src/pipefy/agents.ts +165 -0
- package/src/pipefy/automations.ts +219 -0
- package/src/pipefy/capability.ts +119 -0
- package/src/pipefy/client.ts +267 -0
- package/src/pipefy/discovery.ts +209 -0
- package/src/pipefy/internal.ts +380 -0
- package/src/pipefy/ipaas.ts +365 -0
- package/src/pipefy/reconstruct.ts +775 -0
- package/src/pipefy/reference.ts +251 -0
- package/src/pipefy/snapshot.ts +245 -0
- package/src/pipefy/toolkit.ts +200 -0
- package/src/pipefy/toolkit_bearer.py +137 -0
- package/src/report/integrations.ts +231 -0
- package/src/report/run.ts +475 -0
- package/src/util/fsx.ts +45 -0
- package/src/util/git.ts +32 -0
- package/src/util/json.ts +55 -0
- package/src/util/log.ts +76 -0
- package/src/util/pool.ts +48 -0
- package/src/util/slug.ts +26 -0
- package/src/util/tui.ts +335 -0
- package/src/validate/index.ts +123 -0
- package/src/validate/integrity.ts +387 -0
- package/src/validate/reference.ts +136 -0
- package/src/validate/schema.ts +328 -0
- package/src/workspace/agents.ts +290 -0
- package/src/workspace/docs.ts +407 -0
- package/src/workspace/flows.ts +191 -0
- package/src/workspace/layout.ts +165 -0
- package/src/workspace/lock.ts +148 -0
- package/src/workspace/read.ts +165 -0
- package/src/workspace/reference.ts +24 -0
- package/src/workspace/stamp.ts +301 -0
- package/src/workspace/write.ts +225 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import { dim, info, warn } from '../util/log.ts';
|
|
2
|
+
import { Progress } from '../util/tui.ts';
|
|
3
|
+
import { assertFlowRoundTrip, unlinkFlow } from '../codec/flow.ts';
|
|
4
|
+
import {
|
|
5
|
+
readFlows,
|
|
6
|
+
writeFlows,
|
|
7
|
+
writeFlowBaseline,
|
|
8
|
+
readFlowBaseline,
|
|
9
|
+
unknownFlows,
|
|
10
|
+
flowIdOf,
|
|
11
|
+
type FlowsSection,
|
|
12
|
+
} from '../workspace/flows.ts';
|
|
13
|
+
import { diffFlows, type FlowChangeSet } from '../diff/flow.ts';
|
|
14
|
+
import { buildFlowOperations, compileFlows, type FlowPlan, type FlowStep } from '../apply/flowops.ts';
|
|
15
|
+
import { IpaasClient, IpaasUnavailable, type ConnectionRow } from '../pipefy/ipaas.ts';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The flow half of the pipe loop: read on pull, diff, compile, apply.
|
|
19
|
+
*
|
|
20
|
+
* Kept in one module because the flow path is small and shares nothing with the
|
|
21
|
+
* pipe mutation compiler — the operations are per node against a different
|
|
22
|
+
* endpoint, and pretending otherwise would force both through a registry that
|
|
23
|
+
* describes GraphQL mutations.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** Read a pipe's whole iPaaS workspace, or record why it could not be read. */
|
|
27
|
+
export const pullFlowsFor = async (
|
|
28
|
+
client: IpaasClient,
|
|
29
|
+
pipeId: number,
|
|
30
|
+
opts: { skipRoundTrip?: boolean } = {},
|
|
31
|
+
): Promise<FlowsSection> => {
|
|
32
|
+
const probe = await client.available(pipeId);
|
|
33
|
+
if (!probe.ok) {
|
|
34
|
+
/**
|
|
35
|
+
* Not an error. iPaaS is gated per organization and per pipe, so "no token"
|
|
36
|
+
* is the normal answer for most pipes — and it must never be read as "this
|
|
37
|
+
* pipe has no flows", which is what would license deleting them later.
|
|
38
|
+
*/
|
|
39
|
+
return unknownFlows(probe.reason);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const { projectId } = probe.session;
|
|
43
|
+
const rows = await client.listFlows(pipeId);
|
|
44
|
+
const flows = [];
|
|
45
|
+
const notes: string[] = [];
|
|
46
|
+
|
|
47
|
+
for (const row of rows) {
|
|
48
|
+
const full = await client.getFlow(pipeId, String(row['id']));
|
|
49
|
+
if (!opts.skipRoundTrip) {
|
|
50
|
+
const rt = assertFlowRoundTrip(full);
|
|
51
|
+
if (!rt.ok) {
|
|
52
|
+
// The fold is not lossless for this flow, so the split files would not
|
|
53
|
+
// rebuild it. Reported, and the raw row is what the diff will use.
|
|
54
|
+
notes.push(
|
|
55
|
+
`flow ${row['id']}: codec round trip is not clean (${rt.differences
|
|
56
|
+
.map((d) => `${d.where}: ${d.detail}`)
|
|
57
|
+
.join('; ')})`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
flows.push(unlinkFlow(full));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const [connections, pieces, runs] = await Promise.all([
|
|
65
|
+
client.listConnections(pipeId),
|
|
66
|
+
client.listPieces(pipeId),
|
|
67
|
+
client.listRuns(pipeId),
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
meta: {
|
|
72
|
+
coverage: 'complete',
|
|
73
|
+
projectId,
|
|
74
|
+
readAt: new Date().toISOString(),
|
|
75
|
+
flows: flows.length,
|
|
76
|
+
connections: connections.length,
|
|
77
|
+
pieces: Object.keys(pieces).length,
|
|
78
|
+
notes,
|
|
79
|
+
},
|
|
80
|
+
flows,
|
|
81
|
+
connections,
|
|
82
|
+
pieces,
|
|
83
|
+
runs,
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/** Pull and write, with a line per flow. Returns what was read. */
|
|
88
|
+
export const pullAndWriteFlows = async (
|
|
89
|
+
client: IpaasClient,
|
|
90
|
+
pipeId: number,
|
|
91
|
+
repoDir: string,
|
|
92
|
+
opts: { skipRoundTrip?: boolean; workspaceRoot?: string } = {},
|
|
93
|
+
): Promise<FlowsSection> => {
|
|
94
|
+
const section = await pullFlowsFor(client, pipeId, opts);
|
|
95
|
+
await writeFlows(repoDir, section);
|
|
96
|
+
/**
|
|
97
|
+
* And record it as the baseline.
|
|
98
|
+
*
|
|
99
|
+
* Without this the flow diff had no past to compare against and re-read the
|
|
100
|
+
* pipe live on every invocation — so there was no offline diff, and no record
|
|
101
|
+
* of what a pull actually brought back. The pipe half has stored its baseline
|
|
102
|
+
* since the first version; this is the flow half catching up.
|
|
103
|
+
*/
|
|
104
|
+
if (opts.workspaceRoot) await writeFlowBaseline(opts.workspaceRoot, pipeId, section);
|
|
105
|
+
return section;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The flows diff: what is on disk against what was recorded at the last pull.
|
|
110
|
+
*
|
|
111
|
+
* Mirrors the pipe half — `baseline` needs no network, `live` re-reads the pipe.
|
|
112
|
+
* The default is the baseline, so `pipe diff` stays offline and answers the same
|
|
113
|
+
* question for both halves of a process: what did *I* change.
|
|
114
|
+
*
|
|
115
|
+
* `live` is what an apply uses, because what matters there is the current state
|
|
116
|
+
* of the pipe, not the state someone pulled an hour ago.
|
|
117
|
+
*/
|
|
118
|
+
export const diffFlowsFor = async (
|
|
119
|
+
client: IpaasClient | null,
|
|
120
|
+
pipeId: number,
|
|
121
|
+
repoDir: string,
|
|
122
|
+
opts: { against?: 'baseline' | 'live'; workspaceRoot: string },
|
|
123
|
+
): Promise<{ changeSet: FlowChangeSet; before: FlowsSection | null; authored: FlowsSection | null }> => {
|
|
124
|
+
const authored = await readFlows(repoDir);
|
|
125
|
+
if ((opts.against ?? 'baseline') === 'baseline') {
|
|
126
|
+
const before = await readFlowBaseline(opts.workspaceRoot, pipeId);
|
|
127
|
+
if (!before) {
|
|
128
|
+
const reason =
|
|
129
|
+
'no flow baseline for this repo — re-pull to record one, or use --against live';
|
|
130
|
+
return { changeSet: { changes: [], skipped: [{ reason }] }, before: null, authored };
|
|
131
|
+
}
|
|
132
|
+
return { changeSet: diffFlows(before, authored), before, authored };
|
|
133
|
+
}
|
|
134
|
+
if (!client) {
|
|
135
|
+
const reason = 'a live flow diff needs the network, and no client was supplied';
|
|
136
|
+
return { changeSet: { changes: [], skipped: [{ reason }] }, before: null, authored };
|
|
137
|
+
}
|
|
138
|
+
const live = await pullFlowsFor(client, pipeId, { skipRoundTrip: true });
|
|
139
|
+
return { changeSet: diffFlows(live, authored), before: live, authored };
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/** The flows diff against the live pipe. What an apply compiles from. */
|
|
143
|
+
export const diffFlowsAgainstLive = async (
|
|
144
|
+
client: IpaasClient,
|
|
145
|
+
pipeId: number,
|
|
146
|
+
repoDir: string,
|
|
147
|
+
): Promise<{ changeSet: FlowChangeSet; live: FlowsSection; authored: FlowsSection | null }> => {
|
|
148
|
+
const authored = await readFlows(repoDir);
|
|
149
|
+
const live = await pullFlowsFor(client, pipeId, { skipRoundTrip: true });
|
|
150
|
+
return { changeSet: diffFlows(live, authored), live, authored };
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The revert plan, given both sides already read — pure, no network, so it
|
|
155
|
+
* is the part `test/flows-revert.test.ts` can actually exercise without a
|
|
156
|
+
* live `IpaasClient`.
|
|
157
|
+
*
|
|
158
|
+
* Direction is the mirror of an ordinary apply: `live` is the *current*
|
|
159
|
+
* state (baseline), `before` is the old pre-apply one (intended target). A
|
|
160
|
+
* node the failed apply added and `before` never had comes out as a delete;
|
|
161
|
+
* one it removed comes out as a create — the same diff engine, just pointed
|
|
162
|
+
* backwards.
|
|
163
|
+
*/
|
|
164
|
+
export const buildFlowRevertPlan = (live: FlowsSection, before: FlowsSection): FlowPlan => {
|
|
165
|
+
const changeSet = diffFlows(live, before);
|
|
166
|
+
/**
|
|
167
|
+
* Allowed unconditionally, unlike an ordinary apply's --allow-flow-enable
|
|
168
|
+
* gate: putting a flow back to enabled is restoring what was already true
|
|
169
|
+
* before this apply touched it, not arming something new. The
|
|
170
|
+
* confirmation prompt renders the plan either way, so an enable is never
|
|
171
|
+
* silent.
|
|
172
|
+
*/
|
|
173
|
+
return planFlows(changeSet, { allowArm: true, connections: live.connections });
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* What reverting to a pre-apply flow state would take — computed, not sent.
|
|
178
|
+
*
|
|
179
|
+
* `before` is the live read `diffFlowsAgainstLive` already took right
|
|
180
|
+
* before this apply's own diff was computed — the only rollback point flows
|
|
181
|
+
* have at all, since Pipefy's snapshot restore never covered them (it is
|
|
182
|
+
* pipe structure only; see AGENTS.md). `live` here is read fresh, because
|
|
183
|
+
* some steps may have landed since `before` was taken.
|
|
184
|
+
*/
|
|
185
|
+
export const planFlowRevert = async (
|
|
186
|
+
client: IpaasClient,
|
|
187
|
+
pipeId: number,
|
|
188
|
+
before: FlowsSection,
|
|
189
|
+
): Promise<{ plan: FlowPlan; live: FlowsSection }> => {
|
|
190
|
+
const live = await pullFlowsFor(client, pipeId, { skipRoundTrip: true });
|
|
191
|
+
return { plan: buildFlowRevertPlan(live, before), live };
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
export const planFlows = (
|
|
195
|
+
cs: FlowChangeSet,
|
|
196
|
+
opts: { allowArm?: boolean; connections?: ConnectionRow[] } = {},
|
|
197
|
+
): FlowPlan =>
|
|
198
|
+
compileFlows(cs, {
|
|
199
|
+
allowArm: opts.allowArm,
|
|
200
|
+
connections: new Map(
|
|
201
|
+
(opts.connections ?? []).map((c) => [
|
|
202
|
+
c.externalId,
|
|
203
|
+
// A placeholder is reported as its own status so the compiler can refuse to
|
|
204
|
+
// arm a flow that depends on one.
|
|
205
|
+
{ pieceName: c.pieceName, status: c._placeholder ? 'PLACEHOLDER' : c.status },
|
|
206
|
+
]),
|
|
207
|
+
),
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
export const renderFlowPlan = (plan: FlowPlan): string => {
|
|
211
|
+
const lines: string[] = [];
|
|
212
|
+
if (plan.blocked.length) {
|
|
213
|
+
lines.push('');
|
|
214
|
+
lines.push(` ${plan.blocked.length} flow change(s) cannot be applied:`);
|
|
215
|
+
for (const b of plan.blocked) {
|
|
216
|
+
lines.push(` x ${b.label}`);
|
|
217
|
+
lines.push(` ${b.reason}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (!plan.steps.length) {
|
|
221
|
+
if (!plan.blocked.length) lines.push(' flows: nothing to apply');
|
|
222
|
+
return lines.join('\n');
|
|
223
|
+
}
|
|
224
|
+
lines.push('');
|
|
225
|
+
lines.push(` ${plan.steps.length} flow step(s)`);
|
|
226
|
+
for (const s of plan.steps) {
|
|
227
|
+
const tags = [s.destructive ? 'destructive' : '', s.arms ? 'ARMS THE FLOW' : ''].filter(Boolean);
|
|
228
|
+
lines.push(` ${s.id} ${s.description}${tags.length ? ` ${tags.join(' ')}` : ''}`);
|
|
229
|
+
}
|
|
230
|
+
return lines.join('\n');
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
export type FlowApplyResult = { done: number; failed: number; skipped: number; steps: FlowStep[] };
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Execute the flow steps.
|
|
237
|
+
*
|
|
238
|
+
* A created flow is laid down in one step from the outside — create, then trigger,
|
|
239
|
+
* then each node in execution order — because the flow id does not exist until the
|
|
240
|
+
* create returns, and every subsequent operation needs it. The alternative is
|
|
241
|
+
* placeholders in a second id space, which buys nothing here: the whole sequence
|
|
242
|
+
* is one flow's contents and either all of it lands or the flow is visibly
|
|
243
|
+
* half-built and reported as such.
|
|
244
|
+
*/
|
|
245
|
+
export const applyFlows = async (
|
|
246
|
+
client: IpaasClient,
|
|
247
|
+
pipeId: number,
|
|
248
|
+
plan: FlowPlan,
|
|
249
|
+
opts: { dryRun?: boolean; continueOnError?: boolean } = {},
|
|
250
|
+
): Promise<FlowApplyResult> => {
|
|
251
|
+
const ui = new Progress({
|
|
252
|
+
title: opts.dryRun ? `dry run · ${plan.steps.length} flow steps` : `applying ${plan.steps.length} flow steps`,
|
|
253
|
+
});
|
|
254
|
+
for (const s of plan.steps) {
|
|
255
|
+
ui.add({
|
|
256
|
+
id: s.id,
|
|
257
|
+
label: s.description,
|
|
258
|
+
state: 'pending',
|
|
259
|
+
tags: [...(s.destructive ? ['destructive'] : []), ...(s.arms ? ['arms'] : [])],
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
ui.start();
|
|
263
|
+
|
|
264
|
+
for (const step of plan.steps) {
|
|
265
|
+
if (opts.dryRun) {
|
|
266
|
+
step.status = 'skipped';
|
|
267
|
+
ui.update(step.id, { state: 'done', detail: 'not sent' });
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
step.status = 'running';
|
|
272
|
+
ui.update(step.id, { state: 'running' });
|
|
273
|
+
try {
|
|
274
|
+
if (step.createFlow) {
|
|
275
|
+
const flowId = await client.createFlow(pipeId, step.createFlow.displayName);
|
|
276
|
+
step.flowId = flowId;
|
|
277
|
+
const ops = buildFlowOperations(step.createFlow.tree);
|
|
278
|
+
for (const [i, op] of ops.entries()) {
|
|
279
|
+
ui.update(step.id, { detail: `${i + 1}/${ops.length} ${op.describe}` });
|
|
280
|
+
await client.operate(pipeId, flowId, op.type, op.request);
|
|
281
|
+
}
|
|
282
|
+
step.result = { flowId, operations: ops.length };
|
|
283
|
+
ui.update(step.id, { state: 'done', detail: `flow ${flowId}, ${ops.length} operations` });
|
|
284
|
+
} else if (step.createConnection) {
|
|
285
|
+
const made = await client.createPlaceholderConnection(pipeId, step.createConnection);
|
|
286
|
+
step.result = made;
|
|
287
|
+
ui.update(step.id, {
|
|
288
|
+
state: 'done',
|
|
289
|
+
detail: `${made.externalId} — placeholder, reconnect before enabling`,
|
|
290
|
+
});
|
|
291
|
+
} else if (step.deleteFlow) {
|
|
292
|
+
await client.deleteFlow(pipeId, step.flowId as string);
|
|
293
|
+
step.status = 'done';
|
|
294
|
+
ui.update(step.id, { state: 'done' });
|
|
295
|
+
} else if (step.operation) {
|
|
296
|
+
step.result = await client.operate(
|
|
297
|
+
pipeId,
|
|
298
|
+
step.flowId as string,
|
|
299
|
+
step.operation.type,
|
|
300
|
+
step.operation.request,
|
|
301
|
+
);
|
|
302
|
+
ui.update(step.id, { state: 'done' });
|
|
303
|
+
}
|
|
304
|
+
step.status = 'done';
|
|
305
|
+
} catch (e) {
|
|
306
|
+
step.status = 'failed';
|
|
307
|
+
step.error = (e as Error).message;
|
|
308
|
+
ui.update(step.id, { state: 'failed', detail: step.error });
|
|
309
|
+
if (!opts.continueOnError) break;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const count = (s: FlowStep['status']) => plan.steps.filter((x) => x.status === s).length;
|
|
314
|
+
ui.stop();
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Placeholders are announced after the run, not buried in the step list.
|
|
318
|
+
*
|
|
319
|
+
* The host marks any connection ACTIVE without validating its credential, so
|
|
320
|
+
* nothing downstream will tell you these do not work — the flow simply fails on
|
|
321
|
+
* every trigger once enabled.
|
|
322
|
+
*/
|
|
323
|
+
const placeholders = plan.steps.filter((s) => s.createConnection && s.status === 'done');
|
|
324
|
+
if (placeholders.length && !opts.dryRun) {
|
|
325
|
+
info('');
|
|
326
|
+
warn(
|
|
327
|
+
`${placeholders.length} placeholder connection(s) were created — the flow is built but not ` +
|
|
328
|
+
'authenticated',
|
|
329
|
+
);
|
|
330
|
+
for (const s of placeholders) {
|
|
331
|
+
info(dim(` ${s.createConnection!.externalId} ${s.createConnection!.pieceName}`));
|
|
332
|
+
}
|
|
333
|
+
info(dim(' reconnect each under Advanced Automations > Connections before enabling a flow'));
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return { done: count('done'), failed: count('failed'), skipped: count('skipped'), steps: plan.steps };
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* After applying, read the flows back and confirm every datapill survived exactly.
|
|
341
|
+
*
|
|
342
|
+
* This is the check nothing else can make. A migrating write path — `IMPORT_FLOW`
|
|
343
|
+
* is one — rewrites `{{step_1['output'].x}}` into `{{step_1['output']['output'].x}}`,
|
|
344
|
+
* which is structurally valid, passes every schema check, passes the platform's own
|
|
345
|
+
* validator, and resolves to nothing at runtime. Only comparing the stored string
|
|
346
|
+
* against the intended one finds it.
|
|
347
|
+
*/
|
|
348
|
+
export const verifyFlowDatapills = async (
|
|
349
|
+
client: IpaasClient,
|
|
350
|
+
pipeId: number,
|
|
351
|
+
authored: FlowsSection,
|
|
352
|
+
): Promise<Array<{ flow: string; node: string; at: string; sent: string; stored: string }>> => {
|
|
353
|
+
const drift: Array<{ flow: string; node: string; at: string; sent: string; stored: string }> = [];
|
|
354
|
+
|
|
355
|
+
const pillsOf = (v: unknown, at: string, out: Array<{ at: string; pill: string }> = []) => {
|
|
356
|
+
if (typeof v === 'string') {
|
|
357
|
+
for (const m of v.matchAll(/\{\{[^}]+\}\}/g)) out.push({ at, pill: m[0] });
|
|
358
|
+
return out;
|
|
359
|
+
}
|
|
360
|
+
if (Array.isArray(v)) {
|
|
361
|
+
v.forEach((x, i) => pillsOf(x, `${at}[${i}]`, out));
|
|
362
|
+
return out;
|
|
363
|
+
}
|
|
364
|
+
if (v && typeof v === 'object') for (const [k, val] of Object.entries(v)) pillsOf(val, at ? `${at}.${k}` : k, out);
|
|
365
|
+
return out;
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
for (const tree of authored.flows) {
|
|
369
|
+
const id = flowIdOf(tree);
|
|
370
|
+
if (!id) continue;
|
|
371
|
+
let live;
|
|
372
|
+
try {
|
|
373
|
+
live = unlinkFlow(await client.getFlow(pipeId, id));
|
|
374
|
+
} catch {
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
const liveByName = new Map(live.nodes.map((n) => [n.name, n]));
|
|
378
|
+
for (const node of tree.nodes) {
|
|
379
|
+
const other = liveByName.get(node.name);
|
|
380
|
+
if (!other) continue;
|
|
381
|
+
const sent = pillsOf(node['settings'], 'settings');
|
|
382
|
+
const stored = pillsOf(other['settings'], 'settings');
|
|
383
|
+
for (const one of sent) {
|
|
384
|
+
const sameKey = stored.filter((x) => x.at === one.at);
|
|
385
|
+
if (!sameKey.length) continue;
|
|
386
|
+
if (!sameKey.some((x) => x.pill === one.pill)) {
|
|
387
|
+
drift.push({
|
|
388
|
+
flow: String(tree.version['displayName'] ?? id),
|
|
389
|
+
node: node.name,
|
|
390
|
+
at: one.at,
|
|
391
|
+
sent: one.pill,
|
|
392
|
+
stored: sameKey[0]!.pill,
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return drift;
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
export const reportDatapillDrift = (drift: Awaited<ReturnType<typeof verifyFlowDatapills>>) => {
|
|
402
|
+
if (!drift.length) return;
|
|
403
|
+
warn(`${drift.length} datapill(s) are not stored as they were sent`);
|
|
404
|
+
for (const d of drift.slice(0, 8)) {
|
|
405
|
+
info(dim(` ${d.flow} / ${d.node} ${d.at}`));
|
|
406
|
+
info(dim(` sent ${d.sent}`));
|
|
407
|
+
info(dim(` stored ${d.stored}`));
|
|
408
|
+
}
|
|
409
|
+
if (drift.some((d) => d.stored.includes("['output']['output']"))) {
|
|
410
|
+
info(dim(" ['output']['output'] is a double migration — the write path migrated an already-live flow"));
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
export { IpaasUnavailable };
|