@faststore/api 4.3.0 → 4.4.0-dev.0
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/cjs/index.js +41 -41
- package/dist/cjs/index.js.map +1 -1
- package/dist/es/index.mjs +1705 -1429
- package/dist/es/index.mjs.map +1 -1
- package/dist/src/platforms/vtex/clients/index.d.ts +3 -1
- package/dist/src/platforms/vtex/clients/index.d.ts.map +1 -1
- package/dist/src/platforms/vtex/clients/search/index.d.ts +18 -3
- package/dist/src/platforms/vtex/clients/search/index.d.ts.map +1 -1
- package/dist/src/platforms/vtex/loaders/sku.d.ts.map +1 -1
- package/dist/src/platforms/vtex/resolvers/query.d.ts +2 -1
- package/dist/src/platforms/vtex/resolvers/query.d.ts.map +1 -1
- package/dist/src/platforms/vtex/utils/intelligentSearchRequest.d.ts +52 -0
- package/dist/src/platforms/vtex/utils/intelligentSearchRequest.d.ts.map +1 -0
- package/package.json +3 -3
- package/src/platforms/vtex/clients/search/index.ts +132 -205
- package/src/platforms/vtex/loaders/sku.ts +3 -4
- package/src/platforms/vtex/resolvers/query.ts +70 -39
- package/src/platforms/vtex/utils/intelligentSearchRequest.ts +642 -0
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
import { parse } from 'cookie'
|
|
2
|
+
|
|
3
|
+
export interface IntelligentSearchFacet {
|
|
4
|
+
key: string
|
|
5
|
+
value: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
type FuzzyFacet = {
|
|
9
|
+
key: 'fuzzy'
|
|
10
|
+
value: '0' | '1' | 'auto'
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type OperatorFacet = {
|
|
14
|
+
key: 'operator'
|
|
15
|
+
value: 'and' | 'or'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type SegmentParams = {
|
|
19
|
+
sc?: string | number
|
|
20
|
+
regionId?: string
|
|
21
|
+
country?: string
|
|
22
|
+
locale?: string
|
|
23
|
+
'zip-code'?: string
|
|
24
|
+
coordinates?: string
|
|
25
|
+
pickupPoint?: string
|
|
26
|
+
deliveryZonesHash?: string
|
|
27
|
+
pickupPointHash?: string
|
|
28
|
+
utmSource?: string
|
|
29
|
+
utmCampaign?: string
|
|
30
|
+
utmiCampaign?: string
|
|
31
|
+
campaigns?: string
|
|
32
|
+
priceTables?: string
|
|
33
|
+
productClusterIds?: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type Sort =
|
|
37
|
+
| 'price:desc'
|
|
38
|
+
| 'price:asc'
|
|
39
|
+
| 'orders:desc'
|
|
40
|
+
| 'name:desc'
|
|
41
|
+
| 'name:asc'
|
|
42
|
+
| 'release:desc'
|
|
43
|
+
| 'discount:desc'
|
|
44
|
+
| ''
|
|
45
|
+
|
|
46
|
+
export type ProductIdentifierField = 'id' | 'slug' | 'ean' | 'reference' | 'sku'
|
|
47
|
+
|
|
48
|
+
export type IntelligentSearchEndpoint =
|
|
49
|
+
| 'product-search'
|
|
50
|
+
| 'facets'
|
|
51
|
+
| 'catalog-count'
|
|
52
|
+
| 'products'
|
|
53
|
+
| 'search-suggestions'
|
|
54
|
+
| 'top-searches'
|
|
55
|
+
|
|
56
|
+
export interface IntelligentSearchDefaults {
|
|
57
|
+
salesChannel?: string | number
|
|
58
|
+
regionId?: string
|
|
59
|
+
locale: string
|
|
60
|
+
hideUnavailableItems?: boolean
|
|
61
|
+
simulationBehavior?: 'default' | 'skip' | 'only1P'
|
|
62
|
+
showSponsored?: boolean
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface IntelligentSearchRequestArgs {
|
|
66
|
+
query?: string
|
|
67
|
+
page?: number
|
|
68
|
+
count?: number
|
|
69
|
+
sort?: Sort
|
|
70
|
+
selectedFacets?: IntelligentSearchFacet[]
|
|
71
|
+
showInvisibleItems?: boolean
|
|
72
|
+
hideUnavailableItems?: boolean
|
|
73
|
+
sponsoredCount?: number
|
|
74
|
+
allowRedirect?: boolean
|
|
75
|
+
field?: ProductIdentifierField
|
|
76
|
+
value?: string
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface IntelligentSearchRequestInput {
|
|
80
|
+
endpoint: IntelligentSearchEndpoint
|
|
81
|
+
/** Decoded vtex_segment object (params + extraFacets are extracted internally). */
|
|
82
|
+
segment?: Record<string, unknown>
|
|
83
|
+
defaults?: IntelligentSearchDefaults
|
|
84
|
+
args?: IntelligentSearchRequestArgs
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface IntelligentSearchRequest {
|
|
88
|
+
/** Attribute path segment only (no base URL or endpoint prefix). */
|
|
89
|
+
path: string
|
|
90
|
+
params: URLSearchParams
|
|
91
|
+
toString(): string
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const FUZZY_KEY = 'fuzzy'
|
|
95
|
+
const OPERATOR_KEY = 'operator'
|
|
96
|
+
const PICKUP_POINT_KEY = 'pickupPoint'
|
|
97
|
+
const SHIPPING_KEY = 'shipping'
|
|
98
|
+
const DELIVERY_OPTIONS_KEY = 'delivery-options'
|
|
99
|
+
const IN_STOCK_KEY = 'in-stock'
|
|
100
|
+
const POLICY_KEY = 'trade-policy'
|
|
101
|
+
const REGION_KEY = 'region-id'
|
|
102
|
+
|
|
103
|
+
const PATH_EXCLUDED_KEYS = new Set([
|
|
104
|
+
POLICY_KEY,
|
|
105
|
+
REGION_KEY,
|
|
106
|
+
FUZZY_KEY,
|
|
107
|
+
OPERATOR_KEY,
|
|
108
|
+
PICKUP_POINT_KEY,
|
|
109
|
+
])
|
|
110
|
+
|
|
111
|
+
const SHIPPING_FACET_KEYS = new Set([
|
|
112
|
+
'zip-code',
|
|
113
|
+
'pickupPoint',
|
|
114
|
+
'country',
|
|
115
|
+
'coordinates',
|
|
116
|
+
'deliveryZonesHash',
|
|
117
|
+
'pickupPointsHash',
|
|
118
|
+
])
|
|
119
|
+
|
|
120
|
+
const QUERY_PARAM_FACET_KEYS = new Set(['productClusterIds'])
|
|
121
|
+
|
|
122
|
+
const encodeSafeURI = (uri: string) => encodeURI(decodeURI(uri))
|
|
123
|
+
|
|
124
|
+
const removeDiacriticsFromURL = (url: string) =>
|
|
125
|
+
encodeURIComponent(
|
|
126
|
+
decodeURIComponent(url)
|
|
127
|
+
.normalize('NFD')
|
|
128
|
+
// biome-ignore lint/suspicious/noMisleadingCharacterClass: After NFD normalization, combining marks are separated and can be matched individually
|
|
129
|
+
.replaceAll(/[\u0300-\u036f]/g, '')
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
const PICKUP_IN_POINT_SUFFIX_RE = /^pickup-in-point-(.+)$/
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Reads and decodes the `vtex_segment` cookie. Does not create a segment.
|
|
136
|
+
* Returns an empty object when the cookie is absent or invalid.
|
|
137
|
+
*/
|
|
138
|
+
export function parseSegmentCookie(
|
|
139
|
+
cookieHeader?: string
|
|
140
|
+
): Record<string, unknown> {
|
|
141
|
+
if (!cookieHeader) {
|
|
142
|
+
return {}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const cookies = parse(cookieHeader)
|
|
147
|
+
const segmentToken = cookies.vtex_segment
|
|
148
|
+
|
|
149
|
+
if (!segmentToken) {
|
|
150
|
+
return {}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const decoded = Buffer.from(segmentToken, 'base64').toString('utf-8')
|
|
154
|
+
|
|
155
|
+
return JSON.parse(decoded) as Record<string, unknown>
|
|
156
|
+
} catch {
|
|
157
|
+
return {}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function parseSegmentFacetsString(facetsStr: string): {
|
|
162
|
+
shipping: Record<string, string>
|
|
163
|
+
queryParams: Record<string, string>
|
|
164
|
+
extraFacets: IntelligentSearchFacet[]
|
|
165
|
+
} {
|
|
166
|
+
const shipping: Record<string, string> = {}
|
|
167
|
+
const queryParams: Record<string, string> = {}
|
|
168
|
+
const extraFacets: IntelligentSearchFacet[] = []
|
|
169
|
+
|
|
170
|
+
for (const pair of facetsStr.split(';')) {
|
|
171
|
+
const eqIdx = pair.indexOf('=')
|
|
172
|
+
|
|
173
|
+
if (eqIdx < 0) continue
|
|
174
|
+
|
|
175
|
+
const key = pair.slice(0, eqIdx)
|
|
176
|
+
const value = pair.slice(eqIdx + 1)
|
|
177
|
+
|
|
178
|
+
if (!key || !value) continue
|
|
179
|
+
|
|
180
|
+
if (SHIPPING_FACET_KEYS.has(key)) {
|
|
181
|
+
shipping[key] = value
|
|
182
|
+
} else if (QUERY_PARAM_FACET_KEYS.has(key)) {
|
|
183
|
+
queryParams[key] = value
|
|
184
|
+
} else {
|
|
185
|
+
extraFacets.push({ key, value })
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return { shipping, queryParams, extraFacets }
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function extractSegmentData(segment: Record<string, unknown>): {
|
|
193
|
+
segmentParams: SegmentParams
|
|
194
|
+
extraFacets: IntelligentSearchFacet[]
|
|
195
|
+
} {
|
|
196
|
+
const facetsStr = typeof segment.facets === 'string' ? segment.facets : ''
|
|
197
|
+
const { shipping, queryParams, extraFacets } =
|
|
198
|
+
parseSegmentFacetsString(facetsStr)
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
segmentParams: {
|
|
202
|
+
sc: segment.channel as string | number | undefined,
|
|
203
|
+
regionId: segment.regionId as string | undefined,
|
|
204
|
+
country: (segment.countryCode as string | undefined) ?? shipping.country,
|
|
205
|
+
locale: segment.cultureInfo as string | undefined,
|
|
206
|
+
'zip-code': shipping['zip-code'],
|
|
207
|
+
coordinates: shipping.coordinates,
|
|
208
|
+
pickupPoint: shipping.pickupPoint,
|
|
209
|
+
deliveryZonesHash: shipping.deliveryZonesHash,
|
|
210
|
+
pickupPointHash: shipping.pickupPointsHash,
|
|
211
|
+
utmSource: (segment.utm_source as string | undefined) ?? undefined,
|
|
212
|
+
utmCampaign: (segment.utm_campaign as string | undefined) ?? undefined,
|
|
213
|
+
utmiCampaign: (segment.utmi_campaign as string | undefined) ?? undefined,
|
|
214
|
+
campaigns:
|
|
215
|
+
typeof segment.campaigns === 'string' ? segment.campaigns : undefined,
|
|
216
|
+
priceTables: (segment.priceTables as string | undefined) ?? undefined,
|
|
217
|
+
productClusterIds: queryParams.productClusterIds,
|
|
218
|
+
},
|
|
219
|
+
extraFacets,
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function appendSegmentParams(
|
|
224
|
+
params: URLSearchParams,
|
|
225
|
+
segmentParams: SegmentParams
|
|
226
|
+
) {
|
|
227
|
+
const entries: Array<[string, string | number | undefined]> = [
|
|
228
|
+
['sc', segmentParams.sc],
|
|
229
|
+
['regionId', segmentParams.regionId],
|
|
230
|
+
['country', segmentParams.country],
|
|
231
|
+
['locale', segmentParams.locale],
|
|
232
|
+
['zip-code', segmentParams['zip-code']],
|
|
233
|
+
['coordinates', segmentParams.coordinates],
|
|
234
|
+
['pickupPoint', segmentParams.pickupPoint],
|
|
235
|
+
['deliveryZonesHash', segmentParams.deliveryZonesHash],
|
|
236
|
+
['pickupPointHash', segmentParams.pickupPointHash],
|
|
237
|
+
['utmSource', segmentParams.utmSource],
|
|
238
|
+
['utmCampaign', segmentParams.utmCampaign],
|
|
239
|
+
['utmiCampaign', segmentParams.utmiCampaign],
|
|
240
|
+
['campaigns', segmentParams.campaigns],
|
|
241
|
+
['priceTables', segmentParams.priceTables],
|
|
242
|
+
['productClusterId', segmentParams.productClusterIds],
|
|
243
|
+
]
|
|
244
|
+
|
|
245
|
+
for (const [key, value] of entries) {
|
|
246
|
+
if (value !== undefined && value !== null && value !== '') {
|
|
247
|
+
params.append(key, String(value))
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function extractPickupPointIdFromShippingFacetValue(
|
|
253
|
+
value: string
|
|
254
|
+
): string | undefined {
|
|
255
|
+
const match = PICKUP_IN_POINT_SUFFIX_RE.exec(value)
|
|
256
|
+
|
|
257
|
+
return match?.[1]
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function extractPickupPointIdFromPathShippingFacet(
|
|
261
|
+
selectedFacetsFromPath: IntelligentSearchFacet[]
|
|
262
|
+
): string | undefined {
|
|
263
|
+
const shipping = selectedFacetsFromPath.find((f) => f.key === 'shipping')
|
|
264
|
+
const rawId = shipping
|
|
265
|
+
? extractPickupPointIdFromShippingFacetValue(shipping.value)
|
|
266
|
+
: undefined
|
|
267
|
+
|
|
268
|
+
if (rawId === undefined) {
|
|
269
|
+
return undefined
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
return decodeURIComponent(rawId)
|
|
274
|
+
} catch {
|
|
275
|
+
return rawId
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function mergeSegmentParamsWithPickupFromPath(
|
|
280
|
+
segmentParams: SegmentParams | undefined,
|
|
281
|
+
selectedFacetsFromPath: IntelligentSearchFacet[]
|
|
282
|
+
): SegmentParams | undefined {
|
|
283
|
+
const pathPickupId = extractPickupPointIdFromPathShippingFacet(
|
|
284
|
+
selectedFacetsFromPath
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
if (pathPickupId === undefined) {
|
|
288
|
+
return segmentParams
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
...segmentParams,
|
|
293
|
+
pickupPoint: pathPickupId,
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function normalizePickupInPointShippingFacets(
|
|
298
|
+
selectedFacets: IntelligentSearchFacet[]
|
|
299
|
+
): IntelligentSearchFacet[] {
|
|
300
|
+
return selectedFacets.map((facet) => {
|
|
301
|
+
if (facet.key !== 'shipping') {
|
|
302
|
+
return facet
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const id = extractPickupPointIdFromShippingFacetValue(facet.value)
|
|
306
|
+
|
|
307
|
+
if (id === undefined) {
|
|
308
|
+
return facet
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return { ...facet, value: 'pickup-in-point' }
|
|
312
|
+
})
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function concatSelectedFacets(
|
|
316
|
+
selectedFacets: IntelligentSearchFacet[],
|
|
317
|
+
selectedFacetsFromSegment: IntelligentSearchFacet[]
|
|
318
|
+
): IntelligentSearchFacet[] {
|
|
319
|
+
let result = [...selectedFacets]
|
|
320
|
+
const hasShipping = result.some((f) => f.key === 'shipping')
|
|
321
|
+
|
|
322
|
+
for (const facet of selectedFacetsFromSegment) {
|
|
323
|
+
if (!hasShipping || facet.key !== 'shipping') {
|
|
324
|
+
result.push(facet)
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (result.some((f) => f.key === 'shipping' && f.value === 'ignore')) {
|
|
329
|
+
result = result.filter((f) => f.key !== 'shipping')
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return normalizePickupInPointShippingFacets(result)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function buildAttributePath(selectedFacets: IntelligentSearchFacet[]) {
|
|
336
|
+
return selectedFacets.reduce((attributePath, facet) => {
|
|
337
|
+
let { key, value } = facet
|
|
338
|
+
|
|
339
|
+
if (key === 'priceRange') {
|
|
340
|
+
key = 'price'
|
|
341
|
+
value = value.replace(' TO ', ':')
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return key === 'ft'
|
|
345
|
+
? attributePath
|
|
346
|
+
: `${attributePath}${encodeSafeURI(key)}/${removeDiacriticsFromURL(encodeSafeURI(value)).replaceAll(/ |%20/g, '-')}/`
|
|
347
|
+
}, '')
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const isFuzzyFacet = (facet: IntelligentSearchFacet): facet is FuzzyFacet =>
|
|
351
|
+
facet.key === 'fuzzy' &&
|
|
352
|
+
(facet.value === '0' || facet.value === '1' || facet.value === 'auto')
|
|
353
|
+
|
|
354
|
+
const isOperatorFacet = (
|
|
355
|
+
facet: IntelligentSearchFacet
|
|
356
|
+
): facet is OperatorFacet =>
|
|
357
|
+
facet.key === 'operator' && (facet.value === 'and' || facet.value === 'or')
|
|
358
|
+
|
|
359
|
+
function resolveSegmentData(
|
|
360
|
+
segment: Record<string, unknown> | undefined,
|
|
361
|
+
defaults: IntelligentSearchDefaults | undefined
|
|
362
|
+
): { segmentParams: SegmentParams; extraFacets: IntelligentSearchFacet[] } {
|
|
363
|
+
const { segmentParams, extraFacets } = extractSegmentData(segment ?? {})
|
|
364
|
+
|
|
365
|
+
// Server-provided defaults are authoritative for sales channel/region: the
|
|
366
|
+
// raw `vtex_segment` cookie is client-supplied base64 JSON and must not be
|
|
367
|
+
// able to override the trusted server context. Only fall back to the cookie
|
|
368
|
+
// values when no default is provided.
|
|
369
|
+
const sc = defaults?.salesChannel ?? segmentParams.sc
|
|
370
|
+
const regionId = defaults?.regionId ?? segmentParams.regionId
|
|
371
|
+
const locale = defaults?.locale ?? segmentParams.locale ?? ''
|
|
372
|
+
|
|
373
|
+
return {
|
|
374
|
+
segmentParams: {
|
|
375
|
+
...segmentParams,
|
|
376
|
+
...(sc ? { sc } : {}),
|
|
377
|
+
...(regionId ? { regionId } : {}),
|
|
378
|
+
locale,
|
|
379
|
+
},
|
|
380
|
+
extraFacets,
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function preparePathFacets(
|
|
385
|
+
facets: IntelligentSearchFacet[],
|
|
386
|
+
extraFacets: IntelligentSearchFacet[]
|
|
387
|
+
): IntelligentSearchFacet[] {
|
|
388
|
+
const pathFacets = facets.filter(({ key }) => !PATH_EXCLUDED_KEYS.has(key))
|
|
389
|
+
|
|
390
|
+
const shippingFacet =
|
|
391
|
+
facets.find(
|
|
392
|
+
({ key, value }) =>
|
|
393
|
+
key === SHIPPING_KEY && value !== 'all-delivery-methods'
|
|
394
|
+
) ?? null
|
|
395
|
+
|
|
396
|
+
const deliveryOptionsFacet =
|
|
397
|
+
facets.find(
|
|
398
|
+
({ key, value }) =>
|
|
399
|
+
key === DELIVERY_OPTIONS_KEY && value !== 'all-delivery-options'
|
|
400
|
+
) ?? null
|
|
401
|
+
|
|
402
|
+
const withShippingFacets = [...pathFacets]
|
|
403
|
+
|
|
404
|
+
// `pathFacets` may already contain the shipping/delivery-options facets, so
|
|
405
|
+
// only push them when they are not already present to avoid duplicated path
|
|
406
|
+
// segments like `shipping/delivery/shipping/delivery/`.
|
|
407
|
+
const hasShippingInPath = withShippingFacets.some(
|
|
408
|
+
({ key }) => key === SHIPPING_KEY
|
|
409
|
+
)
|
|
410
|
+
const hasDeliveryOptionsInPath = withShippingFacets.some(
|
|
411
|
+
({ key }) => key === DELIVERY_OPTIONS_KEY
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
if (shippingFacet !== null && !hasShippingInPath) {
|
|
415
|
+
withShippingFacets.push(shippingFacet)
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (deliveryOptionsFacet !== null && !hasDeliveryOptionsInPath) {
|
|
419
|
+
withShippingFacets.push(deliveryOptionsFacet)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return concatSelectedFacets(withShippingFacets, extraFacets)
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function addSearchParamsFacets(
|
|
426
|
+
facets: IntelligentSearchFacet[],
|
|
427
|
+
params: URLSearchParams
|
|
428
|
+
) {
|
|
429
|
+
const fuzzyFacet = facets.find(({ key }) => key === FUZZY_KEY) ?? null
|
|
430
|
+
const operatorFacet = facets.find(({ key }) => key === OPERATOR_KEY) ?? null
|
|
431
|
+
const pickupPointFacet =
|
|
432
|
+
facets.find(({ key }) => key === PICKUP_POINT_KEY) ?? null
|
|
433
|
+
|
|
434
|
+
if (fuzzyFacet && isFuzzyFacet(fuzzyFacet)) {
|
|
435
|
+
params.append(FUZZY_KEY, fuzzyFacet.value)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (operatorFacet && isOperatorFacet(operatorFacet)) {
|
|
439
|
+
params.append(OPERATOR_KEY, operatorFacet.value)
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (pickupPointFacet) {
|
|
443
|
+
params.append(PICKUP_POINT_KEY, pickupPointFacet.value)
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
interface SearchLikeParams {
|
|
448
|
+
includePagination: boolean
|
|
449
|
+
includeSort: boolean
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function buildSearchLikeParams(
|
|
453
|
+
args: IntelligentSearchRequestArgs,
|
|
454
|
+
defaults: IntelligentSearchDefaults | undefined,
|
|
455
|
+
segmentData: {
|
|
456
|
+
segmentParams: SegmentParams
|
|
457
|
+
extraFacets: IntelligentSearchFacet[]
|
|
458
|
+
},
|
|
459
|
+
options: SearchLikeParams
|
|
460
|
+
): { params: URLSearchParams; path: string } {
|
|
461
|
+
const {
|
|
462
|
+
query = '',
|
|
463
|
+
page,
|
|
464
|
+
count,
|
|
465
|
+
sort,
|
|
466
|
+
selectedFacets = [],
|
|
467
|
+
showInvisibleItems,
|
|
468
|
+
sponsoredCount,
|
|
469
|
+
hideUnavailableItems: searchHideUnavailableItems,
|
|
470
|
+
allowRedirect = false,
|
|
471
|
+
} = args
|
|
472
|
+
|
|
473
|
+
const params = new URLSearchParams()
|
|
474
|
+
|
|
475
|
+
if (options.includePagination && page !== undefined && count !== undefined) {
|
|
476
|
+
const from = page * count
|
|
477
|
+
const to = count === 0 ? from : from + count - 1
|
|
478
|
+
|
|
479
|
+
params.append('from', from.toString())
|
|
480
|
+
params.append('to', to.toString())
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (query) {
|
|
484
|
+
params.append('query', query)
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (options.includeSort && sort) {
|
|
488
|
+
params.append('sort', sort)
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const mergedSegmentParams = mergeSegmentParamsWithPickupFromPath(
|
|
492
|
+
segmentData.segmentParams,
|
|
493
|
+
selectedFacets
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
appendSegmentParams(params, mergedSegmentParams ?? segmentData.segmentParams)
|
|
497
|
+
addSearchParamsFacets(selectedFacets, params)
|
|
498
|
+
|
|
499
|
+
if (showInvisibleItems) {
|
|
500
|
+
params.append('show-invisible-items', 'true')
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (defaults?.hideUnavailableItems !== undefined) {
|
|
504
|
+
const inStockFacet = selectedFacets.find(({ key }) => key === IN_STOCK_KEY)
|
|
505
|
+
const shouldHideUnavailableItems = inStockFacet
|
|
506
|
+
? inStockFacet.value
|
|
507
|
+
: (searchHideUnavailableItems?.toString() ??
|
|
508
|
+
defaults.hideUnavailableItems.toString())
|
|
509
|
+
|
|
510
|
+
params.append('hideUnavailableItems', shouldHideUnavailableItems)
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (defaults?.simulationBehavior !== undefined) {
|
|
514
|
+
params.append('simulationBehavior', defaults.simulationBehavior.toString())
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (defaults?.showSponsored !== undefined) {
|
|
518
|
+
params.append('showSponsored', defaults.showSponsored.toString())
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (sponsoredCount !== undefined) {
|
|
522
|
+
params.append('sponsoredCount', sponsoredCount.toString())
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (allowRedirect !== undefined) {
|
|
526
|
+
params.append('allowRedirect', allowRedirect.toString())
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const path = buildAttributePath(
|
|
530
|
+
preparePathFacets(selectedFacets, segmentData.extraFacets)
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
return { params, path }
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function buildProductsParams(
|
|
537
|
+
args: IntelligentSearchRequestArgs,
|
|
538
|
+
segmentParams: SegmentParams
|
|
539
|
+
): URLSearchParams {
|
|
540
|
+
const { field, value, hideUnavailableItems, showInvisibleItems } = args
|
|
541
|
+
|
|
542
|
+
const params = new URLSearchParams({
|
|
543
|
+
field: field ?? '',
|
|
544
|
+
value: value ?? '',
|
|
545
|
+
})
|
|
546
|
+
|
|
547
|
+
appendSegmentParams(params, segmentParams)
|
|
548
|
+
|
|
549
|
+
if (hideUnavailableItems) {
|
|
550
|
+
params.append('hideUnavailableItems', 'true')
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (showInvisibleItems) {
|
|
554
|
+
params.append('show-invisible-items', 'true')
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return params
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function createRequest(
|
|
561
|
+
path: string,
|
|
562
|
+
params: URLSearchParams
|
|
563
|
+
): IntelligentSearchRequest {
|
|
564
|
+
return {
|
|
565
|
+
path,
|
|
566
|
+
params,
|
|
567
|
+
toString() {
|
|
568
|
+
const qs = params.toString()
|
|
569
|
+
|
|
570
|
+
if (!path) {
|
|
571
|
+
return qs ? `?${qs}` : ''
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return qs ? `${path}?${qs}` : path
|
|
575
|
+
},
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Pure builder for intelligent-search v1 path + query params.
|
|
581
|
+
* Callers assemble the full URL: `/api/intelligent-search/v1/${endpoint}/${request.path}?${request.params}`.
|
|
582
|
+
*/
|
|
583
|
+
export function buildIntelligentSearchRequest(
|
|
584
|
+
input: IntelligentSearchRequestInput
|
|
585
|
+
): IntelligentSearchRequest {
|
|
586
|
+
const { endpoint, segment, defaults, args = {} } = input
|
|
587
|
+
const segmentData = resolveSegmentData(segment, defaults)
|
|
588
|
+
|
|
589
|
+
switch (endpoint) {
|
|
590
|
+
case 'product-search':
|
|
591
|
+
case 'facets': {
|
|
592
|
+
const { params, path } = buildSearchLikeParams(
|
|
593
|
+
args,
|
|
594
|
+
defaults,
|
|
595
|
+
segmentData,
|
|
596
|
+
{ includePagination: true, includeSort: true }
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
return createRequest(path, params)
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
case 'catalog-count': {
|
|
603
|
+
const { params, path } = buildSearchLikeParams(
|
|
604
|
+
args,
|
|
605
|
+
defaults,
|
|
606
|
+
segmentData,
|
|
607
|
+
{ includePagination: false, includeSort: false }
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
return createRequest(path, params)
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
case 'products': {
|
|
614
|
+
const params = buildProductsParams(args, segmentData.segmentParams)
|
|
615
|
+
|
|
616
|
+
return createRequest('', params)
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
case 'search-suggestions': {
|
|
620
|
+
const params = new URLSearchParams({
|
|
621
|
+
query: args.query?.toString() ?? '',
|
|
622
|
+
locale: defaults?.locale ?? segmentData.segmentParams.locale ?? '',
|
|
623
|
+
})
|
|
624
|
+
|
|
625
|
+
return createRequest('', params)
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
case 'top-searches': {
|
|
629
|
+
const params = new URLSearchParams({
|
|
630
|
+
locale: defaults?.locale ?? segmentData.segmentParams.locale ?? '',
|
|
631
|
+
})
|
|
632
|
+
|
|
633
|
+
return createRequest('', params)
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
default: {
|
|
637
|
+
const _exhaustive: never = endpoint
|
|
638
|
+
|
|
639
|
+
throw new Error(`Unknown intelligent-search endpoint: ${_exhaustive}`)
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|