@pipefy/pipefy-process-coder 0.1.0 → 0.1.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/.agents/skills/ppc-pipefy-flow-authoring/SKILL.md +262 -262
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/01-main-2-danfe-consulta.json +247 -247
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/01-main-2-webhook-retorno-consulta.json +589 -589
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/01-main-4-recebimento-barramento.json +1391 -1391
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/02-subflow-2-danfe-retorno.json +623 -623
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/02-subflow-4-criacao-operacao.json +636 -636
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/03-subflow-4-criacao-titulo.json +3642 -3642
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/04-subflow-4-criacao-cedente.json +863 -863
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/04-subflow-4-criacao-sacado.json +799 -799
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/README.md +42 -42
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-01-subflow-1-titulo-acompanhamento-cobranca.json +581 -581
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-01-subflow-1-titulo-nfe-monitoramento.json +503 -503
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-01-subflow-1-titulo-retorno-bancario.json +562 -562
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-02-subflow-2-retorno-consulta-cedente.json +557 -557
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-02-subflow-2-retorno-consulta-sacado.json +609 -609
- package/.agents/skills/ppc-pipefy-pipe-authoring/SKILL.md +146 -146
- package/.agents/skills/ppc-pipefy-process-design/SKILL.md +127 -127
- package/.agents/skills/ppc-pipefy-workspace/SKILL.md +91 -91
- package/AGENTS.md +465 -456
- package/README.md +825 -807
- package/bin/pipe.js +25 -4
- package/dist/apply/adopt.js +128 -0
- package/dist/apply/agentops.js +42 -0
- package/dist/apply/compile.js +722 -0
- package/dist/apply/execute.js +277 -0
- package/dist/apply/flowops.js +293 -0
- package/dist/apply/idmap.js +233 -0
- package/dist/apply/mutations.js +704 -0
- package/dist/apply/registry.js +348 -0
- package/dist/apply/types.js +18 -0
- package/dist/cli/args.js +79 -0
- package/dist/cli.js +187 -0
- package/dist/codec/flow.js +164 -0
- package/dist/codec/pack.js +88 -0
- package/dist/codec/roundtrip.js +86 -0
- package/dist/codec/unpack.js +181 -0
- package/dist/commands/agents.js +145 -0
- package/dist/commands/apply.js +982 -0
- package/dist/commands/context.js +79 -0
- package/dist/commands/create.js +63 -0
- package/dist/commands/diff.js +294 -0
- package/dist/commands/flows.js +348 -0
- package/dist/commands/misc.js +528 -0
- package/dist/commands/plan.js +301 -0
- package/dist/commands/pull.js +511 -0
- package/dist/commands/runs.js +74 -0
- package/dist/commands/skills.js +131 -0
- package/dist/commands/verify.js +205 -0
- package/dist/config.js +125 -0
- package/dist/diff/agents.js +80 -0
- package/dist/diff/diff.js +983 -0
- package/dist/diff/flow.js +258 -0
- package/{src/diff/html.ts → dist/diff/html.js} +274 -322
- package/dist/diff/render.js +90 -0
- package/dist/model/payload.js +84 -0
- package/dist/model/tree.js +7 -0
- package/dist/model/volatile.js +54 -0
- package/dist/pipefy/agents.js +122 -0
- package/dist/pipefy/automations.js +155 -0
- package/dist/pipefy/capability.js +84 -0
- package/dist/pipefy/client.js +204 -0
- package/dist/pipefy/discovery.js +148 -0
- package/dist/pipefy/internal.js +315 -0
- package/dist/pipefy/ipaas.js +269 -0
- package/dist/pipefy/reconstruct.js +674 -0
- package/dist/pipefy/reference.js +172 -0
- package/dist/pipefy/snapshot.js +185 -0
- package/dist/pipefy/toolkit.js +158 -0
- package/dist/report/integrations.js +165 -0
- package/dist/report/run.js +355 -0
- package/dist/util/fsx.js +50 -0
- package/dist/util/git.js +25 -0
- package/dist/util/json.js +56 -0
- package/dist/util/log.js +69 -0
- package/dist/util/pool.js +42 -0
- package/dist/util/slug.js +23 -0
- package/dist/util/tui.js +287 -0
- package/dist/validate/index.js +86 -0
- package/dist/validate/integrity.js +370 -0
- package/dist/validate/reference.js +120 -0
- package/dist/validate/schema.js +278 -0
- package/dist/workspace/agents.js +245 -0
- package/{src/workspace/docs.ts → dist/workspace/docs.js} +367 -407
- package/dist/workspace/flows.js +149 -0
- package/dist/workspace/layout.js +146 -0
- package/dist/workspace/lock.js +83 -0
- package/dist/workspace/read.js +168 -0
- package/dist/workspace/reference.js +24 -0
- package/dist/workspace/stamp.js +234 -0
- package/dist/workspace/write.js +194 -0
- package/package.json +37 -35
- package/src/apply/adopt.ts +0 -150
- package/src/apply/agentops.ts +0 -71
- package/src/apply/compile.ts +0 -875
- package/src/apply/execute.ts +0 -336
- package/src/apply/flowops.ts +0 -399
- package/src/apply/idmap.ts +0 -241
- package/src/apply/mutations.ts +0 -955
- package/src/apply/registry.ts +0 -430
- package/src/apply/types.ts +0 -134
- package/src/cli/args.ts +0 -88
- package/src/cli.ts +0 -211
- package/src/codec/flow.ts +0 -199
- package/src/codec/pack.ts +0 -103
- package/src/codec/roundtrip.ts +0 -94
- package/src/codec/unpack.ts +0 -198
- package/src/commands/agents.ts +0 -184
- package/src/commands/apply.ts +0 -1144
- package/src/commands/context.ts +0 -119
- package/src/commands/create.ts +0 -79
- package/src/commands/diff.ts +0 -314
- package/src/commands/flows.ts +0 -414
- package/src/commands/misc.ts +0 -644
- package/src/commands/plan.ts +0 -331
- package/src/commands/pull.ts +0 -567
- package/src/commands/runs.ts +0 -83
- package/src/commands/skills.ts +0 -137
- package/src/commands/verify.ts +0 -253
- package/src/config.ts +0 -168
- package/src/diff/agents.ts +0 -122
- package/src/diff/diff.ts +0 -1130
- package/src/diff/flow.ts +0 -318
- package/src/diff/render.ts +0 -101
- package/src/model/payload.ts +0 -154
- package/src/model/tree.ts +0 -99
- package/src/model/volatile.ts +0 -55
- package/src/pipefy/agents.ts +0 -165
- package/src/pipefy/automations.ts +0 -219
- package/src/pipefy/capability.ts +0 -119
- package/src/pipefy/client.ts +0 -267
- package/src/pipefy/discovery.ts +0 -209
- package/src/pipefy/internal.ts +0 -380
- package/src/pipefy/ipaas.ts +0 -365
- package/src/pipefy/reconstruct.ts +0 -775
- package/src/pipefy/reference.ts +0 -251
- package/src/pipefy/snapshot.ts +0 -245
- package/src/pipefy/toolkit.ts +0 -200
- package/src/report/integrations.ts +0 -231
- package/src/report/run.ts +0 -475
- package/src/util/fsx.ts +0 -45
- package/src/util/git.ts +0 -32
- package/src/util/json.ts +0 -55
- package/src/util/log.ts +0 -76
- package/src/util/pool.ts +0 -48
- package/src/util/slug.ts +0 -26
- package/src/util/tui.ts +0 -335
- package/src/validate/index.ts +0 -123
- package/src/validate/integrity.ts +0 -387
- package/src/validate/reference.ts +0 -136
- package/src/validate/schema.ts +0 -328
- package/src/workspace/agents.ts +0 -290
- package/src/workspace/flows.ts +0 -191
- package/src/workspace/layout.ts +0 -165
- package/src/workspace/lock.ts +0 -148
- package/src/workspace/read.ts +0 -165
- package/src/workspace/reference.ts +0 -24
- package/src/workspace/stamp.ts +0 -301
- package/src/workspace/write.ts +0 -225
- /package/{src → dist}/pipefy/toolkit_bearer.py +0 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { writeJson } from "../util/fsx.js";
|
|
2
|
+
import { debug, dim, green, red, yellow } from "../util/log.js";
|
|
3
|
+
import { Progress } from "../util/tui.js";
|
|
4
|
+
import { IdMap } from "./idmap.js";
|
|
5
|
+
const pluck = (obj, path) => {
|
|
6
|
+
let cur = obj;
|
|
7
|
+
for (const key of path) {
|
|
8
|
+
if (cur && typeof cur === 'object' && key in cur) {
|
|
9
|
+
cur = cur[key];
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return cur;
|
|
16
|
+
};
|
|
17
|
+
export const execute = async (plan, opts) => {
|
|
18
|
+
const idMap = new IdMap(plan.idMap);
|
|
19
|
+
plan.status = 'running';
|
|
20
|
+
/**
|
|
21
|
+
* The step list, live. A plan is a queue of network calls whose count and
|
|
22
|
+
* order the user has already seen in `pipe plan`, so showing that same list
|
|
23
|
+
* fill in is the honest progress indicator — and in a dry run it shows exactly
|
|
24
|
+
* what would be sent, in order, without sending it.
|
|
25
|
+
*/
|
|
26
|
+
const ui = new Progress({
|
|
27
|
+
title: opts.dryRun ? `dry run · ${plan.steps.length} steps` : `applying ${plan.steps.length} steps`,
|
|
28
|
+
});
|
|
29
|
+
for (const s of plan.steps) {
|
|
30
|
+
ui.add({
|
|
31
|
+
id: s.id,
|
|
32
|
+
label: s.description,
|
|
33
|
+
state: s.status === 'done' ? 'done' : 'pending',
|
|
34
|
+
tags: [
|
|
35
|
+
...(s.destructive ? ['destructive'] : []),
|
|
36
|
+
...(s.unverified ? ['unverified'] : []),
|
|
37
|
+
...(s.kind.startsWith('internal') ? ['internal-api'] : []),
|
|
38
|
+
],
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
ui.start();
|
|
42
|
+
const persist = async () => {
|
|
43
|
+
if (!opts.dryRun)
|
|
44
|
+
await writeJson(opts.planPath, plan);
|
|
45
|
+
};
|
|
46
|
+
for (const s of plan.steps) {
|
|
47
|
+
if (s.status === 'done') {
|
|
48
|
+
// Resume: a completed create still has to re-register its placeholder so
|
|
49
|
+
// later steps can resolve against it.
|
|
50
|
+
if (s.produces && s.result !== undefined) {
|
|
51
|
+
const id = Number(pluck(s.result, s.produces.path));
|
|
52
|
+
if (Number.isFinite(id))
|
|
53
|
+
idMap.resolvePlaceholder(s.produces.placeholder, id, s.produces.aliases ?? []);
|
|
54
|
+
}
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const unresolved = idMap.unresolved(s.request.variables ?? s.request.form ?? s.request.path ?? null);
|
|
58
|
+
if (unresolved.length) {
|
|
59
|
+
s.status = 'skipped';
|
|
60
|
+
s.error = `depends on ids that were never created: ${unresolved.join(', ')}`;
|
|
61
|
+
ui.update(s.id, { state: 'skipped', detail: s.error });
|
|
62
|
+
await persist();
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const request = {
|
|
66
|
+
...s.request,
|
|
67
|
+
variables: s.request.variables ? idMap.rewrite(s.request.variables) : undefined,
|
|
68
|
+
form: s.request.form ? idMap.rewrite(s.request.form) : undefined,
|
|
69
|
+
path: s.request.path ? idMap.rewrite({ p: s.request.path }).p : undefined,
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Nothing leaves here still carrying a placeholder.
|
|
73
|
+
*
|
|
74
|
+
* A *registered* placeholder passes the unresolved check above, but is only
|
|
75
|
+
* substituted if its key is known to hold an id — so a new id-bearing key
|
|
76
|
+
* that nobody added to ID_BEARING_KEYS sends the marker to the API verbatim.
|
|
77
|
+
* That happened with `fieldConditionIds`, and the resulting error named the
|
|
78
|
+
* field but said nothing about the cause. This refuses to send, and says
|
|
79
|
+
* which key.
|
|
80
|
+
*/
|
|
81
|
+
const leftovers = [];
|
|
82
|
+
const findMarkers = (v, path) => {
|
|
83
|
+
if (typeof v === 'string') {
|
|
84
|
+
if (v.includes('_new:'))
|
|
85
|
+
leftovers.push(`${path} = ${v}`);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (Array.isArray(v)) {
|
|
89
|
+
v.forEach((x, i) => findMarkers(x, `${path}[${i}]`));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (v && typeof v === 'object') {
|
|
93
|
+
for (const [k, val] of Object.entries(v)) {
|
|
94
|
+
findMarkers(val, path ? `${path}.${k}` : k);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
findMarkers(request.variables ?? null, '');
|
|
99
|
+
findMarkers(request.form ?? null, '');
|
|
100
|
+
if (leftovers.length) {
|
|
101
|
+
s.status = 'failed';
|
|
102
|
+
s.error =
|
|
103
|
+
`unsubstituted placeholder at ${leftovers.join(', ')} — that key is not in ID_BEARING_KEYS, ` +
|
|
104
|
+
'so nothing replaced it. Not sent.';
|
|
105
|
+
s.finishedAt = new Date().toISOString();
|
|
106
|
+
ui.update(s.id, { state: 'failed', detail: s.error });
|
|
107
|
+
await persist();
|
|
108
|
+
if (!opts.continueOnError)
|
|
109
|
+
break;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (opts.dryRun) {
|
|
113
|
+
s.status = 'skipped';
|
|
114
|
+
/**
|
|
115
|
+
* A dry run marks each step done in the display rather than skipped: the
|
|
116
|
+
* step is complete as far as this run goes, and painting a whole plan
|
|
117
|
+
* yellow suggests something went wrong when nothing did. The title says
|
|
118
|
+
* "dry run", and nothing was sent.
|
|
119
|
+
*/
|
|
120
|
+
ui.update(s.id, { state: 'done', detail: 'not sent' });
|
|
121
|
+
debug(JSON.stringify(request.variables ?? request.form ?? {}).slice(0, 400));
|
|
122
|
+
opts.onProgress?.(s, plan);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
s.status = 'running';
|
|
126
|
+
s.startedAt = new Date().toISOString();
|
|
127
|
+
await persist();
|
|
128
|
+
ui.update(s.id, { state: 'running' });
|
|
129
|
+
try {
|
|
130
|
+
let result;
|
|
131
|
+
if (s.kind === 'graphql') {
|
|
132
|
+
try {
|
|
133
|
+
result = await opts.client.raw(request.query, request.variables, s.request.mutation);
|
|
134
|
+
}
|
|
135
|
+
catch (e) {
|
|
136
|
+
/**
|
|
137
|
+
* `phaseSettings` is known to answer "Something went wrong" for a change
|
|
138
|
+
* it has just made — observed twice, on two different runs, with the
|
|
139
|
+
* `phase_team_memberships` row present in the read-back both times and
|
|
140
|
+
* timestamped to the failing call. Its failures are therefore
|
|
141
|
+
* inconclusive, not negative, and saying so is the difference between
|
|
142
|
+
* an operator re-running a harmless write and an operator hunting a
|
|
143
|
+
* change that already exists.
|
|
144
|
+
*/
|
|
145
|
+
if (s.request.mutation === 'phaseSettings') {
|
|
146
|
+
throw new Error(`${e.message} — note that phaseSettings has been observed reporting this ` +
|
|
147
|
+
'for a change it did apply. Re-read the phase before assuming nothing happened.');
|
|
148
|
+
}
|
|
149
|
+
throw e;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
else if (s.kind === 'internal-graphql') {
|
|
153
|
+
result = await runInternalAutomationStep(opts.internal, s, request.variables ?? {});
|
|
154
|
+
}
|
|
155
|
+
else if (s.kind === 'internal-rest') {
|
|
156
|
+
result = await runInternalRestStep(opts.internal, s, request);
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
result = { noop: true };
|
|
160
|
+
}
|
|
161
|
+
s.result = result;
|
|
162
|
+
s.status = 'done';
|
|
163
|
+
s.finishedAt = new Date().toISOString();
|
|
164
|
+
ui.update(s.id, { state: 'done' });
|
|
165
|
+
if (s.produces) {
|
|
166
|
+
const raw = pluck(result, s.produces.path);
|
|
167
|
+
const id = Number(raw);
|
|
168
|
+
if (Number.isFinite(id)) {
|
|
169
|
+
idMap.resolvePlaceholder(s.produces.placeholder, id, s.produces.aliases ?? []);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
ui.note(yellow('! ') + `${s.id} succeeded but produced no id at ${s.produces.path.join('.')} — later steps may skip`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
opts.onProgress?.(s, plan);
|
|
176
|
+
}
|
|
177
|
+
catch (e) {
|
|
178
|
+
s.status = 'failed';
|
|
179
|
+
s.error = e.message;
|
|
180
|
+
s.finishedAt = new Date().toISOString();
|
|
181
|
+
ui.update(s.id, { state: 'failed', detail: s.error });
|
|
182
|
+
await persist();
|
|
183
|
+
opts.onProgress?.(s, plan);
|
|
184
|
+
if (!opts.continueOnError)
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
await persist();
|
|
188
|
+
}
|
|
189
|
+
const counts = plan.steps.reduce((acc, s) => {
|
|
190
|
+
acc[s.status] = (acc[s.status] ?? 0) + 1;
|
|
191
|
+
return acc;
|
|
192
|
+
}, {});
|
|
193
|
+
plan.status =
|
|
194
|
+
(counts['failed'] ?? 0) > 0
|
|
195
|
+
? (counts['done'] ?? 0) > 0
|
|
196
|
+
? 'partially-applied'
|
|
197
|
+
: 'failed'
|
|
198
|
+
: opts.dryRun
|
|
199
|
+
? 'planned'
|
|
200
|
+
: 'applied';
|
|
201
|
+
await persist();
|
|
202
|
+
const done = counts['done'] ?? 0;
|
|
203
|
+
const failed = counts['failed'] ?? 0;
|
|
204
|
+
const skipped = counts['skipped'] ?? 0;
|
|
205
|
+
/**
|
|
206
|
+
* A dry run's steps are recorded as skipped, which is right for the plan file
|
|
207
|
+
* and wrong as a headline: "0 done, 2 skipped" reads like something went
|
|
208
|
+
* wrong. Nothing did — nothing was sent.
|
|
209
|
+
*/
|
|
210
|
+
ui.stop(opts.dryRun
|
|
211
|
+
? ' ' + green(`${plan.steps.length} step${plan.steps.length === 1 ? '' : 's'} simulated`) + dim(', nothing sent')
|
|
212
|
+
: ' ' +
|
|
213
|
+
[
|
|
214
|
+
done ? green(`${done} done`) : dim('0 done'),
|
|
215
|
+
failed ? red(`${failed} failed`) : dim('0 failed'),
|
|
216
|
+
skipped ? yellow(`${skipped} skipped`) : dim('0 skipped'),
|
|
217
|
+
].join(dim(' · ')) +
|
|
218
|
+
dim(` · ${(ui.elapsed / 1000).toFixed(1)}s`));
|
|
219
|
+
return {
|
|
220
|
+
plan,
|
|
221
|
+
done: counts['done'] ?? 0,
|
|
222
|
+
failed: counts['failed'] ?? 0,
|
|
223
|
+
skipped: counts['skipped'] ?? 0,
|
|
224
|
+
pending: counts['pending'] ?? 0,
|
|
225
|
+
idMap,
|
|
226
|
+
};
|
|
227
|
+
};
|
|
228
|
+
const runInternalAutomationStep = async (internal, s, variables) => {
|
|
229
|
+
if (s.request.mutation === 'createAutomation') {
|
|
230
|
+
const created = await internal.createAutomation(variables);
|
|
231
|
+
if (!created)
|
|
232
|
+
throw new Error('createAutomation returned no automation (internal API unavailable or rejected)');
|
|
233
|
+
return { createAutomation: { automation: created } };
|
|
234
|
+
}
|
|
235
|
+
if (s.request.mutation === 'deleteAutomation') {
|
|
236
|
+
const success = await internal.deleteAutomation(String(variables['id']));
|
|
237
|
+
if (!success)
|
|
238
|
+
throw new Error(`deleteAutomation(${String(variables['id'])}) did not report success`);
|
|
239
|
+
return { deleteAutomation: { success } };
|
|
240
|
+
}
|
|
241
|
+
if (s.request.mutation === 'updateAutomation') {
|
|
242
|
+
const res = await internal.gql(`mutation updateAutomation($input: UpdateAutomationInput!) {
|
|
243
|
+
updateAutomation(input: $input) { automation { id } error_details { object_key messages } }
|
|
244
|
+
}`, { input: variables }, 'updateAutomation');
|
|
245
|
+
if (!res)
|
|
246
|
+
throw new Error('updateAutomation failed on the internal endpoint');
|
|
247
|
+
return res;
|
|
248
|
+
}
|
|
249
|
+
throw new Error(`unknown internal mutation ${s.request.mutation}`);
|
|
250
|
+
};
|
|
251
|
+
const runInternalRestStep = async (internal, s, request) => {
|
|
252
|
+
const phaseId = /\/phases\/([^/?]+)/.exec(request.path ?? '')?.[1];
|
|
253
|
+
if (!phaseId)
|
|
254
|
+
throw new Error(`could not read a phase id out of ${request.path}`);
|
|
255
|
+
const targets = (request.form?.['phase[jump_target_ids][]'] ?? []).filter((t) => t !== '');
|
|
256
|
+
/**
|
|
257
|
+
* A jump target is an id and nothing else.
|
|
258
|
+
*
|
|
259
|
+
* The endpoint answers a malformed target with a bare 404, which reads as
|
|
260
|
+
* "that phase does not exist" and sends you looking in the wrong place. Every
|
|
261
|
+
* such 404 so far has been a substitution that did not happen — a placeholder
|
|
262
|
+
* rewritten to `%{344123894}` rather than to `344123894`. Checking the shape
|
|
263
|
+
* here names the real cause.
|
|
264
|
+
*/
|
|
265
|
+
const malformed = targets.filter((t) => !/^[0-9]+$/.test(t));
|
|
266
|
+
if (malformed.length) {
|
|
267
|
+
throw new Error(`jump target(s) ${malformed.join(', ')} are not ids — an id substitution did not happen. Not sent.`);
|
|
268
|
+
}
|
|
269
|
+
const res = await internal.setPhaseJumpTargets(phaseId, targets);
|
|
270
|
+
/**
|
|
271
|
+
* Assert on the echoed next_phase_ids, not on the HTTP status — PLAN.md §2.1.
|
|
272
|
+
* A 200 from an unversioned endpoint is not evidence that anything changed.
|
|
273
|
+
*/
|
|
274
|
+
if (!res.ok)
|
|
275
|
+
throw new Error(res.error ?? 'phase jump write could not be confirmed from the response');
|
|
276
|
+
return res;
|
|
277
|
+
};
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { linkFlow } from "../codec/flow.js";
|
|
2
|
+
/** The node as an operation request: the whole node, minus link and volatile keys. */
|
|
3
|
+
const nodeRequest = (node) => {
|
|
4
|
+
const { _nextAction, _firstLoopAction, _children, ...rest } = node;
|
|
5
|
+
const out = {
|
|
6
|
+
name: rest['name'],
|
|
7
|
+
type: rest['type'],
|
|
8
|
+
valid: rest['valid'] ?? true,
|
|
9
|
+
settings: rest['settings'] ?? {},
|
|
10
|
+
};
|
|
11
|
+
if (rest['displayName'] !== undefined)
|
|
12
|
+
out['displayName'] = rest['displayName'];
|
|
13
|
+
if (rest['skip'] !== undefined)
|
|
14
|
+
out['skip'] = rest['skip'];
|
|
15
|
+
return out;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Where a new node hangs. `ADD_ACTION` takes the parent it follows, plus a
|
|
19
|
+
* location for the cases where "after" is ambiguous.
|
|
20
|
+
*
|
|
21
|
+
* A bare `{ parentStep }` with no location is only unambiguous when the
|
|
22
|
+
* parent is a plain step. When the parent is itself a `LOOP_ON_ITEMS`,
|
|
23
|
+
* "after it" could mean *inside* the loop or *exiting* it, and the endpoint
|
|
24
|
+
* refuses to guess: measured live (`research/ipaas/probe-loop-shape-3.ts`),
|
|
25
|
+
* the identical omitted-location request answers `Loop step parent undefined
|
|
26
|
+
* not found`, and adding `stepLocationRelativeToParent: 'AFTER'` is what
|
|
27
|
+
* makes the same add succeed. Every flow this tool wrote before had no loop
|
|
28
|
+
* at all, so this was never exercised.
|
|
29
|
+
*/
|
|
30
|
+
const parentOf = (tree, nodeName) => {
|
|
31
|
+
for (const candidate of tree.nodes) {
|
|
32
|
+
if (candidate['_nextAction'] === nodeName) {
|
|
33
|
+
return { parentStep: candidate.name, ...(candidate['type'] === 'LOOP_ON_ITEMS' ? { location: 'AFTER' } : {}) };
|
|
34
|
+
}
|
|
35
|
+
if (candidate['_firstLoopAction'] === nodeName) {
|
|
36
|
+
return { parentStep: candidate.name, location: 'INSIDE_LOOP' };
|
|
37
|
+
}
|
|
38
|
+
const children = candidate['_children'] ?? [];
|
|
39
|
+
const at = children.indexOf(nodeName);
|
|
40
|
+
if (at >= 0)
|
|
41
|
+
return { parentStep: candidate.name, location: 'INSIDE_BRANCH', branchIndex: at };
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
};
|
|
45
|
+
/** Every `{{connections['x']}}` a flow references, with the piece that needs it. */
|
|
46
|
+
export const connectionsUsedBy = (tree) => {
|
|
47
|
+
const out = new Map();
|
|
48
|
+
const CONN = /connections\['([^']+)'\]/g;
|
|
49
|
+
for (const node of tree.nodes) {
|
|
50
|
+
const settings = (node['settings'] ?? {});
|
|
51
|
+
const piece = String(settings['pieceName'] ?? '');
|
|
52
|
+
const text = JSON.stringify(settings['input'] ?? {});
|
|
53
|
+
for (const m of text.matchAll(CONN)) {
|
|
54
|
+
const id = m[1];
|
|
55
|
+
if (!out.has(id))
|
|
56
|
+
out.set(id, piece);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return [...out].map(([externalId, pieceName]) => ({ externalId, pieceName }));
|
|
60
|
+
};
|
|
61
|
+
export const compileFlows = (cs, opts = {}) => {
|
|
62
|
+
const steps = [];
|
|
63
|
+
const blocked = [];
|
|
64
|
+
const warnings = [];
|
|
65
|
+
const emit = (partial) => {
|
|
66
|
+
steps.push({ id: `f${String(steps.length + 1).padStart(3, '0')}`, index: steps.length, status: 'pending', ...partial });
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Order matters and is not the diff's order.
|
|
70
|
+
*
|
|
71
|
+
* Creates before the changes that reference them; node deletes before node
|
|
72
|
+
* adds, so a rename that is really a delete-plus-add does not collide on a
|
|
73
|
+
* name; and status last, because arming a flow should be the final thing that
|
|
74
|
+
* happens to it, after its contents are what they should be.
|
|
75
|
+
*/
|
|
76
|
+
const order = {
|
|
77
|
+
'flow.create': 0,
|
|
78
|
+
'flow.rename': 1,
|
|
79
|
+
'node.delete': 2,
|
|
80
|
+
'trigger.update': 3,
|
|
81
|
+
'node.create': 4,
|
|
82
|
+
'node.update': 5,
|
|
83
|
+
'flow.delete': 6,
|
|
84
|
+
'flow.status': 7,
|
|
85
|
+
};
|
|
86
|
+
const sorted = [...cs.changes].sort((a, b) => order[a.op] - order[b.op]);
|
|
87
|
+
/**
|
|
88
|
+
* Connections first: a flow cannot be created referencing one the project does
|
|
89
|
+
* not have. A missing connection used to be a refusal, which stalled the whole
|
|
90
|
+
* build for something the flow structure does not depend on.
|
|
91
|
+
*/
|
|
92
|
+
if (opts.connections) {
|
|
93
|
+
const needed = new Map();
|
|
94
|
+
for (const c of cs.changes) {
|
|
95
|
+
const tree = (c.afterFlow ?? null);
|
|
96
|
+
if (!tree)
|
|
97
|
+
continue;
|
|
98
|
+
for (const use of connectionsUsedBy(tree)) {
|
|
99
|
+
if (opts.connections.has(use.externalId))
|
|
100
|
+
continue;
|
|
101
|
+
if (!needed.has(use.externalId))
|
|
102
|
+
needed.set(use.externalId, use.pieceName);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
for (const [externalId, pieceName] of needed) {
|
|
106
|
+
emit({
|
|
107
|
+
changeKey: `connection:${externalId}`,
|
|
108
|
+
flowId: null,
|
|
109
|
+
createConnection: {
|
|
110
|
+
pieceName,
|
|
111
|
+
externalId,
|
|
112
|
+
displayName: `${pieceName.split('/').pop()} (placeholder, reconnect me)`,
|
|
113
|
+
},
|
|
114
|
+
description: `create placeholder connection ${externalId} for ${pieceName}`,
|
|
115
|
+
});
|
|
116
|
+
warnings.push(`connection ${externalId} does not exist in this project and will be created with a ` +
|
|
117
|
+
'placeholder credential — reconnect it in the product before enabling any flow that uses it');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
for (const c of sorted) {
|
|
121
|
+
switch (c.op) {
|
|
122
|
+
case 'flow.create': {
|
|
123
|
+
const tree = c.afterFlow;
|
|
124
|
+
emit({
|
|
125
|
+
changeKey: c.key,
|
|
126
|
+
flowId: null,
|
|
127
|
+
createFlow: { displayName: c.flowName, tree },
|
|
128
|
+
description: `create flow "${c.flowName}" and add its ${tree.nodes.length} nodes`,
|
|
129
|
+
});
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
case 'flow.delete': {
|
|
133
|
+
emit({
|
|
134
|
+
changeKey: c.key,
|
|
135
|
+
flowId: c.flowId,
|
|
136
|
+
deleteFlow: true,
|
|
137
|
+
description: `delete flow "${c.flowName}"`,
|
|
138
|
+
destructive: true,
|
|
139
|
+
});
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
case 'flow.rename': {
|
|
143
|
+
emit({
|
|
144
|
+
changeKey: c.key,
|
|
145
|
+
flowId: c.flowId,
|
|
146
|
+
operation: { type: 'CHANGE_NAME', request: { displayName: c.flowName } },
|
|
147
|
+
description: `CHANGE_NAME — "${c.flowName}"`,
|
|
148
|
+
});
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
case 'flow.status': {
|
|
152
|
+
const status = String(c.deltas[0]?.after ?? '');
|
|
153
|
+
if (status !== 'ENABLED' && status !== 'DISABLED') {
|
|
154
|
+
blocked.push({ key: c.key, label: c.label, reason: `status ${JSON.stringify(status)} is not ENABLED or DISABLED` });
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Enabling a flow points it at live traffic. It is refused unless asked
|
|
159
|
+
* for explicitly, the same way a phase delete needs --allow-card-moves:
|
|
160
|
+
* the cost of a wrong one is paid by the client, not by the person running
|
|
161
|
+
* the apply.
|
|
162
|
+
*/
|
|
163
|
+
const usesPlaceholder = [...(opts.connections ?? new Map())].some(([, c]) => c['status'] === 'PLACEHOLDER');
|
|
164
|
+
if (status === 'ENABLED' && usesPlaceholder) {
|
|
165
|
+
blocked.push({
|
|
166
|
+
key: c.key,
|
|
167
|
+
label: c.label,
|
|
168
|
+
reason: 'this project has a placeholder connection — reconnect it before enabling a flow, ' +
|
|
169
|
+
'or the flow will run and fail on every trigger',
|
|
170
|
+
});
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
if (status === 'ENABLED' && !opts.allowArm) {
|
|
174
|
+
blocked.push({
|
|
175
|
+
key: c.key,
|
|
176
|
+
label: c.label,
|
|
177
|
+
reason: 'enabling a flow arms it against live traffic — re-run with --allow-flow-enable to do it deliberately',
|
|
178
|
+
});
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
emit({
|
|
182
|
+
changeKey: c.key,
|
|
183
|
+
flowId: c.flowId,
|
|
184
|
+
operation: { type: 'CHANGE_STATUS', request: { status } },
|
|
185
|
+
description: `CHANGE_STATUS — ${status}`,
|
|
186
|
+
arms: status === 'ENABLED',
|
|
187
|
+
});
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
case 'trigger.update': {
|
|
191
|
+
emit({
|
|
192
|
+
changeKey: c.key,
|
|
193
|
+
flowId: c.flowId,
|
|
194
|
+
operation: { type: 'UPDATE_TRIGGER', request: nodeRequest(c.after) },
|
|
195
|
+
description: `UPDATE_TRIGGER — flow "${c.flowName}"`,
|
|
196
|
+
});
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
case 'node.update': {
|
|
200
|
+
emit({
|
|
201
|
+
changeKey: c.key,
|
|
202
|
+
flowId: c.flowId,
|
|
203
|
+
operation: { type: 'UPDATE_ACTION', request: nodeRequest(c.after) },
|
|
204
|
+
description: `UPDATE_ACTION — ${c.nodeName} in "${c.flowName}"`,
|
|
205
|
+
});
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
case 'node.delete': {
|
|
209
|
+
emit({
|
|
210
|
+
changeKey: c.key,
|
|
211
|
+
flowId: c.flowId,
|
|
212
|
+
// `names` is an array — a single `name` is rejected.
|
|
213
|
+
operation: { type: 'DELETE_ACTION', request: { names: [c.nodeName] } },
|
|
214
|
+
description: `DELETE_ACTION — ${c.nodeName} in "${c.flowName}"`,
|
|
215
|
+
destructive: true,
|
|
216
|
+
});
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
case 'node.create': {
|
|
220
|
+
const tree = c.afterFlow;
|
|
221
|
+
const where = tree ? parentOf(tree, c.nodeName) : null;
|
|
222
|
+
if (!where) {
|
|
223
|
+
// Unreachable from the chain — nothing points `_nextAction` /
|
|
224
|
+
// `_firstLoopAction` / a branch's `_children` at this node, so there is
|
|
225
|
+
// no parent to hang it from. The same case `buildFlowOperations` skips
|
|
226
|
+
// for a brand-new flow.
|
|
227
|
+
blocked.push({
|
|
228
|
+
key: c.key,
|
|
229
|
+
label: c.label,
|
|
230
|
+
reason: `${c.nodeName} is not reachable from the trigger — nothing in the authored flow ` +
|
|
231
|
+
'points to it, so there is no parent to add it after.',
|
|
232
|
+
});
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
emit({
|
|
236
|
+
changeKey: c.key,
|
|
237
|
+
flowId: c.flowId,
|
|
238
|
+
operation: {
|
|
239
|
+
type: 'ADD_ACTION',
|
|
240
|
+
request: {
|
|
241
|
+
parentStep: where.parentStep,
|
|
242
|
+
...(where.location ? { stepLocationRelativeToParent: where.location } : {}),
|
|
243
|
+
...(where.branchIndex !== undefined ? { branchIndex: where.branchIndex } : {}),
|
|
244
|
+
action: nodeRequest(c.after),
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
description: `ADD_ACTION — ${c.nodeName} [${c.after.type}] after ${where.parentStep}${where.location ? ` (${where.location})` : ''} in "${c.flowName}"`,
|
|
248
|
+
});
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (blocked.length) {
|
|
254
|
+
warnings.push(`${blocked.length} flow change(s) have no write path yet`);
|
|
255
|
+
}
|
|
256
|
+
return { steps, blocked, warnings };
|
|
257
|
+
};
|
|
258
|
+
/**
|
|
259
|
+
* The operations that build a brand-new flow's contents.
|
|
260
|
+
*
|
|
261
|
+
* A created flow arrives with an `EMPTY` trigger and nothing else, so the tree is
|
|
262
|
+
* laid down trigger-first and then each node in execution order — every node's
|
|
263
|
+
* parent already exists by the time it is added, because execution order is a
|
|
264
|
+
* topological order of the chain.
|
|
265
|
+
*/
|
|
266
|
+
export const buildFlowOperations = (tree) => {
|
|
267
|
+
const ops = [];
|
|
268
|
+
const trigger = tree.nodes[0];
|
|
269
|
+
if (!trigger)
|
|
270
|
+
return ops;
|
|
271
|
+
ops.push({ type: 'UPDATE_TRIGGER', request: nodeRequest(trigger), describe: `trigger [${trigger.type}]` });
|
|
272
|
+
for (const node of tree.nodes.slice(1)) {
|
|
273
|
+
const where = parentOf(tree, node.name);
|
|
274
|
+
if (!where) {
|
|
275
|
+
// Unreachable from the chain: the codec would have dropped it anyway, and
|
|
276
|
+
// adding it with a guessed parent would put it somewhere nobody asked for.
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
ops.push({
|
|
280
|
+
type: 'ADD_ACTION',
|
|
281
|
+
request: {
|
|
282
|
+
parentStep: where.parentStep,
|
|
283
|
+
...(where.location ? { stepLocationRelativeToParent: where.location } : {}),
|
|
284
|
+
...(where.branchIndex !== undefined ? { branchIndex: where.branchIndex } : {}),
|
|
285
|
+
action: nodeRequest(node),
|
|
286
|
+
},
|
|
287
|
+
describe: `${node.name} [${node.type}] after ${where.parentStep}${where.location ? ` (${where.location})` : ''}`,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
return ops;
|
|
291
|
+
};
|
|
292
|
+
/** The API flow row a tree would produce, for verification after an apply. */
|
|
293
|
+
export const intendedFlow = (tree) => linkFlow(tree);
|