@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,204 @@
|
|
|
1
|
+
import { ENDPOINTS } from "../config.js";
|
|
2
|
+
import { debug } from "../util/log.js";
|
|
3
|
+
import { retry } from "../util/pool.js";
|
|
4
|
+
export class GraphQLRequestError extends Error {
|
|
5
|
+
errors;
|
|
6
|
+
retryable;
|
|
7
|
+
status;
|
|
8
|
+
constructor(message, errors, status, retryable) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'GraphQLRequestError';
|
|
11
|
+
this.errors = errors;
|
|
12
|
+
this.status = status;
|
|
13
|
+
this.retryable = retryable;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const RETRYABLE_STATUS = new Set([408, 425, 429, 500, 502, 503, 504]);
|
|
17
|
+
/** Rate limiting and transient upstream faults are retryable; a bad query is not. */
|
|
18
|
+
const isRetryableMessage = (m) => /rate limit|too many requests|timeout|timed out|temporarily|try again|internal server error/i.test(m);
|
|
19
|
+
export class PipefyClient {
|
|
20
|
+
tokenProvider;
|
|
21
|
+
endpoint;
|
|
22
|
+
/** Every request, for `--stats` and for measuring the rate-limit ceiling. */
|
|
23
|
+
requestCount = 0;
|
|
24
|
+
retryCount = 0;
|
|
25
|
+
/** Times a 401 forced a credential refresh. Surfaced by `pipe doctor`. */
|
|
26
|
+
reauthCount = 0;
|
|
27
|
+
typeFieldCache = new Map();
|
|
28
|
+
inputFieldCache = new Map();
|
|
29
|
+
requiredInputCache = new Map();
|
|
30
|
+
mutationInputTypeCache = new Map();
|
|
31
|
+
constructor(opts) {
|
|
32
|
+
this.tokenProvider = typeof opts.token === 'string' ? async () => opts.token : opts.token;
|
|
33
|
+
this.endpoint = opts.endpoint ?? ENDPOINTS.graphql;
|
|
34
|
+
}
|
|
35
|
+
/** The current bearer, refreshed only when something rejected the last one. */
|
|
36
|
+
async token(forceRefresh = false) {
|
|
37
|
+
if (forceRefresh)
|
|
38
|
+
this.reauthCount++;
|
|
39
|
+
return this.tokenProvider({ forceRefresh });
|
|
40
|
+
}
|
|
41
|
+
async raw(query, variables, label) {
|
|
42
|
+
let refreshed = false;
|
|
43
|
+
return retry(async () => {
|
|
44
|
+
this.requestCount++;
|
|
45
|
+
const res = await fetch(this.endpoint, {
|
|
46
|
+
method: 'POST',
|
|
47
|
+
headers: {
|
|
48
|
+
authorization: `Bearer ${await this.token()}`,
|
|
49
|
+
'content-type': 'application/json',
|
|
50
|
+
accept: 'application/json',
|
|
51
|
+
},
|
|
52
|
+
body: JSON.stringify({ query, variables: variables ?? {} }),
|
|
53
|
+
});
|
|
54
|
+
/**
|
|
55
|
+
* An expired OAuth access token looks like a 401. Refresh once and let
|
|
56
|
+
* the retry loop send it again; a second 401 is a real permission
|
|
57
|
+
* problem and must not spin.
|
|
58
|
+
*/
|
|
59
|
+
if ((res.status === 401 || res.status === 403) && !refreshed) {
|
|
60
|
+
refreshed = true;
|
|
61
|
+
await this.token(true);
|
|
62
|
+
const err = new Error(`HTTP ${res.status} — credential refreshed, retrying`);
|
|
63
|
+
err.retryable = true;
|
|
64
|
+
throw err;
|
|
65
|
+
}
|
|
66
|
+
const text = await res.text();
|
|
67
|
+
let body = {};
|
|
68
|
+
try {
|
|
69
|
+
body = text ? JSON.parse(text) : {};
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
const retryable = RETRYABLE_STATUS.has(res.status);
|
|
73
|
+
throw new GraphQLRequestError(`HTTP ${res.status} from ${this.endpoint}: ${text.slice(0, 200)}`, [], res.status, retryable);
|
|
74
|
+
}
|
|
75
|
+
if (body.errors?.length) {
|
|
76
|
+
const msg = body.errors.map((e) => e.message).join('; ');
|
|
77
|
+
const retryable = RETRYABLE_STATUS.has(res.status) || isRetryableMessage(msg);
|
|
78
|
+
throw new GraphQLRequestError(msg, body.errors, res.status, retryable);
|
|
79
|
+
}
|
|
80
|
+
if (!res.ok) {
|
|
81
|
+
throw new GraphQLRequestError(`HTTP ${res.status} from ${this.endpoint}`, [], res.status, RETRYABLE_STATUS.has(res.status));
|
|
82
|
+
}
|
|
83
|
+
if (body.data === undefined || body.data === null) {
|
|
84
|
+
throw new GraphQLRequestError('GraphQL response had no data', [], res.status, false);
|
|
85
|
+
}
|
|
86
|
+
return body.data;
|
|
87
|
+
}, { label: label ?? 'graphql' });
|
|
88
|
+
}
|
|
89
|
+
/** A query whose failure is a fact about the endpoint, not an error. */
|
|
90
|
+
async tryRaw(query, variables, label) {
|
|
91
|
+
try {
|
|
92
|
+
return await this.raw(query, variables, label);
|
|
93
|
+
}
|
|
94
|
+
catch (e) {
|
|
95
|
+
debug(`probe failed (${label ?? 'unnamed'}): ${e.message}`);
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Which fields a type actually has.
|
|
101
|
+
*
|
|
102
|
+
* The reconstruction path (reconstruct.ts) asks for columns that PLAN.md marks
|
|
103
|
+
* unverified. Rather than sending a query that fails wholesale on one unknown
|
|
104
|
+
* field, we introspect once and ask only for what exists. One cheap query
|
|
105
|
+
* buys a read path that degrades field-by-field instead of all-or-nothing.
|
|
106
|
+
*/
|
|
107
|
+
async typeFields(typeName) {
|
|
108
|
+
if (this.typeFieldCache.has(typeName))
|
|
109
|
+
return this.typeFieldCache.get(typeName) ?? null;
|
|
110
|
+
const data = await this.tryRaw('query($n:String!){ __type(name:$n){ fields{ name } } }', { n: typeName }, `introspect ${typeName}`);
|
|
111
|
+
const fields = data?.__type?.fields;
|
|
112
|
+
const set = fields ? new Set(fields.map((f) => f.name)) : null;
|
|
113
|
+
this.typeFieldCache.set(typeName, set);
|
|
114
|
+
return set;
|
|
115
|
+
}
|
|
116
|
+
/** Whether a mutation exists — the basis of capability detection (PLAN.md §1.2). */
|
|
117
|
+
async hasMutation(name) {
|
|
118
|
+
const fields = await this.typeFields('Mutation');
|
|
119
|
+
if (!fields)
|
|
120
|
+
return false;
|
|
121
|
+
return fields.has(name);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* The named type of a mutation's `input` argument.
|
|
125
|
+
*
|
|
126
|
+
* Never guessed. The registry documents traps like `createFieldConditionInput`
|
|
127
|
+
* being lowercase-c while `UpdateFieldConditionInput` is not; asking the schema
|
|
128
|
+
* removes a whole class of "mutation exists, input type name wrong" failures.
|
|
129
|
+
*/
|
|
130
|
+
async mutationInputType(mutation) {
|
|
131
|
+
if (this.mutationInputTypeCache.has(mutation))
|
|
132
|
+
return this.mutationInputTypeCache.get(mutation) ?? null;
|
|
133
|
+
const data = await this.tryRaw(`query {
|
|
134
|
+
__type(name: "Mutation") {
|
|
135
|
+
fields {
|
|
136
|
+
name
|
|
137
|
+
args { name type { kind name ofType { kind name ofType { kind name } } } }
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}`, undefined, 'introspect Mutation args');
|
|
141
|
+
const field = data?.__type?.fields?.find((f) => f.name === mutation);
|
|
142
|
+
const arg = field?.args.find((a) => a.name === 'input') ?? field?.args[0];
|
|
143
|
+
const named = arg ? unwrapTypeName(arg.type) : null;
|
|
144
|
+
this.mutationInputTypeCache.set(mutation, named);
|
|
145
|
+
return named;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Input fields that are NON_NULL with no default — the ones a request must
|
|
149
|
+
* carry a value for.
|
|
150
|
+
*
|
|
151
|
+
* CreatePipeRelationInput makes every boolean required, including
|
|
152
|
+
* autoFillFieldEnabled, which the snapshot payload has no column for at all.
|
|
153
|
+
* Sent without it the mutation is rejected mid-apply; knowing up front turns
|
|
154
|
+
* that into something `pipe plan` can say before anything is written.
|
|
155
|
+
*/
|
|
156
|
+
async requiredInputFields(typeName) {
|
|
157
|
+
const cached = this.requiredInputCache.get(typeName);
|
|
158
|
+
if (cached)
|
|
159
|
+
return cached;
|
|
160
|
+
const data = await this.tryRaw('query($n:String!){ __type(name:$n){ inputFields { name defaultValue type { kind } } } }', { n: typeName }, `introspect required ${typeName}`);
|
|
161
|
+
const req = new Set();
|
|
162
|
+
for (const fld of data?.__type?.inputFields ?? []) {
|
|
163
|
+
if (fld.type.kind === 'NON_NULL' && fld.defaultValue === null)
|
|
164
|
+
req.add(fld.name);
|
|
165
|
+
}
|
|
166
|
+
this.requiredInputCache.set(typeName, req);
|
|
167
|
+
return req;
|
|
168
|
+
}
|
|
169
|
+
/** The fields an input object type accepts. */
|
|
170
|
+
async inputFields(typeName) {
|
|
171
|
+
if (this.inputFieldCache.has(typeName))
|
|
172
|
+
return this.inputFieldCache.get(typeName) ?? null;
|
|
173
|
+
const data = await this.tryRaw('query($n:String!){ __type(name:$n){ inputFields{ name } } }', { n: typeName }, `introspect input ${typeName}`);
|
|
174
|
+
const fields = data?.__type?.inputFields;
|
|
175
|
+
const set = fields ? new Set(fields.map((f) => f.name)) : null;
|
|
176
|
+
this.inputFieldCache.set(typeName, set);
|
|
177
|
+
return set;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Filter a wanted selection down to what the type has. `selection` entries may
|
|
181
|
+
* be a bare field (`"label"`) or a field with a sub-selection
|
|
182
|
+
* (`["connectedRepo", "{ id name }"]`).
|
|
183
|
+
*/
|
|
184
|
+
async prune(typeName, selection) {
|
|
185
|
+
const have = await this.typeFields(typeName);
|
|
186
|
+
const keep = [];
|
|
187
|
+
for (const s of selection) {
|
|
188
|
+
const [name, sub] = Array.isArray(s) ? s : [s, ''];
|
|
189
|
+
if (have && !have.has(name)) {
|
|
190
|
+
debug(`skipping ${typeName}.${name} — not in schema`);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
keep.push(sub ? `${name} ${sub}` : name);
|
|
194
|
+
}
|
|
195
|
+
return keep.join('\n ');
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/** Unwrap NON_NULL / LIST wrappers down to the named type. */
|
|
199
|
+
const unwrapTypeName = (t) => {
|
|
200
|
+
let cur = t;
|
|
201
|
+
while (cur && !cur.name)
|
|
202
|
+
cur = cur.ofType ?? null;
|
|
203
|
+
return cur?.name ?? null;
|
|
204
|
+
};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { debug, step, warn } from "../util/log.js";
|
|
2
|
+
import { rel } from "../model/payload.js";
|
|
3
|
+
const asNum = (v) => {
|
|
4
|
+
if (v === null || v === undefined || v === '')
|
|
5
|
+
return null;
|
|
6
|
+
const n = Number(v);
|
|
7
|
+
return Number.isFinite(n) ? n : null;
|
|
8
|
+
};
|
|
9
|
+
/** The three sources, applied to one already-read payload. */
|
|
10
|
+
export const edgesFromPayload = (p) => {
|
|
11
|
+
const self = Number(p.id);
|
|
12
|
+
const out = [];
|
|
13
|
+
// 1. pipe_relations
|
|
14
|
+
for (const r of rel(p, 'pipe_relations')) {
|
|
15
|
+
for (const key of ['parent_id', 'child_id']) {
|
|
16
|
+
const id = asNum(r[key]);
|
|
17
|
+
if (id !== null && id !== self)
|
|
18
|
+
out.push({ to: id, kind: 'pipe_relation', label: String(r['name'] ?? '') });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
// 2. connector fields — the only source that finds the test pipe's two
|
|
22
|
+
// connections, and the reason this function exists.
|
|
23
|
+
for (const f of rel(p, 'fields')) {
|
|
24
|
+
if (f['type_id'] !== 'connector')
|
|
25
|
+
continue;
|
|
26
|
+
const id = asNum(f['connected_pipe_id']);
|
|
27
|
+
if (id !== null && id !== self)
|
|
28
|
+
out.push({ to: id, kind: 'connector_field', label: String(f['label'] ?? '') });
|
|
29
|
+
}
|
|
30
|
+
// 3. cross-repo automations
|
|
31
|
+
for (const a of rel(p, 'automations')) {
|
|
32
|
+
for (const key of ['event_repo_id', 'action_repo_id']) {
|
|
33
|
+
const id = asNum(a[key]);
|
|
34
|
+
if (id !== null && id !== self)
|
|
35
|
+
out.push({ to: id, kind: 'automation', label: String(a['name'] ?? '') });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Resolve a numeric repo id to what it actually is.
|
|
42
|
+
*
|
|
43
|
+
* **Neither root field discriminates.** Verified live: `pipe(id:)` and
|
|
44
|
+
* `table(id:)` both answer for *every* repo id, and both return plausible data —
|
|
45
|
+
* `pipe.phases` on a database returns its status phases, and `table.table_fields`
|
|
46
|
+
* on a pipe returns its start-form fields. A repo is one object viewed two ways.
|
|
47
|
+
* Trying `pipe` first and taking the win, as an earlier version did, classified
|
|
48
|
+
* every database as a pipe.
|
|
49
|
+
*
|
|
50
|
+
* `Table.url` is the real discriminator: a pipe lives at `/pipes/<id>` and a
|
|
51
|
+
* database at `/apollo_databases/<id>`. One query settles both the kind and
|
|
52
|
+
* PLAN.md §2.2's id duality, since `Table` carries the slug as `id` and the
|
|
53
|
+
* numeric form as `internal_id`.
|
|
54
|
+
*/
|
|
55
|
+
/**
|
|
56
|
+
* Resolve one repo id, in **either** spelling.
|
|
57
|
+
*
|
|
58
|
+
* `table(id:)` accepts a database's numeric internal id and its api id (a slug
|
|
59
|
+
* like `dzmxcuxz`) and always answers with the slug — measured both ways. So
|
|
60
|
+
* `pipe pull 302908356` and `pipe pull 3LhkZu_h` must reach the same database,
|
|
61
|
+
* and the `RepoRef` this returns normalises to the numeric id either way, which
|
|
62
|
+
* is what keeps one database from being pulled twice under two identities.
|
|
63
|
+
*/
|
|
64
|
+
export const resolveRepo = async (client, id) => {
|
|
65
|
+
const res = await client.tryRaw(`query($id: ID!) { table(id: $id) { id internal_id name uuid url } }`, { id: String(id) }, `resolve repo ${id}`);
|
|
66
|
+
const repo = res?.table;
|
|
67
|
+
if (!repo)
|
|
68
|
+
return null;
|
|
69
|
+
const url = String(repo.url ?? '');
|
|
70
|
+
const kind = /apollo_databases/.test(url) ? 'table' : 'pipe';
|
|
71
|
+
debug(`repo ${id} is a ${kind} (${url || 'no url'})`);
|
|
72
|
+
return {
|
|
73
|
+
kind,
|
|
74
|
+
id: Number(repo.internal_id ?? id),
|
|
75
|
+
/** The slug for a database, the numeric string for a pipe. */
|
|
76
|
+
apiId: kind === 'table' ? String(repo.id) : String(repo.internal_id ?? id),
|
|
77
|
+
name: repo.name,
|
|
78
|
+
uuid: repo.uuid ?? null,
|
|
79
|
+
via: [],
|
|
80
|
+
depth: 0,
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Walk outward from a root repo. `read` is injected so discovery works
|
|
85
|
+
* identically whether payloads come from snapshots or from reconstruction.
|
|
86
|
+
*/
|
|
87
|
+
export const discover = async (client, rootId, opts) => {
|
|
88
|
+
const found = new Map();
|
|
89
|
+
const edges = [];
|
|
90
|
+
const warnings = [];
|
|
91
|
+
const root = await resolveRepo(client, rootId);
|
|
92
|
+
if (!root)
|
|
93
|
+
throw new Error(`repo ${rootId} is neither a readable pipe nor a readable table`);
|
|
94
|
+
root.via = ['root'];
|
|
95
|
+
found.set(rootId, root);
|
|
96
|
+
let frontier = [root];
|
|
97
|
+
for (let depth = 0; depth <= opts.depth && frontier.length; depth++) {
|
|
98
|
+
const next = [];
|
|
99
|
+
for (const ref of frontier) {
|
|
100
|
+
if (depth === opts.depth)
|
|
101
|
+
break;
|
|
102
|
+
const payload = await opts.read(ref);
|
|
103
|
+
if (!payload) {
|
|
104
|
+
warnings.push(`${ref.kind} ${ref.id} (${ref.name ?? '?'}) could not be read — its connections are unexplored`);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
for (const e of edgesFromPayload(payload)) {
|
|
108
|
+
edges.push({ from: ref.id, to: e.to, kind: e.kind, label: e.label });
|
|
109
|
+
const seen = found.get(e.to);
|
|
110
|
+
if (seen) {
|
|
111
|
+
if (!seen.via.includes(e.kind))
|
|
112
|
+
seen.via.push(e.kind);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const resolved = await resolveRepo(client, e.to);
|
|
116
|
+
if (!resolved) {
|
|
117
|
+
warnings.push(`${ref.kind} ${ref.id} references repo ${e.to} via ${e.kind}, but the token cannot read it — ` +
|
|
118
|
+
`the pull is partial (PLAN.md §9, cross-repo permission)`);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
resolved.via = [e.kind];
|
|
122
|
+
resolved.depth = depth + 1;
|
|
123
|
+
found.set(e.to, resolved);
|
|
124
|
+
next.push(resolved);
|
|
125
|
+
debug(`discovered ${resolved.kind} ${resolved.id} "${resolved.name}" via ${e.kind}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
frontier = next;
|
|
129
|
+
}
|
|
130
|
+
const repos = [...found.values()].sort((a, b) => a.depth - b.depth || a.id - b.id);
|
|
131
|
+
step(`discovered ${repos.length} repo${repos.length === 1 ? '' : 's'} within depth ${opts.depth}`);
|
|
132
|
+
for (const w of warnings)
|
|
133
|
+
warn(w);
|
|
134
|
+
return { repos, edges, warnings };
|
|
135
|
+
};
|
|
136
|
+
/**
|
|
137
|
+
* `pipeMapGraph` is kept for the graph view and for `hasHiddenAutomations` —
|
|
138
|
+
* never as the discovery mechanism. PLAN.md §2.2 and ask 7.
|
|
139
|
+
*/
|
|
140
|
+
export const mapGraph = async (client, repoId, depth = 2) => {
|
|
141
|
+
const data = await client.tryRaw(`query($id: ID!, $depth: Int) {
|
|
142
|
+
pipeMapGraph(pipeId: $id, depth: $depth) {
|
|
143
|
+
nodes { id name __typename hasHiddenAutomations hasHiddenIntegrations }
|
|
144
|
+
edges { from to }
|
|
145
|
+
}
|
|
146
|
+
}`, { id: String(repoId), depth }, 'pipeMapGraph');
|
|
147
|
+
return data?.pipeMapGraph ?? null;
|
|
148
|
+
};
|