@enricai/barnacle 1.12.30 → 1.12.32
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/scraper/flow-runner.d.ts +35 -25
- package/dist/scraper/flow-runner.d.ts.map +1 -1
- package/dist/scraper/flow-runner.js +83 -34
- package/dist/scraper/flow-runner.js.map +1 -1
- package/dist/scripts/recon-generate-multicall-fixture.d.ts +46 -0
- package/dist/scripts/recon-generate-multicall-fixture.d.ts.map +1 -1
- package/dist/scripts/recon-generate-multicall-fixture.js +143 -0
- package/dist/scripts/recon-generate-multicall-fixture.js.map +1 -1
- package/dist/scripts/recon-generate.d.ts.map +1 -1
- package/dist/scripts/recon-generate.js +186 -52
- package/dist/scripts/recon-generate.js.map +1 -1
- package/package.json +1 -1
|
@@ -2905,7 +2905,15 @@ function pathToFoldAccessorExpr(expr, path, depth = 0) {
|
|
|
2905
2905
|
* can't collide either. Returns the loop's opening/closing lines (for the
|
|
2906
2906
|
* caller to splice its own loop-body lines between) plus the ordered
|
|
2907
2907
|
* (outer-to-inner) ancestor variable names — NOT including the innermost
|
|
2908
|
-
* item, which the caller names itself via `itemVar`.
|
|
2908
|
+
* item, which the caller names itself via `itemVar`. Also splits the
|
|
2909
|
+
* combined open/close into an ancestor-scope segment
|
|
2910
|
+
* (`ancestorOpenLines`/`ancestorCloseLines`, empty when `path` crosses no
|
|
2911
|
+
* wildcard) and an item-scope segment (`itemOpenLines`/`itemCloseLines`),
|
|
2912
|
+
* so a caller whose per-target chain fetch never reads `itemVar` can splice
|
|
2913
|
+
* that fetch between the two segments — once per ancestor tuple — instead
|
|
2914
|
+
* of inside the item loop. `openLines`/`closeLines` remain the straight
|
|
2915
|
+
* concatenation of the two segments, unchanged, for callers that don't need
|
|
2916
|
+
* the split.
|
|
2909
2917
|
*/
|
|
2910
2918
|
function pathToFoldLoopLines(expr, path, itemVar, indent, varSuffix = "", depth = 0) {
|
|
2911
2919
|
const wildcardIndex = path.indexOf(ARRAY_WILDCARD_SEGMENT);
|
|
@@ -2917,21 +2925,21 @@ function pathToFoldLoopLines(expr, path, itemVar, indent, varSuffix = "", depth
|
|
|
2917
2925
|
// single-scope folds must emit byte-identical code, not merely
|
|
2918
2926
|
// equivalent code, since nothing about a flat fold needs the nested-loop
|
|
2919
2927
|
// rewrite in the first place.
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
closeLines: [`${indent}}`],
|
|
2928
|
-
ancestorVars: [],
|
|
2929
|
-
};
|
|
2930
|
-
}
|
|
2928
|
+
const itemOpenLines = depth === 0
|
|
2929
|
+
? [
|
|
2930
|
+
`${indent}const foldItems${varSuffix} = ${finalExpr};`,
|
|
2931
|
+
`${indent}for (const ${itemVar} of foldItems${varSuffix}) {`,
|
|
2932
|
+
]
|
|
2933
|
+
: [`${indent}for (const ${itemVar} of ${finalExpr}) {`];
|
|
2934
|
+
const itemCloseLines = [`${indent}}`];
|
|
2931
2935
|
return {
|
|
2932
|
-
openLines:
|
|
2933
|
-
closeLines:
|
|
2936
|
+
openLines: itemOpenLines,
|
|
2937
|
+
closeLines: itemCloseLines,
|
|
2934
2938
|
ancestorVars: [],
|
|
2939
|
+
ancestorOpenLines: [],
|
|
2940
|
+
itemOpenLines,
|
|
2941
|
+
itemCloseLines,
|
|
2942
|
+
ancestorCloseLines: [],
|
|
2935
2943
|
};
|
|
2936
2944
|
}
|
|
2937
2945
|
const before = path.slice(0, wildcardIndex);
|
|
@@ -2939,10 +2947,19 @@ function pathToFoldLoopLines(expr, path, itemVar, indent, varSuffix = "", depth
|
|
|
2939
2947
|
const groupVar = `g${depth}${varSuffix}`;
|
|
2940
2948
|
const outerExpr = `${expr}${pathToAccessor(before, { assertNonNull: false })}`;
|
|
2941
2949
|
const inner = pathToFoldLoopLines(groupVar, after, itemVar, `${indent} `, varSuffix, depth + 1);
|
|
2950
|
+
const ancestorOpenLines = [
|
|
2951
|
+
`${indent}for (const ${groupVar} of ${outerExpr}) {`,
|
|
2952
|
+
...inner.ancestorOpenLines,
|
|
2953
|
+
];
|
|
2954
|
+
const ancestorCloseLines = [...inner.ancestorCloseLines, `${indent}}`];
|
|
2942
2955
|
return {
|
|
2943
|
-
openLines: [
|
|
2944
|
-
closeLines: [...inner.
|
|
2956
|
+
openLines: [...ancestorOpenLines, ...inner.itemOpenLines],
|
|
2957
|
+
closeLines: [...inner.itemCloseLines, ...ancestorCloseLines],
|
|
2945
2958
|
ancestorVars: [groupVar, ...inner.ancestorVars],
|
|
2959
|
+
ancestorOpenLines,
|
|
2960
|
+
itemOpenLines: inner.itemOpenLines,
|
|
2961
|
+
itemCloseLines: inner.itemCloseLines,
|
|
2962
|
+
ancestorCloseLines,
|
|
2946
2963
|
};
|
|
2947
2964
|
}
|
|
2948
2965
|
/** Suggests a JS-camelCase variable name for a state value path. Falls back
|
|
@@ -3743,7 +3760,7 @@ function emitFoldMatchAndMergeLines(terminalStep, target, itemVar, suffix, joinA
|
|
|
3743
3760
|
if (target.chainArrayPath.length === 0) {
|
|
3744
3761
|
return [
|
|
3745
3762
|
` const foldMatch${suffix} = ${terminalStep.varName} as Record<string, unknown>;`,
|
|
3746
|
-
` Object.assign(${itemVar}, foldMatch${suffix} ?? {});`,
|
|
3763
|
+
` Object.assign(${itemVar}, Object.fromEntries(Object.entries(foldMatch${suffix} ?? {}).filter(([k]) => !(k in ${itemVar}))));`,
|
|
3747
3764
|
];
|
|
3748
3765
|
}
|
|
3749
3766
|
const foldMatchesExpr = pathToFoldAccessorExpr(`(${terminalStep.varName} as ${foldArrayAssertionType(target.chainArrayPath)})`, target.chainArrayPath);
|
|
@@ -3781,7 +3798,7 @@ function emitFoldMatchAndMergeLines(terminalStep, target, itemVar, suffix, joinA
|
|
|
3781
3798
|
return [
|
|
3782
3799
|
` const foldMatches${suffix} = ${foldMatchesExpr};`,
|
|
3783
3800
|
` const foldMatch${suffix} = foldMatches${suffix}.length === 1 && ${soleCandidateFieldsAbsent} ? foldMatches${suffix}[0] : foldMatches${suffix}.find((m) => ${joinCondition});`,
|
|
3784
|
-
` Object.assign(${itemVar}, foldMatch${suffix} ?? {});`,
|
|
3801
|
+
` Object.assign(${itemVar}, Object.fromEntries(Object.entries(foldMatch${suffix} ?? {}).filter(([k]) => !(k in ${itemVar}))));`,
|
|
3785
3802
|
];
|
|
3786
3803
|
}
|
|
3787
3804
|
/** Exported for unit testing — lets tests drive the multipart-upload code path directly
|
|
@@ -4268,8 +4285,16 @@ function emitMultiStepExecuteHttp(actions, inputBody, errorSignals, fieldNameMap
|
|
|
4268
4285
|
// Nested `for` loops (not a `.flatMap`-derived collection) so every
|
|
4269
4286
|
// intermediate array's binding stays addressable inside the innermost
|
|
4270
4287
|
// loop body — see pathToFoldLoopLines's docstring.
|
|
4271
|
-
const {
|
|
4272
|
-
lines.push(...
|
|
4288
|
+
const { ancestorOpenLines, itemOpenLines, itemCloseLines, ancestorCloseLines, ancestorVars } = pathToFoldLoopLines(`(${primaryStep.varName} as ${primaryArrType})`, foldPlan.primaryArrayPath, itemVar, " ", planSuffix);
|
|
4289
|
+
lines.push(...ancestorOpenLines);
|
|
4290
|
+
// Chain fetches for targets whose params + joinFields never reference
|
|
4291
|
+
// itemVar are spliced here, above the item loop but inside the
|
|
4292
|
+
// ancestor loop(s) — fetched once per ancestor tuple and reused by
|
|
4293
|
+
// every descendant item's join/merge, instead of once per item.
|
|
4294
|
+
const hoistedChainLines = [];
|
|
4295
|
+
// Every target's join/merge — plus any non-hoistable target's own
|
|
4296
|
+
// chain fetch — goes here, spliced inside the item loop.
|
|
4297
|
+
const itemScopedLines = [];
|
|
4273
4298
|
for (const [targetIndex, target] of foldPlan.targets.entries()) {
|
|
4274
4299
|
// `firstItem` decides which captured literal `parameterize` rewrites
|
|
4275
4300
|
// — it must be the item at `primaryMatchedItemIndex`, the one THIS
|
|
@@ -4323,7 +4348,7 @@ function emitMultiStepExecuteHttp(actions, inputBody, errorSignals, fieldNameMap
|
|
|
4323
4348
|
// invisible and gets frozen as a literal.
|
|
4324
4349
|
const threadedFields = dedupeThreadedFields([
|
|
4325
4350
|
...target.joinFields.map((field) => ({ varName: itemVar, field })),
|
|
4326
|
-
...findThreadedJoinFields(threadingScopes, chainCapture),
|
|
4351
|
+
...findThreadedJoinFields(threadingScopes, chainCapture, actions.map((a) => a.capture)),
|
|
4327
4352
|
]);
|
|
4328
4353
|
const result = threadedFields.reduce((acc, { varName, field }) => {
|
|
4329
4354
|
const replacement = `\${${scopedAccessor(varName, field)}}`;
|
|
@@ -4366,19 +4391,33 @@ function emitMultiStepExecuteHttp(actions, inputBody, errorSignals, fieldNameMap
|
|
|
4366
4391
|
// templates by the pass above, same as it would be for any two
|
|
4367
4392
|
// sequential non-fold steps) resolves them from this narrower scope.
|
|
4368
4393
|
const chainDeclared = new Set();
|
|
4394
|
+
const chainLines = [];
|
|
4395
|
+
// True once any chain step's parameterized url/headers/body actually
|
|
4396
|
+
// ends up referencing `itemVar` — determined off the SAME
|
|
4397
|
+
// `parameterize` substitutions the emitted call itself uses (not a
|
|
4398
|
+
// fresh value scan), so this can never disagree with what the
|
|
4399
|
+
// request literally interpolates. `ancestorVars.length === 0` means
|
|
4400
|
+
// there is no ancestor loop to hoist into in the first place (a
|
|
4401
|
+
// flat, single-level fold), so it is treated as item-scoped too —
|
|
4402
|
+
// existing flat folds must keep emitting byte-identical code.
|
|
4403
|
+
const itemVarRefPattern = new RegExp(`\\$\\{${itemVar}[.[]`);
|
|
4404
|
+
let referencesItemVar = ancestorVars.length === 0;
|
|
4369
4405
|
for (const chainIndex of target.chain) {
|
|
4370
4406
|
const chainStep = actions[chainIndex];
|
|
4371
4407
|
const chainRendered = rendered[chainIndex];
|
|
4372
|
-
|
|
4373
|
-
const
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
.
|
|
4378
|
-
|
|
4408
|
+
const paramUrl = parameterize(chainRendered.url, chainStep.capture);
|
|
4409
|
+
const paramHeaders = parameterize(chainRendered.headersExpr, chainStep.capture);
|
|
4410
|
+
const paramBody = parameterize(chainRendered.bodyArg, chainStep.capture);
|
|
4411
|
+
if (itemVarRefPattern.test(paramUrl) ||
|
|
4412
|
+
itemVarRefPattern.test(paramHeaders) ||
|
|
4413
|
+
itemVarRefPattern.test(paramBody)) {
|
|
4414
|
+
referencesItemVar = true;
|
|
4415
|
+
}
|
|
4416
|
+
chainLines.push(` const ${chainStep.varName} = (await httpClient(\`${paramUrl}\`, {`, ` method: ${JSON.stringify(chainRendered.method)},`);
|
|
4417
|
+
const joined = [paramHeaders, paramBody].filter((s) => s !== "").join(" ");
|
|
4379
4418
|
if (joined !== "")
|
|
4380
|
-
|
|
4381
|
-
|
|
4419
|
+
chainLines.push(` ${joined}`);
|
|
4420
|
+
chainLines.push(` schema: ${chainRendered.schemaExpr},`, ` })) as Record<string, unknown>;`);
|
|
4382
4421
|
for (const p of chainStep.produces) {
|
|
4383
4422
|
if (p.kind === "header")
|
|
4384
4423
|
continue;
|
|
@@ -4388,13 +4427,20 @@ function emitMultiStepExecuteHttp(actions, inputBody, errorSignals, fieldNameMap
|
|
|
4388
4427
|
continue;
|
|
4389
4428
|
chainDeclared.add(p.name);
|
|
4390
4429
|
const assertion = pathToAssertionType(p.path);
|
|
4391
|
-
|
|
4430
|
+
chainLines.push(` const ${p.name} = (${chainStep.varName} as ${assertion})${pathToAccessor(p.path, { assertNonNull: false })};`);
|
|
4392
4431
|
}
|
|
4393
4432
|
}
|
|
4394
4433
|
const terminalStep = actions[target.chainTerminalIndex];
|
|
4395
|
-
|
|
4434
|
+
const matchLines = emitFoldMatchAndMergeLines(terminalStep, target, itemVar, suffix, joinAccessor);
|
|
4435
|
+
if (referencesItemVar) {
|
|
4436
|
+
itemScopedLines.push(...chainLines, ...matchLines);
|
|
4437
|
+
}
|
|
4438
|
+
else {
|
|
4439
|
+
hoistedChainLines.push(...chainLines);
|
|
4440
|
+
itemScopedLines.push(...matchLines);
|
|
4441
|
+
}
|
|
4396
4442
|
}
|
|
4397
|
-
lines.push(...
|
|
4443
|
+
lines.push(...hoistedChainLines, ...itemOpenLines, ...itemScopedLines, ...itemCloseLines, ...ancestorCloseLines, "");
|
|
4398
4444
|
continue;
|
|
4399
4445
|
}
|
|
4400
4446
|
// Every other chain step (already fully emitted, inline, by the fold
|
|
@@ -4813,21 +4859,53 @@ function findObjectArrayFieldOrWholeObject(value, path = []) {
|
|
|
4813
4859
|
* path segment (e.g. `/orders/{id}`) rather than a query param or body
|
|
4814
4860
|
* field. Numeric leaves are included because a join key is just as often a
|
|
4815
4861
|
* numeric id (threaded as a query param string or a JSON body number
|
|
4816
|
-
* literal) as a string one.
|
|
4817
|
-
|
|
4862
|
+
* literal) as a string one.
|
|
4863
|
+
*
|
|
4864
|
+
* `allCaptures`, when passed, gates every query param and body leaf on
|
|
4865
|
+
* whether its OWN value (by param key / body path, via {@link endpointKey}'s
|
|
4866
|
+
* same-endpoint grouping — the same grouping {@link
|
|
4867
|
+
* findFrozenVaryingDrillParams} uses) ever differs on some other capture of
|
|
4868
|
+
* the same endpoint. A value that never varies (e.g. `adults=2`) is left
|
|
4869
|
+
* out, so callers matching by pure value equality (e.g. {@link
|
|
4870
|
+
* findThreadedJoinFields}) can't bind that constant onto an unrelated field
|
|
4871
|
+
* that coincidentally holds the same literal (e.g. a zero-valued `children`
|
|
4872
|
+
* param matching a discount amount that also happens to be `0` in one
|
|
4873
|
+
* capture, then diverges to a fraction in another). Path segments are
|
|
4874
|
+
* exempt from this gate and always pass through — {@link endpointKey}
|
|
4875
|
+
* already fixes the pathname, so a path segment can never be observed to
|
|
4876
|
+
* vary within a matched group, the same rationale {@link
|
|
4877
|
+
* findFrozenVaryingDrillParams} documents for the same exclusion. When no
|
|
4878
|
+
* other capture of this endpoint exists in `allCaptures`, variance can't be
|
|
4879
|
+
* observed either way, so every value is kept (unfiltered, matching the
|
|
4880
|
+
* behavior when `allCaptures` is omitted). */
|
|
4881
|
+
function collectRequestStringValues(capture, allCaptures) {
|
|
4882
|
+
const sameEndpointCaptures = allCaptures
|
|
4883
|
+
? allCaptures.filter((c) => c !== capture && endpointKey(c.url) === endpointKey(capture.url))
|
|
4884
|
+
: [];
|
|
4885
|
+
const varies = (own, others) => !allCaptures || sameEndpointCaptures.length === 0
|
|
4886
|
+
? true
|
|
4887
|
+
: others.some((other) => other !== undefined && other !== own);
|
|
4818
4888
|
const values = new Set();
|
|
4819
4889
|
try {
|
|
4820
4890
|
const url = new URL(capture.url);
|
|
4821
|
-
for (const v of url.searchParams.values())
|
|
4822
|
-
values.add(v);
|
|
4823
4891
|
for (const segment of url.pathname.split("/").filter(Boolean))
|
|
4824
4892
|
values.add(segment);
|
|
4893
|
+
for (const [paramKey, value] of url.searchParams.entries()) {
|
|
4894
|
+
const otherValues = sameEndpointCaptures.map((c) => {
|
|
4895
|
+
try {
|
|
4896
|
+
return new URL(c.url).searchParams.get(paramKey) ?? undefined;
|
|
4897
|
+
}
|
|
4898
|
+
catch {
|
|
4899
|
+
return undefined;
|
|
4900
|
+
}
|
|
4901
|
+
});
|
|
4902
|
+
if (varies(value, otherValues))
|
|
4903
|
+
values.add(value);
|
|
4904
|
+
}
|
|
4825
4905
|
}
|
|
4826
4906
|
catch {
|
|
4827
4907
|
// Relative or malformed URL — no query params or path segments to contribute.
|
|
4828
4908
|
}
|
|
4829
|
-
for (const v of jsonBodyLeafValues(capture.requestPostData) ?? [])
|
|
4830
|
-
values.add(v);
|
|
4831
4909
|
const parsedBody = (() => {
|
|
4832
4910
|
try {
|
|
4833
4911
|
return typeof capture.requestPostData === "string" && capture.requestPostData.length > 0
|
|
@@ -4839,9 +4917,26 @@ function collectRequestStringValues(capture) {
|
|
|
4839
4917
|
}
|
|
4840
4918
|
})();
|
|
4841
4919
|
if (parsedBody !== undefined) {
|
|
4842
|
-
for (const { value } of walkAllPrimitiveLeaves(parsedBody)) {
|
|
4843
|
-
if (
|
|
4844
|
-
|
|
4920
|
+
for (const { path, value } of walkAllPrimitiveLeaves(parsedBody)) {
|
|
4921
|
+
if (value === null)
|
|
4922
|
+
continue;
|
|
4923
|
+
const stringValue = String(value);
|
|
4924
|
+
const otherValues = sameEndpointCaptures.map((c) => {
|
|
4925
|
+
try {
|
|
4926
|
+
const otherBody = typeof c.requestPostData === "string" && c.requestPostData.length > 0
|
|
4927
|
+
? JSON.parse(c.requestPostData)
|
|
4928
|
+
: undefined;
|
|
4929
|
+
if (otherBody === undefined)
|
|
4930
|
+
return undefined;
|
|
4931
|
+
const otherValue = readValueAtPath(otherBody, path);
|
|
4932
|
+
return otherValue === undefined ? undefined : String(otherValue);
|
|
4933
|
+
}
|
|
4934
|
+
catch {
|
|
4935
|
+
return undefined;
|
|
4936
|
+
}
|
|
4937
|
+
});
|
|
4938
|
+
if (varies(stringValue, otherValues))
|
|
4939
|
+
values.add(stringValue);
|
|
4845
4940
|
}
|
|
4846
4941
|
}
|
|
4847
4942
|
return values;
|
|
@@ -5061,9 +5156,19 @@ function dedupeThreadedFields(fields) {
|
|
|
5061
5156
|
* depends on. `scopes` is searched in the given order (innermost fold item
|
|
5062
5157
|
* first, then each ancestor binding a nested loop keeps addressable), since
|
|
5063
5158
|
* an ancestor field with the same name as an item field must not shadow the
|
|
5064
|
-
* item's own value.
|
|
5065
|
-
|
|
5066
|
-
|
|
5159
|
+
* item's own value.
|
|
5160
|
+
*
|
|
5161
|
+
* `allCaptures`, when passed, is forwarded to {@link
|
|
5162
|
+
* collectRequestStringValues} to gate matching on cross-capture variance:
|
|
5163
|
+
* a param whose own value never varies across other captures of the same
|
|
5164
|
+
* endpoint is excluded from the candidate set entirely, so a constant like
|
|
5165
|
+
* `children=0` can't bind to an unrelated item field (a discount amount, a
|
|
5166
|
+
* departure port) purely because both happen to equal the same literal at
|
|
5167
|
+
* generation time. Omitted for the candidate-array disambiguation callers,
|
|
5168
|
+
* where narrowing by variance is a different concern than the URL/body
|
|
5169
|
+
* over-threading this gate exists to prevent. */
|
|
5170
|
+
function findThreadedJoinFields(scopes, drillCapture, allCaptures) {
|
|
5171
|
+
const requestValues = collectRequestStringValues(drillCapture, allCaptures);
|
|
5067
5172
|
if (requestValues.size === 0)
|
|
5068
5173
|
return [];
|
|
5069
5174
|
return scopes.flatMap(({ varName, obj }) => [...walkItemFieldPaths(obj)]
|
|
@@ -6300,7 +6405,7 @@ function foldResponseBodyForShapeInference(actionSteps, foldPlan, initialBody =
|
|
|
6300
6405
|
String(readValueAtPath(matchedItem, f.split("."))))) ?? drillItems?.[0];
|
|
6301
6406
|
if (!primaryItems || !matchedItem || !drillMatch)
|
|
6302
6407
|
return body;
|
|
6303
|
-
return replaceByReference(body, matchedItem, { ...
|
|
6408
|
+
return replaceByReference(body, matchedItem, { ...drillMatch, ...matchedItem });
|
|
6304
6409
|
}, initialBody);
|
|
6305
6410
|
}
|
|
6306
6411
|
/**
|
|
@@ -6903,7 +7008,7 @@ const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottlenec
|
|
|
6903
7008
|
return foldPlan.primaryArrayPath.slice(level.length);
|
|
6904
7009
|
})()
|
|
6905
7010
|
: foldPlan.primaryArrayPath;
|
|
6906
|
-
const {
|
|
7011
|
+
const { ancestorOpenLines, itemOpenLines, itemCloseLines, ancestorCloseLines, ancestorVars } = itemsOverride
|
|
6907
7012
|
? pathToFoldLoopLines(residualPath.length === 0
|
|
6908
7013
|
? itemsOverride.expr
|
|
6909
7014
|
: `(${itemsOverride.expr} as ${pathAccessTypeExpr(`${pascal}Response`, itemsOverride.level)}[number][])`, residualPath, itemVar, " ", planSuffix)
|
|
@@ -6914,7 +7019,17 @@ const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottlenec
|
|
|
6914
7019
|
// crossings correspond to `ancestorVars` above, so slice the
|
|
6915
7020
|
// design-time ancestor chain to match.
|
|
6916
7021
|
const residualAncestorCount = residualPath.filter((s) => s === ARRAY_WILDCARD_SEGMENT).length;
|
|
6917
|
-
lines.push(...
|
|
7022
|
+
lines.push(...ancestorOpenLines);
|
|
7023
|
+
// Chain fetches for targets whose parameterized URL never interpolates
|
|
7024
|
+
// `itemVar` are spliced here, above the item loop but inside the
|
|
7025
|
+
// ancestor loop(s) — fetched once per ancestor tuple and reused by
|
|
7026
|
+
// every descendant item's join/merge, mirroring
|
|
7027
|
+
// emitMultiStepExecuteHttp's identical hoist (see its own comment).
|
|
7028
|
+
const hoistedChainLines = [];
|
|
7029
|
+
// Every target's join/merge — plus any non-hoistable target's own
|
|
7030
|
+
// chain fetch — goes here, spliced inside the item loop.
|
|
7031
|
+
const itemScopedLines = [];
|
|
7032
|
+
const itemVarRefPattern = new RegExp(`\\$\\{${itemVar}[.[]`);
|
|
6918
7033
|
for (const [targetIndex, target] of foldPlan.targets.entries()) {
|
|
6919
7034
|
const matchedPrimaryItem = primaryItemsWithAncestors[target.primaryMatchedItemIndex];
|
|
6920
7035
|
if (!matchedPrimaryItem) {
|
|
@@ -6953,7 +7068,7 @@ const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottlenec
|
|
|
6953
7068
|
: rawUrl;
|
|
6954
7069
|
const threadedFields = dedupeThreadedFields([
|
|
6955
7070
|
...target.joinFields.map((field) => ({ varName: itemVar, field })),
|
|
6956
|
-
...findThreadedJoinFields(threadingScopes, chainCapture),
|
|
7071
|
+
...findThreadedJoinFields(threadingScopes, chainCapture, actionSteps.map((s) => s.capture)),
|
|
6957
7072
|
]);
|
|
6958
7073
|
const result = threadedFields.reduce((acc, { varName, field }) => {
|
|
6959
7074
|
const scopeObj = varName === itemVar ? firstItem : ancestorObjByVar.get(varName);
|
|
@@ -6970,25 +7085,44 @@ const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottlenec
|
|
|
6970
7085
|
assertNoFrozenVaryingDrillParams("emitContractTs", chainCapture, result, actionSteps.map((s) => s.capture));
|
|
6971
7086
|
return result;
|
|
6972
7087
|
};
|
|
7088
|
+
const chainLines = [];
|
|
7089
|
+
// True once any chain step's parameterized URL actually ends up
|
|
7090
|
+
// referencing `itemVar`, off the SAME `parameterizeUrl` call the
|
|
7091
|
+
// request literally interpolates — see emitMultiStepExecuteHttp's
|
|
7092
|
+
// identical `referencesItemVar` for why this can't disagree with
|
|
7093
|
+
// what's emitted. `ancestorVars.length === 0` means there's no
|
|
7094
|
+
// ancestor loop to hoist into (a flat, single-level fold), so it's
|
|
7095
|
+
// treated as item-scoped too — flat folds must keep emitting
|
|
7096
|
+
// byte-identical code.
|
|
7097
|
+
let referencesItemVar = ancestorVars.length === 0;
|
|
6973
7098
|
for (const chainIndex of target.chain) {
|
|
6974
7099
|
const chainStep = actionSteps[chainIndex];
|
|
6975
7100
|
if (!chainStep)
|
|
6976
7101
|
continue;
|
|
6977
7102
|
const url = parameterizeUrl(chainStep.capture.url, chainStep.capture);
|
|
7103
|
+
if (itemVarRefPattern.test(url))
|
|
7104
|
+
referencesItemVar = true;
|
|
6978
7105
|
const schemaExpr = inferZodSchema(chainStep.capture.responseBody, 0, "", {
|
|
6979
7106
|
looseServerResponse: true,
|
|
6980
7107
|
aggregateUnitBasisFindingsByPath: groupAggregateUnitBasisFindingsByPath([
|
|
6981
7108
|
chainStep.capture.responseBody,
|
|
6982
7109
|
]),
|
|
6983
7110
|
});
|
|
6984
|
-
|
|
7111
|
+
chainLines.push(` const ${chainStep.varName} = (await httpClient(\`${url}\`, {`, ` method: ${JSON.stringify(chainStep.capture.method)},`, ` schema: ${schemaExpr},`, ` })) as Record<string, unknown>;`);
|
|
6985
7112
|
}
|
|
6986
7113
|
const terminalStep = actionSteps[target.chainTerminalIndex];
|
|
6987
7114
|
if (!terminalStep)
|
|
6988
7115
|
continue;
|
|
6989
|
-
|
|
7116
|
+
const matchLines = emitFoldMatchAndMergeLines(terminalStep, target, itemVar, suffix, joinAccessor);
|
|
7117
|
+
if (referencesItemVar) {
|
|
7118
|
+
itemScopedLines.push(...chainLines, ...matchLines);
|
|
7119
|
+
}
|
|
7120
|
+
else {
|
|
7121
|
+
hoistedChainLines.push(...chainLines);
|
|
7122
|
+
itemScopedLines.push(...matchLines);
|
|
7123
|
+
}
|
|
6990
7124
|
}
|
|
6991
|
-
lines.push(...
|
|
7125
|
+
lines.push(...hoistedChainLines, ...itemOpenLines, ...itemScopedLines, ...itemCloseLines, ...ancestorCloseLines, "");
|
|
6992
7126
|
}
|
|
6993
7127
|
return lines;
|
|
6994
7128
|
};
|