@pipefy/pipefy-process-coder 0.1.0 → 0.1.2
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 +476 -456
- package/LICENSE +202 -0
- package/NOTICE +11 -0
- package/README.md +830 -808
- 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 +196 -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 +562 -0
- package/dist/commands/plan.js +301 -0
- package/dist/commands/pull.js +549 -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 +131 -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 +406 -0
- package/dist/pipefy/ipaas.js +269 -0
- package/dist/pipefy/reconstruct.js +676 -0
- package/dist/pipefy/reference.js +172 -0
- package/dist/pipefy/snapshot.js +185 -0
- package/dist/pipefy/toolkit.js +174 -0
- package/{src → dist}/pipefy/toolkit_bearer.py +60 -14
- package/dist/report/integrations.js +165 -0
- package/dist/report/run.js +356 -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/version.js +18 -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 +38 -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
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import { ENDPOINTS } from "../config.js";
|
|
2
|
+
import { debug, warn } from "../util/log.js";
|
|
3
|
+
import { retry } from "../util/pool.js";
|
|
4
|
+
import { AI_BEHAVIOR_PARAMS_SELECTION } from "./agents.js";
|
|
5
|
+
/**
|
|
6
|
+
* The internal-API adapter. PLAN.md §2.1 flags this as an unversioned endpoint
|
|
7
|
+
* with no compatibility contract, so it is isolated here, behind one module,
|
|
8
|
+
* and every entry point degrades to `null`/`unsupported` rather than throwing.
|
|
9
|
+
*
|
|
10
|
+
* Two things live only here — both taken from the Change Migrator recipes,
|
|
11
|
+
* which have been running them in production:
|
|
12
|
+
*
|
|
13
|
+
* 1. Automations. Public `getAutomations` returns a summary; the recipes use
|
|
14
|
+
* `loadAutomationToEdit` to get `action_params.field_map`, `searchFor`,
|
|
15
|
+
* `responseSchema`, `schedulerCron` and `aiParams` — everything a
|
|
16
|
+
* round trip needs and the summary omits.
|
|
17
|
+
* 2. Phase jumps. `GET/PUT /internal_api/settings/phases/:id`, set-replacing.
|
|
18
|
+
*
|
|
19
|
+
* Both accept the same personal API token as the public endpoint, so there is
|
|
20
|
+
* no session-cookie or CSRF subsystem here.
|
|
21
|
+
*/
|
|
22
|
+
/** Sent by `listAutomations`; module-level so `checkQueries` validates the same text. */
|
|
23
|
+
const LIST_AUTOMATIONS_QUERY = `
|
|
24
|
+
query getAutomations($orgId: ID!, $repoId: ID) {
|
|
25
|
+
automations(organizationId: $orgId, repoId: $repoId) {
|
|
26
|
+
id
|
|
27
|
+
name
|
|
28
|
+
action_id
|
|
29
|
+
event_id
|
|
30
|
+
created_at
|
|
31
|
+
active
|
|
32
|
+
action_repo: action_repo_v2 {
|
|
33
|
+
__typename
|
|
34
|
+
... on Pipe { id }
|
|
35
|
+
... on Table { id }
|
|
36
|
+
}
|
|
37
|
+
event_repo { __typename id }
|
|
38
|
+
}
|
|
39
|
+
}`;
|
|
40
|
+
/** Sent by `loadAutomation`; module-level so `checkQueries` validates the same text. */
|
|
41
|
+
const LOAD_AUTOMATION_QUERY = `
|
|
42
|
+
query loadAutomationToEdit($id: ID!) {
|
|
43
|
+
automation(id: $id) {
|
|
44
|
+
name
|
|
45
|
+
event_id
|
|
46
|
+
active
|
|
47
|
+
event_repo { id }
|
|
48
|
+
event_params {
|
|
49
|
+
toPhaseId: to_phase_id
|
|
50
|
+
fromPhaseId
|
|
51
|
+
inPhaseId
|
|
52
|
+
triggerFieldIds
|
|
53
|
+
kindOfSla
|
|
54
|
+
triggerAutomationId
|
|
55
|
+
}
|
|
56
|
+
action_id
|
|
57
|
+
action_repo: action_repo_v2 {
|
|
58
|
+
... on Pipe { id uuid }
|
|
59
|
+
... on Table { id uuid }
|
|
60
|
+
}
|
|
61
|
+
scheduler_frequency
|
|
62
|
+
schedulerCron { minute hour dayOfWeek dayOfMonth month }
|
|
63
|
+
searchFor { id field operation value }
|
|
64
|
+
responseSchema
|
|
65
|
+
action_params {
|
|
66
|
+
toPhaseId: to_phase_id
|
|
67
|
+
fieldsMapOrder: fields_map_order
|
|
68
|
+
email_template_id
|
|
69
|
+
fieldMap: field_map { fieldId value inputMode }
|
|
70
|
+
cardId: card_id
|
|
71
|
+
headers
|
|
72
|
+
body
|
|
73
|
+
url
|
|
74
|
+
httpMethod
|
|
75
|
+
authenticationKey
|
|
76
|
+
hasAuthenticationValue
|
|
77
|
+
authenticationAddTo
|
|
78
|
+
authenticationType
|
|
79
|
+
strategy
|
|
80
|
+
# taskParams is not in the recipe's query, and the omission is not
|
|
81
|
+
# cosmetic: every send_a_task automation came back with an empty
|
|
82
|
+
# action_params, so a reconstructed read lost its recipients and
|
|
83
|
+
# title, and a recreate would have rebuilt it blank. Introspected
|
|
84
|
+
# from AutomationActionParams on the internal endpoint.
|
|
85
|
+
taskParams { recipients title }
|
|
86
|
+
aiParams { fieldIds value skillsIds }
|
|
87
|
+
aiBehaviorParams ${AI_BEHAVIOR_PARAMS_SELECTION}
|
|
88
|
+
slaParams {
|
|
89
|
+
monday { startHour endHour enabled }
|
|
90
|
+
tuesday { startHour endHour enabled }
|
|
91
|
+
wednesday { startHour endHour enabled }
|
|
92
|
+
thursday { startHour endHour enabled }
|
|
93
|
+
friday { startHour endHour enabled }
|
|
94
|
+
saturday { startHour endHour enabled }
|
|
95
|
+
sunday { startHour endHour enabled }
|
|
96
|
+
timezone
|
|
97
|
+
holidays { date recurrence description }
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
condition {
|
|
101
|
+
expressions_structure
|
|
102
|
+
expressions { id field_address operation value structure_id }
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}`;
|
|
106
|
+
/**
|
|
107
|
+
* Every read query document this adapter sends over GraphQL, with variables
|
|
108
|
+
* that match nothing. GraphQL validates the document before resolving any of
|
|
109
|
+
* it, so a query that has drifted from the schema is refused the same way
|
|
110
|
+
* whichever id it names — and a valid one answers `data` with a not-found
|
|
111
|
+
* error. No write is in this list, and none may be added: probing is not free
|
|
112
|
+
* on this platform (AGENTS.md §4).
|
|
113
|
+
*/
|
|
114
|
+
const READ_QUERIES = [
|
|
115
|
+
{ label: 'listAutomations', query: LIST_AUTOMATIONS_QUERY, variables: { orgId: '0', repoId: '0' } },
|
|
116
|
+
{ label: 'loadAutomation', query: LOAD_AUTOMATION_QUERY, variables: { id: '0' } },
|
|
117
|
+
];
|
|
118
|
+
export class InternalApi {
|
|
119
|
+
tokenProvider;
|
|
120
|
+
status = 'unknown';
|
|
121
|
+
requestCount = 0;
|
|
122
|
+
/** Set once, so a broken endpoint produces one warning and not one per call. */
|
|
123
|
+
warned = false;
|
|
124
|
+
constructor(token) {
|
|
125
|
+
this.tokenProvider = typeof token === 'string' ? async () => token : token;
|
|
126
|
+
}
|
|
127
|
+
async bearer() {
|
|
128
|
+
return this.tokenProvider();
|
|
129
|
+
}
|
|
130
|
+
markUnavailable(reason) {
|
|
131
|
+
if (this.status !== 'incompatible')
|
|
132
|
+
this.status = 'unavailable';
|
|
133
|
+
if (!this.warned) {
|
|
134
|
+
this.warned = true;
|
|
135
|
+
warn(`internal API unavailable (${reason}). Automation details and phase jumps ` +
|
|
136
|
+
`will be reported as unknown rather than empty — see PLAN.md §2.1.`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/** One answered call does not clear a refused query: that stays visible. */
|
|
140
|
+
markOk() {
|
|
141
|
+
if (this.status !== 'incompatible')
|
|
142
|
+
this.status = 'ok';
|
|
143
|
+
}
|
|
144
|
+
markIncompatible(label, reason) {
|
|
145
|
+
this.status = 'incompatible';
|
|
146
|
+
if (!this.warned) {
|
|
147
|
+
this.warned = true;
|
|
148
|
+
warn(`internal API rejected this tool's ${label} query as invalid (${reason}). ` +
|
|
149
|
+
`That is a bug in pipe, not an outage — the endpoint changed under it. Automation ` +
|
|
150
|
+
`details will be reported as unknown rather than empty; \`pipe doctor\` checks every internal query.`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Validate every read query against the live schema without reading anything
|
|
155
|
+
* real. Null when the endpoint itself could not be reached, which says
|
|
156
|
+
* nothing about the queries.
|
|
157
|
+
*/
|
|
158
|
+
async checkQueries() {
|
|
159
|
+
const results = [];
|
|
160
|
+
for (const { label, query, variables } of READ_QUERIES) {
|
|
161
|
+
let body;
|
|
162
|
+
try {
|
|
163
|
+
this.requestCount++;
|
|
164
|
+
const res = await fetch(ENDPOINTS.internal, {
|
|
165
|
+
method: 'POST',
|
|
166
|
+
headers: {
|
|
167
|
+
authorization: `Bearer ${await this.bearer()}`,
|
|
168
|
+
'content-type': 'application/json',
|
|
169
|
+
accept: 'application/json',
|
|
170
|
+
},
|
|
171
|
+
body: JSON.stringify({ query, variables }),
|
|
172
|
+
});
|
|
173
|
+
if (!res.ok) {
|
|
174
|
+
debug(`internal query check ${label}: HTTP ${res.status}`);
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
body = JSON.parse(await res.text());
|
|
178
|
+
}
|
|
179
|
+
catch (e) {
|
|
180
|
+
debug(`internal query check ${label}: ${e.message}`);
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
if ('data' in body)
|
|
184
|
+
results.push({ label, ok: true });
|
|
185
|
+
else
|
|
186
|
+
results.push({ label, ok: false, message: (body.errors ?? []).map((e) => e.message).join('; ') || 'no data' });
|
|
187
|
+
}
|
|
188
|
+
this.status = results.every((r) => r.ok) ? 'ok' : 'incompatible';
|
|
189
|
+
return results;
|
|
190
|
+
}
|
|
191
|
+
/** GraphQL over the internal endpoint. Returns null when the endpoint fails. */
|
|
192
|
+
async gql(query, variables, label) {
|
|
193
|
+
try {
|
|
194
|
+
const data = await retry(async () => {
|
|
195
|
+
this.requestCount++;
|
|
196
|
+
const res = await fetch(ENDPOINTS.internal, {
|
|
197
|
+
method: 'POST',
|
|
198
|
+
headers: {
|
|
199
|
+
authorization: `Bearer ${await this.bearer()}`,
|
|
200
|
+
'content-type': 'application/json',
|
|
201
|
+
accept: 'application/json',
|
|
202
|
+
},
|
|
203
|
+
body: JSON.stringify({ query, variables }),
|
|
204
|
+
});
|
|
205
|
+
const text = await res.text();
|
|
206
|
+
if (!res.ok) {
|
|
207
|
+
const err = new Error(`HTTP ${res.status}: ${text.slice(0, 160)}`);
|
|
208
|
+
err.retryable = res.status >= 500 || res.status === 429;
|
|
209
|
+
throw err;
|
|
210
|
+
}
|
|
211
|
+
const body = JSON.parse(text);
|
|
212
|
+
if (body.errors?.length) {
|
|
213
|
+
const err = new Error(body.errors.map((e) => e.message).join('; '));
|
|
214
|
+
err.retryable = false;
|
|
215
|
+
// No `data` key at all is a document the schema refused, not a
|
|
216
|
+
// resolver failing on this one id — see InternalStatus.
|
|
217
|
+
err.invalidQuery = !('data' in body);
|
|
218
|
+
throw err;
|
|
219
|
+
}
|
|
220
|
+
return body.data ?? null;
|
|
221
|
+
}, { label });
|
|
222
|
+
if (data)
|
|
223
|
+
this.markOk();
|
|
224
|
+
return data;
|
|
225
|
+
}
|
|
226
|
+
catch (e) {
|
|
227
|
+
debug(`internal gql ${label} failed: ${e.message}`);
|
|
228
|
+
if (e.invalidQuery)
|
|
229
|
+
this.markIncompatible(label, e.message);
|
|
230
|
+
else
|
|
231
|
+
this.markUnavailable(`${label}: ${e.message}`);
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// ── Automations ────────────────────────────────────────────────────────────
|
|
236
|
+
/** Recipe `Pipe Data Treatment` step 156. */
|
|
237
|
+
async listAutomations(orgId, repoId) {
|
|
238
|
+
const data = await this.gql(LIST_AUTOMATIONS_QUERY, { orgId: String(orgId), repoId: String(repoId) }, 'listAutomations');
|
|
239
|
+
return data?.automations ?? null;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Recipe `Apply Changes` step 118 — the full editable definition. This is the
|
|
243
|
+
* only read path that returns `action_params.field_map`, `searchFor`,
|
|
244
|
+
* `responseSchema` and `aiParams`, which is why the reconstruction path
|
|
245
|
+
* cannot avoid the internal API for automations.
|
|
246
|
+
*/
|
|
247
|
+
async loadAutomation(id) {
|
|
248
|
+
const data = await this.gql(LOAD_AUTOMATION_QUERY, { id: String(id) }, `loadAutomation ${id}`);
|
|
249
|
+
return data?.automation ?? null;
|
|
250
|
+
}
|
|
251
|
+
/** Recipe `Apply Changes` step 121. `input` is the AutomationInput variable set. */
|
|
252
|
+
async createAutomation(vars) {
|
|
253
|
+
const q = `
|
|
254
|
+
mutation createAutomation(
|
|
255
|
+
$name: String!, $action_id: ID!, $event_id: ID!,
|
|
256
|
+
$action_repo_id: ID, $event_repo_id: ID,
|
|
257
|
+
$event_params: AutomationEventParamsInput,
|
|
258
|
+
$action_params: AutomationActionParamsInput,
|
|
259
|
+
$condition: ConditionInput,
|
|
260
|
+
$scheduler_frequency: String,
|
|
261
|
+
$searchFor: [SearchConditionInput],
|
|
262
|
+
$schedulerCron: CronInput,
|
|
263
|
+
$responseSchema: JSON
|
|
264
|
+
) {
|
|
265
|
+
createAutomation(input: {
|
|
266
|
+
name: $name, action_id: $action_id, event_id: $event_id,
|
|
267
|
+
action_repo_id: $action_repo_id, event_repo_id: $event_repo_id,
|
|
268
|
+
event_params: $event_params, action_params: $action_params,
|
|
269
|
+
condition: $condition, scheduler_frequency: $scheduler_frequency,
|
|
270
|
+
searchFor: $searchFor, schedulerCron: $schedulerCron,
|
|
271
|
+
responseSchema: $responseSchema
|
|
272
|
+
}) {
|
|
273
|
+
automation { id }
|
|
274
|
+
error_details { object_key messages }
|
|
275
|
+
}
|
|
276
|
+
}`;
|
|
277
|
+
const data = await this.gql(q, vars, 'createAutomation');
|
|
278
|
+
const payload = data?.createAutomation;
|
|
279
|
+
if (!payload)
|
|
280
|
+
return null;
|
|
281
|
+
if (payload.error_details?.length) {
|
|
282
|
+
const detail = payload.error_details.map((d) => `${d.object_key}: ${d.messages.join(', ')}`).join(' | ');
|
|
283
|
+
throw new Error(`createAutomation rejected — ${detail}`);
|
|
284
|
+
}
|
|
285
|
+
return payload.automation;
|
|
286
|
+
}
|
|
287
|
+
/** Recipe `Apply Changes` step 126. */
|
|
288
|
+
async deleteAutomation(id) {
|
|
289
|
+
const q = `
|
|
290
|
+
mutation deleteAutomation($id: ID!) {
|
|
291
|
+
deleteAutomation(input: { id: $id }) { success }
|
|
292
|
+
}`;
|
|
293
|
+
const data = await this.gql(q, { id: String(id) }, `deleteAutomation ${id}`);
|
|
294
|
+
return Boolean(data?.deleteAutomation?.success);
|
|
295
|
+
}
|
|
296
|
+
// ── Phase jumps ────────────────────────────────────────────────────────────
|
|
297
|
+
/**
|
|
298
|
+
* `GET /internal_api/settings/phases/:id` — recipe step 23.
|
|
299
|
+
*
|
|
300
|
+
* **The GET and the PUT answer in different shapes.** Both captured live:
|
|
301
|
+
*
|
|
302
|
+
* GET { data: { attributes: { name, phases: [[id, name, done], …],
|
|
303
|
+
* jump_targets: [344054275] } } }
|
|
304
|
+
* PUT { data: { name, back_phase_ids: [], next_phase_ids: [344054275] } }
|
|
305
|
+
*
|
|
306
|
+
* The GET's `jump_targets` is one merged set, which is exactly what the
|
|
307
|
+
* payload's `phase_jumps` array holds. Reading it as `data.next_phase_ids` —
|
|
308
|
+
* the shape PLAN.md §2.1 recorded, from the PUT — yields an empty set for
|
|
309
|
+
* every phase without any error, and a diff that then proposes deleting every
|
|
310
|
+
* jump in the pipe.
|
|
311
|
+
*/
|
|
312
|
+
async getPhaseSettings(phaseId) {
|
|
313
|
+
try {
|
|
314
|
+
const res = await retry(async () => {
|
|
315
|
+
this.requestCount++;
|
|
316
|
+
const r = await fetch(`${ENDPOINTS.internalSettings}/phases/${phaseId}`, {
|
|
317
|
+
method: 'GET',
|
|
318
|
+
headers: { authorization: `Bearer ${await this.bearer()}`, accept: 'application/json' },
|
|
319
|
+
});
|
|
320
|
+
if (!r.ok) {
|
|
321
|
+
const err = new Error(`HTTP ${r.status}`);
|
|
322
|
+
err.retryable = r.status >= 500 || r.status === 429;
|
|
323
|
+
throw err;
|
|
324
|
+
}
|
|
325
|
+
return (await r.json());
|
|
326
|
+
}, { label: `getPhaseSettings ${phaseId}` });
|
|
327
|
+
const d = res.data ?? {};
|
|
328
|
+
const attrs = d.attributes ?? {};
|
|
329
|
+
const targets = attrs.jump_targets ?? d.jump_targets ?? [...(d.next_phase_ids ?? []), ...(d.back_phase_ids ?? [])];
|
|
330
|
+
this.markOk();
|
|
331
|
+
return {
|
|
332
|
+
jump_targets: [...new Set(targets.map(Number).filter((n) => Number.isFinite(n)))],
|
|
333
|
+
phases: (attrs.phases ?? []).map(([id, name, done]) => ({ id: Number(id), name: String(name), done: Boolean(done) })),
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
catch (e) {
|
|
337
|
+
debug(`getPhaseSettings ${phaseId} failed: ${e.message}`);
|
|
338
|
+
this.markUnavailable(`getPhaseSettings: ${e.message}`);
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* `PUT /internal_api/settings/phases/:id` with the complete desired jump set.
|
|
344
|
+
* Recipe step 24 does the same thing in Python. Set-replacing, so we send the
|
|
345
|
+
* whole set and assert on the echoed `next_phase_ids` rather than on the HTTP
|
|
346
|
+
* status — PLAN.md §2.1.
|
|
347
|
+
*/
|
|
348
|
+
async setPhaseJumpTargets(phaseId, targetPhaseIds) {
|
|
349
|
+
const params = new URLSearchParams();
|
|
350
|
+
if (targetPhaseIds.length === 0) {
|
|
351
|
+
// An empty set still has to be expressible, or "remove the last jump"
|
|
352
|
+
// becomes impossible. The form convention for an empty array is a single
|
|
353
|
+
// blank entry.
|
|
354
|
+
params.append('phase[jump_target_ids][]', '');
|
|
355
|
+
}
|
|
356
|
+
for (const t of targetPhaseIds)
|
|
357
|
+
params.append('phase[jump_target_ids][]', String(t));
|
|
358
|
+
try {
|
|
359
|
+
const res = await retry(async () => {
|
|
360
|
+
this.requestCount++;
|
|
361
|
+
const r = await fetch(`${ENDPOINTS.internalSettings}/phases/${phaseId}`, {
|
|
362
|
+
method: 'PUT',
|
|
363
|
+
headers: {
|
|
364
|
+
authorization: `Bearer ${await this.bearer()}`,
|
|
365
|
+
'content-type': 'application/x-www-form-urlencoded',
|
|
366
|
+
accept: 'application/json',
|
|
367
|
+
},
|
|
368
|
+
body: params.toString(),
|
|
369
|
+
});
|
|
370
|
+
const text = await r.text();
|
|
371
|
+
if (!r.ok) {
|
|
372
|
+
const err = new Error(`HTTP ${r.status}: ${text.slice(0, 160)}`);
|
|
373
|
+
err.retryable = r.status >= 500 || r.status === 429;
|
|
374
|
+
throw err;
|
|
375
|
+
}
|
|
376
|
+
return JSON.parse(text);
|
|
377
|
+
}, { label: `setPhaseJumpTargets ${phaseId}` });
|
|
378
|
+
/**
|
|
379
|
+
* The echo is the union of both directions.
|
|
380
|
+
*
|
|
381
|
+
* `next_phase_ids` alone is forward-only, so a jump to an *earlier* phase
|
|
382
|
+
* echoes back as an empty next list even though the write succeeded — which
|
|
383
|
+
* this assertion reported as a failed write. Asserting on the union is what
|
|
384
|
+
* the sent set actually means.
|
|
385
|
+
*/
|
|
386
|
+
const d = res.data ?? {};
|
|
387
|
+
const echoed = [
|
|
388
|
+
...(d.attributes?.jump_targets ?? []),
|
|
389
|
+
...(d.jump_targets ?? []),
|
|
390
|
+
...(d.next_phase_ids ?? []),
|
|
391
|
+
...(d.back_phase_ids ?? []),
|
|
392
|
+
].map(Number);
|
|
393
|
+
const want = [...new Set(targetPhaseIds.map(Number))].sort((a, b) => a - b);
|
|
394
|
+
const got = [...new Set(echoed)].sort((a, b) => a - b);
|
|
395
|
+
const ok = want.length === got.length && want.every((v, i) => v === got[i]);
|
|
396
|
+
this.markOk();
|
|
397
|
+
return ok
|
|
398
|
+
? { ok, echoed }
|
|
399
|
+
: { ok, echoed, error: `echo mismatch: wanted [${want.join(',')}], endpoint reports [${got.join(',')}]` };
|
|
400
|
+
}
|
|
401
|
+
catch (e) {
|
|
402
|
+
this.markUnavailable(`setPhaseJumpTargets: ${e.message}`);
|
|
403
|
+
return { ok: false, echoed: [], error: e.message };
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { debug } from "../util/log.js";
|
|
2
|
+
/**
|
|
3
|
+
* A pipe's iPaaS workspace — "Advanced Automations", Activepieces underneath.
|
|
4
|
+
*
|
|
5
|
+
* Every pipe has its own workspace, and the session proves it: the identity that
|
|
6
|
+
* comes back has the pipe's name as `firstName` and its id as `lastName`. Flows
|
|
7
|
+
* and connections are scoped to it, so a flow belongs to exactly one pipe even
|
|
8
|
+
* though it may orchestrate across many.
|
|
9
|
+
*
|
|
10
|
+
* Authentication needs no new credential. Two hops from the token `pipe` already
|
|
11
|
+
* holds:
|
|
12
|
+
*
|
|
13
|
+
* 1. the INTERNAL API mints a short-lived, pipe-scoped JWT
|
|
14
|
+
* (`advancedAutomationsToken(repoId:)`)
|
|
15
|
+
* 2. `POST /managed-authn/external-token` exchanges it for a session — which
|
|
16
|
+
* also hands back the projectId, so nothing has to be configured
|
|
17
|
+
*
|
|
18
|
+
* The OAuth + PKCE dance in Pipefy's own gateway exists only to reach the iPaaS
|
|
19
|
+
* MCP tool surface. The REST API used here answers the step-2 session directly.
|
|
20
|
+
*/
|
|
21
|
+
export const IPAAS_BASE = 'https://ipaas.pipefy.com/api/v1';
|
|
22
|
+
/**
|
|
23
|
+
* Operations that act on an empty request instead of rejecting it.
|
|
24
|
+
*
|
|
25
|
+
* Learned the hard way: probing the enum by sending every candidate with
|
|
26
|
+
* `request: {}` published and enabled a draft flow, because these four do not
|
|
27
|
+
* validate a request they can default. Nothing in this module may send one
|
|
28
|
+
* without an explicit, complete request.
|
|
29
|
+
*/
|
|
30
|
+
export const ACTS_ON_EMPTY_REQUEST = new Set([
|
|
31
|
+
'CHANGE_STATUS',
|
|
32
|
+
'LOCK_AND_PUBLISH',
|
|
33
|
+
'LOCK_FLOW',
|
|
34
|
+
'CHANGE_FOLDER',
|
|
35
|
+
'UPDATE_METADATA',
|
|
36
|
+
]);
|
|
37
|
+
export class IpaasError extends Error {
|
|
38
|
+
status;
|
|
39
|
+
step;
|
|
40
|
+
constructor(step, status, body) {
|
|
41
|
+
super(`iPaaS ${step} failed (HTTP ${status}): ${body.slice(0, 300)}`);
|
|
42
|
+
this.name = 'IpaasError';
|
|
43
|
+
this.status = status;
|
|
44
|
+
this.step = step;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** iPaaS is off for the org, or the caller cannot administer this pipe's automations. */
|
|
48
|
+
export class IpaasUnavailable extends Error {
|
|
49
|
+
pipeId;
|
|
50
|
+
constructor(pipeId, reason) {
|
|
51
|
+
super(reason);
|
|
52
|
+
this.name = 'IpaasUnavailable';
|
|
53
|
+
this.pipeId = pipeId;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export class IpaasClient {
|
|
57
|
+
internal;
|
|
58
|
+
base;
|
|
59
|
+
/** pipeId → session, because minting is two round trips and a pull does many reads. */
|
|
60
|
+
sessions = new Map();
|
|
61
|
+
/** A session lives a while, but not forever; re-mint rather than discover expiry mid-apply. */
|
|
62
|
+
static TTL_MS = 10 * 60 * 1000;
|
|
63
|
+
constructor(internal, base = IPAAS_BASE) {
|
|
64
|
+
this.internal = internal;
|
|
65
|
+
this.base = base.replace(/\/$/, '');
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Whether this pipe has a reachable iPaaS workspace.
|
|
69
|
+
*
|
|
70
|
+
* A refusal here is `unknown` coverage, never "no flows" — the distinction the
|
|
71
|
+
* whole coverage doctrine rests on. Minting requires iPaaS enabled on the
|
|
72
|
+
* organization and permission to create automations on the pipe.
|
|
73
|
+
*/
|
|
74
|
+
async available(pipeId) {
|
|
75
|
+
try {
|
|
76
|
+
const session = await this.session(pipeId);
|
|
77
|
+
return { ok: true, session };
|
|
78
|
+
}
|
|
79
|
+
catch (e) {
|
|
80
|
+
return { ok: false, reason: e instanceof Error ? e.message : String(e) };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async session(pipeId) {
|
|
84
|
+
const hit = this.sessions.get(pipeId);
|
|
85
|
+
if (hit && Date.now() - hit.at < IpaasClient.TTL_MS)
|
|
86
|
+
return hit.session;
|
|
87
|
+
const minted = await this.internal.gql(`query GetAdvancedAutomationsToken($repoId: ID!) {
|
|
88
|
+
advancedAutomationsToken(repoId: $repoId) { token }
|
|
89
|
+
}`, { repoId: String(pipeId) }, 'GetAdvancedAutomationsToken');
|
|
90
|
+
const token = minted?.advancedAutomationsToken?.token;
|
|
91
|
+
if (!token) {
|
|
92
|
+
throw new IpaasUnavailable(pipeId, `no advanced-automations token for pipe ${pipeId} — iPaaS may be off for this organization, ` +
|
|
93
|
+
'or this credential may not be allowed to create automations on the pipe');
|
|
94
|
+
}
|
|
95
|
+
const res = await fetch(`${this.base}/managed-authn/external-token`, {
|
|
96
|
+
method: 'POST',
|
|
97
|
+
headers: { 'Content-Type': 'application/json' },
|
|
98
|
+
body: JSON.stringify({ externalAccessToken: token }),
|
|
99
|
+
});
|
|
100
|
+
const body = await res.text();
|
|
101
|
+
if (!res.ok)
|
|
102
|
+
throw new IpaasError('session exchange', res.status, body);
|
|
103
|
+
const parsed = JSON.parse(body);
|
|
104
|
+
if (!parsed.token || !parsed.projectId) {
|
|
105
|
+
throw new IpaasError('session exchange', res.status, 'the response carried no token or projectId');
|
|
106
|
+
}
|
|
107
|
+
const session = { pipeId, projectId: parsed.projectId, userId: parsed.id };
|
|
108
|
+
this.sessions.set(pipeId, { session, token: parsed.token, at: Date.now() });
|
|
109
|
+
debug(`ipaas session for pipe ${pipeId}: project ${parsed.projectId}`);
|
|
110
|
+
return session;
|
|
111
|
+
}
|
|
112
|
+
async token(pipeId) {
|
|
113
|
+
await this.session(pipeId);
|
|
114
|
+
return this.sessions.get(pipeId).token;
|
|
115
|
+
}
|
|
116
|
+
async request(pipeId, method, path, body) {
|
|
117
|
+
const token = await this.token(pipeId);
|
|
118
|
+
const res = await fetch(`${this.base}/${path.replace(/^\//, '')}`, {
|
|
119
|
+
method,
|
|
120
|
+
// Content-Type only with a body: a bodyless DELETE carrying it is rejected
|
|
121
|
+
// outright with FST_ERR_CTP_EMPTY_JSON_BODY, so deleting a flow used to fail
|
|
122
|
+
// on a header rather than on anything to do with the flow.
|
|
123
|
+
headers: { Authorization: `Bearer ${token}`, ...(body === undefined ? {} : { 'Content-Type': 'application/json' }) },
|
|
124
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
125
|
+
});
|
|
126
|
+
const text = await res.text();
|
|
127
|
+
if (res.status === 401) {
|
|
128
|
+
// The session aged out mid-run; mint once more before giving up.
|
|
129
|
+
this.sessions.delete(pipeId);
|
|
130
|
+
const retryToken = await this.token(pipeId);
|
|
131
|
+
const again = await fetch(`${this.base}/${path.replace(/^\//, '')}`, {
|
|
132
|
+
method,
|
|
133
|
+
headers: { Authorization: `Bearer ${retryToken}`, ...(body === undefined ? {} : { 'Content-Type': 'application/json' }) },
|
|
134
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
135
|
+
});
|
|
136
|
+
const againText = await again.text();
|
|
137
|
+
if (!again.ok)
|
|
138
|
+
throw new IpaasError(`${method} ${path}`, again.status, againText);
|
|
139
|
+
return (againText ? JSON.parse(againText) : null);
|
|
140
|
+
}
|
|
141
|
+
if (!res.ok)
|
|
142
|
+
throw new IpaasError(`${method} ${path}`, res.status, text);
|
|
143
|
+
return (text ? JSON.parse(text) : null);
|
|
144
|
+
}
|
|
145
|
+
// ── reads ─────────────────────────────────────────────────────────────────
|
|
146
|
+
async listFlows(pipeId) {
|
|
147
|
+
const { projectId } = await this.session(pipeId);
|
|
148
|
+
const out = await this.request(pipeId, 'GET', `flows?projectId=${projectId}`);
|
|
149
|
+
return out?.data ?? [];
|
|
150
|
+
}
|
|
151
|
+
async getFlow(pipeId, flowId) {
|
|
152
|
+
return this.request(pipeId, 'GET', `flows/${flowId}`);
|
|
153
|
+
}
|
|
154
|
+
async listConnections(pipeId) {
|
|
155
|
+
const { projectId } = await this.session(pipeId);
|
|
156
|
+
const out = await this.request(pipeId, 'GET', `app-connections?projectId=${projectId}`);
|
|
157
|
+
// Only the four columns a reference needs. A credential is never one of them.
|
|
158
|
+
return (out?.data ?? []).map((c) => ({
|
|
159
|
+
externalId: String(c['externalId'] ?? ''),
|
|
160
|
+
pieceName: String(c['pieceName'] ?? ''),
|
|
161
|
+
displayName: String(c['displayName'] ?? ''),
|
|
162
|
+
status: String(c['status'] ?? ''),
|
|
163
|
+
}));
|
|
164
|
+
}
|
|
165
|
+
async listRuns(pipeId) {
|
|
166
|
+
const { projectId } = await this.session(pipeId);
|
|
167
|
+
const out = await this.request(pipeId, 'GET', `flow-runs?projectId=${projectId}`);
|
|
168
|
+
return (out?.data ?? []).map((r) => ({
|
|
169
|
+
id: String(r['id'] ?? ''),
|
|
170
|
+
flowId: String(r['flowId'] ?? ''),
|
|
171
|
+
status: String(r['status'] ?? ''),
|
|
172
|
+
startTime: String(r['startTime'] ?? ''),
|
|
173
|
+
}));
|
|
174
|
+
}
|
|
175
|
+
/** pieceName → the version this instance offers. Instance-wide, not per project. */
|
|
176
|
+
async listPieces(pipeId) {
|
|
177
|
+
const raw = await this.request(pipeId, 'GET', 'pieces');
|
|
178
|
+
return Object.fromEntries((raw ?? []).filter((p) => p.name && p.version).map((p) => [p.name, p.version]));
|
|
179
|
+
}
|
|
180
|
+
/** One piece's action and trigger names, for checking a node against the catalogue. */
|
|
181
|
+
async pieceMembers(pipeId, pieceName) {
|
|
182
|
+
const meta = await this.request(pipeId, 'GET', `pieces/${encodeURIComponent(pieceName)}`);
|
|
183
|
+
return { actions: Object.keys(meta?.actions ?? {}), triggers: Object.keys(meta?.triggers ?? {}) };
|
|
184
|
+
}
|
|
185
|
+
// ── writes ────────────────────────────────────────────────────────────────
|
|
186
|
+
/**
|
|
187
|
+
* Create a connection with a placeholder credential.
|
|
188
|
+
*
|
|
189
|
+
* A pipe whose iPaaS workspace has no connections cannot host a flow, and the
|
|
190
|
+
* credential is not obtainable here: `piece-pipefy` authenticates by
|
|
191
|
+
* `client_credentials` against Pipefy's own OAuth token endpoint, and this
|
|
192
|
+
* host has no OAuth client registered for it — only for Slack. Connections are
|
|
193
|
+
* also hard-isolated per project: the update endpoint accepts `scope` and
|
|
194
|
+
* `projectIds` and returns 200 while ignoring both, and a different project's
|
|
195
|
+
* session gets 403 on the connection itself.
|
|
196
|
+
*
|
|
197
|
+
* So a placeholder is created instead, which is how Pipefy's own import path
|
|
198
|
+
* behaves — import, then remap the connection in the UI. The flow structure is
|
|
199
|
+
* what is being built and it does not depend on the credential working.
|
|
200
|
+
*
|
|
201
|
+
* **The host does not validate the value.** It answers 201 and marks the
|
|
202
|
+
* connection ACTIVE whatever is sent, so `status: ACTIVE` is not evidence that
|
|
203
|
+
* a connection works. Everything this tool creates is tagged `_placeholder` so
|
|
204
|
+
* a flow that depends on one can be reported as unreconnected, and a flow with
|
|
205
|
+
* a placeholder connection is never enabled.
|
|
206
|
+
*/
|
|
207
|
+
async createPlaceholderConnection(pipeId, spec) {
|
|
208
|
+
const { projectId } = await this.session(pipeId);
|
|
209
|
+
const externalId = spec.externalId ?? `pipe-placeholder-${spec.pieceName.split('/').pop()}`;
|
|
210
|
+
const type = 'CUSTOM_AUTH';
|
|
211
|
+
const out = await this.request(pipeId, 'POST', 'app-connections', {
|
|
212
|
+
externalId,
|
|
213
|
+
displayName: spec.displayName,
|
|
214
|
+
pieceName: spec.pieceName,
|
|
215
|
+
projectId,
|
|
216
|
+
// The discriminator goes in both positions; the host wants it twice.
|
|
217
|
+
type,
|
|
218
|
+
value: {
|
|
219
|
+
type,
|
|
220
|
+
props: {
|
|
221
|
+
// Named so anyone reading the connection in the product knows what it is.
|
|
222
|
+
placeholder: 'created by pipe — reconnect this before enabling any flow',
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
return {
|
|
227
|
+
externalId: String(out?.['externalId'] ?? externalId),
|
|
228
|
+
pieceName: spec.pieceName,
|
|
229
|
+
displayName: spec.displayName,
|
|
230
|
+
status: String(out?.['status'] ?? 'UNKNOWN'),
|
|
231
|
+
_placeholder: true,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
async deleteConnection(pipeId, connectionId) {
|
|
235
|
+
await this.request(pipeId, 'DELETE', `app-connections/${connectionId}`);
|
|
236
|
+
}
|
|
237
|
+
async createFlow(pipeId, displayName) {
|
|
238
|
+
const { projectId } = await this.session(pipeId);
|
|
239
|
+
const out = await this.request(pipeId, 'POST', 'flows', { projectId, displayName });
|
|
240
|
+
if (!out?.id)
|
|
241
|
+
throw new IpaasError('POST flows', 200, 'the response carried no flow id');
|
|
242
|
+
return out.id;
|
|
243
|
+
}
|
|
244
|
+
async deleteFlow(pipeId, flowId) {
|
|
245
|
+
await this.request(pipeId, 'DELETE', `flows/${flowId}`);
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* One flow operation.
|
|
249
|
+
*
|
|
250
|
+
* `IMPORT_FLOW` is refused here on purpose. It replaces the whole tree *and*
|
|
251
|
+
* runs the schema migration an old export needs, so a flow already in live form
|
|
252
|
+
* has every datapill rewritten — `{{step_1['output'].x}}` becomes
|
|
253
|
+
* `{{step_1['output']['output'].x}}` — and then resolves nothing at runtime,
|
|
254
|
+
* while every validator still calls it valid. The granular operations do not
|
|
255
|
+
* migrate. If an actual export ever needs importing, that is a separate path
|
|
256
|
+
* with its own conversion.
|
|
257
|
+
*/
|
|
258
|
+
async operate(pipeId, flowId, type, request) {
|
|
259
|
+
if (type === 'IMPORT_FLOW') {
|
|
260
|
+
throw new Error('IMPORT_FLOW is not used by this tool: it migrates the tree it is given, which silently ' +
|
|
261
|
+
'doubles every datapill of a flow already in live form. Use the granular operations.');
|
|
262
|
+
}
|
|
263
|
+
if (ACTS_ON_EMPTY_REQUEST.has(type) && Object.keys(request).length === 0) {
|
|
264
|
+
throw new Error(`${type} acts on an empty request rather than rejecting it — sending one would take a ` +
|
|
265
|
+
'default nobody asked for. Supply the request explicitly.');
|
|
266
|
+
}
|
|
267
|
+
return this.request(pipeId, 'POST', `flows/${flowId}`, { type, request });
|
|
268
|
+
}
|
|
269
|
+
}
|