Skip to content

feat(api): Migrate to VTEX Intelligent Search V1 API#3385

Merged
hellofanny merged 4 commits into
devfrom
feat-new-is-api
Jun 19, 2026
Merged

feat(api): Migrate to VTEX Intelligent Search V1 API#3385
hellofanny merged 4 commits into
devfrom
feat-new-is-api

Conversation

@salesfelipe

@salesfelipe salesfelipe commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Ports the changes from #3376 (originally opened against v3) onto the dev (v4) line.

  • Migrates the VTEX search client to the Intelligent Search V1 API (/api/intelligent-search/v1/...), adding fetchProduct, productsByIdentifier, and productCount.
  • Centralizes request construction (pagination, facets, segment/region/locale handling) in the new utils/intelligentSearchRequest.ts util, with parseSegmentCookie for segment-aware requests.
  • PDP, the products resolver, cross-selling, and the SKU loader now hydrate products via productsByIdentifier / fetchProduct, honoring hideUnavailableItems.
  • Updates integration mocks and tests to match the V1 request/response shapes; adds unit tests for the request builder.

v3 → v4 adaptations made while porting

  • Kept v4's GraphqlContext (the v3 PR used Context) and treated Options as the global type rather than importing it.
  • Removed the now-unused IStoreSelectedFacet import in the search client.
  • Preserved v4's extra .filter(...) in the products resolver.
  • Added explicit vitest imports in the new unit test (v4 does not enable vitest globals).

Test plan

  • pnpm test:unit in packages/api (26 passed)
  • pnpm test:int in packages/api (44 passed)
  • Biome lint clean on changed files
  • Validate in a starter store via the ci/codesandbox check

Made with Cursor

Summary by CodeRabbit

  • Refactor
    • Migrated product, facets, suggestions, top searches, and product counting to the latest intelligent-search v1 flow using a shared request builder for more consistent query construction.
    • Added dedicated SKU and identifier-based product retrieval, including support for cross-selling hydration in a two-step process.
  • Bug Fixes
    • Improved “product not found” handling by tightening fallback conditions for SKU lookup failures.
  • Tests
    • Updated mocks for the new endpoints and added unit coverage for request-building behavior and segment/cookie-derived defaults.

Port the Intelligent Search V1 migration from PR #3376 onto the v4 (dev)
line. The search client now targets the IS V1 endpoints and exposes
fetchProduct/productsByIdentifier/productCount, with request building
centralized in the new intelligentSearchRequest util. PDP, products and
SKU loaders hydrate via productsByIdentifier.

Co-authored-by: Cursor <cursoragent@cursor.com>
@salesfelipe salesfelipe requested a review from a team as a code owner June 9, 2026 17:29
@salesfelipe salesfelipe requested review from hellofanny and lucasfp13 and removed request for a team June 9, 2026 17:29
@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 341fbe32-e9bb-422d-a0e3-ee6fa0aacd1e

📥 Commits

Reviewing files that changed from the base of the PR and between 75f4b5d and c440ebd.

📒 Files selected for processing (2)
  • packages/api/src/platforms/vtex/clients/search/index.ts
  • packages/api/src/platforms/vtex/utils/intelligentSearchRequest.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/api/src/platforms/vtex/clients/search/index.ts
  • packages/api/src/platforms/vtex/utils/intelligentSearchRequest.ts

Walkthrough

VTEX intelligent search client refactored to use a unified v1 request-building utility with segment cookie parsing and structured facet handling. Search client, GraphQL resolvers, and SKU loader migrated to new single-product and bulk-identifier lookup APIs. All test mocks and integration tests updated for v1 endpoint shapes and response structures.

Changes

Intelligent Search v1 Client Migration

Layer / File(s) Summary
Request builder types, constants, and segment parsing
packages/api/src/platforms/vtex/utils/intelligentSearchRequest.ts
Exports intelligent-search v1 request contract types and constants controlling facet key handling. Implements parseSegmentCookie for safe vtex_segment base64/JSON decoding with fallback, and extractSegmentData to parse segment facets and map segment fields into normalized query parameters.
Facet normalization, path building, and parameter assembly
packages/api/src/platforms/vtex/utils/intelligentSearchRequest.ts
Implements segment parameter appending, pickup-point extraction from facets, facet merging for pickup-in-point normalization, VTEX attribute path construction with priceRange formatting, server defaults resolution, and preparePathFacets to assemble final facet list excluding reserved keys and preventing duplicate shipping/delivery facets.
Search-like and product endpoint parameter builders
packages/api/src/platforms/vtex/utils/intelligentSearchRequest.ts
Implements buildSearchLikeParams for pagination, query, sort, visibility flags, and sponsored parameters; buildProductsParams for field/value plus segment flags; and buildIntelligentSearchRequest switch routing to endpoint-specific builders for product-search, facets, catalog-count, products, search-suggestions, and top-searches.
Unit tests for request builder behavior
packages/api/test/unit/platforms/vtex/utils/intelligentSearchRequest.test.ts
Vitest suite validating pagination from/to derivation, segment parsing with defaults precedence, facet-to-path and facet-to-query mapping, shipping/delivery facet normalization, endpoint-specific parameter handling, and real vtex_segment cookie decoding with geo and cluster ID extraction.
Search client refactoring to shared builder
packages/api/src/platforms/vtex/clients/search/index.ts
Refactored to use buildIntelligentSearchRequest for all endpoints; re-exports Sort and ProductIdentifierField from shared utilities; adds FetchProductArgs and ProductsByIdentifierArgs interfaces; introduces fetchProduct (single product v1 lookup) and productsByIdentifier (bulk identifier fetch with p-limit concurrency); updates productCount to call v1 catalog-count endpoint; wires GraphqlContext-derived defaults for sales channel, computed regionId, and cookie-derived locale.
GraphQL resolver updates for new search APIs
packages/api/src/platforms/vtex/resolvers/query.ts
product resolver narrows SKU fallback conditions and uses search.fetchProduct for fallback; search resolver builds searchArgs and rewrites cross-selling as two-step ID fetch then productsByIdentifier hydration; products resolver calls productsByIdentifier({ field: 'id', values }) instead of composing id: query strings.
SKU loader identifier-based queries
packages/api/src/platforms/vtex/loaders/sku.ts
Loader batch function calls productsByIdentifier with field: 'sku' instead of composing manual sku: query strings and explicit pagination.
Test mock endpoint URLs and response shape updates
packages/api/test/mocks/ProductQuery.ts, packages/api/test/mocks/SearchQuery.ts, packages/api/test/mocks/AllProductsQuery.ts, packages/api/test/mocks/RedirectQuery.ts, packages/api/test/integration/queries.test.ts
Mock URLs updated to v1 intelligent-search endpoint format with from/to pagination; productSearchFetch replaced with pdpFetch containing single product response payload; fixture info URLs for product-search, facets, and redirect endpoints aligned to new v1 format.
Integration test fetch handler and mutation test updates
packages/api/test/integration/mutations.test.ts
pickFetchAPICallResult enhanced to detect intelligent-search/v1/products requests and dynamically generate product results by extracting SKU from query parameters; ValidateCartMutation test adjusted to expect reduced fetch call count from 3 to 2.
Checkout fixture cartEtag and product response updates
packages/api/test/mocks/ValidateCartMutation.ts
Checkout order-form cartEtag values updated; productSearchPage1Count1Fetch replaced with v1/products endpoint URL and direct product response structure (cacheId, productId, items, origin); createProductFetchResultForSku helper adapted to new response layout while preserving external signature.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Suggested reviewers

  • lemagnetic
  • renatomaurovtex
  • gabpaladino

🔍 A search engine refactored grand,
V1 endpoints now expand,
Segments parsed, facets aligned,
Product lookups crystallized,
Tests and loaders redesigned. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main change: migrating the VTEX search client to the Intelligent Search V1 API.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-new-is-api

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codesandbox-ci

codesandbox-ci Bot commented Jun 9, 2026

Copy link
Copy Markdown

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

@salesfelipe salesfelipe self-assigned this Jun 9, 2026
@salesfelipe salesfelipe added the enhancement New feature or request label Jun 9, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/api/src/platforms/vtex/loaders/sku.ts (1)

15-22: ⚠️ Potential issue | 🟠 Major

VTEX SKU loader can fan out to up to 99 parallel requests (N+1)

productsByIdentifier is implemented as Promise.all(values.map(fetchProduct)), and fetchProduct calls the VTEX v1 /api/intelligent-search/v1/products endpoint with a single field+value pair (no bulk/CSV support in the request builder). With maxBatchSize: 99 in packages/api/src/platforms/vtex/loaders/sku.ts, a full batch can trigger up to 99 concurrent HTTP requests, risking higher latency and potential rate-limit/throttling under load. Consider adding a concurrency limiter / lowering maxBatchSize, or redesigning the integration to use a bulk-capable lookup; otherwise add a quick rationale/benchmark for why this level of parallelism is safe.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/api/src/platforms/vtex/loaders/sku.ts` around lines 15 - 22, The
VTEX SKU loader currently lets productsByIdentifier (which internally runs
Promise.all over values via fetchProduct) fan out up to maxBatchSize (99)
concurrent HTTP calls; reduce concurrency by either lowering maxBatchSize or
adding a concurrency limiter around the batch call—specifically modify the
loader in sku.ts to wrap the values iteration (the call site that invokes
clients.search.productsByIdentifier / fetchProduct) with a worker pool/limit
(e.g., p-limit or a simple semaphore) or set maxBatchSize to a safer value and
document the choice; ensure the change targets the code that constructs the
values array and calls productsByIdentifier/fetchProduct so the loader no longer
issues 99 parallel requests.
🧹 Nitpick comments (5)
packages/api/test/unit/platforms/vtex/utils/intelligentSearchRequest.test.ts (1)

374-411: ⚡ Quick win

Add explicit malformed/missing-cookie tests for parseSegmentCookie fallback contract.

This block validates real valid cookies well, but it misses the failure-path contract ({} on invalid/missing vtex_segment). Adding those tests will lock in the non-throw behavior and prevent regressions.

Suggested test additions
 describe('real vtex_segment cookie examples', () => {
+  it('returns empty object when cookie header is missing', () => {
+    expect(parseSegmentCookie(undefined)).toEqual({})
+  })
+
+  it('returns empty object for malformed base64/json cookie', () => {
+    expect(parseSegmentCookie('vtex_segment=not-base64')).toEqual({})
+  })
+
   // '{"campaigns":null,"channel":"1",...,"facets":"zip-code=01002020;country=BRA;..."}'
   const segmentWithShippingBase64 =
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/api/test/unit/platforms/vtex/utils/intelligentSearchRequest.test.ts`
around lines 374 - 411, Add unit tests for parseSegmentCookie that assert its
fallback contract: when vtex_segment is missing or malformed it should return an
empty object ({}), not throw. Use the existing cookieHeader helper to pass an
empty string (no cookie) and an invalid base64 string (e.g. 'invalid') into
parseSegmentCookie, and add assertions like expect(segment).toEqual({}) for both
cases to lock in the non-throw behavior.
packages/api/src/platforms/vtex/clients/search/index.ts (2)

154-159: ⚡ Quick win

Unnecessary Promise.resolve wrapper.

fetchAPI already returns a Promise, so wrapping it in Promise.resolve() adds no value.

♻️ Suggested simplification
-    return Promise.resolve(
-      fetchAPI(
-        `${base}/api/intelligent-search/v1/products?${request.params.toString()}`,
-        { headers }
-      )
-    )
+    return fetchAPI(
+      `${base}/api/intelligent-search/v1/products?${request.params.toString()}`,
+      { headers }
+    )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/api/src/platforms/vtex/clients/search/index.ts` around lines 154 -
159, The return currently wraps fetchAPI(...) in Promise.resolve unnecessarily;
update the function to return fetchAPI(...) directly instead of
Promise.resolve(fetchAPI(...))—locate the return statement that builds the URL
with `${base}/api/intelligent-search/v1/products?${request.params.toString()}`
and headers and replace the wrapped call so it simply returns the Promise from
fetchAPI with the same arguments.

162-182: Bound N-request parallelism in productsByIdentifier (no IS v1 bulk endpoint)

productsByIdentifier issues one Intelligent Search v1 /products request per values entry via Promise.all. In the SKU loader path it’s capped by DataLoader’s maxBatchSize: 99, but it can still burst up to 99 concurrent upstream calls. Since VTEX Intelligent Search v1 doesn’t expose a bulk/batch products endpoint, consider adding a concurrency limit (or otherwise throttling) and document the expected max values cardinality.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/api/src/platforms/vtex/clients/search/index.ts` around lines 162 -
182, productsByIdentifier currently fires one fetchProduct call per value via
Promise.all which can burst many concurrent requests (up to DataLoader
maxBatchSize), so limit concurrency: replace Promise.all over values with a
controlled parallelism runner (e.g., use p-map/p-limit or an internal
semaphore/queue) to call fetchProduct with a fixed concurrency (choose a
sensible default like 5–10) and keep the same return filtering; reference
productsByIdentifier and fetchProduct when making the change and add a short
code comment/docstring noting the chosen max concurrency and expected max values
cardinality so callers understand throttling behavior.
packages/api/test/integration/mutations.test.ts (1)

101-113: ⚡ Quick win

Clarify the expected fetch calls in the comment.

The comment states "only checkout calls are made" but the test expects 2 calls while only checkoutOrderFormValidFetch is in the explicit array. The second call appears to be a dynamic v1/products fetch handled by the interceptor at lines 70-74. Update the comment to explicitly state both calls for easier debugging.

📝 Suggested comment revision
-  // When cart is valid and etag is up to date, only checkout calls are made.
+  // When cart is valid and etag is up to date:
+  // 1. GET orderForm (checkoutOrderFormValidFetch)
+  // 2. GET product data via v1/products (handled dynamically)
   expect(mockedFetch).toHaveBeenCalledTimes(2)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/api/test/integration/mutations.test.ts` around lines 101 - 113, The
test comment for the `validateCart` case is misleading: it says "only checkout
calls are made" but `mockedFetch` is expected to be called twice because the
test's fetch interceptor (`pickFetchAPICallResult`) handles both the explicit
`checkoutOrderFormValidFetch` and an implicit dynamic v1/products call; update
the inline comment to enumerate both expected calls (checkout and v1/products)
so future readers understand why `mockedFetch` is called twice — locate the test
around `ValidateCartMutation`, `mockedFetch`, `checkoutOrderFormValidFetch`, and
the `pickFetchAPICallResult` usage to change the comment accordingly.
packages/api/test/mocks/ValidateCartMutation.ts (1)

499-512: ⚡ Quick win

Add explicit return type annotation.

The createProductFetchResultForSku helper would benefit from an explicit return type for better TypeScript safety and IDE autocomplete.

🔧 Suggested type annotation
-export const createProductFetchResultForSku = (skuId: string) => {
+export const createProductFetchResultForSku = (skuId: string): typeof productSearchPage1Count1Fetch.result => {
   const { items, ...product } = productSearchPage1Count1Fetch.result
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/api/test/mocks/ValidateCartMutation.ts` around lines 499 - 512, The
helper createProductFetchResultForSku currently returns an inferred object; add
an explicit return type to its function signature (for example use the
appropriate existing shape such as the type of
productSearchPage1Count1Fetch.result or the concrete ProductFetchResult
interface if one exists) so TypeScript and IDEs get proper typing; update the
function declaration to: export const createProductFetchResultForSku = (skuId:
string): <appropriate-type> => { ... } and ensure the chosen type matches the
structure (productId, items with itemId) and import or reference that type where
needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/api/src/platforms/vtex/loaders/sku.ts`:
- Around line 15-22: The VTEX SKU loader currently lets productsByIdentifier
(which internally runs Promise.all over values via fetchProduct) fan out up to
maxBatchSize (99) concurrent HTTP calls; reduce concurrency by either lowering
maxBatchSize or adding a concurrency limiter around the batch call—specifically
modify the loader in sku.ts to wrap the values iteration (the call site that
invokes clients.search.productsByIdentifier / fetchProduct) with a worker
pool/limit (e.g., p-limit or a simple semaphore) or set maxBatchSize to a safer
value and document the choice; ensure the change targets the code that
constructs the values array and calls productsByIdentifier/fetchProduct so the
loader no longer issues 99 parallel requests.

---

Nitpick comments:
In `@packages/api/src/platforms/vtex/clients/search/index.ts`:
- Around line 154-159: The return currently wraps fetchAPI(...) in
Promise.resolve unnecessarily; update the function to return fetchAPI(...)
directly instead of Promise.resolve(fetchAPI(...))—locate the return statement
that builds the URL with
`${base}/api/intelligent-search/v1/products?${request.params.toString()}` and
headers and replace the wrapped call so it simply returns the Promise from
fetchAPI with the same arguments.
- Around line 162-182: productsByIdentifier currently fires one fetchProduct
call per value via Promise.all which can burst many concurrent requests (up to
DataLoader maxBatchSize), so limit concurrency: replace Promise.all over values
with a controlled parallelism runner (e.g., use p-map/p-limit or an internal
semaphore/queue) to call fetchProduct with a fixed concurrency (choose a
sensible default like 5–10) and keep the same return filtering; reference
productsByIdentifier and fetchProduct when making the change and add a short
code comment/docstring noting the chosen max concurrency and expected max values
cardinality so callers understand throttling behavior.

In `@packages/api/test/integration/mutations.test.ts`:
- Around line 101-113: The test comment for the `validateCart` case is
misleading: it says "only checkout calls are made" but `mockedFetch` is expected
to be called twice because the test's fetch interceptor
(`pickFetchAPICallResult`) handles both the explicit
`checkoutOrderFormValidFetch` and an implicit dynamic v1/products call; update
the inline comment to enumerate both expected calls (checkout and v1/products)
so future readers understand why `mockedFetch` is called twice — locate the test
around `ValidateCartMutation`, `mockedFetch`, `checkoutOrderFormValidFetch`, and
the `pickFetchAPICallResult` usage to change the comment accordingly.

In `@packages/api/test/mocks/ValidateCartMutation.ts`:
- Around line 499-512: The helper createProductFetchResultForSku currently
returns an inferred object; add an explicit return type to its function
signature (for example use the appropriate existing shape such as the type of
productSearchPage1Count1Fetch.result or the concrete ProductFetchResult
interface if one exists) so TypeScript and IDEs get proper typing; update the
function declaration to: export const createProductFetchResultForSku = (skuId:
string): <appropriate-type> => { ... } and ensure the chosen type matches the
structure (productId, items with itemId) and import or reference that type where
needed.

In
`@packages/api/test/unit/platforms/vtex/utils/intelligentSearchRequest.test.ts`:
- Around line 374-411: Add unit tests for parseSegmentCookie that assert its
fallback contract: when vtex_segment is missing or malformed it should return an
empty object ({}), not throw. Use the existing cookieHeader helper to pass an
empty string (no cookie) and an invalid base64 string (e.g. 'invalid') into
parseSegmentCookie, and add assertions like expect(segment).toEqual({}) for both
cases to lock in the non-throw behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b4c420f0-51ca-43b6-9a9d-b249ad135771

📥 Commits

Reviewing files that changed from the base of the PR and between ed0edfd and 2efe494.

📒 Files selected for processing (12)
  • packages/api/src/platforms/vtex/clients/search/index.ts
  • packages/api/src/platforms/vtex/loaders/sku.ts
  • packages/api/src/platforms/vtex/resolvers/query.ts
  • packages/api/src/platforms/vtex/utils/intelligentSearchRequest.ts
  • packages/api/test/integration/mutations.test.ts
  • packages/api/test/integration/queries.test.ts
  • packages/api/test/mocks/AllProductsQuery.ts
  • packages/api/test/mocks/ProductQuery.ts
  • packages/api/test/mocks/RedirectQuery.ts
  • packages/api/test/mocks/SearchQuery.ts
  • packages/api/test/mocks/ValidateCartMutation.ts
  • packages/api/test/unit/platforms/vtex/utils/intelligentSearchRequest.test.ts

Co-authored-by: Cursor <cursoragent@cursor.com>
@pkg-pr-new

pkg-pr-new Bot commented Jun 9, 2026

Copy link
Copy Markdown

Open in StackBlitz

@faststore/api

npm i https://pkg.pr.new/vtex/faststore/@faststore/api@c440ebd

@faststore/cli

npm i https://pkg.pr.new/vtex/faststore/@faststore/cli@c440ebd

@faststore/components

npm i https://pkg.pr.new/vtex/faststore/@faststore/components@c440ebd

@faststore/core

npm i https://pkg.pr.new/vtex/faststore/@faststore/core@c440ebd

@faststore/diagnostics

npm i https://pkg.pr.new/vtex/faststore/@faststore/diagnostics@c440ebd

@faststore/lighthouse

npm i https://pkg.pr.new/vtex/faststore/@faststore/lighthouse@c440ebd

@faststore/sdk

npm i https://pkg.pr.new/vtex/faststore/@faststore/sdk@c440ebd

@faststore/ui

npm i https://pkg.pr.new/vtex/faststore/@faststore/ui@c440ebd

commit: c440ebd

page: Math.ceil(after / first) || 0,
count: first,
query: term ?? undefined,
sort: SORT_MAP[sort ?? 'score_desc'],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If sort is defined but not present on the SORT_MAP it would pass sort as undefined.

Suggested change
sort: SORT_MAP[sort ?? 'score_desc'],
sort: SORT_MAP[sort] ?? SORT_MAP['score_desc'],

sponsoredCount: sponsoredCount ?? undefined,
const productSearchPromise: Promise<ProductSearchResult> =
ctx.clients.search
.productsByIdentifier({ field: 'id', values: productIds })

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we are hiting the productsByIdentifier endpoint directly without any dataloader controlling how many requests it should batch as it is done in the getSKULoader. Shouldn't it have some logic to control max request fired simultaneously?

page: 0,
count: productIds.length,
query,
const products = await search.productsByIdentifier({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as before. Should we hit this directly without a dataloader controlling the batch size and caching repeated keys requests?

@renatomaurovtex renatomaurovtex left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the port of the Intelligent Search v1 migration onto dev (v4). The refactor is clean and the request builder is well factored; the trust-boundary handling in resolveSegmentData (server defaults authoritative over the client-supplied vtex_segment cookie) is exactly right and good to see called out in a comment. A few things before merge.

packages/api/src/platforms/vtex/clients/search/index.ts

🟠 [bug] productsByIdentifier rejects the whole batch on a single missing identifier

fetchProduct returns fetchAPI(...), and fetchAPI throws (NotFoundError, etc.) on any non-2xx — it never resolves to null. So the Promise.all has no per-item catch, which means one 404 (a removed/invalid SKU, or a cross-selling id the catalog no longer has) rejects the entire call. That makes the trailing .filter((product) => product !== null) dead code, and it's a behavior regression vs the old single sku:a;b;c / id:a;b;c query, which silently omitted missing items. This path feeds the SKU loader (cart) and the cross-selling / productsById shelves, so one bad id can blank an entire shelf or fail a cart load.

    const productsResult = await Promise.all(
      values.map((value) =>
        fetchProduct({
          field,
          value,
          hideUnavailableItems: hideUnavailable,
          showInvisibleItems,
        }).catch(() => null)
      )
    )

    return productsResult.filter(
      (product): product is Product => product !== null
    )

Can you confirm what the v1 products endpoint returns for a valid-but-unavailable SKU under hideUnavailableItems (404 vs product with empty sellers)? Either way the per-item catch restores the old resilience, and a small int test for "one identifier missing → the rest still return" would lock it in.

💬 [perf] Unbounded fan-out

productsByIdentifier now issues one HTTP request per identifier instead of one batched query. For a large cart or a wide cross-sell that's N round-trips to IS with no concurrency cap. p-limit is already a dependency here — worth wrapping the map to bound concurrency and avoid hammering / rate-limiting IS.

packages/api/src/platforms/vtex/resolvers/query.ts

💬 [behavior] cross-selling now returns a synthetic ProductSearchResult with empty pagination (recordsFiltered: products.length, all proxyURLs ''). Since we slice to first and hydrate by id, paging beyond the first set is gone. Was cross-selling pagination relied on anywhere downstream, or is bounding to first the intended contract now?

💬 [behavior] shouldFallbackToProductRoute is a nice tightening — the product resolver no longer masks infra/transient errors by falling back to the product route, only NotFound / InvalidSkuId / slug-mismatch. Good change; just flagging it's a behavior shift (a 5xx on skuLoader now surfaces instead of silently retrying via route).

💬 [nit] The title says "with caching" but I don't see cache logic in the diff — is that referring to upstream caching enabled by the v1 endpoint, or is a caching layer expected in this PR?

Verdict: Approved with comments

Blocking (🟠):

  • productsByIdentifier: add a per-item catch(() => null) (or confirm v1 never 404s here) so a single missing identifier doesn't fail the whole cart/shelf; the current .filter is dead code.

Non-blocking (🟡/💬):

  • Bound the per-identifier fan-out with the already-present p-limit.
  • Confirm cross-selling no longer needs real pagination metadata.
  • Note the product resolver fallback narrowing (intended).
  • Clarify what "caching" in the title refers to.

Checks to confirm before merge: pnpm lint · build · pnpm test:unit + pnpm test:int in packages/api (incl. a partial-failure test for productsByIdentifier) · validate against an account already on IS v1.

@salesfelipe salesfelipe changed the title feat(api): Migrate to VTEX Intelligent Search V1 API with caching feat(api): Migrate to VTEX Intelligent Search V1 API Jun 15, 2026
@sonar-workflows

Copy link
Copy Markdown

@hellofanny

Copy link
Copy Markdown
Contributor

Thanks @salesfelipe!
I've run some manual testing on the brandless account (local dev, it-IT locale) — covering the core IS V1 changes and validating the localization pre-conditions needed for the upcoming PLP localization work.

  1. Localized product slugs on PLP | ✅ IS V1 returns linkText translated
     locale: "it-IT" confirmed in every search payload
image
  1. PDP loads ✅
  2. Add to cart + cart drawer | ✅ skuLoader / productsByIdentifier working
image
  1. Search results | ✅ Localized slugs returned
    searched for polo
image
  1. Price filter | ✅ Correct results; payload shows key: "price" with range value
    Payload shows { key: "price", value: "1043.00-to-1054.00" } & url updated
image
  1. Cross-selling shelf | ✅ Renders on PDP
  2. getSegmentLocale fallback chain works correctly for SSR (no vtex_segment cookie) and client-side requests ✅

Comment thread packages/api/src/platforms/vtex/clients/search/index.ts
@salesfelipe salesfelipe requested a review from hellofanny June 17, 2026 17:50
@hellofanny hellofanny merged commit 74e034a into dev Jun 19, 2026
12 of 13 checks passed
@hellofanny hellofanny deleted the feat-new-is-api branch June 19, 2026 14:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants