@enricai/barnacle 1.12.76 → 1.12.77
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/phantom-click.d.ts +21 -3
- package/dist/scraper/phantom-click.d.ts.map +1 -1
- package/dist/scraper/phantom-click.js +81 -5
- package/dist/scraper/phantom-click.js.map +1 -1
- package/dist/scripts/recon-browser.d.ts +4 -1
- package/dist/scripts/recon-browser.d.ts.map +1 -1
- package/dist/scripts/recon-browser.js +5 -5
- package/dist/scripts/recon-browser.js.map +1 -1
- package/dist/scripts/recon-generate.d.ts +36 -2
- package/dist/scripts/recon-generate.d.ts.map +1 -1
- package/dist/scripts/recon-generate.js +234 -21
- package/dist/scripts/recon-generate.js.map +1 -1
- package/package.json +2 -2
|
@@ -66,6 +66,7 @@ exports.collectUnboundLiterals = collectUnboundLiterals;
|
|
|
66
66
|
exports.emitMultiStepExecuteHttp = emitMultiStepExecuteHttp;
|
|
67
67
|
exports.countSpliceableFacets = countSpliceableFacets;
|
|
68
68
|
exports.spliceFacetsIntoStringVariable = spliceFacetsIntoStringVariable;
|
|
69
|
+
exports.spliceFacetsIntoArrayVariable = spliceFacetsIntoArrayVariable;
|
|
69
70
|
exports.findAllObjectArrayFields = findAllObjectArrayFields;
|
|
70
71
|
exports.findAllObjectArrayFieldsOrWholeObject = findAllObjectArrayFieldsOrWholeObject;
|
|
71
72
|
exports.applyDrillParamBindings = applyDrillParamBindings;
|
|
@@ -508,7 +509,7 @@ function extractNavigateToFacetOrder(flowSteps) {
|
|
|
508
509
|
previousNavigateToHash = currentHash;
|
|
509
510
|
if (value === null || value.length === 0)
|
|
510
511
|
continue;
|
|
511
|
-
order.push({ value, field: step.payloadField });
|
|
512
|
+
order.push({ value, field: step.payloadField, optional: step.optional === true });
|
|
512
513
|
}
|
|
513
514
|
return order;
|
|
514
515
|
}
|
|
@@ -3971,7 +3972,12 @@ function locateFormEnvelopePath(parsedBody) {
|
|
|
3971
3972
|
* and swap for `${item.<field>}`. Freezing either into an opaque
|
|
3972
3973
|
* `${JSON.stringify(payload.tokens)}` blob here would silently drop the
|
|
3973
3974
|
* value the request depends on, breaking the fold at exactly the case an
|
|
3974
|
-
* ARRAY/OBJECT-wrapped join field represents.
|
|
3975
|
+
* ARRAY/OBJECT-wrapped join field represents. A candidate whose leaves
|
|
3976
|
+
* include a value already registered as a payload-accessor literal (e.g. a
|
|
3977
|
+
* facet value a prior step derived and already wired for splicing elsewhere,
|
|
3978
|
+
* such as a `departmentCode` recurring inside a `tags` array) is excluded on
|
|
3979
|
+
* the same unconditional basis — it is already-threaded text waiting on the
|
|
3980
|
+
* substring-splice pass, not caller-supplied history data.
|
|
3975
3981
|
*
|
|
3976
3982
|
* A body with no detected form envelope worth swallowing into (a facet/
|
|
3977
3983
|
* search body — all scalars, if any, sit directly at the root) still needs
|
|
@@ -3982,7 +3988,7 @@ function locateFormEnvelopePath(parsedBody) {
|
|
|
3982
3988
|
*
|
|
3983
3989
|
* Site-agnostic: operates only on the recon body's own shape.
|
|
3984
3990
|
*
|
|
3985
|
-
* A top-level-ARRAY-shaped body (e.g. a
|
|
3991
|
+
* A top-level-ARRAY-shaped body (e.g. a travel-brand multi-room search that
|
|
3986
3992
|
* batches per-room criteria as `[{...}, {...}]`) is walked element by
|
|
3987
3993
|
* element, applying this same envelope logic to each object element in
|
|
3988
3994
|
* textual order — otherwise a structured field living inside an array
|
|
@@ -3997,7 +4003,35 @@ function locateFormEnvelopePath(parsedBody) {
|
|
|
3997
4003
|
* search there stops an earlier element's already-rewritten (or still-frozen)
|
|
3998
4004
|
* span from swallowing a later element's identically-named key.
|
|
3999
4005
|
*/
|
|
4000
|
-
function
|
|
4006
|
+
function isTokenBoundaryChar(ch) {
|
|
4007
|
+
return ch === undefined || !/[A-Za-z0-9]/.test(ch);
|
|
4008
|
+
}
|
|
4009
|
+
/**
|
|
4010
|
+
* True when `needle` occurs in `haystack` at a token boundary (flanked only
|
|
4011
|
+
* by non-alphanumeric characters or the string's own edges) — e.g. a
|
|
4012
|
+
* navigateTo facet's bare literal recurring inside a captured array element
|
|
4013
|
+
* that suffixes it with a constant delimiter string (`"<literal>;filterId=
|
|
4014
|
+
* urlFriendlyId"`). Exact equality alone misses this recurrence shape since
|
|
4015
|
+
* the leaf carries the delimiter-joined text, not the bare literal. Mirrors
|
|
4016
|
+
* the same boundary discipline `bindsWithoutCollision` already applies when
|
|
4017
|
+
* registering a value for substring splicing in the first place.
|
|
4018
|
+
*/
|
|
4019
|
+
function containsValueAtTokenBoundary(haystack, needle) {
|
|
4020
|
+
if (needle.length === 0 || needle === haystack)
|
|
4021
|
+
return false;
|
|
4022
|
+
let from = 0;
|
|
4023
|
+
while (true) {
|
|
4024
|
+
const at = haystack.indexOf(needle, from);
|
|
4025
|
+
if (at === -1)
|
|
4026
|
+
return false;
|
|
4027
|
+
if (isTokenBoundaryChar(haystack[at - 1]) &&
|
|
4028
|
+
isTokenBoundaryChar(haystack[at + needle.length])) {
|
|
4029
|
+
return true;
|
|
4030
|
+
}
|
|
4031
|
+
from = at + needle.length;
|
|
4032
|
+
}
|
|
4033
|
+
}
|
|
4034
|
+
function applyStructuredValuePayloadSubstitutionsForEnvelope(template, envelope, outStructuredKeys, unconditionalExcludeValues, restrictedExcludeSourceByValue, searchFrom, envelopePath, rootBody, payloadAccessorExcludePattern) {
|
|
4001
4035
|
let result = template;
|
|
4002
4036
|
let cursor = searchFrom;
|
|
4003
4037
|
// The body root's own entries are eligible too, whenever the located
|
|
@@ -4019,7 +4053,9 @@ function applyStructuredValuePayloadSubstitutionsForEnvelope(template, envelope,
|
|
|
4019
4053
|
Object.keys(value).length > 0;
|
|
4020
4054
|
if (!isNonEmptyArray && !isNestedObject)
|
|
4021
4055
|
continue;
|
|
4022
|
-
if (unconditionalExcludeValues.size > 0 ||
|
|
4056
|
+
if (unconditionalExcludeValues.size > 0 ||
|
|
4057
|
+
restrictedExcludeSourceByValue.size > 0 ||
|
|
4058
|
+
payloadAccessorExcludePattern !== null) {
|
|
4023
4059
|
// A name-free join/chain value (unconditionalExcludeValues) excludes the
|
|
4024
4060
|
// whole field on ANY leaf coincidence, same as before. A RESTRICTED prior-
|
|
4025
4061
|
// step value (one with a real source field name) additionally requires
|
|
@@ -4030,12 +4066,39 @@ function applyStructuredValuePayloadSubstitutionsForEnvelope(template, envelope,
|
|
|
4030
4066
|
// count/id) merely equalling SOME other step's produced value gets the
|
|
4031
4067
|
// ENTIRE array/object excluded from threading, even though nothing about
|
|
4032
4068
|
// that leaf's key names the same concept as the matched value's source.
|
|
4069
|
+
// A registered payload-accessor literal (payloadAccessorExcludePattern)
|
|
4070
|
+
// excludes unconditionally too, but on a DELIMITED-SUBSTRING match, not
|
|
4071
|
+
// exact leaf equality — the reported shape is a facet value recurring
|
|
4072
|
+
// inside a captured array element with a constant suffix still attached
|
|
4073
|
+
// (e.g. a hash-derived facet embedded in a `tags` entry alongside a
|
|
4074
|
+
// trailing marker), so requiring the whole leaf to equal the bare facet
|
|
4075
|
+
// literal would miss exactly the case this exclusion exists for. Reuses
|
|
4076
|
+
// {@link buildValueAlternationPattern}'s own token-boundary rule so a
|
|
4077
|
+
// facet value that merely happens to be a substring of an unrelated,
|
|
4078
|
+
// dash/dot-concatenated compound token is still correctly left alone.
|
|
4079
|
+
//
|
|
4080
|
+
// Beyond exact equality, a leaf that carries an excluded value as a
|
|
4081
|
+
// token-bounded SUBSTRING (e.g. a navigateTo facet literal recurring
|
|
4082
|
+
// inside an array element suffixed by a constant delimiter string, like
|
|
4083
|
+
// `"<literal>;filterId=urlFriendlyId"`) must exclude the field too —
|
|
4084
|
+
// otherwise the field gets frozen wholesale before the later
|
|
4085
|
+
// literal-value interpolation pass (which operates on text, not parsed
|
|
4086
|
+
// values) ever gets a chance to splice the facet out of that element.
|
|
4033
4087
|
const carriesThreadedValue = [...walkAllPrimitiveLeaves(value)].some(({ value: leaf, path: leafPath }) => {
|
|
4034
4088
|
if (leaf === null)
|
|
4035
4089
|
return false;
|
|
4036
4090
|
const leafValue = String(leaf);
|
|
4037
4091
|
if (unconditionalExcludeValues.has(leafValue))
|
|
4038
4092
|
return true;
|
|
4093
|
+
if (payloadAccessorExcludePattern !== null) {
|
|
4094
|
+
payloadAccessorExcludePattern.lastIndex = 0;
|
|
4095
|
+
if (payloadAccessorExcludePattern.test(leafValue))
|
|
4096
|
+
return true;
|
|
4097
|
+
}
|
|
4098
|
+
for (const excluded of unconditionalExcludeValues) {
|
|
4099
|
+
if (containsValueAtTokenBoundary(leafValue, excluded))
|
|
4100
|
+
return true;
|
|
4101
|
+
}
|
|
4039
4102
|
const sourceName = restrictedExcludeSourceByValue.get(leafValue);
|
|
4040
4103
|
if (sourceName === undefined)
|
|
4041
4104
|
return false;
|
|
@@ -4083,6 +4146,19 @@ function applyStructuredValuePayloadSubstitutionsForEnvelope(template, envelope,
|
|
|
4083
4146
|
}
|
|
4084
4147
|
if (spanEnd === -1)
|
|
4085
4148
|
continue;
|
|
4149
|
+
// An earlier pass in this same pipeline run (e.g. the optional-facet
|
|
4150
|
+
// array-element splice) may have already spliced a `${payload.<field>}`
|
|
4151
|
+
// accessor into this exact span — e.g. turning one element of a
|
|
4152
|
+
// search/filter array into `...(payload.sortOrder ? [...] : [])`. The
|
|
4153
|
+
// exclusion-source checks above only ever see the ORIGINAL parsed leaf
|
|
4154
|
+
// values, so they can't detect this; inspecting the located span's
|
|
4155
|
+
// CURRENT text catches it and leaves the pre-spliced accessor intact
|
|
4156
|
+
// instead of clobbering it with a wholesale replacement. Matching on the
|
|
4157
|
+
// literal template-placeholder prefix `${payload.` (rather than the bare
|
|
4158
|
+
// substring `payload.`) avoids false-positiving on scraped leaf data that
|
|
4159
|
+
// merely happens to contain the word "payload." as ordinary text.
|
|
4160
|
+
if (result.slice(spanStart, spanEnd).includes("${payload."))
|
|
4161
|
+
continue;
|
|
4086
4162
|
const replacement = `$${"{"}JSON.stringify(payload.${key})${"}"}`;
|
|
4087
4163
|
result = result.slice(0, spanStart) + replacement + result.slice(spanEnd);
|
|
4088
4164
|
cursor = Math.max(cursor, spanStart + replacement.length);
|
|
@@ -4098,7 +4174,7 @@ function applyStructuredValuePayloadSubstitutionsForEnvelope(template, envelope,
|
|
|
4098
4174
|
* pass to it. Shared by the top-level-object and top-level-array branches of
|
|
4099
4175
|
* {@link applyStructuredValuePayloadSubstitutions} below.
|
|
4100
4176
|
*/
|
|
4101
|
-
function applyStructuredValuePayloadSubstitutionsForObjectBody(template, objectBody, outStructuredKeys, unconditionalExcludeValues, restrictedExcludeSourceByValue, searchFrom) {
|
|
4177
|
+
function applyStructuredValuePayloadSubstitutionsForObjectBody(template, objectBody, outStructuredKeys, unconditionalExcludeValues, restrictedExcludeSourceByValue, searchFrom, payloadAccessorExcludePattern) {
|
|
4102
4178
|
const envelopePath = locateFormEnvelopePath(objectBody);
|
|
4103
4179
|
let envelope = objectBody;
|
|
4104
4180
|
for (const seg of envelopePath) {
|
|
@@ -4109,7 +4185,7 @@ function applyStructuredValuePayloadSubstitutionsForObjectBody(template, objectB
|
|
|
4109
4185
|
if (envelope === null || typeof envelope !== "object" || Array.isArray(envelope)) {
|
|
4110
4186
|
return { result: template, nextSearchFrom: searchFrom };
|
|
4111
4187
|
}
|
|
4112
|
-
return applyStructuredValuePayloadSubstitutionsForEnvelope(template, envelope, outStructuredKeys, unconditionalExcludeValues, restrictedExcludeSourceByValue, searchFrom, envelopePath, objectBody);
|
|
4188
|
+
return applyStructuredValuePayloadSubstitutionsForEnvelope(template, envelope, outStructuredKeys, unconditionalExcludeValues, restrictedExcludeSourceByValue, searchFrom, envelopePath, objectBody, payloadAccessorExcludePattern);
|
|
4113
4189
|
}
|
|
4114
4190
|
/**
|
|
4115
4191
|
* `priorStepStateBindings` carries the SAME value→binding map
|
|
@@ -4118,13 +4194,23 @@ function applyStructuredValuePayloadSubstitutionsForObjectBody(template, objectB
|
|
|
4118
4194
|
* field when its own leaf's key {@link keyNamesCorrelate}s with that source
|
|
4119
4195
|
* name — an unrestricted binding (name-free, e.g. a bare array-index chain
|
|
4120
4196
|
* hop) or a `joinFieldValues` entry excludes unconditionally on any leaf
|
|
4121
|
-
* match, same as before.
|
|
4197
|
+
* match, same as before. A `payloadAccessorExcludeValues` key excludes
|
|
4198
|
+
* unconditionally too, but on a delimited-SUBSTRING match of the leaf (see
|
|
4199
|
+
* {@link applyStructuredValuePayloadSubstitutionsForEnvelope}), not exact
|
|
4200
|
+
* equality — the reported shape is a facet value recurring inside a captured
|
|
4201
|
+
* array element with a constant suffix still attached (e.g. a hash-derived
|
|
4202
|
+
* facet embedded in a `tags` entry alongside a trailing marker), so a
|
|
4203
|
+
* candidate field whose leaf merely CONTAINS one of those literals at a token
|
|
4204
|
+
* boundary is left alone here too, instead of being wholesale-swallowed into
|
|
4205
|
+
* an opaque `${JSON.stringify(payload.<key>)}` blob before the substring-
|
|
4206
|
+
* splice pass ({@link interpolateStateValues}) gets a chance to thread it.
|
|
4207
|
+
* Without the correlation requirement on the restricted source, an array/
|
|
4122
4208
|
* object field whose own unrelated short scalar leaf (a count/id) merely
|
|
4123
4209
|
* EQUALS some other step's produced value gets the whole field wrongly
|
|
4124
4210
|
* excluded from threading — the same value-coincidence class of bug
|
|
4125
4211
|
* {@link keyNamesCorrelate}'s other call sites already guard against.
|
|
4126
4212
|
*/
|
|
4127
|
-
function applyStructuredValuePayloadSubstitutions(template, parsedBody, outStructuredKeys, priorStepStateBindings = new Map(), joinFieldValues = new Set()) {
|
|
4213
|
+
function applyStructuredValuePayloadSubstitutions(template, parsedBody, outStructuredKeys, priorStepStateBindings = new Map(), joinFieldValues = new Set(), payloadAccessorExcludeValues = new Map()) {
|
|
4128
4214
|
if (parsedBody === null || typeof parsedBody !== "object") {
|
|
4129
4215
|
return template;
|
|
4130
4216
|
}
|
|
@@ -4136,7 +4222,10 @@ function applyStructuredValuePayloadSubstitutions(template, parsedBody, outStruc
|
|
|
4136
4222
|
else
|
|
4137
4223
|
unconditionalExcludeValues.add(value);
|
|
4138
4224
|
}
|
|
4139
|
-
|
|
4225
|
+
const payloadAccessorExcludePattern = payloadAccessorExcludeValues.size > 0
|
|
4226
|
+
? buildValueAlternationPattern([...payloadAccessorExcludeValues.keys()].sort((a, b) => b.length - a.length))
|
|
4227
|
+
: null;
|
|
4228
|
+
// A travel-brand-style multi-room/multi-guest search body commonly batches
|
|
4140
4229
|
// per-element criteria as a top-level JSON ARRAY rather than a single
|
|
4141
4230
|
// object. Walk each element in textual order (via the growing searchFrom
|
|
4142
4231
|
// cursor) instead of bailing out here — otherwise any structured field
|
|
@@ -4148,13 +4237,13 @@ function applyStructuredValuePayloadSubstitutions(template, parsedBody, outStruc
|
|
|
4148
4237
|
for (const element of parsedBody) {
|
|
4149
4238
|
if (element === null || typeof element !== "object" || Array.isArray(element))
|
|
4150
4239
|
continue;
|
|
4151
|
-
const { result: nextResult, nextSearchFrom } = applyStructuredValuePayloadSubstitutionsForObjectBody(result, element, outStructuredKeys, unconditionalExcludeValues, restrictedExcludeSourceByValue, cursor);
|
|
4240
|
+
const { result: nextResult, nextSearchFrom } = applyStructuredValuePayloadSubstitutionsForObjectBody(result, element, outStructuredKeys, unconditionalExcludeValues, restrictedExcludeSourceByValue, cursor, payloadAccessorExcludePattern);
|
|
4152
4241
|
result = nextResult;
|
|
4153
4242
|
cursor = nextSearchFrom;
|
|
4154
4243
|
}
|
|
4155
4244
|
return result;
|
|
4156
4245
|
}
|
|
4157
|
-
const { result } = applyStructuredValuePayloadSubstitutionsForObjectBody(template, parsedBody, outStructuredKeys, unconditionalExcludeValues, restrictedExcludeSourceByValue, 0);
|
|
4246
|
+
const { result } = applyStructuredValuePayloadSubstitutionsForObjectBody(template, parsedBody, outStructuredKeys, unconditionalExcludeValues, restrictedExcludeSourceByValue, 0, payloadAccessorExcludePattern);
|
|
4158
4247
|
return result;
|
|
4159
4248
|
}
|
|
4160
4249
|
/** Maximum length to guard against indexing massive blobs (HTML fragments,
|
|
@@ -5781,6 +5870,63 @@ responseBodiesForCapacitySignal = []) {
|
|
|
5781
5870
|
* steps declared any field to correlate against) or when a leaf's value has
|
|
5782
5871
|
* no `key:value` segment matching one of them.
|
|
5783
5872
|
*/
|
|
5873
|
+
/** Yields every array-valued leaf in `value` (at any object nesting depth) —
|
|
5874
|
+
* `{@link applyOptionalArrayFacetSplicePayloadSubstitutions}`'s raw-array
|
|
5875
|
+
* search surface. Does not recurse INTO an array's own elements (a facet
|
|
5876
|
+
* array is always a flat array of primitives in every observed recon shape),
|
|
5877
|
+
* matching {@link walkAllPrimitiveLeaves}'s depth-first walk of everything
|
|
5878
|
+
* else. */
|
|
5879
|
+
function* walkArrayLeaves(value, path = []) {
|
|
5880
|
+
if (value === null || typeof value !== "object")
|
|
5881
|
+
return;
|
|
5882
|
+
if (Array.isArray(value)) {
|
|
5883
|
+
yield { value, path };
|
|
5884
|
+
return;
|
|
5885
|
+
}
|
|
5886
|
+
for (const [key, inner] of Object.entries(value)) {
|
|
5887
|
+
yield* walkArrayLeaves(inner, [...path, key]);
|
|
5888
|
+
}
|
|
5889
|
+
}
|
|
5890
|
+
/**
|
|
5891
|
+
* REST-body counterpart to {@link spliceFacetsIntoArrayVariable}'s GraphQL
|
|
5892
|
+
* use: a navigateTo facet's literal recurring as one array element (suffixed
|
|
5893
|
+
* by a constant delimiter) inside a REST JSON body gets its element text
|
|
5894
|
+
* swapped for `${payload.<field>}` by the plain `interpolateStateValues`
|
|
5895
|
+
* splice later in the pipeline — but that pass only rewrites literal text
|
|
5896
|
+
* IN PLACE, so when the matched facet is optional it would leave the array's
|
|
5897
|
+
* frozen `undefined`-shaped element (and its JSON delimiter/comma) in the
|
|
5898
|
+
* output whenever the caller omits the field, instead of omitting the
|
|
5899
|
+
* element entirely.
|
|
5900
|
+
*
|
|
5901
|
+
* This pass runs first and, for any array that matches at least one OPTIONAL
|
|
5902
|
+
* facet, rewrites the array's own frozen `JSON.stringify(...)` occurrence in
|
|
5903
|
+
* the body template to a single `${JSON.stringify([...])}` splice point
|
|
5904
|
+
* built from {@link spliceFacetsIntoArrayVariable}'s per-element conditional-
|
|
5905
|
+
* emission shape — the same `...(payload.<field> ? [...] : [])` convention
|
|
5906
|
+
* {@link spliceFacetsIntoStringVariable} already uses for the `key:value`
|
|
5907
|
+
* grammar. A required-only match (no optional facet in that array) is left
|
|
5908
|
+
* for the later plain splice to handle exactly as before — this pass is a
|
|
5909
|
+
* no-op whenever nothing it rewrites would differ, so the existing
|
|
5910
|
+
* required-only REST array-element shape (recon-generate-navigateto-array-
|
|
5911
|
+
* element-facet-splice-e2e.test.ts) never changes.
|
|
5912
|
+
*/
|
|
5913
|
+
function applyOptionalArrayFacetSplicePayloadSubstitutions(template, inputBody, navigateToFacets) {
|
|
5914
|
+
if (!navigateToFacets.some((facet) => facet.optional))
|
|
5915
|
+
return template;
|
|
5916
|
+
if (inputBody === null || typeof inputBody !== "object")
|
|
5917
|
+
return template;
|
|
5918
|
+
let result = template;
|
|
5919
|
+
for (const { value } of walkArrayLeaves(inputBody)) {
|
|
5920
|
+
const spliced = spliceFacetsIntoArrayVariable(value, navigateToFacets);
|
|
5921
|
+
if (spliced === null || !spliced.includes("...(payload."))
|
|
5922
|
+
continue;
|
|
5923
|
+
const target = JSON.stringify(value);
|
|
5924
|
+
if (!result.includes(target))
|
|
5925
|
+
continue;
|
|
5926
|
+
result = result.split(target).join(`\${JSON.stringify(${spliced})}`);
|
|
5927
|
+
}
|
|
5928
|
+
return result;
|
|
5929
|
+
}
|
|
5784
5930
|
function applyFacetSplicePayloadSubstitutions(template, inputBody, payloadFieldNames, optionalFieldNames) {
|
|
5785
5931
|
if (payloadFieldNames.size === 0)
|
|
5786
5932
|
return template;
|
|
@@ -6285,7 +6431,14 @@ flowSteps = [], vocabulary = undefined) {
|
|
|
6285
6431
|
const appearsAnywhereInCapture = (value) => actions.some((action) => action.capture.url.includes(value) ||
|
|
6286
6432
|
(action.capture.requestPostData?.includes(value) ?? false) ||
|
|
6287
6433
|
Object.values(action.capture.requestHeaders).some((h) => h.includes(value)));
|
|
6288
|
-
|
|
6434
|
+
// Hoisted once so both the unreachable-facet fallback above and the
|
|
6435
|
+
// REST array-element optional splice below (applyOptionalArrayFacetSplice
|
|
6436
|
+
// PayloadSubstitutions) share the exact same (value, field, optional)
|
|
6437
|
+
// triples — the array splice must see each binding's `optional` flag
|
|
6438
|
+
// precisely as the flow declared it, same as the already-fixed GraphQL
|
|
6439
|
+
// array-variable path ({@link spliceFacetsIntoArrayVariable}).
|
|
6440
|
+
const navigateToFacetOrder = extractNavigateToFacetOrder(flowSteps);
|
|
6441
|
+
const unreachableNavigateToFacets = navigateToFacetOrder.filter(({ value }) => !appearsAnywhereInCapture(value));
|
|
6289
6442
|
if (unreachableNavigateToFacets.length > 0) {
|
|
6290
6443
|
const correlated = correlateUnreachableNavigateToFacets(unreachableNavigateToFacets, actions);
|
|
6291
6444
|
for (const { field } of correlated === null ? [] : unreachableNavigateToFacets) {
|
|
@@ -6554,6 +6707,16 @@ flowSteps = [], vocabulary = undefined) {
|
|
|
6554
6707
|
const rawBodyWithFacetSplices = parsedBody !== null
|
|
6555
6708
|
? applyFacetSplicePayloadSubstitutions(rawBodyWithFormSubs, parsedBody, restPayloadFieldNames, restOptionalFieldNames)
|
|
6556
6709
|
: rawBodyWithFormSubs;
|
|
6710
|
+
// Optional-facet array-element splice — a navigateTo facet literal
|
|
6711
|
+
// recurring as one element of an array field (not the `key:value`
|
|
6712
|
+
// grammar above) must drop its own element, not freeze an `undefined`,
|
|
6713
|
+
// when its binding is optional and the caller omits it. Runs BEFORE
|
|
6714
|
+
// Mechanism B (below) can swallow the whole array wholesale as an
|
|
6715
|
+
// unparameterized structured value — see
|
|
6716
|
+
// {@link applyOptionalArrayFacetSplicePayloadSubstitutions}.
|
|
6717
|
+
const rawBodyWithOptionalArrayFacetSplices = parsedBody !== null
|
|
6718
|
+
? applyOptionalArrayFacetSplicePayloadSubstitutions(rawBodyWithFacetSplices, parsedBody, navigateToFacetOrder)
|
|
6719
|
+
: rawBodyWithFacetSplices;
|
|
6557
6720
|
// Mechanism B — parameterize whole nested caller structures
|
|
6558
6721
|
// (experienceData/educationData history, opaque eventData) BEFORE value
|
|
6559
6722
|
// substitution reaches inside them: swallowing the entire array/object first
|
|
@@ -6564,7 +6727,7 @@ flowSteps = [], vocabulary = undefined) {
|
|
|
6564
6727
|
// dependent-drill-down join field (e.g. a bulk `{"tokens":[...]}` lookup)
|
|
6565
6728
|
// must stay reachable for state threading, not get frozen as caller data.
|
|
6566
6729
|
const rawBodyWithStructuredSubs = parsedBody !== null
|
|
6567
|
-
? applyStructuredValuePayloadSubstitutions(
|
|
6730
|
+
? applyStructuredValuePayloadSubstitutions(rawBodyWithOptionalArrayFacetSplices, parsedBody, outStructuredKeys, deriveStateVarByValue(prior, cap), joinFieldValuesByStep.get(i) ?? new Set(), payloadAccessorByValue)
|
|
6568
6731
|
: rawBodyWithFormSubs;
|
|
6569
6732
|
// Whole-value caller coordinates bind here — after structured subs, BEFORE
|
|
6570
6733
|
// state threading — so a composite coordinate (a jobLocation or jobSeqNo
|
|
@@ -7487,6 +7650,50 @@ function spliceFacetsIntoStringVariable(value, fields, optionalFields = new Set(
|
|
|
7487
7650
|
}
|
|
7488
7651
|
return `[${elements.join(", ")}].join("")`;
|
|
7489
7652
|
}
|
|
7653
|
+
/**
|
|
7654
|
+
* Splices a captured array-valued GraphQL variable's elements against
|
|
7655
|
+
* navigateTo facet bindings instead of the `key:value` segment grammar
|
|
7656
|
+
* {@link spliceFacetsIntoStringVariable} matches — an array element like
|
|
7657
|
+
* `widget-x;filterId=urlFriendlyId` carries a bare literal plus a constant
|
|
7658
|
+
* trailing delimiter, with no `key:` prefix to key off of. Matching is
|
|
7659
|
+
* token-boundary guarded the same way {@link bindsWithoutCollision} (recon-
|
|
7660
|
+
* generate.ts ~7223) guards persona bindings elsewhere in this file, so a
|
|
7661
|
+
* facet literal that's merely a substring of an unrelated element never
|
|
7662
|
+
* misfires. A matched binding whose `optional` flag is set is emitted as a
|
|
7663
|
+
* conditionally-included array element (`...(payload.<field> ? [...] : [])`)
|
|
7664
|
+
* instead of inline, the same optional-splice convention
|
|
7665
|
+
* {@link spliceFacetsIntoStringVariable} uses, so an absent optional facet
|
|
7666
|
+
* drops its element rather than emitting `undefined`.
|
|
7667
|
+
*/
|
|
7668
|
+
function spliceFacetsIntoArrayVariable(value, facets) {
|
|
7669
|
+
if (!Array.isArray(value))
|
|
7670
|
+
return null;
|
|
7671
|
+
const isAlnum = (ch) => ch !== undefined && /[A-Za-z0-9]/.test(ch);
|
|
7672
|
+
const matchFacet = (element) => facets.find(({ value: literal }) => {
|
|
7673
|
+
if (element === literal)
|
|
7674
|
+
return true;
|
|
7675
|
+
if (!element.startsWith(literal))
|
|
7676
|
+
return false;
|
|
7677
|
+
return !isAlnum(element[literal.length]);
|
|
7678
|
+
});
|
|
7679
|
+
let matchCount = 0;
|
|
7680
|
+
const elements = value.map((element) => {
|
|
7681
|
+
if (typeof element !== "string")
|
|
7682
|
+
return JSON.stringify(element);
|
|
7683
|
+
const matched = matchFacet(element);
|
|
7684
|
+
if (!matched)
|
|
7685
|
+
return JSON.stringify(element);
|
|
7686
|
+
matchCount++;
|
|
7687
|
+
const suffix = element.slice(matched.value.length);
|
|
7688
|
+
const elementLiteral = `\`\${payload.${matched.field}}${escapeForTemplateLiteral(suffix)}\``;
|
|
7689
|
+
return matched.optional
|
|
7690
|
+
? `...(payload.${matched.field} ? [${elementLiteral}] : [])`
|
|
7691
|
+
: elementLiteral;
|
|
7692
|
+
});
|
|
7693
|
+
if (matchCount === 0)
|
|
7694
|
+
return null;
|
|
7695
|
+
return `[${elements.join(", ")}]`;
|
|
7696
|
+
}
|
|
7490
7697
|
/**
|
|
7491
7698
|
* Renders the variables literal for the primary-operation getGql() call —
|
|
7492
7699
|
* each key from the selected capture's own recorded variables is bound to
|
|
@@ -7498,9 +7705,12 @@ function spliceFacetsIntoStringVariable(value, fields, optionalFields = new Set(
|
|
|
7498
7705
|
* other value is emitted verbatim via JSON.stringify. `optionalFieldNames`
|
|
7499
7706
|
* marks which of `payloadFieldNames` a facet-string splice should treat as
|
|
7500
7707
|
* optional (see {@link spliceFacetsIntoStringVariable}); it has no effect on
|
|
7501
|
-
* top-level key correlation.
|
|
7708
|
+
* top-level key correlation. When a value is an array (not itself a string
|
|
7709
|
+
* key correlation candidate), its elements are matched against
|
|
7710
|
+
* `navigateToFacets` instead — see {@link spliceFacetsIntoArrayVariable} for
|
|
7711
|
+
* why that's a distinct grammar from the `key:value` one above.
|
|
7502
7712
|
*/
|
|
7503
|
-
function renderGqlVariablesExpr(variables, payloadFieldNames, optionalFieldNames = new Set()) {
|
|
7713
|
+
function renderGqlVariablesExpr(variables, payloadFieldNames, optionalFieldNames = new Set(), navigateToFacets = []) {
|
|
7504
7714
|
if (variables === null || typeof variables !== "object" || Array.isArray(variables))
|
|
7505
7715
|
return "{}";
|
|
7506
7716
|
const fields = payloadFieldNames ? [...payloadFieldNames] : [];
|
|
@@ -7508,7 +7718,8 @@ function renderGqlVariablesExpr(variables, payloadFieldNames, optionalFieldNames
|
|
|
7508
7718
|
const matchedField = fields.find((field) => field.toLowerCase() === key.toLowerCase());
|
|
7509
7719
|
const facetSpliceExpr = matchedField
|
|
7510
7720
|
? null
|
|
7511
|
-
: spliceFacetsIntoStringVariable(value, fields, optionalFieldNames)
|
|
7721
|
+
: (spliceFacetsIntoStringVariable(value, fields, optionalFieldNames) ??
|
|
7722
|
+
spliceFacetsIntoArrayVariable(value, navigateToFacets));
|
|
7512
7723
|
const valueExpr = matchedField
|
|
7513
7724
|
? `payload.${matchedField}`
|
|
7514
7725
|
: (facetSpliceExpr ?? JSON.stringify(value));
|
|
@@ -7945,7 +8156,7 @@ function isExplainedByCorrelatedSiblingField(path, parsedBody, otherBodies, sibl
|
|
|
7945
8156
|
* iteration's request, exactly the defect described in
|
|
7946
8157
|
* docs/recon-generate-nested-fold-flatmaps-away-the-parent-so-drill-params-freeze.md
|
|
7947
8158
|
* (a `packageCode`/`groupId`/`sailDate` triple that provably varies per
|
|
7948
|
-
*
|
|
8159
|
+
* voyage, silently frozen because no threaded field explained it). Path
|
|
7949
8160
|
* segments are deliberately not checked: {@link endpointKey} requires an
|
|
7950
8161
|
* identical pathname to group two captures at all, so no path segment can
|
|
7951
8162
|
* ever be observed to vary within a matched group.
|
|
@@ -10522,7 +10733,7 @@ function buildContractChecklist(opts) {
|
|
|
10522
10733
|
/** Generates a complete contract.ts source string for a plugin — exported so
|
|
10523
10734
|
* unit tests can drive the emitter directly without spawning the CLI. */
|
|
10524
10735
|
function emitContractTs(opts) {
|
|
10525
|
-
const { siteId, displayName, pascal, baseUrl, baseHeaders, minTime, safeRps, hasRateLimitProbeData = false, responseBody, responseBodySamples = [responseBody], gql, gqlQuery, endpointPath, gqlOperationName, gqlVariables, allCaptures = [], auxFiles, multiStepBody, omitExecuteHttp = false, isSubmissionFlow = false, inputBody, hasMultipartStep = false, actionSteps = [], foldReturnSpec = null, discoveredFormFields, fieldOptionsMap, discoveredOptionFields, discoveredRawOptionFields, discoveredAdditionalBodyKeys, discoveredStructuredKeys, valueConstraints = value_constraints_1.EMPTY_VALUE_CONSTRAINTS, payloadFieldNames, optionalPayloadFieldNames = new Set(), headerBindings = [], unpopulatedDeclaredVariables = [], fallbackGateSpec = {}, } = opts;
|
|
10736
|
+
const { siteId, displayName, pascal, baseUrl, baseHeaders, minTime, safeRps, hasRateLimitProbeData = false, responseBody, responseBodySamples = [responseBody], gql, gqlQuery, endpointPath, gqlOperationName, gqlVariables, allCaptures = [], auxFiles, multiStepBody, omitExecuteHttp = false, isSubmissionFlow = false, inputBody, hasMultipartStep = false, actionSteps = [], foldReturnSpec = null, discoveredFormFields, fieldOptionsMap, discoveredOptionFields, discoveredRawOptionFields, discoveredAdditionalBodyKeys, discoveredStructuredKeys, valueConstraints = value_constraints_1.EMPTY_VALUE_CONSTRAINTS, payloadFieldNames, optionalPayloadFieldNames = new Set(), flowSteps: contractFlowSteps = [], headerBindings = [], unpopulatedDeclaredVariables = [], fallbackGateSpec = {}, } = opts;
|
|
10526
10737
|
const { browserFallbackGate, httpTimeoutMs } = fallbackGateSpec;
|
|
10527
10738
|
/** Single source of truth for every GraphQL-emission decision below
|
|
10528
10739
|
* (clientImport, gqlCacheBlock, queryConst, and both executeHttpBody
|
|
@@ -11032,10 +11243,11 @@ const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottlenec
|
|
|
11032
11243
|
// the same captured request-variables object the signal was itself
|
|
11033
11244
|
// detected from (see paginationOperationIdentity above) — the default
|
|
11034
11245
|
// `{ q: payload.query }` REST body has no skip/count container to advance.
|
|
11246
|
+
const navigateToFacets = extractNavigateToFacetOrder(contractFlowSteps);
|
|
11035
11247
|
const gqlVariablesExpr = gqlOperationName
|
|
11036
|
-
? renderGqlVariablesExpr(gqlVariables, payloadFieldNames, optionalPayloadFieldNames)
|
|
11248
|
+
? renderGqlVariablesExpr(gqlVariables, payloadFieldNames, optionalPayloadFieldNames, navigateToFacets)
|
|
11037
11249
|
: paginationSignal
|
|
11038
|
-
? renderGqlVariablesExpr(gqlVariables, payloadFieldNames, optionalPayloadFieldNames)
|
|
11250
|
+
? renderGqlVariablesExpr(gqlVariables, payloadFieldNames, optionalPayloadFieldNames, navigateToFacets)
|
|
11039
11251
|
: "{ q: payload.query }";
|
|
11040
11252
|
/** Builds the nested `for` loop block(s) — see {@link pathToFoldLoopLines}
|
|
11041
11253
|
* — that fold every resolved plan's drill-down data onto `dataVarName`'s
|
|
@@ -12906,6 +13118,7 @@ async function main() {
|
|
|
12906
13118
|
isSubmissionFlow,
|
|
12907
13119
|
inputBody,
|
|
12908
13120
|
hasMultipartStep,
|
|
13121
|
+
flowSteps,
|
|
12909
13122
|
actionSteps,
|
|
12910
13123
|
foldReturnSpec,
|
|
12911
13124
|
discoveredFormFields,
|