@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,365 @@
|
|
|
1
|
+
import { debug } from '../util/log.ts';
|
|
2
|
+
import type { InternalApi } from './internal.ts';
|
|
3
|
+
import type { Row } from '../model/payload.ts';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A pipe's iPaaS workspace — "Advanced Automations", Activepieces underneath.
|
|
7
|
+
*
|
|
8
|
+
* Every pipe has its own workspace, and the session proves it: the identity that
|
|
9
|
+
* comes back has the pipe's name as `firstName` and its id as `lastName`. Flows
|
|
10
|
+
* and connections are scoped to it, so a flow belongs to exactly one pipe even
|
|
11
|
+
* though it may orchestrate across many.
|
|
12
|
+
*
|
|
13
|
+
* Authentication needs no new credential. Two hops from the token `pipe` already
|
|
14
|
+
* holds:
|
|
15
|
+
*
|
|
16
|
+
* 1. the INTERNAL API mints a short-lived, pipe-scoped JWT
|
|
17
|
+
* (`advancedAutomationsToken(repoId:)`)
|
|
18
|
+
* 2. `POST /managed-authn/external-token` exchanges it for a session — which
|
|
19
|
+
* also hands back the projectId, so nothing has to be configured
|
|
20
|
+
*
|
|
21
|
+
* The OAuth + PKCE dance in Pipefy's own gateway exists only to reach the iPaaS
|
|
22
|
+
* MCP tool surface. The REST API used here answers the step-2 session directly.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export const IPAAS_BASE = 'https://ipaas.pipefy.com/api/v1';
|
|
26
|
+
|
|
27
|
+
export type FlowRow = Row & { id: string; status: string; version: Row };
|
|
28
|
+
export type ConnectionRow = {
|
|
29
|
+
externalId: string;
|
|
30
|
+
pieceName: string;
|
|
31
|
+
displayName: string;
|
|
32
|
+
status: string;
|
|
33
|
+
/** Set by this tool when it created the connection with a placeholder credential. */
|
|
34
|
+
_placeholder?: boolean;
|
|
35
|
+
/** Authored, not yet created. */
|
|
36
|
+
_new?: boolean;
|
|
37
|
+
};
|
|
38
|
+
export type RunRow = { id: string; flowId: string; status: string; startTime: string };
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The flow operations this endpoint accepts, probed against it. `UPDATE_BRANCH`
|
|
42
|
+
* is deliberately absent: it is not in the enum, and a router's branches are
|
|
43
|
+
* edited by sending the router through `UPDATE_ACTION`, since `settings.branches`
|
|
44
|
+
* is part of its settings.
|
|
45
|
+
*/
|
|
46
|
+
export type FlowOperation =
|
|
47
|
+
| 'IMPORT_FLOW'
|
|
48
|
+
| 'UPDATE_TRIGGER'
|
|
49
|
+
| 'ADD_ACTION'
|
|
50
|
+
| 'UPDATE_ACTION'
|
|
51
|
+
| 'DELETE_ACTION'
|
|
52
|
+
| 'MOVE_ACTION'
|
|
53
|
+
| 'DUPLICATE_ACTION'
|
|
54
|
+
| 'CHANGE_NAME'
|
|
55
|
+
| 'CHANGE_STATUS'
|
|
56
|
+
| 'LOCK_AND_PUBLISH'
|
|
57
|
+
| 'LOCK_FLOW'
|
|
58
|
+
| 'USE_AS_DRAFT'
|
|
59
|
+
| 'CHANGE_FOLDER'
|
|
60
|
+
| 'UPDATE_METADATA'
|
|
61
|
+
| 'SAVE_SAMPLE_DATA'
|
|
62
|
+
| 'SET_SKIP_ACTION'
|
|
63
|
+
| 'ADD_BRANCH'
|
|
64
|
+
| 'DELETE_BRANCH'
|
|
65
|
+
| 'DUPLICATE_BRANCH';
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Operations that act on an empty request instead of rejecting it.
|
|
69
|
+
*
|
|
70
|
+
* Learned the hard way: probing the enum by sending every candidate with
|
|
71
|
+
* `request: {}` published and enabled a draft flow, because these four do not
|
|
72
|
+
* validate a request they can default. Nothing in this module may send one
|
|
73
|
+
* without an explicit, complete request.
|
|
74
|
+
*/
|
|
75
|
+
export const ACTS_ON_EMPTY_REQUEST = new Set<FlowOperation>([
|
|
76
|
+
'CHANGE_STATUS',
|
|
77
|
+
'LOCK_AND_PUBLISH',
|
|
78
|
+
'LOCK_FLOW',
|
|
79
|
+
'CHANGE_FOLDER',
|
|
80
|
+
'UPDATE_METADATA',
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
export class IpaasError extends Error {
|
|
84
|
+
readonly status: number;
|
|
85
|
+
readonly step: string;
|
|
86
|
+
constructor(step: string, status: number, body: string) {
|
|
87
|
+
super(`iPaaS ${step} failed (HTTP ${status}): ${body.slice(0, 300)}`);
|
|
88
|
+
this.name = 'IpaasError';
|
|
89
|
+
this.status = status;
|
|
90
|
+
this.step = step;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** iPaaS is off for the org, or the caller cannot administer this pipe's automations. */
|
|
95
|
+
export class IpaasUnavailable extends Error {
|
|
96
|
+
readonly pipeId: number;
|
|
97
|
+
constructor(pipeId: number, reason: string) {
|
|
98
|
+
super(reason);
|
|
99
|
+
this.name = 'IpaasUnavailable';
|
|
100
|
+
this.pipeId = pipeId;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export type IpaasSession = {
|
|
105
|
+
pipeId: number;
|
|
106
|
+
projectId: string;
|
|
107
|
+
/** The pipe's iPaaS user id, kept for the record rather than used. */
|
|
108
|
+
userId: string;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export class IpaasClient {
|
|
112
|
+
private internal: InternalApi;
|
|
113
|
+
private base: string;
|
|
114
|
+
/** pipeId → session, because minting is two round trips and a pull does many reads. */
|
|
115
|
+
private sessions = new Map<number, { session: IpaasSession; token: string; at: number }>();
|
|
116
|
+
/** A session lives a while, but not forever; re-mint rather than discover expiry mid-apply. */
|
|
117
|
+
private static readonly TTL_MS = 10 * 60 * 1000;
|
|
118
|
+
|
|
119
|
+
constructor(internal: InternalApi, base = IPAAS_BASE) {
|
|
120
|
+
this.internal = internal;
|
|
121
|
+
this.base = base.replace(/\/$/, '');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Whether this pipe has a reachable iPaaS workspace.
|
|
126
|
+
*
|
|
127
|
+
* A refusal here is `unknown` coverage, never "no flows" — the distinction the
|
|
128
|
+
* whole coverage doctrine rests on. Minting requires iPaaS enabled on the
|
|
129
|
+
* organization and permission to create automations on the pipe.
|
|
130
|
+
*/
|
|
131
|
+
async available(pipeId: number): Promise<{ ok: true; session: IpaasSession } | { ok: false; reason: string }> {
|
|
132
|
+
try {
|
|
133
|
+
const session = await this.session(pipeId);
|
|
134
|
+
return { ok: true, session };
|
|
135
|
+
} catch (e) {
|
|
136
|
+
return { ok: false, reason: e instanceof Error ? e.message : String(e) };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async session(pipeId: number): Promise<IpaasSession> {
|
|
141
|
+
const hit = this.sessions.get(pipeId);
|
|
142
|
+
if (hit && Date.now() - hit.at < IpaasClient.TTL_MS) return hit.session;
|
|
143
|
+
|
|
144
|
+
const minted = await this.internal.gql<{ advancedAutomationsToken: { token: string } | null }>(
|
|
145
|
+
`query GetAdvancedAutomationsToken($repoId: ID!) {
|
|
146
|
+
advancedAutomationsToken(repoId: $repoId) { token }
|
|
147
|
+
}`,
|
|
148
|
+
{ repoId: String(pipeId) },
|
|
149
|
+
'GetAdvancedAutomationsToken',
|
|
150
|
+
);
|
|
151
|
+
const token = minted?.advancedAutomationsToken?.token;
|
|
152
|
+
if (!token) {
|
|
153
|
+
throw new IpaasUnavailable(
|
|
154
|
+
pipeId,
|
|
155
|
+
`no advanced-automations token for pipe ${pipeId} — iPaaS may be off for this organization, ` +
|
|
156
|
+
'or this credential may not be allowed to create automations on the pipe',
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const res = await fetch(`${this.base}/managed-authn/external-token`, {
|
|
161
|
+
method: 'POST',
|
|
162
|
+
headers: { 'Content-Type': 'application/json' },
|
|
163
|
+
body: JSON.stringify({ externalAccessToken: token }),
|
|
164
|
+
});
|
|
165
|
+
const body = await res.text();
|
|
166
|
+
if (!res.ok) throw new IpaasError('session exchange', res.status, body);
|
|
167
|
+
|
|
168
|
+
const parsed = JSON.parse(body) as { token: string; projectId: string; id: string };
|
|
169
|
+
if (!parsed.token || !parsed.projectId) {
|
|
170
|
+
throw new IpaasError('session exchange', res.status, 'the response carried no token or projectId');
|
|
171
|
+
}
|
|
172
|
+
const session: IpaasSession = { pipeId, projectId: parsed.projectId, userId: parsed.id };
|
|
173
|
+
this.sessions.set(pipeId, { session, token: parsed.token, at: Date.now() });
|
|
174
|
+
debug(`ipaas session for pipe ${pipeId}: project ${parsed.projectId}`);
|
|
175
|
+
return session;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private async token(pipeId: number): Promise<string> {
|
|
179
|
+
await this.session(pipeId);
|
|
180
|
+
return this.sessions.get(pipeId)!.token;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private async request<T>(pipeId: number, method: string, path: string, body?: unknown): Promise<T> {
|
|
184
|
+
const token = await this.token(pipeId);
|
|
185
|
+
const res = await fetch(`${this.base}/${path.replace(/^\//, '')}`, {
|
|
186
|
+
method,
|
|
187
|
+
// Content-Type only with a body: a bodyless DELETE carrying it is rejected
|
|
188
|
+
// outright with FST_ERR_CTP_EMPTY_JSON_BODY, so deleting a flow used to fail
|
|
189
|
+
// on a header rather than on anything to do with the flow.
|
|
190
|
+
headers: { Authorization: `Bearer ${token}`, ...(body === undefined ? {} : { 'Content-Type': 'application/json' }) },
|
|
191
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
192
|
+
});
|
|
193
|
+
const text = await res.text();
|
|
194
|
+
if (res.status === 401) {
|
|
195
|
+
// The session aged out mid-run; mint once more before giving up.
|
|
196
|
+
this.sessions.delete(pipeId);
|
|
197
|
+
const retryToken = await this.token(pipeId);
|
|
198
|
+
const again = await fetch(`${this.base}/${path.replace(/^\//, '')}`, {
|
|
199
|
+
method,
|
|
200
|
+
headers: { Authorization: `Bearer ${retryToken}`, ...(body === undefined ? {} : { 'Content-Type': 'application/json' }) },
|
|
201
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
202
|
+
});
|
|
203
|
+
const againText = await again.text();
|
|
204
|
+
if (!again.ok) throw new IpaasError(`${method} ${path}`, again.status, againText);
|
|
205
|
+
return (againText ? JSON.parse(againText) : null) as T;
|
|
206
|
+
}
|
|
207
|
+
if (!res.ok) throw new IpaasError(`${method} ${path}`, res.status, text);
|
|
208
|
+
return (text ? JSON.parse(text) : null) as T;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ── reads ─────────────────────────────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
async listFlows(pipeId: number): Promise<FlowRow[]> {
|
|
214
|
+
const { projectId } = await this.session(pipeId);
|
|
215
|
+
const out = await this.request<{ data: FlowRow[] }>(pipeId, 'GET', `flows?projectId=${projectId}`);
|
|
216
|
+
return out?.data ?? [];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async getFlow(pipeId: number, flowId: string): Promise<FlowRow> {
|
|
220
|
+
return this.request<FlowRow>(pipeId, 'GET', `flows/${flowId}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async listConnections(pipeId: number): Promise<ConnectionRow[]> {
|
|
224
|
+
const { projectId } = await this.session(pipeId);
|
|
225
|
+
const out = await this.request<{ data: Array<Record<string, unknown>> }>(
|
|
226
|
+
pipeId,
|
|
227
|
+
'GET',
|
|
228
|
+
`app-connections?projectId=${projectId}`,
|
|
229
|
+
);
|
|
230
|
+
// Only the four columns a reference needs. A credential is never one of them.
|
|
231
|
+
return (out?.data ?? []).map((c) => ({
|
|
232
|
+
externalId: String(c['externalId'] ?? ''),
|
|
233
|
+
pieceName: String(c['pieceName'] ?? ''),
|
|
234
|
+
displayName: String(c['displayName'] ?? ''),
|
|
235
|
+
status: String(c['status'] ?? ''),
|
|
236
|
+
}));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async listRuns(pipeId: number): Promise<RunRow[]> {
|
|
240
|
+
const { projectId } = await this.session(pipeId);
|
|
241
|
+
const out = await this.request<{ data: Array<Record<string, unknown>> }>(
|
|
242
|
+
pipeId,
|
|
243
|
+
'GET',
|
|
244
|
+
`flow-runs?projectId=${projectId}`,
|
|
245
|
+
);
|
|
246
|
+
return (out?.data ?? []).map((r) => ({
|
|
247
|
+
id: String(r['id'] ?? ''),
|
|
248
|
+
flowId: String(r['flowId'] ?? ''),
|
|
249
|
+
status: String(r['status'] ?? ''),
|
|
250
|
+
startTime: String(r['startTime'] ?? ''),
|
|
251
|
+
}));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** pieceName → the version this instance offers. Instance-wide, not per project. */
|
|
255
|
+
async listPieces(pipeId: number): Promise<Record<string, string>> {
|
|
256
|
+
const raw = await this.request<Array<{ name: string; version: string }>>(pipeId, 'GET', 'pieces');
|
|
257
|
+
return Object.fromEntries((raw ?? []).filter((p) => p.name && p.version).map((p) => [p.name, p.version]));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** One piece's action and trigger names, for checking a node against the catalogue. */
|
|
261
|
+
async pieceMembers(pipeId: number, pieceName: string): Promise<{ actions: string[]; triggers: string[] }> {
|
|
262
|
+
const meta = await this.request<{ actions?: Record<string, unknown>; triggers?: Record<string, unknown> }>(
|
|
263
|
+
pipeId,
|
|
264
|
+
'GET',
|
|
265
|
+
`pieces/${encodeURIComponent(pieceName)}`,
|
|
266
|
+
);
|
|
267
|
+
return { actions: Object.keys(meta?.actions ?? {}), triggers: Object.keys(meta?.triggers ?? {}) };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ── writes ────────────────────────────────────────────────────────────────
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Create a connection with a placeholder credential.
|
|
274
|
+
*
|
|
275
|
+
* A pipe whose iPaaS workspace has no connections cannot host a flow, and the
|
|
276
|
+
* credential is not obtainable here: `piece-pipefy` authenticates by
|
|
277
|
+
* `client_credentials` against Pipefy's own OAuth token endpoint, and this
|
|
278
|
+
* host has no OAuth client registered for it — only for Slack. Connections are
|
|
279
|
+
* also hard-isolated per project: the update endpoint accepts `scope` and
|
|
280
|
+
* `projectIds` and returns 200 while ignoring both, and a different project's
|
|
281
|
+
* session gets 403 on the connection itself.
|
|
282
|
+
*
|
|
283
|
+
* So a placeholder is created instead, which is how Pipefy's own import path
|
|
284
|
+
* behaves — import, then remap the connection in the UI. The flow structure is
|
|
285
|
+
* what is being built and it does not depend on the credential working.
|
|
286
|
+
*
|
|
287
|
+
* **The host does not validate the value.** It answers 201 and marks the
|
|
288
|
+
* connection ACTIVE whatever is sent, so `status: ACTIVE` is not evidence that
|
|
289
|
+
* a connection works. Everything this tool creates is tagged `_placeholder` so
|
|
290
|
+
* a flow that depends on one can be reported as unreconnected, and a flow with
|
|
291
|
+
* a placeholder connection is never enabled.
|
|
292
|
+
*/
|
|
293
|
+
async createPlaceholderConnection(
|
|
294
|
+
pipeId: number,
|
|
295
|
+
spec: { pieceName: string; displayName: string; externalId?: string },
|
|
296
|
+
): Promise<ConnectionRow> {
|
|
297
|
+
const { projectId } = await this.session(pipeId);
|
|
298
|
+
const externalId = spec.externalId ?? `pipe-placeholder-${spec.pieceName.split('/').pop()}`;
|
|
299
|
+
const type = 'CUSTOM_AUTH';
|
|
300
|
+
const out = await this.request<Record<string, unknown>>(pipeId, 'POST', 'app-connections', {
|
|
301
|
+
externalId,
|
|
302
|
+
displayName: spec.displayName,
|
|
303
|
+
pieceName: spec.pieceName,
|
|
304
|
+
projectId,
|
|
305
|
+
// The discriminator goes in both positions; the host wants it twice.
|
|
306
|
+
type,
|
|
307
|
+
value: {
|
|
308
|
+
type,
|
|
309
|
+
props: {
|
|
310
|
+
// Named so anyone reading the connection in the product knows what it is.
|
|
311
|
+
placeholder: 'created by pipe — reconnect this before enabling any flow',
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
return {
|
|
316
|
+
externalId: String(out?.['externalId'] ?? externalId),
|
|
317
|
+
pieceName: spec.pieceName,
|
|
318
|
+
displayName: spec.displayName,
|
|
319
|
+
status: String(out?.['status'] ?? 'UNKNOWN'),
|
|
320
|
+
_placeholder: true,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async deleteConnection(pipeId: number, connectionId: string): Promise<void> {
|
|
325
|
+
await this.request<null>(pipeId, 'DELETE', `app-connections/${connectionId}`);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async createFlow(pipeId: number, displayName: string): Promise<string> {
|
|
329
|
+
const { projectId } = await this.session(pipeId);
|
|
330
|
+
const out = await this.request<{ id: string }>(pipeId, 'POST', 'flows', { projectId, displayName });
|
|
331
|
+
if (!out?.id) throw new IpaasError('POST flows', 200, 'the response carried no flow id');
|
|
332
|
+
return out.id;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async deleteFlow(pipeId: number, flowId: string): Promise<void> {
|
|
336
|
+
await this.request<null>(pipeId, 'DELETE', `flows/${flowId}`);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* One flow operation.
|
|
341
|
+
*
|
|
342
|
+
* `IMPORT_FLOW` is refused here on purpose. It replaces the whole tree *and*
|
|
343
|
+
* runs the schema migration an old export needs, so a flow already in live form
|
|
344
|
+
* has every datapill rewritten — `{{step_1['output'].x}}` becomes
|
|
345
|
+
* `{{step_1['output']['output'].x}}` — and then resolves nothing at runtime,
|
|
346
|
+
* while every validator still calls it valid. The granular operations do not
|
|
347
|
+
* migrate. If an actual export ever needs importing, that is a separate path
|
|
348
|
+
* with its own conversion.
|
|
349
|
+
*/
|
|
350
|
+
async operate(pipeId: number, flowId: string, type: FlowOperation, request: Record<string, unknown>): Promise<FlowRow> {
|
|
351
|
+
if (type === 'IMPORT_FLOW') {
|
|
352
|
+
throw new Error(
|
|
353
|
+
'IMPORT_FLOW is not used by this tool: it migrates the tree it is given, which silently ' +
|
|
354
|
+
'doubles every datapill of a flow already in live form. Use the granular operations.',
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
if (ACTS_ON_EMPTY_REQUEST.has(type) && Object.keys(request).length === 0) {
|
|
358
|
+
throw new Error(
|
|
359
|
+
`${type} acts on an empty request rather than rejecting it — sending one would take a ` +
|
|
360
|
+
'default nobody asked for. Supply the request explicitly.',
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
return this.request<FlowRow>(pipeId, 'POST', `flows/${flowId}`, { type, request });
|
|
364
|
+
}
|
|
365
|
+
}
|