@enricai/barnacle 1.12.29 → 1.12.30
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.map +1 -1
- package/dist/scraper/flow-runner.js +18 -7
- package/dist/scraper/flow-runner.js.map +1 -1
- package/dist/scripts/recon-generate-multicall-fixture.d.ts +34 -0
- package/dist/scripts/recon-generate-multicall-fixture.d.ts.map +1 -1
- package/dist/scripts/recon-generate-multicall-fixture.js +68 -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 +389 -119
- package/dist/scripts/recon-generate.js.map +1 -1
- package/package.json +1 -1
|
@@ -2890,6 +2890,61 @@ function pathToFoldAccessorExpr(expr, path, depth = 0) {
|
|
|
2890
2890
|
const outerExpr = `${expr}${pathToAccessor(before, { assertNonNull: false })}`;
|
|
2891
2891
|
return `${outerExpr}.flatMap((${groupVar}) => ${pathToFoldAccessorExpr(groupVar, after, depth + 1)})`;
|
|
2892
2892
|
}
|
|
2893
|
+
/**
|
|
2894
|
+
* Emits nested `for` loops descending through every {@link
|
|
2895
|
+
* ARRAY_WILDCARD_SEGMENT} in `path`, instead of {@link
|
|
2896
|
+
* pathToFoldAccessorExpr}'s single `.flatMap` chain — a `.flatMap` callback
|
|
2897
|
+
* only ever sees the group it flattens FROM, so the moment it returns the
|
|
2898
|
+
* inner array, the outer (ancestor) object is gone from scope for the rest
|
|
2899
|
+
* of the emitted fold body. A nested loop keeps every intermediate binding
|
|
2900
|
+
* (`g0`, `g1`, ...) addressable inside the innermost loop, so a drill param
|
|
2901
|
+
* that only lives on an ancestor object (e.g. a parent group id) can be read
|
|
2902
|
+
* off that binding instead of being frozen as a literal. `varSuffix` mirrors
|
|
2903
|
+
* the `itemVar`/`foldItemsVar` disambiguation suffix used when multiple fold
|
|
2904
|
+
* plans share one function scope, so ancestor bindings from different plans
|
|
2905
|
+
* can't collide either. Returns the loop's opening/closing lines (for the
|
|
2906
|
+
* caller to splice its own loop-body lines between) plus the ordered
|
|
2907
|
+
* (outer-to-inner) ancestor variable names — NOT including the innermost
|
|
2908
|
+
* item, which the caller names itself via `itemVar`.
|
|
2909
|
+
*/
|
|
2910
|
+
function pathToFoldLoopLines(expr, path, itemVar, indent, varSuffix = "", depth = 0) {
|
|
2911
|
+
const wildcardIndex = path.indexOf(ARRAY_WILDCARD_SEGMENT);
|
|
2912
|
+
if (wildcardIndex === -1) {
|
|
2913
|
+
const finalExpr = `${expr}${pathToAccessor(path, { assertNonNull: false })}`;
|
|
2914
|
+
// At depth 0 with no wildcard crossing at all (the overwhelmingly common
|
|
2915
|
+
// single-level fold), keep emitting the original `const foldItems = ...;`
|
|
2916
|
+
// binding rather than inlining the accessor into the `for` — existing
|
|
2917
|
+
// single-scope folds must emit byte-identical code, not merely
|
|
2918
|
+
// equivalent code, since nothing about a flat fold needs the nested-loop
|
|
2919
|
+
// rewrite in the first place.
|
|
2920
|
+
if (depth === 0) {
|
|
2921
|
+
const foldItemsVar = `foldItems${varSuffix}`;
|
|
2922
|
+
return {
|
|
2923
|
+
openLines: [
|
|
2924
|
+
`${indent}const ${foldItemsVar} = ${finalExpr};`,
|
|
2925
|
+
`${indent}for (const ${itemVar} of ${foldItemsVar}) {`,
|
|
2926
|
+
],
|
|
2927
|
+
closeLines: [`${indent}}`],
|
|
2928
|
+
ancestorVars: [],
|
|
2929
|
+
};
|
|
2930
|
+
}
|
|
2931
|
+
return {
|
|
2932
|
+
openLines: [`${indent}for (const ${itemVar} of ${finalExpr}) {`],
|
|
2933
|
+
closeLines: [`${indent}}`],
|
|
2934
|
+
ancestorVars: [],
|
|
2935
|
+
};
|
|
2936
|
+
}
|
|
2937
|
+
const before = path.slice(0, wildcardIndex);
|
|
2938
|
+
const after = path.slice(wildcardIndex + 1);
|
|
2939
|
+
const groupVar = `g${depth}${varSuffix}`;
|
|
2940
|
+
const outerExpr = `${expr}${pathToAccessor(before, { assertNonNull: false })}`;
|
|
2941
|
+
const inner = pathToFoldLoopLines(groupVar, after, itemVar, `${indent} `, varSuffix, depth + 1);
|
|
2942
|
+
return {
|
|
2943
|
+
openLines: [`${indent}for (const ${groupVar} of ${outerExpr}) {`, ...inner.openLines],
|
|
2944
|
+
closeLines: [...inner.closeLines, `${indent}}`],
|
|
2945
|
+
ancestorVars: [groupVar, ...inner.ancestorVars],
|
|
2946
|
+
};
|
|
2947
|
+
}
|
|
2893
2948
|
/** Suggests a JS-camelCase variable name for a state value path. Falls back
|
|
2894
2949
|
* up the path if the tail is numeric or not a valid JS identifier. */
|
|
2895
2950
|
function pathToVarName(path) {
|
|
@@ -3692,29 +3747,40 @@ function emitFoldMatchAndMergeLines(terminalStep, target, itemVar, suffix, joinA
|
|
|
3692
3747
|
];
|
|
3693
3748
|
}
|
|
3694
3749
|
const foldMatchesExpr = pathToFoldAccessorExpr(`(${terminalStep.varName} as ${foldArrayAssertionType(target.chainArrayPath)})`, target.chainArrayPath);
|
|
3750
|
+
const matchAccessorFor = (f, varName, optionalRoot) => {
|
|
3751
|
+
const segments = f.split(".");
|
|
3752
|
+
// The drill-down response is a DIFFERENT payload than the
|
|
3753
|
+
// primary item, so it has no obligation to mirror the
|
|
3754
|
+
// primary item's own nesting for the join key (e.g. a
|
|
3755
|
+
// primary item's `identifiers.sku` is typically echoed
|
|
3756
|
+
// back flat, as `sku`, on the drill response). Try the
|
|
3757
|
+
// full nested path first (optional-chained, since an
|
|
3758
|
+
// intermediate segment may not exist on a flat response),
|
|
3759
|
+
// then fall back to the bare last segment.
|
|
3760
|
+
const lastSegment = segments[segments.length - 1];
|
|
3761
|
+
const bracket = (segment) => optionalRoot ? `?.[${JSON.stringify(segment)}]` : `[${JSON.stringify(segment)}]`;
|
|
3762
|
+
const optionalBracketAccessor = segments
|
|
3763
|
+
.map((segment) => `?.[${JSON.stringify(segment)}]`)
|
|
3764
|
+
.join("");
|
|
3765
|
+
return segments.length > 1
|
|
3766
|
+
? `(${varName}${optionalBracketAccessor} ?? ${varName}${bracket(lastSegment)})`
|
|
3767
|
+
: `${varName}${bracket(lastSegment)}`;
|
|
3768
|
+
};
|
|
3769
|
+
const joinCondition = target.joinFields
|
|
3770
|
+
.map((f) => `String(${matchAccessorFor(f, "m", false)}) === String(${joinAccessor(f)})`)
|
|
3771
|
+
.join(" && ");
|
|
3772
|
+
// A sole candidate whose join field(s) aren't present on it at all
|
|
3773
|
+
// (common: many drill endpoints don't echo the request key back onto
|
|
3774
|
+
// the response row) is trusted as-is — there's no sibling it could be
|
|
3775
|
+
// confused with. But a sole candidate that DOES carry the join field
|
|
3776
|
+
// with a different value is a genuine mismatch and must not be grafted
|
|
3777
|
+
// on; two-or-more candidates always require an actual join-key match.
|
|
3778
|
+
const soleCandidateFieldsAbsent = target.joinFields
|
|
3779
|
+
.map((f) => `${matchAccessorFor(f, `foldMatches${suffix}[0]`, true)} === undefined`)
|
|
3780
|
+
.join(" && ");
|
|
3695
3781
|
return [
|
|
3696
3782
|
` const foldMatches${suffix} = ${foldMatchesExpr};`,
|
|
3697
|
-
` const foldMatch${suffix} = foldMatches${suffix}.find((m) => ${
|
|
3698
|
-
.map((f) => {
|
|
3699
|
-
const segments = f.split(".");
|
|
3700
|
-
// The drill-down response is a DIFFERENT payload than the
|
|
3701
|
-
// primary item, so it has no obligation to mirror the
|
|
3702
|
-
// primary item's own nesting for the join key (e.g. a
|
|
3703
|
-
// primary item's `identifiers.sku` is typically echoed
|
|
3704
|
-
// back flat, as `sku`, on the drill response). Try the
|
|
3705
|
-
// full nested path first (optional-chained, since an
|
|
3706
|
-
// intermediate segment may not exist on a flat response),
|
|
3707
|
-
// then fall back to the bare last segment.
|
|
3708
|
-
const lastSegment = segments[segments.length - 1];
|
|
3709
|
-
const optionalBracketAccessor = segments
|
|
3710
|
-
.map((segment) => `?.[${JSON.stringify(segment)}]`)
|
|
3711
|
-
.join("");
|
|
3712
|
-
const matchAccessor = segments.length > 1
|
|
3713
|
-
? `(m${optionalBracketAccessor} ?? m[${JSON.stringify(lastSegment)}])`
|
|
3714
|
-
: `m[${JSON.stringify(lastSegment)}]`;
|
|
3715
|
-
return `String(${matchAccessor}) === String(${joinAccessor(f)})`;
|
|
3716
|
-
})
|
|
3717
|
-
.join(" && ")}) ?? foldMatches${suffix}[0];`,
|
|
3783
|
+
` const foldMatch${suffix} = foldMatches${suffix}.length === 1 && ${soleCandidateFieldsAbsent} ? foldMatches${suffix}[0] : foldMatches${suffix}.find((m) => ${joinCondition});`,
|
|
3718
3784
|
` Object.assign(${itemVar}, foldMatch${suffix} ?? {});`,
|
|
3719
3785
|
];
|
|
3720
3786
|
}
|
|
@@ -4187,77 +4253,110 @@ function emitMultiStepExecuteHttp(actions, inputBody, errorSignals, fieldNameMap
|
|
|
4187
4253
|
const primaryStep = actions[foldPlan.primaryStepIndex];
|
|
4188
4254
|
// Read at the plan's OWN path rather than re-running the DFS: a
|
|
4189
4255
|
// flow-declared `resultsPath` (see FoldReturnSpec) can name a different
|
|
4190
|
-
// array than findObjectArrayField's first match.
|
|
4191
|
-
|
|
4256
|
+
// array than findObjectArrayField's first match. Ancestor-aware so a
|
|
4257
|
+
// drill param living only on a parent object (e.g. a group id) has a
|
|
4258
|
+
// real object to be read off of, not just the flattened leaf item.
|
|
4259
|
+
const primaryItemsWithAncestors = objectItemsWithAncestorsAtPath(primaryStep.capture.responseBody, foldPlan.primaryArrayPath);
|
|
4192
4260
|
const primaryArrType = foldArrayAssertionType(foldPlan.primaryArrayPath);
|
|
4193
4261
|
// Plan-level suffix mirrors the target-level suffix below: multiple
|
|
4194
4262
|
// loop blocks now sharing the same function scope can't declare
|
|
4195
|
-
// unsuffixed `
|
|
4263
|
+
// unsuffixed `item`/ancestor-binding locals without colliding. The
|
|
4196
4264
|
// overwhelmingly common single-plan case keeps the original
|
|
4197
4265
|
// unsuffixed names.
|
|
4198
4266
|
const planSuffix = foldPlans.length > 1 ? String(matchingPlanIndex) : "";
|
|
4199
|
-
const foldItemsVar = `foldItems${planSuffix}`;
|
|
4200
|
-
const foldItemsExpr = pathToFoldAccessorExpr(`(${primaryStep.varName} as ${primaryArrType})`, foldPlan.primaryArrayPath);
|
|
4201
4267
|
const itemVar = `item${planSuffix}`;
|
|
4202
|
-
|
|
4268
|
+
// Nested `for` loops (not a `.flatMap`-derived collection) so every
|
|
4269
|
+
// intermediate array's binding stays addressable inside the innermost
|
|
4270
|
+
// loop body — see pathToFoldLoopLines's docstring.
|
|
4271
|
+
const { openLines, closeLines, ancestorVars } = pathToFoldLoopLines(`(${primaryStep.varName} as ${primaryArrType})`, foldPlan.primaryArrayPath, itemVar, " ", planSuffix);
|
|
4272
|
+
lines.push(...openLines);
|
|
4203
4273
|
for (const [targetIndex, target] of foldPlan.targets.entries()) {
|
|
4204
4274
|
// `firstItem` decides which captured literal `parameterize` rewrites
|
|
4205
4275
|
// — it must be the item at `primaryMatchedItemIndex`, the one THIS
|
|
4206
4276
|
// target's drill request was actually built from, not always index
|
|
4207
4277
|
// 0, and can differ per target even though every target now shares
|
|
4208
4278
|
// the same runtime loop item.
|
|
4209
|
-
const
|
|
4210
|
-
if (!
|
|
4279
|
+
const matchedPrimaryItem = primaryItemsWithAncestors[target.primaryMatchedItemIndex];
|
|
4280
|
+
if (!matchedPrimaryItem) {
|
|
4211
4281
|
throw new Error(`emitMultiStepExecuteHttp: fold plan primary step ${primaryStep.varName} no longer resolves an object array at ${foldPlan.primaryArrayPath.join(".")} — the fold plan and this emitter have drifted out of sync`);
|
|
4212
4282
|
}
|
|
4283
|
+
const { item: firstItem, ancestors: firstItemAncestors } = matchedPrimaryItem;
|
|
4284
|
+
// Each ancestor binding (`g0`, `g1`, ...) paired with the design-time
|
|
4285
|
+
// object it holds at runtime, so a threaded field resolved against
|
|
4286
|
+
// that binding can be read off the same object `readValueAtPath`
|
|
4287
|
+
// needs, and the runtime accessor built from the SAME variable name
|
|
4288
|
+
// the emitted nested loop actually declares.
|
|
4289
|
+
const ancestorObjByVar = new Map(ancestorVars.map((varName, idx) => [varName, firstItemAncestors[idx]]));
|
|
4290
|
+
// Searched innermost-scope-first: the item's own field wins over an
|
|
4291
|
+
// ancestor field of the same name.
|
|
4292
|
+
const threadingScopes = [
|
|
4293
|
+
{ varName: itemVar, obj: firstItem },
|
|
4294
|
+
...ancestorVars.map((varName) => ({ varName, obj: ancestorObjByVar.get(varName) })),
|
|
4295
|
+
];
|
|
4213
4296
|
// Each target's chain variables and merge result get their own
|
|
4214
4297
|
// suffixed local names so multiple independent targets sharing the
|
|
4215
4298
|
// same loop body can each declare their own locals without
|
|
4216
4299
|
// colliding on `foldMatches`/`foldMatch`. The overwhelmingly common
|
|
4217
4300
|
// single-target case keeps the original unsuffixed names.
|
|
4218
4301
|
const suffix = foldPlan.targets.length > 1 ? `${planSuffix}${targetIndex}` : planSuffix;
|
|
4219
|
-
const
|
|
4220
|
-
|
|
4221
|
-
// (URL query params) or as an already-generic `${payload.<field>}`
|
|
4222
|
-
// reference (top-level JSON body keys — see
|
|
4223
|
-
// applyPayloadKeyValueSubstitutions, which payload-ifies every scalar
|
|
4224
|
-
// body key regardless of length, running BEFORE this fold branch ever
|
|
4225
|
-
// sees the value). Both must resolve to the loop item's own field, not
|
|
4226
|
-
// a caller-supplied payload value shared across every iteration.
|
|
4302
|
+
const scopedAccessor = (varName, field) => `${varName}${pathToAccessor(field.split("."), { assertNonNull: false })}`;
|
|
4303
|
+
const joinAccessor = (field) => scopedAccessor(itemVar, field);
|
|
4227
4304
|
// Word-boundary anchored: a plain `.split(value).join(...)` would also
|
|
4228
4305
|
// rewrite unrelated substrings that happen to contain the join value
|
|
4229
4306
|
// (e.g. a "p1" product id colliding with a "/v1/" path segment or a
|
|
4230
4307
|
// "p10" sibling id), corrupting parts of the request the join field
|
|
4231
4308
|
// never touched.
|
|
4232
4309
|
const replaceWholeValue = (haystack, value, replacement) => haystack.replace(new RegExp(`\\b${value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "g"), replacement);
|
|
4233
|
-
const parameterize = (text
|
|
4234
|
-
|
|
4235
|
-
//
|
|
4236
|
-
//
|
|
4237
|
-
//
|
|
4238
|
-
//
|
|
4239
|
-
//
|
|
4240
|
-
//
|
|
4241
|
-
//
|
|
4242
|
-
//
|
|
4243
|
-
//
|
|
4244
|
-
//
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
.
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
const
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4310
|
+
const parameterize = (text, chainCapture) => {
|
|
4311
|
+
// A join field can reach the render either as the raw captured
|
|
4312
|
+
// literal (URL query params) or as an already-generic
|
|
4313
|
+
// `${payload.<field>}` reference (top-level JSON body keys — see
|
|
4314
|
+
// applyPayloadKeyValueSubstitutions, which payload-ifies every
|
|
4315
|
+
// scalar body key regardless of length, running BEFORE this fold
|
|
4316
|
+
// branch ever sees the value). Both must resolve to the loop
|
|
4317
|
+
// item's (or ancestor's) own field, not a caller-supplied payload
|
|
4318
|
+
// value shared across every iteration. Widened beyond
|
|
4319
|
+
// `target.joinFields` to every field this specific chain hop's own
|
|
4320
|
+
// captured request actually varies on (via findThreadedJoinFields,
|
|
4321
|
+
// searched across the item AND every ancestor binding) — a param
|
|
4322
|
+
// living only on a parent object is otherwise structurally
|
|
4323
|
+
// invisible and gets frozen as a literal.
|
|
4324
|
+
const threadedFields = dedupeThreadedFields([
|
|
4325
|
+
...target.joinFields.map((field) => ({ varName: itemVar, field })),
|
|
4326
|
+
...findThreadedJoinFields(threadingScopes, chainCapture),
|
|
4327
|
+
]);
|
|
4328
|
+
const result = threadedFields.reduce((acc, { varName, field }) => {
|
|
4329
|
+
const replacement = `\${${scopedAccessor(varName, field)}}`;
|
|
4330
|
+
// applyPayloadKeyValueSubstitutions only ever names a payload
|
|
4331
|
+
// accessor after the DRILL REQUEST's own top-level JSON key
|
|
4332
|
+
// (`${payload.sku}`), never after `field`'s dot path into the
|
|
4333
|
+
// PRIMARY ITEM — those are unrelated structures that only
|
|
4334
|
+
// happen to share a leaf name for a top-level join field. A
|
|
4335
|
+
// nested join field (e.g. `identifiers.sku`) must therefore
|
|
4336
|
+
// also match on its bare last segment, or the accessor swap
|
|
4337
|
+
// silently no-ops and leaves an undefined `payload.sku`
|
|
4338
|
+
// reference behind once the literal value itself has already
|
|
4339
|
+
// been replaced by the payload-key-value pass.
|
|
4340
|
+
const lastSegment = field.split(".").pop();
|
|
4341
|
+
const withAccessorSwapped = acc
|
|
4342
|
+
.split(`\${payload.${field}}`)
|
|
4343
|
+
.join(replacement)
|
|
4344
|
+
.split(`\${payload.${lastSegment}}`)
|
|
4345
|
+
.join(replacement);
|
|
4346
|
+
const scopeObj = varName === itemVar ? firstItem : ancestorObjByVar.get(varName);
|
|
4347
|
+
const value = readValueAtPath(scopeObj, field.split("."));
|
|
4348
|
+
const stringValue = typeof value === "string" && value.length > 0
|
|
4349
|
+
? value
|
|
4350
|
+
: typeof value === "number" || typeof value === "boolean"
|
|
4351
|
+
? String(value)
|
|
4352
|
+
: null;
|
|
4353
|
+
return stringValue !== null
|
|
4354
|
+
? replaceWholeValue(withAccessorSwapped, stringValue, replacement)
|
|
4355
|
+
: withAccessorSwapped;
|
|
4356
|
+
}, text);
|
|
4357
|
+
assertNoFrozenVaryingDrillParams("emitMultiStepExecuteHttp", chainCapture, result, actions.map((a) => a.capture));
|
|
4358
|
+
return result;
|
|
4359
|
+
};
|
|
4261
4360
|
// Every chain step's response and produces are block-scoped to this
|
|
4262
4361
|
// `for` — they never escape to the rest of the function. That is
|
|
4263
4362
|
// exactly the constraint the previous (now-removed) throw enforced by
|
|
@@ -4270,10 +4369,10 @@ function emitMultiStepExecuteHttp(actions, inputBody, errorSignals, fieldNameMap
|
|
|
4270
4369
|
for (const chainIndex of target.chain) {
|
|
4271
4370
|
const chainStep = actions[chainIndex];
|
|
4272
4371
|
const chainRendered = rendered[chainIndex];
|
|
4273
|
-
lines.push(` const ${chainStep.varName} = (await httpClient(\`${parameterize(chainRendered.url)}\`, {`, ` method: ${JSON.stringify(chainRendered.method)},`);
|
|
4372
|
+
lines.push(` const ${chainStep.varName} = (await httpClient(\`${parameterize(chainRendered.url, chainStep.capture)}\`, {`, ` method: ${JSON.stringify(chainRendered.method)},`);
|
|
4274
4373
|
const joined = [
|
|
4275
|
-
parameterize(chainRendered.headersExpr),
|
|
4276
|
-
parameterize(chainRendered.bodyArg),
|
|
4374
|
+
parameterize(chainRendered.headersExpr, chainStep.capture),
|
|
4375
|
+
parameterize(chainRendered.bodyArg, chainStep.capture),
|
|
4277
4376
|
]
|
|
4278
4377
|
.filter((s) => s !== "")
|
|
4279
4378
|
.join(" ");
|
|
@@ -4295,7 +4394,7 @@ function emitMultiStepExecuteHttp(actions, inputBody, errorSignals, fieldNameMap
|
|
|
4295
4394
|
const terminalStep = actions[target.chainTerminalIndex];
|
|
4296
4395
|
lines.push(...emitFoldMatchAndMergeLines(terminalStep, target, itemVar, suffix, joinAccessor));
|
|
4297
4396
|
}
|
|
4298
|
-
lines.push(
|
|
4397
|
+
lines.push(...closeLines, "");
|
|
4299
4398
|
continue;
|
|
4300
4399
|
}
|
|
4301
4400
|
// Every other chain step (already fully emitted, inline, by the fold
|
|
@@ -4747,6 +4846,112 @@ function collectRequestStringValues(capture) {
|
|
|
4747
4846
|
}
|
|
4748
4847
|
return values;
|
|
4749
4848
|
}
|
|
4849
|
+
/**
|
|
4850
|
+
* Finds every query param or JSON body leaf on `capture`'s own request that
|
|
4851
|
+
* (a) was left as a literal in `renderedText` — never swapped for a
|
|
4852
|
+
* `${...}` accessor by the threading pass above — and (b) took a different
|
|
4853
|
+
* value on some OTHER capture matching the exact same endpoint (same
|
|
4854
|
+
* origin+pathname, via {@link endpointKey}) anywhere in the run. Freezing
|
|
4855
|
+
* such a value bakes ONE capture's drill parameter into every fold
|
|
4856
|
+
* iteration's request, exactly the defect described in
|
|
4857
|
+
* docs/recon-generate-nested-fold-flatmaps-away-the-parent-so-drill-params-freeze.md
|
|
4858
|
+
* (a `packageCode`/`groupId`/`sailDate` triple that provably varies per
|
|
4859
|
+
* cruise, silently frozen because no threaded field explained it). Path
|
|
4860
|
+
* segments are deliberately not checked: {@link endpointKey} requires an
|
|
4861
|
+
* identical pathname to group two captures at all, so no path segment can
|
|
4862
|
+
* ever be observed to vary within a matched group.
|
|
4863
|
+
*/
|
|
4864
|
+
function findFrozenVaryingDrillParams(capture, renderedText, allCaptures) {
|
|
4865
|
+
const key = endpointKey(capture.url);
|
|
4866
|
+
const sameEndpointCaptures = allCaptures.filter((c) => c !== capture && endpointKey(c.url) === key);
|
|
4867
|
+
if (sameEndpointCaptures.length === 0)
|
|
4868
|
+
return [];
|
|
4869
|
+
const isFrozenLiteral = (value) => value.length > 0 && renderedText.includes(value);
|
|
4870
|
+
const frozen = [];
|
|
4871
|
+
try {
|
|
4872
|
+
const url = new URL(capture.url);
|
|
4873
|
+
for (const [paramKey, value] of url.searchParams.entries()) {
|
|
4874
|
+
if (!isFrozenLiteral(value))
|
|
4875
|
+
continue;
|
|
4876
|
+
const differing = sameEndpointCaptures
|
|
4877
|
+
.map((c) => {
|
|
4878
|
+
try {
|
|
4879
|
+
return new URL(c.url).searchParams.get(paramKey);
|
|
4880
|
+
}
|
|
4881
|
+
catch {
|
|
4882
|
+
return null;
|
|
4883
|
+
}
|
|
4884
|
+
})
|
|
4885
|
+
.find((v) => v !== null && v !== value);
|
|
4886
|
+
if (differing !== undefined) {
|
|
4887
|
+
frozen.push({
|
|
4888
|
+
location: "query parameter",
|
|
4889
|
+
key: paramKey,
|
|
4890
|
+
frozenValue: value,
|
|
4891
|
+
differingValue: differing,
|
|
4892
|
+
});
|
|
4893
|
+
}
|
|
4894
|
+
}
|
|
4895
|
+
}
|
|
4896
|
+
catch {
|
|
4897
|
+
// Relative or malformed URL — no query params to check.
|
|
4898
|
+
}
|
|
4899
|
+
const parsedBody = (() => {
|
|
4900
|
+
try {
|
|
4901
|
+
return typeof capture.requestPostData === "string" && capture.requestPostData.length > 0
|
|
4902
|
+
? JSON.parse(capture.requestPostData)
|
|
4903
|
+
: undefined;
|
|
4904
|
+
}
|
|
4905
|
+
catch {
|
|
4906
|
+
return undefined;
|
|
4907
|
+
}
|
|
4908
|
+
})();
|
|
4909
|
+
if (parsedBody !== undefined) {
|
|
4910
|
+
for (const { path, value } of walkAllPrimitiveLeaves(parsedBody)) {
|
|
4911
|
+
if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean")
|
|
4912
|
+
continue;
|
|
4913
|
+
const stringValue = String(value);
|
|
4914
|
+
if (!isFrozenLiteral(stringValue))
|
|
4915
|
+
continue;
|
|
4916
|
+
const fieldPath = path.join(".");
|
|
4917
|
+
const differing = sameEndpointCaptures
|
|
4918
|
+
.map((c) => {
|
|
4919
|
+
try {
|
|
4920
|
+
const otherBody = typeof c.requestPostData === "string" && c.requestPostData.length > 0
|
|
4921
|
+
? JSON.parse(c.requestPostData)
|
|
4922
|
+
: undefined;
|
|
4923
|
+
return otherBody === undefined ? undefined : readValueAtPath(otherBody, path);
|
|
4924
|
+
}
|
|
4925
|
+
catch {
|
|
4926
|
+
return undefined;
|
|
4927
|
+
}
|
|
4928
|
+
})
|
|
4929
|
+
.find((v) => v !== undefined && String(v) !== stringValue);
|
|
4930
|
+
if (differing !== undefined) {
|
|
4931
|
+
frozen.push({
|
|
4932
|
+
location: "body field",
|
|
4933
|
+
key: fieldPath,
|
|
4934
|
+
frozenValue: stringValue,
|
|
4935
|
+
differingValue: String(differing),
|
|
4936
|
+
});
|
|
4937
|
+
}
|
|
4938
|
+
}
|
|
4939
|
+
}
|
|
4940
|
+
return frozen;
|
|
4941
|
+
}
|
|
4942
|
+
/** Throws when {@link findFrozenVaryingDrillParams} finds any frozen-but-
|
|
4943
|
+
* varying literal — shared by {@link parameterizeUrl} (below) and
|
|
4944
|
+
* `emitMultiStepExecuteHttp`'s own `parameterize` so the two emitters can't
|
|
4945
|
+
* drift on this guard. */
|
|
4946
|
+
function assertNoFrozenVaryingDrillParams(emitterName, capture, renderedText, allCaptures) {
|
|
4947
|
+
const frozen = findFrozenVaryingDrillParams(capture, renderedText, allCaptures);
|
|
4948
|
+
if (frozen.length === 0)
|
|
4949
|
+
return;
|
|
4950
|
+
const described = frozen
|
|
4951
|
+
.map((f) => `${f.location} "${f.key}" (froze "${f.frozenValue}", also captured as "${f.differingValue}")`)
|
|
4952
|
+
.join("; ");
|
|
4953
|
+
throw new Error(`${emitterName}: drill request to ${endpointKey(capture.url)} would freeze a value that varied across this run's own captures: ${described} — no threaded field (item or ancestor) explains it, so baking in one capture's literal would silently reuse it for every fold iteration; add the missing field to joinFields or make it resolvable from an ancestor binding`);
|
|
4954
|
+
}
|
|
4750
4955
|
/**
|
|
4751
4956
|
* Yields every string/numeric/boolean leaf reachable from `item` by walking nested
|
|
4752
4957
|
* plain objects only (not arrays — a join key is a scalar field of the item
|
|
@@ -4834,15 +5039,38 @@ function collectCandidateIndicesAscending(requestStringValueIndex, values, after
|
|
|
4834
5039
|
}
|
|
4835
5040
|
return [...candidates].sort((a, b) => a - b);
|
|
4836
5041
|
}
|
|
4837
|
-
|
|
5042
|
+
/** Drops duplicate `(varName, field)` pairs, keeping the first (innermost-
|
|
5043
|
+
* scope-first, by {@link findThreadedJoinFields}'s scope ordering)
|
|
5044
|
+
* occurrence — the same field can otherwise appear twice when both
|
|
5045
|
+
* `target.joinFields` and the request-value scan resolve to it. */
|
|
5046
|
+
function dedupeThreadedFields(fields) {
|
|
5047
|
+
const seen = new Set();
|
|
5048
|
+
return fields.filter(({ varName, field }) => {
|
|
5049
|
+
const key = `${varName}.${field}`;
|
|
5050
|
+
if (seen.has(key))
|
|
5051
|
+
return false;
|
|
5052
|
+
seen.add(key);
|
|
5053
|
+
return true;
|
|
5054
|
+
});
|
|
5055
|
+
}
|
|
5056
|
+
/** Every per-item field, across `scopes`, whose value also appears somewhere
|
|
5057
|
+
* in `drillCapture`'s own request (URL, body, headers) — the fields a drill
|
|
5058
|
+
* request actually threads out of its per-item scope(s), used both to widen
|
|
5059
|
+
* URL/body parameterization beyond a fold target's own `joinFields` and (via
|
|
5060
|
+
* a single-scope call) to disambiguate which candidate array a chained step
|
|
5061
|
+
* depends on. `scopes` is searched in the given order (innermost fold item
|
|
5062
|
+
* first, then each ancestor binding a nested loop keeps addressable), since
|
|
5063
|
+
* an ancestor field with the same name as an item field must not shadow the
|
|
5064
|
+
* item's own value. */
|
|
5065
|
+
function findThreadedJoinFields(scopes, drillCapture) {
|
|
4838
5066
|
const requestValues = collectRequestStringValues(drillCapture);
|
|
4839
5067
|
if (requestValues.size === 0)
|
|
4840
5068
|
return [];
|
|
4841
|
-
return [...walkItemFieldPaths(
|
|
5069
|
+
return scopes.flatMap(({ varName, obj }) => [...walkItemFieldPaths(obj)]
|
|
4842
5070
|
.filter(({ value: v }) => (typeof v === "string" && v.length > 0 && requestValues.has(v)) ||
|
|
4843
5071
|
(typeof v === "number" && requestValues.has(String(v))) ||
|
|
4844
5072
|
(typeof v === "boolean" && requestValues.has(String(v))))
|
|
4845
|
-
.map(({ path }) => path.join("."));
|
|
5073
|
+
.map(({ path }) => ({ varName, field: path.join(".") })));
|
|
4846
5074
|
}
|
|
4847
5075
|
/** Every string, numeric, and boolean leaf value present anywhere in a response —
|
|
4848
5076
|
* the set a chained drill-down step's request must overlap with for that
|
|
@@ -4918,7 +5146,7 @@ function selectDisambiguatedCandidate(responseBody, capture) {
|
|
|
4918
5146
|
if (candidates.length === 0)
|
|
4919
5147
|
return null;
|
|
4920
5148
|
const requestValues = collectRequestValuesIncludingHeaders(capture);
|
|
4921
|
-
const threaded = candidates.find((candidate) => candidate.items.some((item) => findThreadedJoinFields(item, capture).length > 0));
|
|
5149
|
+
const threaded = candidates.find((candidate) => candidate.items.some((item) => findThreadedJoinFields([{ varName: "item", obj: item }], capture).length > 0));
|
|
4922
5150
|
if (threaded)
|
|
4923
5151
|
return threaded;
|
|
4924
5152
|
return candidates.reduce((richest, candidate) => {
|
|
@@ -5070,10 +5298,10 @@ function scanPrimaryCandidateGroups(actions, primaryIndex, globallyConsumedIndic
|
|
|
5070
5298
|
// Every item in THIS candidate array is searched, not just items[0]
|
|
5071
5299
|
// — a flow that only ever drilled into a later item (never the
|
|
5072
5300
|
// first) must still resolve.
|
|
5073
|
-
const primaryMatchedItemIndex = primaryArray.items.findIndex((item) => findThreadedJoinFields(item, drill.capture).length > 0);
|
|
5301
|
+
const primaryMatchedItemIndex = primaryArray.items.findIndex((item) => findThreadedJoinFields([{ varName: "item", obj: item }], drill.capture).length > 0);
|
|
5074
5302
|
if (primaryMatchedItemIndex === -1)
|
|
5075
5303
|
continue;
|
|
5076
|
-
const joinFields = findThreadedJoinFields(primaryArray.items[primaryMatchedItemIndex], drill.capture);
|
|
5304
|
+
const joinFields = findThreadedJoinFields([{ varName: "item", obj: primaryArray.items[primaryMatchedItemIndex] }], drill.capture).map((f) => f.field);
|
|
5077
5305
|
// Widened to a flat (non-array) object response when the drill step has
|
|
5078
5306
|
// no object-array field of its own — see
|
|
5079
5307
|
// findAllObjectArrayFieldsOrWholeObject. A detail-by-id response (e.g.
|
|
@@ -5341,6 +5569,32 @@ function objectItemsAtPath(body, path) {
|
|
|
5341
5569
|
const items = outer.flatMap((element) => objectItemsAtPath(element, after) ?? []);
|
|
5342
5570
|
return items.length > 0 ? items : null;
|
|
5343
5571
|
}
|
|
5572
|
+
/** Same flattening as {@link objectItemsAtPath}, but pairs each flattened
|
|
5573
|
+
* leaf item with the ordered (outer-to-inner) chain of ancestor objects a
|
|
5574
|
+
* `.flatMap` accessor would discard — one entry per {@link
|
|
5575
|
+
* ARRAY_WILDCARD_SEGMENT} crossed on the way down. Index-aligned with
|
|
5576
|
+
* {@link objectItemsAtPath}'s own output (same DFS/outer-array order), so a
|
|
5577
|
+
* `FoldTarget.primaryMatchedItemIndex` resolves the same leaf item either
|
|
5578
|
+
* way; this variant additionally exposes the ancestor objects a nested-loop
|
|
5579
|
+
* emission ({@link pathToFoldLoopLines}) binds to `g0`/`g1`/... so drill
|
|
5580
|
+
* threading can read a param off an ancestor scope, not only the item. */
|
|
5581
|
+
function objectItemsWithAncestorsAtPath(body, path, ancestors = []) {
|
|
5582
|
+
const wildcardIndex = path.indexOf(ARRAY_WILDCARD_SEGMENT);
|
|
5583
|
+
if (wildcardIndex === -1) {
|
|
5584
|
+
const value = readValueAtPath(body, path);
|
|
5585
|
+
if (Array.isArray(value)) {
|
|
5586
|
+
return value.filter(isObjectArrayItem).map((item) => ({ item, ancestors: [...ancestors] }));
|
|
5587
|
+
}
|
|
5588
|
+
return isObjectArrayItem(value) ? [{ item: value, ancestors: [...ancestors] }] : [];
|
|
5589
|
+
}
|
|
5590
|
+
const outer = readValueAtPath(body, path.slice(0, wildcardIndex));
|
|
5591
|
+
if (!Array.isArray(outer))
|
|
5592
|
+
return [];
|
|
5593
|
+
const after = path.slice(wildcardIndex + 1);
|
|
5594
|
+
return outer.flatMap((element) => isObjectArrayItem(element)
|
|
5595
|
+
? objectItemsWithAncestorsAtPath(element, after, [...ancestors, element])
|
|
5596
|
+
: []);
|
|
5597
|
+
}
|
|
5344
5598
|
/**
|
|
5345
5599
|
* Builds a {@link FoldPlan} from a flow-declared {@link FoldReturnSpec}, so a
|
|
5346
5600
|
* site author can express a fold the structural heuristic misses.
|
|
@@ -6605,15 +6859,15 @@ const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottlenec
|
|
|
6605
6859
|
const gqlVariablesExpr = gqlOperationName
|
|
6606
6860
|
? renderGqlVariablesExpr(gqlVariables, payloadFieldNames)
|
|
6607
6861
|
: "{ q: payload.query }";
|
|
6608
|
-
/** Builds the `for (
|
|
6609
|
-
* every resolved plan's drill-down data onto `dataVarName`'s
|
|
6610
|
-
* — the single-primary counterpart of
|
|
6611
|
-
* per-item loop, sharing its match/merge
|
|
6612
|
-
* {@link emitFoldMatchAndMergeLines} so the two can't describe
|
|
6613
|
-
* merge semantics. Chain hops beyond the drill step itself are
|
|
6614
|
-
* directly off each hop's own captured request (no state-
|
|
6615
|
-
* pipeline) since a single-primary read flow carries no
|
|
6616
|
-
* for those calls to reference.
|
|
6862
|
+
/** Builds the nested `for` loop block(s) — see {@link pathToFoldLoopLines}
|
|
6863
|
+
* — that fold every resolved plan's drill-down data onto `dataVarName`'s
|
|
6864
|
+
* primary array — the single-primary counterpart of
|
|
6865
|
+
* emitMultiStepExecuteHttp's own per-item loop, sharing its match/merge
|
|
6866
|
+
* tail via {@link emitFoldMatchAndMergeLines} so the two can't describe
|
|
6867
|
+
* different merge semantics. Chain hops beyond the drill step itself are
|
|
6868
|
+
* rendered directly off each hop's own captured request (no state-
|
|
6869
|
+
* threading pipeline) since a single-primary read flow carries no
|
|
6870
|
+
* submitted payload for those calls to reference.
|
|
6617
6871
|
*
|
|
6618
6872
|
* `itemsOverride`, when given, replaces the `dataVarName`+`primaryArrayPath`
|
|
6619
6873
|
* accessor with a caller-supplied items expression — used by the paginated
|
|
@@ -6623,73 +6877,87 @@ const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottlenec
|
|
|
6623
6877
|
* response object. `foldPlan.primaryArrayPath` may still declare depth
|
|
6624
6878
|
* BELOW `itemsOverride.level` (a fold plan resolved against a nested array
|
|
6625
6879
|
* inside each paginated item) — the residual suffix beyond `level` is
|
|
6626
|
-
* descended into via the same
|
|
6627
|
-
*
|
|
6628
|
-
*
|
|
6880
|
+
* descended into via the same nested-loop emission the non-override branch
|
|
6881
|
+
* already uses, so no depth the fold plan declares is silently dropped.
|
|
6882
|
+
* Ancestor bindings inside `level` itself have no runtime loop variable
|
|
6883
|
+
* (the paginated merge already flattened across them), so only the
|
|
6884
|
+
* residual's OWN ancestor crossings are addressable for threading here. */
|
|
6629
6885
|
const buildFoldMergeLines = (dataVarName, itemsOverride) => {
|
|
6630
6886
|
const lines = [];
|
|
6631
6887
|
for (const [planIndex, foldPlan] of singlePrimaryFoldPlans.entries()) {
|
|
6632
6888
|
const primaryStep = actionSteps[foldPlan.primaryStepIndex];
|
|
6633
6889
|
if (!primaryStep)
|
|
6634
6890
|
continue;
|
|
6635
|
-
const
|
|
6891
|
+
const primaryItemsWithAncestors = objectItemsWithAncestorsAtPath(primaryStep.capture.responseBody, foldPlan.primaryArrayPath);
|
|
6636
6892
|
const primaryArrType = foldArrayAssertionType(foldPlan.primaryArrayPath);
|
|
6637
6893
|
const planSuffix = singlePrimaryFoldPlans.length > 1 ? String(planIndex) : "";
|
|
6638
|
-
const
|
|
6639
|
-
const
|
|
6894
|
+
const itemVar = `item${planSuffix}`;
|
|
6895
|
+
const residualPath = itemsOverride
|
|
6640
6896
|
? (() => {
|
|
6641
|
-
const {
|
|
6897
|
+
const { level } = itemsOverride;
|
|
6642
6898
|
const levelPrefixesPrimaryArrayPath = level.length <= foldPlan.primaryArrayPath.length &&
|
|
6643
6899
|
level.every((segment, i) => segment === foldPlan.primaryArrayPath[i]);
|
|
6644
6900
|
if (!levelPrefixesPrimaryArrayPath) {
|
|
6645
6901
|
throw new Error(`emitContractTs: fold plan primary array path ${foldPlan.primaryArrayPath.join(".")} no longer extends the paginated collection's own array path ${level.join(".")} — the fold plan and this emitter have drifted out of sync`);
|
|
6646
6902
|
}
|
|
6647
|
-
|
|
6648
|
-
// ARRAY_WILDCARD_SEGMENT whenever it crosses the array boundary
|
|
6649
|
-
// `level` itself sits at (by construction, since `level` names
|
|
6650
|
-
// an array path) — `expr` stands in for that same array, so
|
|
6651
|
-
// pathToFoldAccessorExpr's own flatMap over `expr` consumes it;
|
|
6652
|
-
// prepending a second one here would double-flatten.
|
|
6653
|
-
const residualPath = foldPlan.primaryArrayPath.slice(level.length);
|
|
6654
|
-
if (residualPath.length === 0)
|
|
6655
|
-
return expr;
|
|
6656
|
-
const itemTypeExpr = `${pathAccessTypeExpr(`${pascal}Response`, level)}[number]`;
|
|
6657
|
-
return pathToFoldAccessorExpr(`(${expr} as ${itemTypeExpr}[])`, residualPath);
|
|
6903
|
+
return foldPlan.primaryArrayPath.slice(level.length);
|
|
6658
6904
|
})()
|
|
6659
|
-
:
|
|
6660
|
-
const
|
|
6661
|
-
|
|
6905
|
+
: foldPlan.primaryArrayPath;
|
|
6906
|
+
const { openLines, closeLines, ancestorVars } = itemsOverride
|
|
6907
|
+
? pathToFoldLoopLines(residualPath.length === 0
|
|
6908
|
+
? itemsOverride.expr
|
|
6909
|
+
: `(${itemsOverride.expr} as ${pathAccessTypeExpr(`${pascal}Response`, itemsOverride.level)}[number][])`, residualPath, itemVar, " ", planSuffix)
|
|
6910
|
+
: pathToFoldLoopLines(`(${dataVarName} as ${primaryArrType})`, foldPlan.primaryArrayPath, itemVar, " ", planSuffix);
|
|
6911
|
+
// Ancestor bindings within `itemsOverride.level` (if any) have no
|
|
6912
|
+
// runtime loop variable, since the paginated merge already flattened
|
|
6913
|
+
// across them — only the trailing `residualPath.length` ancestor
|
|
6914
|
+
// crossings correspond to `ancestorVars` above, so slice the
|
|
6915
|
+
// design-time ancestor chain to match.
|
|
6916
|
+
const residualAncestorCount = residualPath.filter((s) => s === ARRAY_WILDCARD_SEGMENT).length;
|
|
6917
|
+
lines.push(...openLines);
|
|
6662
6918
|
for (const [targetIndex, target] of foldPlan.targets.entries()) {
|
|
6663
|
-
const
|
|
6664
|
-
if (!
|
|
6919
|
+
const matchedPrimaryItem = primaryItemsWithAncestors[target.primaryMatchedItemIndex];
|
|
6920
|
+
if (!matchedPrimaryItem) {
|
|
6665
6921
|
throw new Error(`emitContractTs: fold plan primary step ${foldPlan.primaryStepIndex} no longer resolves an object array at ${foldPlan.primaryArrayPath.join(".")} — the fold plan and this emitter have drifted out of sync`);
|
|
6666
6922
|
}
|
|
6923
|
+
const { item: firstItem, ancestors: fullAncestors } = matchedPrimaryItem;
|
|
6924
|
+
const residualAncestors = fullAncestors.slice(fullAncestors.length - residualAncestorCount);
|
|
6925
|
+
const ancestorObjByVar = new Map(ancestorVars.map((varName, idx) => [varName, residualAncestors[idx]]));
|
|
6926
|
+
const threadingScopes = [
|
|
6927
|
+
{ varName: itemVar, obj: firstItem },
|
|
6928
|
+
...ancestorVars.map((varName) => ({ varName, obj: ancestorObjByVar.get(varName) })),
|
|
6929
|
+
];
|
|
6667
6930
|
const suffix = foldPlan.targets.length > 1 ? `${planSuffix}${targetIndex}` : planSuffix;
|
|
6668
|
-
const
|
|
6931
|
+
const scopedAccessor = (varName, field) => `${varName}${pathToAccessor(field.split("."), { assertNonNull: false })}`;
|
|
6932
|
+
const joinAccessor = (field) => scopedAccessor(itemVar, field);
|
|
6669
6933
|
// Same word-boundary-anchored swap emitMultiStepExecuteHttp's own
|
|
6670
6934
|
// `parameterize` performs — a plain split/join would also rewrite
|
|
6671
6935
|
// unrelated substrings that happen to contain the join value.
|
|
6672
6936
|
//
|
|
6673
6937
|
// Parameterizes off EVERY per-item field this specific chain hop's
|
|
6674
|
-
// own captured request actually varies on (via findThreadedJoinFields
|
|
6675
|
-
//
|
|
6676
|
-
//
|
|
6938
|
+
// own captured request actually varies on (via findThreadedJoinFields,
|
|
6939
|
+
// searched across the item AND every ancestor binding), not just
|
|
6940
|
+
// `target.joinFields` — `target.joinFields` names the field used to
|
|
6941
|
+
// MATCH the drill's RESPONSE back onto the primary item (see
|
|
6677
6942
|
// emitFoldMatchAndMergeLines), which for a spec-declared foldReturn
|
|
6678
6943
|
// can legitimately be a field the request never carries at all (e.g.
|
|
6679
6944
|
// an `id` echoed only in the response, while the request is keyed by
|
|
6680
6945
|
// an unrelated field like a package code). Building the URL from only
|
|
6681
6946
|
// `target.joinFields` in that case would leave it unparameterized —
|
|
6682
|
-
// every item would fetch the SAME first-captured URL.
|
|
6947
|
+
// every item would fetch the SAME first-captured URL. Searching
|
|
6948
|
+
// ancestor scopes too is what lets a param that only lives on a
|
|
6949
|
+
// parent object (e.g. a group id) resolve at all.
|
|
6683
6950
|
const parameterizeUrl = (rawUrl, chainCapture) => {
|
|
6684
6951
|
const withBase = baseUrl.length > 0 && rawUrl.startsWith(baseUrl)
|
|
6685
6952
|
? `\${context.baseUrl}${rawUrl.slice(baseUrl.length)}`
|
|
6686
6953
|
: rawUrl;
|
|
6687
|
-
const threadedFields =
|
|
6688
|
-
...target.joinFields,
|
|
6689
|
-
...findThreadedJoinFields(
|
|
6954
|
+
const threadedFields = dedupeThreadedFields([
|
|
6955
|
+
...target.joinFields.map((field) => ({ varName: itemVar, field })),
|
|
6956
|
+
...findThreadedJoinFields(threadingScopes, chainCapture),
|
|
6690
6957
|
]);
|
|
6691
|
-
|
|
6692
|
-
const
|
|
6958
|
+
const result = threadedFields.reduce((acc, { varName, field }) => {
|
|
6959
|
+
const scopeObj = varName === itemVar ? firstItem : ancestorObjByVar.get(varName);
|
|
6960
|
+
const value = readValueAtPath(scopeObj, field.split("."));
|
|
6693
6961
|
const stringValue = typeof value === "string" && value.length > 0
|
|
6694
6962
|
? value
|
|
6695
6963
|
: typeof value === "number" || typeof value === "boolean"
|
|
@@ -6697,8 +6965,10 @@ const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottlenec
|
|
|
6697
6965
|
: null;
|
|
6698
6966
|
if (stringValue === null)
|
|
6699
6967
|
return acc;
|
|
6700
|
-
return acc.replace(new RegExp(`\\b${stringValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "g"), `\${${
|
|
6968
|
+
return acc.replace(new RegExp(`\\b${stringValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "g"), `\${${scopedAccessor(varName, field)}}`);
|
|
6701
6969
|
}, withBase);
|
|
6970
|
+
assertNoFrozenVaryingDrillParams("emitContractTs", chainCapture, result, actionSteps.map((s) => s.capture));
|
|
6971
|
+
return result;
|
|
6702
6972
|
};
|
|
6703
6973
|
for (const chainIndex of target.chain) {
|
|
6704
6974
|
const chainStep = actionSteps[chainIndex];
|
|
@@ -6718,7 +6988,7 @@ const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottlenec
|
|
|
6718
6988
|
continue;
|
|
6719
6989
|
lines.push(...emitFoldMatchAndMergeLines(terminalStep, target, itemVar, suffix, joinAccessor));
|
|
6720
6990
|
}
|
|
6721
|
-
lines.push(
|
|
6991
|
+
lines.push(...closeLines, "");
|
|
6722
6992
|
}
|
|
6723
6993
|
return lines;
|
|
6724
6994
|
};
|