@enricai/barnacle 1.12.20 → 1.12.21
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/dist/scripts/recon-generate-multicall-fixture.d.ts +129 -0
- package/dist/scripts/recon-generate-multicall-fixture.d.ts.map +1 -1
- package/dist/scripts/recon-generate-multicall-fixture.js +353 -2
- package/dist/scripts/recon-generate-multicall-fixture.js.map +1 -1
- package/dist/scripts/recon-generate.d.ts +31 -27
- package/dist/scripts/recon-generate.d.ts.map +1 -1
- package/dist/scripts/recon-generate.js +326 -44
- package/dist/scripts/recon-generate.js.map +1 -1
- package/package.json +1 -1
|
@@ -1414,16 +1414,24 @@ function resolveManifestActionSequence(runRoot, captures) {
|
|
|
1414
1414
|
* JWT refresh, reference-lookup) that a browser fires incidentally. Absent
|
|
1415
1415
|
* patterns preserve the noise heuristic exactly.
|
|
1416
1416
|
*
|
|
1417
|
+
* When the flow declares a `foldReturnSpec`, a GET whose URL matches its
|
|
1418
|
+
* `endpointPattern` is admitted despite the GET drop above — the same scoped
|
|
1419
|
+
* rule `buildFoldPlanFromSpec` later uses to resolve the fold plan, so a
|
|
1420
|
+
* spec-declared GET drill-down survives to reach it instead of being
|
|
1421
|
+
* dropped before the fold pipeline ever sees it. Every other GET is still
|
|
1422
|
+
* dropped.
|
|
1423
|
+
*
|
|
1417
1424
|
* Exported for tests: this predicate decides what a generated plugin will POST
|
|
1418
1425
|
* at a live site, and it is the only gate between a browser's incidental
|
|
1419
1426
|
* chatter and the emitted hot path.
|
|
1420
1427
|
*/
|
|
1421
|
-
function extractActionSequence(captures, submitPatterns = null) {
|
|
1428
|
+
function extractActionSequence(captures, submitPatterns = null, foldReturnSpec = null) {
|
|
1422
1429
|
const matchesSubmit = compileSubmitMatcher(submitPatterns);
|
|
1430
|
+
const matchesFoldReturn = compileFoldReturnEndpointMatcher(foldReturnSpec);
|
|
1423
1431
|
return captures
|
|
1424
1432
|
.map((capture, index) => ({ capture, index }))
|
|
1425
1433
|
.filter(({ capture }) => {
|
|
1426
|
-
if (capture.method === "GET")
|
|
1434
|
+
if (capture.method === "GET" && !matchesFoldReturn(capture))
|
|
1427
1435
|
return false;
|
|
1428
1436
|
if (capture.status < 200 || capture.status >= 300)
|
|
1429
1437
|
return false;
|
|
@@ -1445,11 +1453,23 @@ function extractActionSequence(captures, submitPatterns = null) {
|
|
|
1445
1453
|
* what let a chronologically-first fallback pick an unrelated query. Host is
|
|
1446
1454
|
* NOT a filter criterion, matching {@link extractActionSequence}.
|
|
1447
1455
|
*
|
|
1456
|
+
* When the flow declares a `foldReturnSpec`, a non-mutation capture whose
|
|
1457
|
+
* URL matches its `endpointPattern` is admitted despite the query drop
|
|
1458
|
+
* above, mirroring {@link extractActionSequence}'s GET admission. So is a
|
|
1459
|
+
* non-mutation capture whose response resolves the spec's own `resultsPath`
|
|
1460
|
+
* — the GraphQL-primary read op the drill-down folds onto, which
|
|
1461
|
+
* `endpointPattern` (naming the drill, not the primary) never matches on its
|
|
1462
|
+
* own; without this a declared spec would admit the drill-down but leave
|
|
1463
|
+
* `resolveFoldPlan` with no primary capture to resolve `resultsPath`
|
|
1464
|
+
* against. Every other non-mutation capture is still dropped.
|
|
1465
|
+
*
|
|
1448
1466
|
* Exported for tests: this predicate decides what a generated GraphQL plugin
|
|
1449
1467
|
* will send at a live site.
|
|
1450
1468
|
*/
|
|
1451
|
-
function extractGraphQLActionSequence(captures, submitPatterns = null) {
|
|
1469
|
+
function extractGraphQLActionSequence(captures, submitPatterns = null, foldReturnSpec = null) {
|
|
1452
1470
|
const matchesSubmit = compileSubmitMatcher(submitPatterns);
|
|
1471
|
+
const matchesFoldReturn = compileFoldReturnEndpointMatcher(foldReturnSpec);
|
|
1472
|
+
const matchesFoldReturnResults = compileFoldReturnResultsMatcher(foldReturnSpec);
|
|
1453
1473
|
return captures
|
|
1454
1474
|
.map((capture, index) => ({ capture, index }))
|
|
1455
1475
|
.filter(({ capture }) => {
|
|
@@ -1459,7 +1479,9 @@ function extractGraphQLActionSequence(captures, submitPatterns = null) {
|
|
|
1459
1479
|
return false;
|
|
1460
1480
|
if (!matchesSubmit(capture))
|
|
1461
1481
|
return false;
|
|
1462
|
-
|
|
1482
|
+
if (capture.query !== null && /^\s*mutation\b/.test(capture.query))
|
|
1483
|
+
return true;
|
|
1484
|
+
return matchesFoldReturn(capture) || matchesFoldReturnResults(capture);
|
|
1463
1485
|
});
|
|
1464
1486
|
}
|
|
1465
1487
|
/**
|
|
@@ -1530,8 +1552,10 @@ function jsonBodyLeafValues(requestPostData) {
|
|
|
1530
1552
|
if (parsed === undefined)
|
|
1531
1553
|
return null;
|
|
1532
1554
|
const values = [];
|
|
1533
|
-
for (const { value } of
|
|
1534
|
-
|
|
1555
|
+
for (const { value } of walkAllPrimitiveLeaves(parsed)) {
|
|
1556
|
+
if (value !== null)
|
|
1557
|
+
values.push(String(value));
|
|
1558
|
+
}
|
|
1535
1559
|
return values;
|
|
1536
1560
|
}
|
|
1537
1561
|
/**
|
|
@@ -2622,11 +2646,29 @@ function* walkSetCookiePairs(rawSetCookie) {
|
|
|
2622
2646
|
* Exception: values in `PLACEHOLDER_STATE_VALUES` are skipped entirely so
|
|
2623
2647
|
* the LATER non-placeholder occurrence at the same JSON path becomes the
|
|
2624
2648
|
* canonical binding instead.
|
|
2649
|
+
*
|
|
2650
|
+
* `forceIncludeValues` (see {@link collectDependentDrillDownChainValues})
|
|
2651
|
+
* bypasses `MIN_STATE_VALUE_LENGTH` for the specific values it names — a
|
|
2652
|
+
* value already confirmed, by the fold-chain detector itself, to be threaded
|
|
2653
|
+
* from one dependent-drill-down chain hop's response into the next hop's
|
|
2654
|
+
* request is exactly as legitimate a produced state value as a long one; a
|
|
2655
|
+
* length floor exists to keep an UNRELATED short value (an enum code, a page
|
|
2656
|
+
* number) from being mistaken for reused state by blind substring/value
|
|
2657
|
+
* matching, and a value the chain detector already confirmed is threaded
|
|
2658
|
+
* carries no such ambiguity. Every other filter (MAX length, placeholder,
|
|
2659
|
+
* shielded UUID, GET-non-UUID) still applies.
|
|
2625
2660
|
*/
|
|
2626
2661
|
/** Exported for unit testing — lets tests exercise the produces[] walk (body
|
|
2627
2662
|
* AND header/cookie origins) directly against synthetic Capture sequences. */
|
|
2628
|
-
function indexStateValues(captures, shieldedUuids = new Set(), actionCaptureIndices = new Set()) {
|
|
2663
|
+
function indexStateValues(captures, shieldedUuids = new Set(), actionCaptureIndices = new Set(), forceIncludeValues = new Set()) {
|
|
2629
2664
|
const index = new Map();
|
|
2665
|
+
// Computed structurally off the SAME captures being indexed (no
|
|
2666
|
+
// foldReturnSpec available at this layer) — a spec-declared fold's own
|
|
2667
|
+
// chain values reach here via the caller-supplied `forceIncludeValues`
|
|
2668
|
+
// (see recon-generate's top-level `collectDependentDrillDownChainValues`
|
|
2669
|
+
// call), so this indexes a chain-produced value regardless of whether the
|
|
2670
|
+
// fold plan that confirmed it is structural or spec-declared.
|
|
2671
|
+
const chainForceIncludeValues = collectDependentDrillDownChainValues(captures.map((capture) => ({ capture })), null);
|
|
2630
2672
|
// First pass: identify the earliest origin among ACTION captures for each
|
|
2631
2673
|
// value. Action-only earliest-origin tracking is what compileActionSteps'
|
|
2632
2674
|
// produces[] check needs — it ignores non-action captures (telemetry GETs,
|
|
@@ -2645,7 +2687,13 @@ function indexStateValues(captures, shieldedUuids = new Set(), actionCaptureIndi
|
|
|
2645
2687
|
const rawSetCookie = Object.entries(c.responseHeaders).find(([k]) => k.toLowerCase() === "set-cookie")?.[1];
|
|
2646
2688
|
if (rawSetCookie !== undefined) {
|
|
2647
2689
|
for (const { name, value } of walkSetCookiePairs(rawSetCookie)) {
|
|
2648
|
-
|
|
2690
|
+
// Same chain/force exemption as the body-value MIN_STATE_VALUE_LENGTH
|
|
2691
|
+
// floor below: a cookie-sourced value the fold-chain detector already
|
|
2692
|
+
// confirmed is threaded into a later hop's request is exactly as
|
|
2693
|
+
// legitimate as a long one, so it must not be dropped for being short.
|
|
2694
|
+
if (value.length < MIN_STATE_VALUE_LENGTH &&
|
|
2695
|
+
!chainForceIncludeValues.has(value) &&
|
|
2696
|
+
!forceIncludeValues.has(value))
|
|
2649
2697
|
continue;
|
|
2650
2698
|
if (value.length > MAX_COOKIE_STATE_VALUE_LENGTH)
|
|
2651
2699
|
continue;
|
|
@@ -2661,6 +2709,31 @@ function indexStateValues(captures, shieldedUuids = new Set(), actionCaptureIndi
|
|
|
2661
2709
|
}
|
|
2662
2710
|
}
|
|
2663
2711
|
}
|
|
2712
|
+
// Non-cookie response headers are only indexed for values the fold-chain
|
|
2713
|
+
// detector already confirmed are threaded from this hop's response into a
|
|
2714
|
+
// later hop's request (`chainForceIncludeValues`) — unlike Set-Cookie,
|
|
2715
|
+
// which is always a plausible token mint, an arbitrary header (e.g.
|
|
2716
|
+
// `X-Conversation-Id`) is indexed as producible state only when chain
|
|
2717
|
+
// detection itself has already established that reuse, so this never
|
|
2718
|
+
// sweeps every header value as noise.
|
|
2719
|
+
for (const [headerName, headerValue] of Object.entries(c.responseHeaders)) {
|
|
2720
|
+
if (headerName.toLowerCase() === "set-cookie")
|
|
2721
|
+
continue;
|
|
2722
|
+
if (!chainForceIncludeValues.has(headerValue))
|
|
2723
|
+
continue;
|
|
2724
|
+
if (headerValue.length > MAX_COOKIE_STATE_VALUE_LENGTH)
|
|
2725
|
+
continue;
|
|
2726
|
+
if (PLACEHOLDER_STATE_VALUES.has(headerValue))
|
|
2727
|
+
continue;
|
|
2728
|
+
if (!index.has(headerValue)) {
|
|
2729
|
+
index.set(headerValue, {
|
|
2730
|
+
value: headerValue,
|
|
2731
|
+
originIndex: i,
|
|
2732
|
+
path: [],
|
|
2733
|
+
headerOrigin: { sourceHeader: headerName },
|
|
2734
|
+
});
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2664
2737
|
if (c.responseBody === undefined || c.responseBody === null)
|
|
2665
2738
|
continue;
|
|
2666
2739
|
// For GET captures, only index UUID-shaped strings. GET captures (today,
|
|
@@ -2671,8 +2744,13 @@ function indexStateValues(captures, shieldedUuids = new Set(), actionCaptureIndi
|
|
|
2671
2744
|
// e.g. "candidate" as a state value gets substituted INSIDE an already-
|
|
2672
2745
|
// emitted ${candidateId} interpolation, producing ${${entityTypeCode}Id}.
|
|
2673
2746
|
const isGet = c.method === "GET";
|
|
2674
|
-
for (const { value, path } of
|
|
2675
|
-
if (
|
|
2747
|
+
for (const { value: rawValue, path } of walkAllPrimitiveLeaves(c.responseBody)) {
|
|
2748
|
+
if (rawValue === null)
|
|
2749
|
+
continue;
|
|
2750
|
+
const value = String(rawValue);
|
|
2751
|
+
if (value.length < MIN_STATE_VALUE_LENGTH &&
|
|
2752
|
+
!chainForceIncludeValues.has(value) &&
|
|
2753
|
+
!forceIncludeValues.has(value))
|
|
2676
2754
|
continue;
|
|
2677
2755
|
if (value.length > MAX_STATE_VALUE_LENGTH)
|
|
2678
2756
|
continue;
|
|
@@ -2685,7 +2763,16 @@ function indexStateValues(captures, shieldedUuids = new Set(), actionCaptureIndi
|
|
|
2685
2763
|
// corrupt T2/T3's already-substituted Values.
|
|
2686
2764
|
if (shieldedUuids.has(value))
|
|
2687
2765
|
continue;
|
|
2688
|
-
|
|
2766
|
+
// Same chain/force exemption as the MIN_STATE_VALUE_LENGTH floor above:
|
|
2767
|
+
// a value the fold-chain detector already confirmed is threaded from
|
|
2768
|
+
// this GET hop's response into a later hop's request is exactly as
|
|
2769
|
+
// legitimate as a UUID anchor, so it must not be dropped just because
|
|
2770
|
+
// this hop happens to be a GET rather than every existing fixture's
|
|
2771
|
+
// POST.
|
|
2772
|
+
if (isGet &&
|
|
2773
|
+
!UUID_REGEX.test(value) &&
|
|
2774
|
+
!chainForceIncludeValues.has(value) &&
|
|
2775
|
+
!forceIncludeValues.has(value))
|
|
2689
2776
|
continue;
|
|
2690
2777
|
if (!index.has(value)) {
|
|
2691
2778
|
index.set(value, { value, originIndex: i, path });
|
|
@@ -2891,8 +2978,48 @@ function compileActionSteps(actions, stateIndex) {
|
|
|
2891
2978
|
});
|
|
2892
2979
|
}
|
|
2893
2980
|
}
|
|
2981
|
+
// Non-cookie response-header-origin produces — mirrors the Set-Cookie
|
|
2982
|
+
// block above but for a plain header (e.g. `X-Price-Token`) whose value
|
|
2983
|
+
// `indexStateValues` indexed with `headerOrigin.sourceHeader` set to the
|
|
2984
|
+
// real header name. Only emitted when the value is actually consumed as
|
|
2985
|
+
// a REQUEST HEADER downstream (`usedValueTargetHeader`) — `createHttpClient`'s
|
|
2986
|
+
// `bind` option (see http-client.ts) is the only mechanism that can
|
|
2987
|
+
// thread a header-origin value forward, since the emitted response
|
|
2988
|
+
// variable never exposes response headers to the rest of the generated
|
|
2989
|
+
// code the way it exposes the parsed body.
|
|
2990
|
+
for (const [headerName, headerValue] of Object.entries(capture.responseHeaders)) {
|
|
2991
|
+
if (headerName.toLowerCase() === "set-cookie")
|
|
2992
|
+
continue;
|
|
2993
|
+
if (!usedValues.has(headerValue))
|
|
2994
|
+
continue;
|
|
2995
|
+
const sv = stateIndex.get(headerValue);
|
|
2996
|
+
if (!sv || sv.originIndex !== index || !sv.headerOrigin)
|
|
2997
|
+
continue;
|
|
2998
|
+
const targetHeader = usedValueTargetHeader.get(headerValue);
|
|
2999
|
+
if (!targetHeader)
|
|
3000
|
+
continue;
|
|
3001
|
+
let name = `${headerName.replace(/[^A-Za-z0-9]/g, "")}Header`;
|
|
3002
|
+
if (!/^[A-Za-z_$]/.test(name))
|
|
3003
|
+
name = `_${name}`;
|
|
3004
|
+
let suffix = 1;
|
|
3005
|
+
while (seenNames.has(name)) {
|
|
3006
|
+
suffix++;
|
|
3007
|
+
name = `${headerName.replace(/[^A-Za-z0-9]/g, "")}Header${suffix}`;
|
|
3008
|
+
}
|
|
3009
|
+
seenNames.add(name);
|
|
3010
|
+
produces.push({
|
|
3011
|
+
kind: "header",
|
|
3012
|
+
name,
|
|
3013
|
+
sourceHeader: sv.headerOrigin.sourceHeader,
|
|
3014
|
+
cookieName: sv.headerOrigin.cookieName,
|
|
3015
|
+
targetHeader,
|
|
3016
|
+
});
|
|
3017
|
+
}
|
|
2894
3018
|
if (capture.responseBody !== undefined && capture.responseBody !== null) {
|
|
2895
|
-
for (const { value, path } of
|
|
3019
|
+
for (const { value: rawValue, path } of walkAllPrimitiveLeaves(capture.responseBody)) {
|
|
3020
|
+
if (rawValue === null)
|
|
3021
|
+
continue;
|
|
3022
|
+
const value = String(rawValue);
|
|
2896
3023
|
if (!usedValues.has(value))
|
|
2897
3024
|
continue;
|
|
2898
3025
|
const sv = stateIndex.get(value);
|
|
@@ -2977,7 +3104,9 @@ function resolveResponsePathValue(responseBody, path) {
|
|
|
2977
3104
|
return null;
|
|
2978
3105
|
}
|
|
2979
3106
|
}
|
|
2980
|
-
return typeof cursor === "string"
|
|
3107
|
+
return typeof cursor === "string" || typeof cursor === "number" || typeof cursor === "boolean"
|
|
3108
|
+
? String(cursor)
|
|
3109
|
+
: null;
|
|
2981
3110
|
}
|
|
2982
3111
|
/**
|
|
2983
3112
|
* Replaces occurrences of state values in `template` with `${varName}`
|
|
@@ -3738,7 +3867,8 @@ function emitMultiStepExecuteHttp(actions, inputBody, errorSignals, fieldNameMap
|
|
|
3738
3867
|
const matchesJoinField = target.joinFields.some((field) => {
|
|
3739
3868
|
const value = readValueAtPath(firstItem, field.split("."));
|
|
3740
3869
|
return ((typeof value === "string" && value.length > 0 && value === headerValue) ||
|
|
3741
|
-
(typeof value === "number"
|
|
3870
|
+
((typeof value === "number" || typeof value === "boolean") &&
|
|
3871
|
+
String(value) === headerValue));
|
|
3742
3872
|
});
|
|
3743
3873
|
if (matchesJoinField)
|
|
3744
3874
|
headerNames.add(headerName);
|
|
@@ -3750,7 +3880,7 @@ function emitMultiStepExecuteHttp(actions, inputBody, errorSignals, fieldNameMap
|
|
|
3750
3880
|
const value = readValueAtPath(firstItem, field.split("."));
|
|
3751
3881
|
if (typeof value === "string" && value.length > 0)
|
|
3752
3882
|
joinValues.add(value);
|
|
3753
|
-
else if (typeof value === "number")
|
|
3883
|
+
else if (typeof value === "number" || typeof value === "boolean")
|
|
3754
3884
|
joinValues.add(String(value));
|
|
3755
3885
|
}
|
|
3756
3886
|
if (joinValues.size > 0)
|
|
@@ -4056,7 +4186,7 @@ function emitMultiStepExecuteHttp(actions, inputBody, errorSignals, fieldNameMap
|
|
|
4056
4186
|
const value = readValueAtPath(firstItem, field.split("."));
|
|
4057
4187
|
const stringValue = typeof value === "string" && value.length > 0
|
|
4058
4188
|
? value
|
|
4059
|
-
: typeof value === "number"
|
|
4189
|
+
: typeof value === "number" || typeof value === "boolean"
|
|
4060
4190
|
? String(value)
|
|
4061
4191
|
: null;
|
|
4062
4192
|
return stringValue !== null
|
|
@@ -4539,14 +4669,14 @@ function collectRequestStringValues(capture) {
|
|
|
4539
4669
|
})();
|
|
4540
4670
|
if (parsedBody !== undefined) {
|
|
4541
4671
|
for (const { value } of walkAllPrimitiveLeaves(parsedBody)) {
|
|
4542
|
-
if (typeof value === "number")
|
|
4672
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
4543
4673
|
values.add(String(value));
|
|
4544
4674
|
}
|
|
4545
4675
|
}
|
|
4546
4676
|
return values;
|
|
4547
4677
|
}
|
|
4548
4678
|
/**
|
|
4549
|
-
* Yields every string/numeric leaf reachable from `item` by walking nested
|
|
4679
|
+
* Yields every string/numeric/boolean leaf reachable from `item` by walking nested
|
|
4550
4680
|
* plain objects only (not arrays — a join key is a scalar field of the item
|
|
4551
4681
|
* or one of its nested objects, never an element drawn from a nested array),
|
|
4552
4682
|
* paired with its dot-separated path from `item`'s root. A bare top-level
|
|
@@ -4565,8 +4695,8 @@ function* walkItemFieldPaths(item, path = []) {
|
|
|
4565
4695
|
}
|
|
4566
4696
|
}
|
|
4567
4697
|
/**
|
|
4568
|
-
* Finds the ordered list of an array item's string/numeric field
|
|
4569
|
-
* values are threaded into `drillCapture`'s outbound request — the join key a
|
|
4698
|
+
* Finds the ordered list of an array item's string/numeric/boolean field
|
|
4699
|
+
* paths whose values are threaded into `drillCapture`'s outbound request — the join key a
|
|
4570
4700
|
* dependent drill-down call was built from. Each entry is a dot-separated
|
|
4571
4701
|
* path (see {@link readValueAtPath} / {@link pathToAccessor}), so a bare
|
|
4572
4702
|
* top-level field stays a single segment (e.g. `"sku"`) and a field nested
|
|
@@ -4582,23 +4712,31 @@ function findThreadedJoinFields(item, drillCapture) {
|
|
|
4582
4712
|
return [];
|
|
4583
4713
|
return [...walkItemFieldPaths(item)]
|
|
4584
4714
|
.filter(({ value: v }) => (typeof v === "string" && v.length > 0 && requestValues.has(v)) ||
|
|
4585
|
-
(typeof v === "number" && requestValues.has(String(v)))
|
|
4715
|
+
(typeof v === "number" && requestValues.has(String(v))) ||
|
|
4716
|
+
(typeof v === "boolean" && requestValues.has(String(v))))
|
|
4586
4717
|
.map(({ path }) => path.join("."));
|
|
4587
4718
|
}
|
|
4588
|
-
/** Every string and
|
|
4719
|
+
/** Every string, numeric, and boolean leaf value present anywhere in a response —
|
|
4589
4720
|
* the set a chained drill-down step's request must overlap with for that
|
|
4590
4721
|
* step to count as depending on this response. Deliberately walks the WHOLE
|
|
4591
4722
|
* body (not just object-array items, unlike {@link findThreadedJoinFields})
|
|
4592
4723
|
* since a chained step can thread any response value, not only a per-item
|
|
4593
|
-
* join field.
|
|
4594
|
-
|
|
4724
|
+
* join field. Also walks every response HEADER value, mirroring
|
|
4725
|
+
* {@link collectRequestValuesIncludingHeaders} on the request side, since a
|
|
4726
|
+
* chain hop can just as easily mint its join token in a response header
|
|
4727
|
+
* (e.g. a `Location` or custom correlation header) as in the body. */
|
|
4728
|
+
function collectResponseLeafValues(capture) {
|
|
4595
4729
|
const values = new Set();
|
|
4596
|
-
for (const { value } of walkAllPrimitiveLeaves(responseBody)) {
|
|
4730
|
+
for (const { value } of walkAllPrimitiveLeaves(capture.responseBody)) {
|
|
4597
4731
|
if (typeof value === "string" && value.length > 0)
|
|
4598
4732
|
values.add(value);
|
|
4599
4733
|
if (typeof value === "number")
|
|
4600
4734
|
values.add(String(value));
|
|
4735
|
+
if (typeof value === "boolean")
|
|
4736
|
+
values.add(String(value));
|
|
4601
4737
|
}
|
|
4738
|
+
for (const v of Object.values(capture.responseHeaders))
|
|
4739
|
+
values.add(v);
|
|
4602
4740
|
return values;
|
|
4603
4741
|
}
|
|
4604
4742
|
/**
|
|
@@ -4657,7 +4795,7 @@ function computeFoldChain(actions, drillStepIndex, drillArrayPath) {
|
|
|
4657
4795
|
const requestValues = collectRequestValuesIncludingHeaders(candidate.capture);
|
|
4658
4796
|
const dependsOnChain = chain.some((chainIndex) => {
|
|
4659
4797
|
const chainStepCapture = actions[chainIndex].capture;
|
|
4660
|
-
const responseValues = collectResponseLeafValues(chainStepCapture
|
|
4798
|
+
const responseValues = collectResponseLeafValues(chainStepCapture);
|
|
4661
4799
|
const echoedValues = collectRequestValuesIncludingHeaders(chainStepCapture);
|
|
4662
4800
|
return [...responseValues].some((v) => !echoedValues.has(v) && requestValues.has(v));
|
|
4663
4801
|
});
|
|
@@ -4725,7 +4863,7 @@ function directPrimitiveChildCountExcludingEchoed(obj, requestValues) {
|
|
|
4725
4863
|
let n = 0;
|
|
4726
4864
|
for (const v of Object.values(obj)) {
|
|
4727
4865
|
if (v === null || (typeof v !== "object" && typeof v !== "function")) {
|
|
4728
|
-
if (typeof v === "string" || typeof v === "number") {
|
|
4866
|
+
if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
|
|
4729
4867
|
if (requestValues.has(String(v)))
|
|
4730
4868
|
continue;
|
|
4731
4869
|
}
|
|
@@ -5051,12 +5189,52 @@ function resolveSpecMatchedPrimaryItemIndex(primaryItems, joinFields, drillCaptu
|
|
|
5051
5189
|
const matchedIndex = primaryItems.findIndex((item) => joinFields.every((field) => {
|
|
5052
5190
|
const value = readValueAtPath(item, field.split("."));
|
|
5053
5191
|
return ((typeof value === "string" && value.length > 0 && requestValues.has(value)) ||
|
|
5054
|
-
(typeof value === "number" && requestValues.has(String(value)))
|
|
5192
|
+
(typeof value === "number" && requestValues.has(String(value))) ||
|
|
5193
|
+
(typeof value === "boolean" && requestValues.has(String(value))));
|
|
5055
5194
|
}));
|
|
5056
5195
|
return matchedIndex === -1 ? null : matchedIndex;
|
|
5057
5196
|
}
|
|
5058
|
-
|
|
5059
|
-
|
|
5197
|
+
/** Like {@link resolveSpecMatchedPrimaryItemIndex}, but also looks upstream of
|
|
5198
|
+
* `drillStepIndex` for the join key when `drillStepIndex`'s own request
|
|
5199
|
+
* doesn't carry it — a `foldReturn` spec's `endpointPattern` naturally names
|
|
5200
|
+
* the chain TERMINAL (the response actually holding the data an author wants
|
|
5201
|
+
* folded), not the opaque entry hop that carries the join key onward (e.g.
|
|
5202
|
+
* as a header-threaded token). Walks every earlier step back to
|
|
5203
|
+
* `primaryStepIndex`, in reverse so the closest (least ambiguous) candidate
|
|
5204
|
+
* wins first, and accepts one only once {@link computeFoldChain} confirms it
|
|
5205
|
+
* actually chains FORWARD to `drillStepIndex` — otherwise an unrelated
|
|
5206
|
+
* earlier step matching the join key by coincidence could hijack the fold.
|
|
5207
|
+
* Returns the resolved `entryIndex` alongside the matched item index so the
|
|
5208
|
+
* caller can build the fold's `chain` starting from the step that ACTUALLY
|
|
5209
|
+
* carries the join key, not from `drillStepIndex` — a chain built from
|
|
5210
|
+
* `drillStepIndex` alone would never include this upstream entry hop, so it
|
|
5211
|
+
* would never be re-executed (header-parameterized) per primary item at
|
|
5212
|
+
* runtime. */
|
|
5213
|
+
function resolveSpecMatchedPrimaryItemIndexAlongChain(actions, primaryItems, joinFields, primaryStepIndex, drillStepIndex) {
|
|
5214
|
+
for (let entryIndex = drillStepIndex; entryIndex > primaryStepIndex; entryIndex--) {
|
|
5215
|
+
const matched = resolveSpecMatchedPrimaryItemIndex(primaryItems, joinFields, actions[entryIndex].capture);
|
|
5216
|
+
if (matched === null)
|
|
5217
|
+
continue;
|
|
5218
|
+
if (entryIndex === drillStepIndex)
|
|
5219
|
+
return { entryIndex, primaryMatchedItemIndex: matched };
|
|
5220
|
+
const { chain } = computeFoldChain(actions, entryIndex, []);
|
|
5221
|
+
if (chain.includes(drillStepIndex))
|
|
5222
|
+
return { entryIndex, primaryMatchedItemIndex: matched };
|
|
5223
|
+
}
|
|
5224
|
+
return null;
|
|
5225
|
+
}
|
|
5226
|
+
/**
|
|
5227
|
+
* Compiles a flow-declared {@link FoldReturnSpec.endpointPattern} into a
|
|
5228
|
+
* capture predicate, or a predicate that always returns `false` when `spec`
|
|
5229
|
+
* is `null` or its pattern isn't a valid regex — the same null-safe
|
|
5230
|
+
* try/catch shape {@link buildFoldPlanFromSpec} already applied inline,
|
|
5231
|
+
* shared here so the action-sequence extractors can admit a spec-matched
|
|
5232
|
+
* drill-down capture under the identical rule that later resolves its fold
|
|
5233
|
+
* plan, instead of dropping it before the fold pipeline ever sees it.
|
|
5234
|
+
*/
|
|
5235
|
+
function compileFoldReturnEndpointMatcher(spec) {
|
|
5236
|
+
if (spec === null)
|
|
5237
|
+
return () => false;
|
|
5060
5238
|
const endpointRx = (() => {
|
|
5061
5239
|
try {
|
|
5062
5240
|
return new RegExp(spec.endpointPattern);
|
|
@@ -5066,7 +5244,31 @@ function buildFoldPlanFromSpec(actions, spec) {
|
|
|
5066
5244
|
}
|
|
5067
5245
|
})();
|
|
5068
5246
|
if (endpointRx === null)
|
|
5069
|
-
return
|
|
5247
|
+
return () => false;
|
|
5248
|
+
return (capture) => endpointRx.test(capture.url);
|
|
5249
|
+
}
|
|
5250
|
+
/**
|
|
5251
|
+
* A capture predicate matching whichever capture actually holds
|
|
5252
|
+
* {@link FoldReturnSpec.resultsPath}'s object array — the flow's own PRIMARY
|
|
5253
|
+
* results source, as opposed to {@link compileFoldReturnEndpointMatcher}'s
|
|
5254
|
+
* `endpointPattern` match (the drill-down). REST's `extractActionSequence`
|
|
5255
|
+
* never needs this: a REST primary is a POST/non-GET and is admitted
|
|
5256
|
+
* unconditionally regardless of `foldReturnSpec`. GraphQL's primary is
|
|
5257
|
+
* always a `query`, and `extractGraphQLActionSequence` drops every
|
|
5258
|
+
* non-mutation capture by default, so without this predicate a declared
|
|
5259
|
+
* `foldReturnSpec` would admit only the drill-down capture and never the
|
|
5260
|
+
* read op whose response the drill-down folds onto — leaving
|
|
5261
|
+
* `buildFoldPlanFromSpec` with no `primaryStepIndex` to resolve against.
|
|
5262
|
+
*/
|
|
5263
|
+
function compileFoldReturnResultsMatcher(spec) {
|
|
5264
|
+
if (spec === null)
|
|
5265
|
+
return () => false;
|
|
5266
|
+
const resultsPath = spec.resultsPath.split(".");
|
|
5267
|
+
return (capture) => objectItemsAtPath(capture.responseBody, resultsPath) !== null;
|
|
5268
|
+
}
|
|
5269
|
+
function buildFoldPlanFromSpec(actions, spec) {
|
|
5270
|
+
const primaryArrayPath = spec.resultsPath.split(".");
|
|
5271
|
+
const matchesFoldReturnEndpoint = compileFoldReturnEndpointMatcher(spec);
|
|
5070
5272
|
let freshestPlan = null;
|
|
5071
5273
|
for (let primaryStepIndex = 0; primaryStepIndex < actions.length; primaryStepIndex++) {
|
|
5072
5274
|
const primaryItems = objectItemsAtPath(actions[primaryStepIndex].capture.responseBody, primaryArrayPath);
|
|
@@ -5074,7 +5276,7 @@ function buildFoldPlanFromSpec(actions, spec) {
|
|
|
5074
5276
|
continue;
|
|
5075
5277
|
for (let drillStepIndex = primaryStepIndex + 1; drillStepIndex < actions.length; drillStepIndex++) {
|
|
5076
5278
|
const drill = actions[drillStepIndex];
|
|
5077
|
-
if (!
|
|
5279
|
+
if (!matchesFoldReturnEndpoint(drill.capture))
|
|
5078
5280
|
continue;
|
|
5079
5281
|
// Widened to a flat (non-array) object response the same way the
|
|
5080
5282
|
// structural heuristic is (see findAllObjectArrayFieldsOrWholeObject):
|
|
@@ -5090,17 +5292,31 @@ function buildFoldPlanFromSpec(actions, spec) {
|
|
|
5090
5292
|
// now does (see detectDrillDownFoldPlan). Validated after the chain
|
|
5091
5293
|
// resolves, below, since `[]` is a valid empty baseline for
|
|
5092
5294
|
// computeFoldChain but not a valid final drillArrayPath on its own.
|
|
5295
|
+
const matchResult = resolveSpecMatchedPrimaryItemIndexAlongChain(actions, primaryItems, spec.joinFields, primaryStepIndex, drillStepIndex);
|
|
5296
|
+
if (matchResult === null)
|
|
5297
|
+
continue;
|
|
5298
|
+
const { entryIndex, primaryMatchedItemIndex } = matchResult;
|
|
5299
|
+
// Rooted at `entryIndex` — the step whose request ACTUALLY carries the
|
|
5300
|
+
// join key — not `drillStepIndex`, so an upstream entry hop the join
|
|
5301
|
+
// key was only resolvable through (e.g. a header-threaded token) is
|
|
5302
|
+
// itself part of `chain` and gets re-executed (join-parameterized) per
|
|
5303
|
+
// primary item at runtime, same as every structurally-detected chain.
|
|
5304
|
+
// `drillResultsPath`, when given, still targets `drillStepIndex`'s own
|
|
5305
|
+
// response specifically (the endpoint the spec names); it is otherwise
|
|
5306
|
+
// left for computeFoldChain's own forward richness walk to resolve,
|
|
5307
|
+
// exactly as it does for every step beyond the chain's entry.
|
|
5093
5308
|
const drillArrayPath = (() => {
|
|
5309
|
+
if (entryIndex !== drillStepIndex) {
|
|
5310
|
+
return (findObjectArrayFieldOrWholeObject(actions[entryIndex].capture.responseBody)?.path ??
|
|
5311
|
+
null);
|
|
5312
|
+
}
|
|
5094
5313
|
if (spec.drillResultsPath === undefined) {
|
|
5095
5314
|
return findObjectArrayFieldOrWholeObject(drill.capture.responseBody)?.path ?? null;
|
|
5096
5315
|
}
|
|
5097
5316
|
const path = spec.drillResultsPath.split(".");
|
|
5098
5317
|
return objectItemsAtPath(drill.capture.responseBody, path) ? path : null;
|
|
5099
5318
|
})();
|
|
5100
|
-
const
|
|
5101
|
-
if (primaryMatchedItemIndex === null)
|
|
5102
|
-
continue;
|
|
5103
|
-
const { chain, chainArrayPath, chainTerminalIndex } = computeFoldChain(actions, drillStepIndex, drillArrayPath ?? []);
|
|
5319
|
+
const { chain, chainArrayPath, chainTerminalIndex } = computeFoldChain(actions, entryIndex, drillArrayPath ?? []);
|
|
5104
5320
|
// The chain's resolved terminal must actually hold foldable data —
|
|
5105
5321
|
// an intermediate drill step with neither its own candidate NOR a
|
|
5106
5322
|
// later chained step that resolves one has nothing to merge, so it
|
|
@@ -5114,7 +5330,7 @@ function buildFoldPlanFromSpec(actions, spec) {
|
|
|
5114
5330
|
targets: [
|
|
5115
5331
|
{
|
|
5116
5332
|
joinFields: spec.joinFields,
|
|
5117
|
-
drillStepIndex,
|
|
5333
|
+
drillStepIndex: entryIndex,
|
|
5118
5334
|
drillArrayPath: drillArrayPath ?? [],
|
|
5119
5335
|
primaryMatchedItemIndex,
|
|
5120
5336
|
chain,
|
|
@@ -5207,6 +5423,62 @@ function mergeSpecPlanOntoSamePrimary(structuralPlans, actions, foldReturnSpec)
|
|
|
5207
5423
|
* plan that survives multipart disqualification, letting every downstream
|
|
5208
5424
|
* emitter/shape-inference caller fold each of them.
|
|
5209
5425
|
*/
|
|
5426
|
+
/**
|
|
5427
|
+
* Every value ACTUALLY threaded from one dependent-drill-down chain hop's
|
|
5428
|
+
* response into a LATER chain hop's own request/headers — the same overlap
|
|
5429
|
+
* `computeFoldChain`'s `dependsOnChain` check already computes to decide a
|
|
5430
|
+
* step belongs in the chain at all — across every fold target
|
|
5431
|
+
* `detectDrillDownFoldPlan` / `buildFoldPlanFromSpec` resolve. These are the
|
|
5432
|
+
* values `indexStateValues` must index as producible state regardless of
|
|
5433
|
+
* `MIN_STATE_VALUE_LENGTH`, so `compileActionSteps` can thread a
|
|
5434
|
+
* chain-produced join value into the next hop's request even when that
|
|
5435
|
+
* value is a short one (e.g. a bare numeric status token).
|
|
5436
|
+
*
|
|
5437
|
+
* Deliberately NOT "every leaf value on a chain hop's response" — a chain
|
|
5438
|
+
* terminal's response commonly ECHOES the primary item's own join field
|
|
5439
|
+
* back (e.g. `sku` on the folded record), and that echoed value is a
|
|
5440
|
+
* PRIMARY-ITEM field the fold loop's own per-item render already threads
|
|
5441
|
+
* literally (see {@link findThreadedJoinFields}'s docstring); sweeping it
|
|
5442
|
+
* into state-threading's single-earliest-origin index would collapse every
|
|
5443
|
+
* item's distinct value onto whichever item's capture was indexed first.
|
|
5444
|
+
* Restricting to values that themselves reappear in a STRICTLY LATER chain
|
|
5445
|
+
* hop's own request — real cross-call threading, not an echo sitting still
|
|
5446
|
+
* in one response — keeps this exact to the shape state-threading is
|
|
5447
|
+
* actually needed for.
|
|
5448
|
+
*
|
|
5449
|
+
* Runs directly off raw actions (not `resolveFoldPlan`, which needs
|
|
5450
|
+
* `isMultipart` — unavailable before `compileActionSteps` has run) since
|
|
5451
|
+
* fold-plan DETECTION depends only on each action's `capture`.
|
|
5452
|
+
*/
|
|
5453
|
+
function collectDependentDrillDownChainValues(actions, foldReturnSpec) {
|
|
5454
|
+
const structuralPlans = detectDrillDownFoldPlan(actions);
|
|
5455
|
+
const specPlan = foldReturnSpec === null ? null : buildFoldPlanFromSpec(actions, foldReturnSpec);
|
|
5456
|
+
const plans = structuralPlans.length > 0 ? structuralPlans : specPlan === null ? [] : [specPlan];
|
|
5457
|
+
const values = new Set();
|
|
5458
|
+
for (const plan of plans) {
|
|
5459
|
+
for (const target of plan.targets) {
|
|
5460
|
+
for (let j = 0; j < target.chain.length; j++) {
|
|
5461
|
+
const priorIndex = target.chain[j];
|
|
5462
|
+
const priorCapture = actions[priorIndex]?.capture;
|
|
5463
|
+
if (!priorCapture)
|
|
5464
|
+
continue;
|
|
5465
|
+
const responseValues = collectResponseLeafValues(priorCapture);
|
|
5466
|
+
const echoedValues = collectRequestValuesIncludingHeaders(priorCapture);
|
|
5467
|
+
for (let k = j + 1; k < target.chain.length; k++) {
|
|
5468
|
+
const laterCapture = actions[target.chain[k]]?.capture;
|
|
5469
|
+
if (!laterCapture)
|
|
5470
|
+
continue;
|
|
5471
|
+
const laterRequestValues = collectRequestValuesIncludingHeaders(laterCapture);
|
|
5472
|
+
for (const v of responseValues) {
|
|
5473
|
+
if (!echoedValues.has(v) && laterRequestValues.has(v))
|
|
5474
|
+
values.add(v);
|
|
5475
|
+
}
|
|
5476
|
+
}
|
|
5477
|
+
}
|
|
5478
|
+
}
|
|
5479
|
+
}
|
|
5480
|
+
return values;
|
|
5481
|
+
}
|
|
5210
5482
|
function resolveFoldPlan(actions, foldReturnSpec = null) {
|
|
5211
5483
|
const structuralPlans = detectDrillDownFoldPlan(actions);
|
|
5212
5484
|
const plans = structuralPlans.length > 0
|
|
@@ -6573,7 +6845,9 @@ async function main() {
|
|
|
6573
6845
|
// Hoisted so both the primary-operation gate below and rawActionCaptures
|
|
6574
6846
|
// (further down) read the same computed sequence instead of calling the
|
|
6575
6847
|
// extractor twice.
|
|
6576
|
-
const graphqlActionSequence = gql
|
|
6848
|
+
const graphqlActionSequence = gql
|
|
6849
|
+
? extractGraphQLActionSequence(captures, submitPatterns, foldReturnSpec)
|
|
6850
|
+
: [];
|
|
6577
6851
|
const primaryGraphQLOperation = gql && graphqlActionSequence.length === 0
|
|
6578
6852
|
? selectPrimaryGraphQLOperation(captures, flowSteps, vocabulary, process.env, ownBackendHostnames, fallbackDomain)
|
|
6579
6853
|
: null;
|
|
@@ -6624,7 +6898,7 @@ async function main() {
|
|
|
6624
6898
|
// heuristic extraction finds.
|
|
6625
6899
|
const patternedHeuristicActionCaptures = gql
|
|
6626
6900
|
? graphqlActionSequence
|
|
6627
|
-
: collapseRedundantPatches(extractActionSequence(captures, submitPatterns));
|
|
6901
|
+
: collapseRedundantPatches(extractActionSequence(captures, submitPatterns, foldReturnSpec));
|
|
6628
6902
|
// The same undercount hazard applies one layer below the manifest: a
|
|
6629
6903
|
// flow-declared submitEndpointPattern that matches only one section's URL
|
|
6630
6904
|
// (the natural way to describe "the button that finishes the wizard")
|
|
@@ -6637,8 +6911,8 @@ async function main() {
|
|
|
6637
6911
|
const unfilteredHeuristicActionCaptures = submitPatterns.endpoint === null && submitPatterns.body === null
|
|
6638
6912
|
? patternedHeuristicActionCaptures
|
|
6639
6913
|
: gql
|
|
6640
|
-
? extractGraphQLActionSequence(captures, null)
|
|
6641
|
-
: collapseRedundantPatches(extractActionSequence(captures, null));
|
|
6914
|
+
? extractGraphQLActionSequence(captures, null, foldReturnSpec)
|
|
6915
|
+
: collapseRedundantPatches(extractActionSequence(captures, null, foldReturnSpec));
|
|
6642
6916
|
const patternUndercounts = patternedHeuristicActionCaptures.length < unfilteredHeuristicActionCaptures.length;
|
|
6643
6917
|
if (patternUndercounts) {
|
|
6644
6918
|
logger.info(`submission selection: ignoring submitEndpointPattern/submitBodyPattern (${patternedHeuristicActionCaptures.length} capture(s)) as an undercount of the unfiltered heuristic action sequence (${unfilteredHeuristicActionCaptures.length} capture(s))`);
|
|
@@ -6669,8 +6943,8 @@ async function main() {
|
|
|
6669
6943
|
const rawUnfilteredActionCaptures = submitEndpointPattern === null
|
|
6670
6944
|
? null
|
|
6671
6945
|
: gql
|
|
6672
|
-
? extractGraphQLActionSequence(captures, null)
|
|
6673
|
-
: collapseRedundantPatches(extractActionSequence(captures, null));
|
|
6946
|
+
? extractGraphQLActionSequence(captures, null, foldReturnSpec)
|
|
6947
|
+
: collapseRedundantPatches(extractActionSequence(captures, null, foldReturnSpec));
|
|
6674
6948
|
// Form-schema detection runs BEFORE state-indexing so the field-id/option-id
|
|
6675
6949
|
// UUIDs can be shielded from indexing — those UUIDs are stable schema
|
|
6676
6950
|
// anchors that T2/T3 substitution depends on remaining literal in body
|
|
@@ -6718,8 +6992,16 @@ async function main() {
|
|
|
6718
6992
|
];
|
|
6719
6993
|
})();
|
|
6720
6994
|
const actionCaptureIndices = new Set(actionCaptures.map((a) => a.index));
|
|
6995
|
+
// Resolved off raw actionCaptures — fold-plan DETECTION depends only on
|
|
6996
|
+
// each action's capture, so this runs before compileActionSteps/
|
|
6997
|
+
// indexStateValues even exist — so a short numeric join value threaded
|
|
6998
|
+
// through a dependent-drill-down chain hop still gets indexed as
|
|
6999
|
+
// producible state (see collectDependentDrillDownChainValues).
|
|
7000
|
+
const dependentDrillDownChainValues = actionCaptures.length > 1
|
|
7001
|
+
? collectDependentDrillDownChainValues(actionCaptures, foldReturnSpec)
|
|
7002
|
+
: new Set();
|
|
6721
7003
|
const stateIndex = actionCaptures.length > 1
|
|
6722
|
-
? indexStateValues(captures, shieldedUuids, actionCaptureIndices)
|
|
7004
|
+
? indexStateValues(captures, shieldedUuids, actionCaptureIndices, dependentDrillDownChainValues)
|
|
6723
7005
|
: new Map();
|
|
6724
7006
|
const actionSteps = actionCaptures.length > 1 ? compileActionSteps(actionCaptures, stateIndex) : [];
|
|
6725
7007
|
const isSubmissionFlow = actionSteps.length > 1;
|