@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
package/src/pipefy/internal.ts
DELETED
|
@@ -1,380 +0,0 @@
|
|
|
1
|
-
import { ENDPOINTS, type TokenProvider } from '../config.ts';
|
|
2
|
-
import { debug, warn } from '../util/log.ts';
|
|
3
|
-
import { retry } from '../util/pool.ts';
|
|
4
|
-
import type { Row } from '../model/payload.ts';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* The internal-API adapter. PLAN.md §2.1 flags this as an unversioned endpoint
|
|
8
|
-
* with no compatibility contract, so it is isolated here, behind one module,
|
|
9
|
-
* and every entry point degrades to `null`/`unsupported` rather than throwing.
|
|
10
|
-
*
|
|
11
|
-
* Two things live only here — both taken from the Change Migrator recipes,
|
|
12
|
-
* which have been running them in production:
|
|
13
|
-
*
|
|
14
|
-
* 1. Automations. Public `getAutomations` returns a summary; the recipes use
|
|
15
|
-
* `loadAutomationToEdit` to get `action_params.field_map`, `searchFor`,
|
|
16
|
-
* `responseSchema`, `schedulerCron` and `aiParams` — everything a
|
|
17
|
-
* round trip needs and the summary omits.
|
|
18
|
-
* 2. Phase jumps. `GET/PUT /internal_api/settings/phases/:id`, set-replacing.
|
|
19
|
-
*
|
|
20
|
-
* Both accept the same personal API token as the public endpoint, so there is
|
|
21
|
-
* no session-cookie or CSRF subsystem here.
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
export type InternalStatus = 'unknown' | 'ok' | 'unavailable';
|
|
25
|
-
|
|
26
|
-
export class InternalApi {
|
|
27
|
-
private readonly tokenProvider: TokenProvider;
|
|
28
|
-
status: InternalStatus = 'unknown';
|
|
29
|
-
requestCount = 0;
|
|
30
|
-
/** Set once, so a broken endpoint produces one warning and not one per call. */
|
|
31
|
-
private warned = false;
|
|
32
|
-
|
|
33
|
-
constructor(token: string | TokenProvider) {
|
|
34
|
-
this.tokenProvider = typeof token === 'string' ? async () => token : token;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
private async bearer(): Promise<string> {
|
|
38
|
-
return this.tokenProvider();
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
private markUnavailable(reason: string) {
|
|
42
|
-
this.status = 'unavailable';
|
|
43
|
-
if (!this.warned) {
|
|
44
|
-
this.warned = true;
|
|
45
|
-
warn(
|
|
46
|
-
`internal API unavailable (${reason}). Automation details and phase jumps ` +
|
|
47
|
-
`will be reported as unknown rather than empty — see PLAN.md §2.1.`,
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** GraphQL over the internal endpoint. Returns null when the endpoint fails. */
|
|
53
|
-
async gql<T>(query: string, variables: Record<string, unknown>, label: string): Promise<T | null> {
|
|
54
|
-
try {
|
|
55
|
-
const data = await retry(
|
|
56
|
-
async () => {
|
|
57
|
-
this.requestCount++;
|
|
58
|
-
const res = await fetch(ENDPOINTS.internal, {
|
|
59
|
-
method: 'POST',
|
|
60
|
-
headers: {
|
|
61
|
-
authorization: `Bearer ${await this.bearer()}`,
|
|
62
|
-
'content-type': 'application/json',
|
|
63
|
-
accept: 'application/json',
|
|
64
|
-
},
|
|
65
|
-
body: JSON.stringify({ query, variables }),
|
|
66
|
-
});
|
|
67
|
-
const text = await res.text();
|
|
68
|
-
if (!res.ok) {
|
|
69
|
-
const err = new Error(`HTTP ${res.status}: ${text.slice(0, 160)}`);
|
|
70
|
-
(err as { retryable?: boolean }).retryable = res.status >= 500 || res.status === 429;
|
|
71
|
-
throw err;
|
|
72
|
-
}
|
|
73
|
-
const body = JSON.parse(text) as { data?: T; errors?: Array<{ message: string }> };
|
|
74
|
-
if (body.errors?.length) {
|
|
75
|
-
const err = new Error(body.errors.map((e) => e.message).join('; '));
|
|
76
|
-
(err as { retryable?: boolean }).retryable = false;
|
|
77
|
-
throw err;
|
|
78
|
-
}
|
|
79
|
-
return body.data ?? null;
|
|
80
|
-
},
|
|
81
|
-
{ label },
|
|
82
|
-
);
|
|
83
|
-
if (data) this.status = 'ok';
|
|
84
|
-
return data;
|
|
85
|
-
} catch (e) {
|
|
86
|
-
debug(`internal gql ${label} failed: ${(e as Error).message}`);
|
|
87
|
-
this.markUnavailable(`${label}: ${(e as Error).message}`);
|
|
88
|
-
return null;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// ── Automations ────────────────────────────────────────────────────────────
|
|
93
|
-
|
|
94
|
-
/** Recipe `Pipe Data Treatment` step 156. */
|
|
95
|
-
async listAutomations(orgId: string, repoId: string | number): Promise<Row[] | null> {
|
|
96
|
-
const q = `
|
|
97
|
-
query getAutomations($orgId: ID!, $repoId: ID) {
|
|
98
|
-
automations(organizationId: $orgId, repoId: $repoId) {
|
|
99
|
-
id
|
|
100
|
-
name
|
|
101
|
-
action_id
|
|
102
|
-
event_id
|
|
103
|
-
created_at
|
|
104
|
-
active
|
|
105
|
-
action_repo: action_repo_v2 {
|
|
106
|
-
__typename
|
|
107
|
-
... on Pipe { id }
|
|
108
|
-
... on Table { id }
|
|
109
|
-
}
|
|
110
|
-
event_repo { __typename id }
|
|
111
|
-
}
|
|
112
|
-
}`;
|
|
113
|
-
const data = await this.gql<{ automations: Row[] }>(q, { orgId: String(orgId), repoId: String(repoId) }, 'listAutomations');
|
|
114
|
-
return data?.automations ?? null;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Recipe `Apply Changes` step 118 — the full editable definition. This is the
|
|
119
|
-
* only read path that returns `action_params.field_map`, `searchFor`,
|
|
120
|
-
* `responseSchema` and `aiParams`, which is why the reconstruction path
|
|
121
|
-
* cannot avoid the internal API for automations.
|
|
122
|
-
*/
|
|
123
|
-
async loadAutomation(id: string | number): Promise<Row | null> {
|
|
124
|
-
const q = `
|
|
125
|
-
query loadAutomationToEdit($id: ID!) {
|
|
126
|
-
automation(id: $id) {
|
|
127
|
-
name
|
|
128
|
-
event_id
|
|
129
|
-
active
|
|
130
|
-
event_repo { id }
|
|
131
|
-
event_params {
|
|
132
|
-
toPhaseId: to_phase_id
|
|
133
|
-
fromPhaseId
|
|
134
|
-
inPhaseId
|
|
135
|
-
triggerFieldIds
|
|
136
|
-
kindOfSla
|
|
137
|
-
triggerAutomationId
|
|
138
|
-
}
|
|
139
|
-
action_id
|
|
140
|
-
action_repo: action_repo_v2 {
|
|
141
|
-
... on Pipe { id uuid }
|
|
142
|
-
... on Table { id uuid }
|
|
143
|
-
}
|
|
144
|
-
scheduler_frequency
|
|
145
|
-
schedulerCron { minute hour dayOfWeek dayOfMonth month }
|
|
146
|
-
searchFor { id field operation value }
|
|
147
|
-
responseSchema
|
|
148
|
-
action_params {
|
|
149
|
-
toPhaseId: to_phase_id
|
|
150
|
-
fieldsMapOrder: fields_map_order
|
|
151
|
-
email_template_id
|
|
152
|
-
fieldMap: field_map { fieldId value inputMode }
|
|
153
|
-
cardId: card_id
|
|
154
|
-
headers
|
|
155
|
-
body
|
|
156
|
-
url
|
|
157
|
-
httpMethod
|
|
158
|
-
authenticationKey
|
|
159
|
-
hasAuthenticationValue
|
|
160
|
-
authenticationAddTo
|
|
161
|
-
authenticationType
|
|
162
|
-
strategy
|
|
163
|
-
# taskParams is not in the recipe's query, and the omission is not
|
|
164
|
-
# cosmetic: every send_a_task automation came back with an empty
|
|
165
|
-
# action_params, so a reconstructed read lost its recipients and
|
|
166
|
-
# title, and a recreate would have rebuilt it blank. Introspected
|
|
167
|
-
# from AutomationActionParams on the internal endpoint.
|
|
168
|
-
taskParams { recipients title }
|
|
169
|
-
aiParams { fieldIds value skillsIds }
|
|
170
|
-
aiBehaviorParams
|
|
171
|
-
slaParams {
|
|
172
|
-
monday { startHour endHour enabled }
|
|
173
|
-
tuesday { startHour endHour enabled }
|
|
174
|
-
wednesday { startHour endHour enabled }
|
|
175
|
-
thursday { startHour endHour enabled }
|
|
176
|
-
friday { startHour endHour enabled }
|
|
177
|
-
saturday { startHour endHour enabled }
|
|
178
|
-
sunday { startHour endHour enabled }
|
|
179
|
-
timezone
|
|
180
|
-
holidays { date recurrence description }
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
condition {
|
|
184
|
-
expressions_structure
|
|
185
|
-
expressions { id field_address operation value structure_id }
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}`;
|
|
189
|
-
const data = await this.gql<{ automation: Row | null }>(q, { id: String(id) }, `loadAutomation ${id}`);
|
|
190
|
-
return data?.automation ?? null;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/** Recipe `Apply Changes` step 121. `input` is the AutomationInput variable set. */
|
|
194
|
-
async createAutomation(vars: Record<string, unknown>): Promise<{ id: string } | null> {
|
|
195
|
-
const q = `
|
|
196
|
-
mutation createAutomation(
|
|
197
|
-
$name: String!, $action_id: ID!, $event_id: ID!,
|
|
198
|
-
$action_repo_id: ID, $event_repo_id: ID,
|
|
199
|
-
$event_params: AutomationEventParamsInput,
|
|
200
|
-
$action_params: AutomationActionParamsInput,
|
|
201
|
-
$condition: ConditionInput,
|
|
202
|
-
$scheduler_frequency: String,
|
|
203
|
-
$searchFor: [SearchConditionInput],
|
|
204
|
-
$schedulerCron: CronInput,
|
|
205
|
-
$responseSchema: JSON
|
|
206
|
-
) {
|
|
207
|
-
createAutomation(input: {
|
|
208
|
-
name: $name, action_id: $action_id, event_id: $event_id,
|
|
209
|
-
action_repo_id: $action_repo_id, event_repo_id: $event_repo_id,
|
|
210
|
-
event_params: $event_params, action_params: $action_params,
|
|
211
|
-
condition: $condition, scheduler_frequency: $scheduler_frequency,
|
|
212
|
-
searchFor: $searchFor, schedulerCron: $schedulerCron,
|
|
213
|
-
responseSchema: $responseSchema
|
|
214
|
-
}) {
|
|
215
|
-
automation { id }
|
|
216
|
-
error_details { object_key messages }
|
|
217
|
-
}
|
|
218
|
-
}`;
|
|
219
|
-
const data = await this.gql<{
|
|
220
|
-
createAutomation: { automation: { id: string } | null; error_details?: Array<{ object_key: string; messages: string[] }> };
|
|
221
|
-
}>(q, vars, 'createAutomation');
|
|
222
|
-
const payload = data?.createAutomation;
|
|
223
|
-
if (!payload) return null;
|
|
224
|
-
if (payload.error_details?.length) {
|
|
225
|
-
const detail = payload.error_details.map((d) => `${d.object_key}: ${d.messages.join(', ')}`).join(' | ');
|
|
226
|
-
throw new Error(`createAutomation rejected — ${detail}`);
|
|
227
|
-
}
|
|
228
|
-
return payload.automation;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/** Recipe `Apply Changes` step 126. */
|
|
232
|
-
async deleteAutomation(id: string | number): Promise<boolean> {
|
|
233
|
-
const q = `
|
|
234
|
-
mutation deleteAutomation($id: ID!) {
|
|
235
|
-
deleteAutomation(input: { id: $id }) { success }
|
|
236
|
-
}`;
|
|
237
|
-
const data = await this.gql<{ deleteAutomation: { success: boolean } }>(q, { id: String(id) }, `deleteAutomation ${id}`);
|
|
238
|
-
return Boolean(data?.deleteAutomation?.success);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// ── Phase jumps ────────────────────────────────────────────────────────────
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* `GET /internal_api/settings/phases/:id` — recipe step 23.
|
|
245
|
-
*
|
|
246
|
-
* **The GET and the PUT answer in different shapes.** Both captured live:
|
|
247
|
-
*
|
|
248
|
-
* GET { data: { attributes: { name, phases: [[id, name, done], …],
|
|
249
|
-
* jump_targets: [344054275] } } }
|
|
250
|
-
* PUT { data: { name, back_phase_ids: [], next_phase_ids: [344054275] } }
|
|
251
|
-
*
|
|
252
|
-
* The GET's `jump_targets` is one merged set, which is exactly what the
|
|
253
|
-
* payload's `phase_jumps` array holds. Reading it as `data.next_phase_ids` —
|
|
254
|
-
* the shape PLAN.md §2.1 recorded, from the PUT — yields an empty set for
|
|
255
|
-
* every phase without any error, and a diff that then proposes deleting every
|
|
256
|
-
* jump in the pipe.
|
|
257
|
-
*/
|
|
258
|
-
async getPhaseSettings(
|
|
259
|
-
phaseId: string | number,
|
|
260
|
-
): Promise<{ jump_targets: number[]; phases: Array<{ id: number; name: string; done: boolean }> } | null> {
|
|
261
|
-
try {
|
|
262
|
-
const res = await retry(
|
|
263
|
-
async () => {
|
|
264
|
-
this.requestCount++;
|
|
265
|
-
const r = await fetch(`${ENDPOINTS.internalSettings}/phases/${phaseId}`, {
|
|
266
|
-
method: 'GET',
|
|
267
|
-
headers: { authorization: `Bearer ${await this.bearer()}`, accept: 'application/json' },
|
|
268
|
-
});
|
|
269
|
-
if (!r.ok) {
|
|
270
|
-
const err = new Error(`HTTP ${r.status}`);
|
|
271
|
-
(err as { retryable?: boolean }).retryable = r.status >= 500 || r.status === 429;
|
|
272
|
-
throw err;
|
|
273
|
-
}
|
|
274
|
-
return (await r.json()) as {
|
|
275
|
-
data?: {
|
|
276
|
-
attributes?: { jump_targets?: number[]; phases?: Array<[number, string, boolean]> };
|
|
277
|
-
/** The PUT-style shape, accepted in case a deployment answers with it here. */
|
|
278
|
-
jump_targets?: number[];
|
|
279
|
-
next_phase_ids?: number[];
|
|
280
|
-
back_phase_ids?: number[];
|
|
281
|
-
};
|
|
282
|
-
};
|
|
283
|
-
},
|
|
284
|
-
{ label: `getPhaseSettings ${phaseId}` },
|
|
285
|
-
);
|
|
286
|
-
|
|
287
|
-
const d = res.data ?? {};
|
|
288
|
-
const attrs = d.attributes ?? {};
|
|
289
|
-
const targets = attrs.jump_targets ?? d.jump_targets ?? [...(d.next_phase_ids ?? []), ...(d.back_phase_ids ?? [])];
|
|
290
|
-
|
|
291
|
-
this.status = 'ok';
|
|
292
|
-
return {
|
|
293
|
-
jump_targets: [...new Set(targets.map(Number).filter((n) => Number.isFinite(n)))],
|
|
294
|
-
phases: (attrs.phases ?? []).map(([id, name, done]) => ({ id: Number(id), name: String(name), done: Boolean(done) })),
|
|
295
|
-
};
|
|
296
|
-
} catch (e) {
|
|
297
|
-
debug(`getPhaseSettings ${phaseId} failed: ${(e as Error).message}`);
|
|
298
|
-
this.markUnavailable(`getPhaseSettings: ${(e as Error).message}`);
|
|
299
|
-
return null;
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* `PUT /internal_api/settings/phases/:id` with the complete desired jump set.
|
|
305
|
-
* Recipe step 24 does the same thing in Python. Set-replacing, so we send the
|
|
306
|
-
* whole set and assert on the echoed `next_phase_ids` rather than on the HTTP
|
|
307
|
-
* status — PLAN.md §2.1.
|
|
308
|
-
*/
|
|
309
|
-
async setPhaseJumpTargets(
|
|
310
|
-
phaseId: string | number,
|
|
311
|
-
targetPhaseIds: Array<string | number>,
|
|
312
|
-
): Promise<{ ok: boolean; echoed: number[]; error?: string }> {
|
|
313
|
-
const params = new URLSearchParams();
|
|
314
|
-
if (targetPhaseIds.length === 0) {
|
|
315
|
-
// An empty set still has to be expressible, or "remove the last jump"
|
|
316
|
-
// becomes impossible. The form convention for an empty array is a single
|
|
317
|
-
// blank entry.
|
|
318
|
-
params.append('phase[jump_target_ids][]', '');
|
|
319
|
-
}
|
|
320
|
-
for (const t of targetPhaseIds) params.append('phase[jump_target_ids][]', String(t));
|
|
321
|
-
|
|
322
|
-
try {
|
|
323
|
-
const res = await retry(
|
|
324
|
-
async () => {
|
|
325
|
-
this.requestCount++;
|
|
326
|
-
const r = await fetch(`${ENDPOINTS.internalSettings}/phases/${phaseId}`, {
|
|
327
|
-
method: 'PUT',
|
|
328
|
-
headers: {
|
|
329
|
-
authorization: `Bearer ${await this.bearer()}`,
|
|
330
|
-
'content-type': 'application/x-www-form-urlencoded',
|
|
331
|
-
accept: 'application/json',
|
|
332
|
-
},
|
|
333
|
-
body: params.toString(),
|
|
334
|
-
});
|
|
335
|
-
const text = await r.text();
|
|
336
|
-
if (!r.ok) {
|
|
337
|
-
const err = new Error(`HTTP ${r.status}: ${text.slice(0, 160)}`);
|
|
338
|
-
(err as { retryable?: boolean }).retryable = r.status >= 500 || r.status === 429;
|
|
339
|
-
throw err;
|
|
340
|
-
}
|
|
341
|
-
return JSON.parse(text) as {
|
|
342
|
-
data?: {
|
|
343
|
-
next_phase_ids?: number[];
|
|
344
|
-
back_phase_ids?: number[];
|
|
345
|
-
jump_targets?: number[];
|
|
346
|
-
attributes?: { jump_targets?: number[] };
|
|
347
|
-
};
|
|
348
|
-
};
|
|
349
|
-
},
|
|
350
|
-
{ label: `setPhaseJumpTargets ${phaseId}` },
|
|
351
|
-
);
|
|
352
|
-
|
|
353
|
-
/**
|
|
354
|
-
* The echo is the union of both directions.
|
|
355
|
-
*
|
|
356
|
-
* `next_phase_ids` alone is forward-only, so a jump to an *earlier* phase
|
|
357
|
-
* echoes back as an empty next list even though the write succeeded — which
|
|
358
|
-
* this assertion reported as a failed write. Asserting on the union is what
|
|
359
|
-
* the sent set actually means.
|
|
360
|
-
*/
|
|
361
|
-
const d = res.data ?? {};
|
|
362
|
-
const echoed = [
|
|
363
|
-
...(d.attributes?.jump_targets ?? []),
|
|
364
|
-
...(d.jump_targets ?? []),
|
|
365
|
-
...(d.next_phase_ids ?? []),
|
|
366
|
-
...(d.back_phase_ids ?? []),
|
|
367
|
-
].map(Number);
|
|
368
|
-
const want = [...new Set(targetPhaseIds.map(Number))].sort((a, b) => a - b);
|
|
369
|
-
const got = [...new Set(echoed)].sort((a, b) => a - b);
|
|
370
|
-
const ok = want.length === got.length && want.every((v, i) => v === got[i]);
|
|
371
|
-
this.status = 'ok';
|
|
372
|
-
return ok
|
|
373
|
-
? { ok, echoed }
|
|
374
|
-
: { ok, echoed, error: `echo mismatch: wanted [${want.join(',')}], endpoint reports [${got.join(',')}]` };
|
|
375
|
-
} catch (e) {
|
|
376
|
-
this.markUnavailable(`setPhaseJumpTargets: ${(e as Error).message}`);
|
|
377
|
-
return { ok: false, echoed: [], error: (e as Error).message };
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
}
|