[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-resolver/node/services/banners.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { buildAttributePath } from '../commons/compatibility-layer'

export async function fetchBanners(
  ctx: Context,
  args: { fullText: string; selectedFacets: SelectedFacet[] }
) {
  const { intsch } = ctx.clients

  const argumentsToFetchBanners = {
    query: args.fullText,
    path: buildAttributePath(args.selectedFacets),
  }

  const locale = (ctx.vtex.segment?.cultureInfo ||
    ctx.vtex.tenant?.locale ||
    ctx.vtex.locale) as string

  return intsch.fetchBannersV1({ ...argumentsToFetchBanners, locale })
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-resolver/node/services/facets.test.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { fetchFacets } from './facets'
import { createContext } from '../mocks/contextFactory'
import type { FacetsInput } from '../typings/Search'
import * as compareResultsModule from '../utils/compareResults'

describe('fetchFacets service', () => {
  const mockFacetsResponse = {
    facets: [
      {
        name: 'Category',
        values: [
          { name: 'Electronics', quantity: 10 },
          { name: 'Clothing', quantity: 5 },
        ],
      },
    ],
    translated: false,
  }

  const mockArgs: FacetsInput = {
    fullText: 'test query',
    query: 'test',
    map: 'ft',
    selectedFacets: [],
    removeHiddenFacets: false,
    hideUnavailableItems: false,
    categoryTreeBehavior: 'default',
  }

  const mockSelectedFacets: SelectedFacet[] = []

  beforeEach(() => {
    jest.clearAllMocks()
  })

  it('should use intsch when shouldUseNewPLPEndpoint is true', async () => {
    const ctx = createContext({
      accountName: 'testaccount',
      appSettings: {
        shouldUseNewPLPEndpoint: true,
      },
      intschSettings: {
        facets: mockFacetsResponse,
      },
    })

    const result = await fetchFacets(ctx, {
      args: mockArgs,
      selectedFacets: mockSelectedFacets,
    })

    expect(ctx.clients.intsch.facets).toHaveBeenCalled()
    expect(ctx.clients.intelligentSearchApi.facets).not.toHaveBeenCalled()
    expect(result).toEqual(mockFacetsResponse)
  })

  it('should compare both APIs when shouldUseNewPLPEndpoint is undefined', async () => {
    const ctx = createContext({
      accountName: 'testaccount',
      appSettings: {
        shouldUseNewPLPEndpoint: undefined,
      },
      intelligentSearchApiSettings: {
        facets: mockFacetsResponse,
      },
      intschSettings: {
        facets: mockFacetsResponse,
      },
    })

    const compareApiResultsSpy = jest
      .spyOn(compareResultsModule, 'compareApiResults')
      .mockResolvedValue(mockFacetsResponse)

    const result = await fetchFacets(ctx, {
      args: mockArgs,
      selectedFacets: mockSelectedFacets,
    })

    expect(compareApiResultsSpy).toHaveBeenCalled()
    expect(result).toEqual(mockFacetsResponse)

    compareApiResultsSpy.mockRestore()
  })

  it('should handle shipping options correctly', async () => {
    const ctx = createContext({
      accountName: 'testaccount',
      appSettings: {
        shouldUseNewPLPEndpoint: false,
      },
      intelligentSearchApiSettings: {
        facets: mockFacetsResponse,
      },
    })

    const shippingOptions = ['delivery', 'pickup']

    await fetchFacets(ctx, {
      args: mockArgs,
      selectedFacets: mockSelectedFacets,
      shippingOptions,
    })

    expect(ctx.clients.intelligentSearchApi.facets).toHaveBeenCalledWith(
      expect.objectContaining({ query: 'test query' }),
      expect.any(String),
      shippingOptions
    )
  })

  it('should set translated flag in context when tenant is present', async () => {
    const ctx = createContext({
      accountName: 'testaccount',
      appSettings: {
        shouldUseNewPLPEndpoint: false,
      },
      intelligentSearchApiSettings: {
        facets: { ...mockFacetsResponse, translated: true },
      },
      tenantLocale: 'en-US',
    })

    await fetchFacets(ctx, {
      args: mockArgs,
      selectedFacets: mockSelectedFacets,
    })

    expect(ctx.translated).toBe(true)
  })
})

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-resolver/node/services/product.test.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { fetchProduct, buildVtexSegment } from './product'
import { createContext } from '../mocks/contextFactory'

describe('fetchProduct service', () => {
  const mockProduct = {
    productId: 'test-product',
    productName: 'Test Product',
    brand: 'Test Brand',
  } as SearchProduct

  beforeEach(() => {
    jest.clearAllMocks()
  })

  it('should build vtex segment correctly', () => {
    const vtexSegment = buildVtexSegment({
      vtexSegment: {
        campaigns: null,
        channel: '1',
        priceTables: null,
        regionId: 'U1cjMQ==',
        utm_campaign: null,
        utm_source: null,
        utmi_campaign: null,
        currencyCode: 'ARS',
        currencySymbol: '$',
        countryCode: 'ARG',
        cultureInfo: 'es-AR',
        admin_cultureInfo: 'es-AR',
        channelPrivacy: 'public',
      },
      salesChannel: '2',
      regionId: '3',
    })

    const result = JSON.parse(Buffer.from(vtexSegment, 'base64').toString())

    expect(result.regionId).toBe('3')
    expect(result.countryCode).toBe('ARG')
    expect(result.currencySymbol).toBe('$')
    expect(result.currencyCode).toBe('ARS')
    expect(result.channel).toBe('2')
    expect(result.cultureInfo).toBe('es-AR')
    expect(result.admin_cultureInfo).toBe('es-AR')
    expect(result.channelPrivacy).toBe('public')
    expect(result.campaigns).toBeNull()
    expect(result.priceTables).toBeNull()
    expect(result.utm_campaign).toBeNull()
    expect(result.utm_source).toBeNull()
    expect(result.utmi_campaign).toBeNull()
  })

  it('should use intsch directly for b2bstoreqa account', async () => {
    const ctx = createContext({
      accountName: 'b2bstoreqa',
      appSettings: {
        shouldUseNewPDPEndpoint: true,
      },
    })

    // Mock the intsch client to return a product
    jest
      .spyOn(ctx.clients.intsch, 'fetchProduct')
      .mockImplementation()
      .mockResolvedValue(mockProduct)

    const args = {
      identifier: { field: 'id' as const, value: 'test-id' },
      salesChannel: 1,
    }

    const result = await fetchProduct(ctx, args)

    expect(ctx.clients.intsch.fetchProduct).toHaveBeenCalled()
    expect(result).toEqual([mockProduct])
  })

  it('should use intsch directly for biggy account', async () => {
    const ctx = createContext({
      accountName: 'biggy',
      appSettings: {
        shouldUseNewPDPEndpoint: true,
      },
    })

    // Mock the intsch client to return a product
    jest
      .spyOn(ctx.clients.intsch, 'fetchProduct')
      .mockImplementation()
      .mockResolvedValue(mockProduct)

    const args = {
      identifier: { field: 'id' as const, value: 'test-id' },
      salesChannel: 1,
    }

    const result = await fetchProduct(ctx, args)

    expect(ctx.clients.intsch.fetchProduct).toHaveBeenCalled()
    expect(result).toEqual([mockProduct])
  })
})

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-resolver/node/services/productSearch.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
import {
  buildAttributePath,
  convertOrderBy,
} from '../commons/compatibility-layer'
import { getWorkspaceSearchParamsFromStorage } from '../routes/workspaceSearchParams'
import { compareApiResults } from '../utils/compareResults'
import { fetchAppSettings } from './settings'
import type { ProductSearchInput } from '../typings/Search'
import { decodeQuery } from '../clients/intelligent-search-api'
import { parseState } from '../utils/searchState'

/**
 * Builds a query string from an object of params, filtering out undefined/null values
 */
function buildQueryString(params: Record<string, any>): string {
  const searchParams = new URLSearchParams()

  for (const [key, value] of Object.entries(params)) {
    if (value !== undefined && value !== null && value !== '') {
      searchParams.append(key, String(value))
    }
  }

  return searchParams.toString()
}

/**
 * Builds curl commands for debugging API requests
 */
function buildCurlCommands(
  ctx: Context,
  path: string,
  params: Record<string, any>,
  shippingOptions?: string[]
): { biggyCurl: string; intschCurl: string } {
  const { workspace, account } = ctx.vtex
  const queryString = buildQueryString(params)

  const shippingHeader = shippingOptions?.length
    ? ` -H "x-vtex-shipping-options: ${shippingOptions.join(',')}"`
    : ''

  const biggyCurl = `curl "https://${workspace}--${account}.myvtex.com/_v/api/intelligent-search/product_search/${path}?${queryString}"${shippingHeader}`
  const intschCurl = `curl "https://${account}.myvtex.com/api/intelligent-search/v0/product-search/${path}?${queryString}"${shippingHeader}`

  return { biggyCurl, intschCurl }
}

const defaultAdvertisementOptions = {
  showSponsored: false,
  sponsoredCount: 3,
  repeatSponsoredProducts: true,
}

/**
 * Fetches product search results using the intelligentSearchApi client (Biggy)
 */
// eslint-disable-next-line max-params
async function fetchProductSearchFromBiggy(
  ctx: Context,
  args: ProductSearchInput,
  selectedFacets: SelectedFacet[],
  shippingOptions?: string[]
) {
  const { intelligentSearchApi } = ctx.clients
  const { fullText, advertisementOptions = defaultAdvertisementOptions } = args

  const workspaceSearchParams = await getWorkspaceSearchParamsFromStorage(ctx)

  const biggyArgs: { [key: string]: any } = {
    ...advertisementOptions,
    ...args,
    query: fullText,
    sort: convertOrderBy(args.orderBy),
    ...args.options,
    ...workspaceSearchParams,
  }

  // unnecessary field. It's is an object and breaks the @vtex/api cache
  delete biggyArgs.selectedFacets
  delete biggyArgs.advertisementOptions

  const result: any = await intelligentSearchApi.productSearch(
    { ...biggyArgs },
    buildAttributePath(selectedFacets),
    shippingOptions
  )

  if (ctx.vtex.tenant && !args.productOriginVtex) {
    ctx.translated = result.translated
  }

  return {
    searchState: args.searchState,
    ...result,
  }
}

/**
 * Fetches product search results using the intsch client (Intelligent Search)
 */
// eslint-disable-next-line max-params
async function fetchProductSearchFromIntsch(
  ctx: Context,
  args: ProductSearchInput,
  selectedFacets: SelectedFacet[],
  shippingOptions?: string[]
) {
  const { intsch } = ctx.clients
  const { fullText, advertisementOptions = defaultAdvertisementOptions } = args

  const workspaceSearchParams = await getWorkspaceSearchParamsFromStorage(ctx)

  const intschArgs: { [key: string]: any } = {
    ...advertisementOptions,
    ...args,
    query: fullText,
    sort: convertOrderBy(args.orderBy),
    ...args.options,
    ...workspaceSearchParams,
  }

  // unnecessary field. It's is an object and breaks the @vtex/api cache
  delete intschArgs.selectedFacets
  delete intschArgs.advertisementOptions

  const result: any = await intsch.productSearch(
    { ...intschArgs },
    buildAttributePath(selectedFacets),
    shippingOptions
  )

  if (ctx.vtex.tenant && !args.productOriginVtex) {
    ctx.translated = result.translated
  }

  return {
    searchState: args.searchState,
    ...result,
  }
}

/**
 * ProductSearch service that extracts product search fetching logic and implements comparison or flag-based routing
 */
// eslint-disable-next-line max-params
export async function fetchProductSearch(
  ctx: Context,
  args: ProductSearchInput,
  selectedFacets: SelectedFacet[],
  shippingOptions?: string[]
) {
  const { shouldUseNewPLPEndpoint } = await fetchAppSettings(ctx)

  if (shouldUseNewPLPEndpoint) {
    return fetchProductSearchFromIntsch(
      ctx,
      args,
      selectedFacets,
      shippingOptions
    )
  }

  // Build the exact request params as the clients do for debugging
  const path = buildAttributePath(selectedFacets)
  const workspaceSearchParams = await getWorkspaceSearchParamsFromStorage(ctx)
  const { advertisementOptions = defaultAdvertisementOptions } = args

  const clientArgs: { [key: string]: any } = {
    ...advertisementOptions,
    ...args,
    query: args.fullText,
    sort: convertOrderBy(args.orderBy),
    ...args.options,
    ...workspaceSearchParams,
  }

  delete clientArgs.selectedFacets
  delete clientArgs.advertisementOptions

  const { leap, searchState } = args as { leap?: boolean; searchState?: string }

  const requestParams = {
    query: args.fullText && decodeQuery(args.fullText),
    locale: ctx.vtex.locale ?? ctx.vtex.tenant?.locale,
    bgy_leap: leap ? true : undefined,
    ...parseState(searchState),
    ...clientArgs,
  }

  const { biggyCurl, intschCurl } = buildCurlCommands(
    ctx,
    path,
    requestParams,
    shippingOptions
  )

  return compareApiResults(
    () =>
      fetchProductSearchFromBiggy(ctx, args, selectedFacets, shippingOptions),
    () =>
      fetchProductSearchFromIntsch(ctx, args, selectedFacets, shippingOptions),
    ctx.vtex.production ? 1 : 100,
    ctx.vtex.logger,
    {
      logPrefix: 'ProductSearch',
      args: {
        biggyCurl,
        intschCurl,
      },
    }
  )
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-resolver/node/clients/intsch/types.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
import type { SearchResultArgs } from '../../typings/Search'
import type { FacetsArgs } from '../intelligent-search-api'

export type AutocompleteSuggestionsArgs = {
  query: string
}

export type AutocompleteSuggestionsArgsV1 = {
  locale: string
  query: string
}

export type AutocompleteSuggestionsResponse = {
  searches: Array<{
    term: string
    count: number
    attributes?: Array<{
      key: string
      value: string
      labelKey: string
      labelValue: string
    }>
  }>
}

export type TopSearchesResponse = {
  searches: Array<{
    term: string
    count: number
  }>
}

export type SearchSuggestionsArgs = {
  query: string
}

export type SearchSuggestionsArgsV1 = {
  query: string
  locale: string
}

export type SearchSuggestionsResponse = {
  searches: Array<{
    term: string
    count: number
  }>
}

export type CorrectionArgs = {
  query: string
}

export type CorrectionArgsV1 = {
  query: string
  locale: string
}

export type CorrectionResponse = {
  correction: {
    text: string
    highlighted: string
    misspelled: boolean
    correction: boolean
  }
}

export type FetchBannersArgs = {
  query: string
  path: string
}

export type FetchBannersArgsV1 = {
  locale: string
  query: string
  path: string
}

export type FetchBannersResponse = {
  banners: Array<{
    id: string
    name: string
    area: string
    html: string
  }>
}

export type FetchProductArgs = {
  field: 'id' | 'slug' | 'ean' | 'reference' | 'sku'
  value: string
  salesChannel?: string
  regionId?: string
  locale?: string
}

export type FetchProductResponse = SearchProduct

export type ProductSearchResponse = {
  products: SearchProduct[]
}

export type FacetsResponse = any

export interface IIntelligentSearchClient {
  fetchAutocompleteSuggestions(
    args: AutocompleteSuggestionsArgs
  ): Promise<AutocompleteSuggestionsResponse>
  fetchTopSearches(): Promise<TopSearchesResponse>
  fetchSearchSuggestions(
    args: SearchSuggestionsArgs
  ): Promise<SearchSuggestionsResponse>
  fetchCorrection(args: CorrectionArgs): Promise<CorrectionResponse>
  fetchBanners(args: FetchBannersArgs): Promise<FetchBannersResponse>
  fetchProduct(args: FetchProductArgs): Promise<FetchProductResponse>
  fetchAutocompleteSuggestionsV1(
    args: AutocompleteSuggestionsArgsV1
  ): Promise<AutocompleteSuggestionsResponse>
  fetchTopSearchesV1(locale: string): Promise<TopSearchesResponse>
  fetchSearchSuggestionsV1(
    args: SearchSuggestionsArgsV1
  ): Promise<SearchSuggestionsResponse>
  fetchCorrectionV1(args: CorrectionArgsV1): Promise<CorrectionResponse>
  fetchBannersV1(args: FetchBannersArgsV1): Promise<FetchBannersResponse>
  productSearch(
    args: SearchResultArgs,
    path: string,
    shippingHeader?: string[]
  ): Promise<ProductSearchResponse>
  facets(
    params: FacetsArgs,
    path: string,
    shippingHeader?: string[]
  ): Promise<FacetsResponse>
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-resolver/node/clients/intsch/index.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
import type { InstanceOptions, IOContext } from '@vtex/api'
import { JanusClient } from '@vtex/api'

import type {
  AutocompleteSuggestionsArgs,
  AutocompleteSuggestionsArgsV1,
  AutocompleteSuggestionsResponse,
  CorrectionArgs,
  CorrectionArgsV1,
  CorrectionResponse,
  FacetsResponse,
  FetchBannersArgs,
  FetchBannersArgsV1,
  FetchBannersResponse,
  FetchProductArgs,
  FetchProductResponse,
  IIntelligentSearchClient,
  ProductSearchResponse,
  SearchSuggestionsArgs,
  SearchSuggestionsArgsV1,
  SearchSuggestionsResponse,
  TopSearchesResponse,
} from './types'
import type { SearchResultArgs } from '../../typings/Search'
import type { FacetsArgs } from '../intelligent-search-api'
import { decodeQuery, isPathTraversal } from '../intelligent-search-api'
import { parseState } from '../../utils/searchState'

export class Intsch extends JanusClient implements IIntelligentSearchClient {
  private locale: string | undefined

  constructor(ctx: IOContext, options?: InstanceOptions) {
    const env = ctx.production ? 'stable' : 'beta'

    super(ctx, options, env)

    const { locale, tenant } = ctx

    this.locale = locale ?? tenant?.locale
  }

  public fetchProduct(args: FetchProductArgs): Promise<FetchProductResponse> {
    // The admin auth token is releavant for CallCenter users when the sales channel is private.
    const authToken =
      this.context.storeUserAuthToken ?? this.context.adminUserAuthToken

    return this.http.get('/api/intelligent-search/v1/products', {
      params: {
        field: args.field,
        value: args.value,
        sc: args.salesChannel ?? 1,
        regionId: args.regionId,
        locale: args.locale,
      },
      metric: 'search-product-new',
      headers: {
        ...(authToken ? { VtexIdclientAutCookie: authToken } : {}),
      },
    })
  }

  public fetchAutocompleteSuggestions(
    args: AutocompleteSuggestionsArgs
  ): Promise<AutocompleteSuggestionsResponse> {
    return this.http.get(
      '/api/intelligent-search/v0/autocomplete-suggestions',
      {
        params: { query: args.query, locale: this.locale },
        metric: 'autocompleteSearchSuggestions-new',
      }
    )
  }

  public fetchTopSearches(): Promise<TopSearchesResponse> {
    return this.http.get('/api/intelligent-search/v0/top-searches', {
      params: { locale: this.locale },
      metric: 'topSearches-new',
    })
  }

  public fetchSearchSuggestions(
    args: SearchSuggestionsArgs
  ): Promise<SearchSuggestionsResponse> {
    return this.http.get('/api/intelligent-search/v0/search-suggestions', {
      params: { query: args.query, locale: this.locale },
      metric: 'searchSuggestions-new',
    })
  }

  public fetchCorrection(args: CorrectionArgs): Promise<CorrectionResponse> {
    return this.http.get('/api/intelligent-search/v0/correction-search', {
      params: { query: args.query, locale: this.locale },
      metric: 'correction-new',
    })
  }

  public fetchBanners(args: FetchBannersArgs): Promise<FetchBannersResponse> {
    return this.http.get(`/api/intelligent-search/v0/banners/${args.path}`, {
      params: { query: args.query, locale: this.locale },
      metric: 'banners-new',
    })
  }

  public fetchAutocompleteSuggestionsV1(
    args: AutocompleteSuggestionsArgsV1
  ): Promise<AutocompleteSuggestionsResponse> {
    return this.http.get(
      '/api/intelligent-search/v1/autocomplete-suggestions',
      {
        params: { query: args.query, locale: args.locale },
        metric: 'autocompleteSearchSuggestions-new-v1',
      }
    )
  }

  public fetchTopSearchesV1(locale: string): Promise<TopSearchesResponse> {
    return this.http.get('/api/intelligent-search/v1/top-searches', {
      params: { locale },
      metric: 'topSearches-new-v1',
    })
  }

  public fetchSearchSuggestionsV1(
    args: SearchSuggestionsArgsV1
  ): Promise<SearchSuggestionsResponse> {
    return this.http.get('/api/intelligent-search/v1/search-suggestions', {
      params: { query: args.query, locale: args.locale },
      metric: 'searchSuggestions-new-v1',
    })
  }

  public fetchCorrectionV1(
    args: CorrectionArgsV1
  ): Promise<CorrectionResponse> {
    return this.http.get('/api/intelligent-search/v1/correction-search', {
      params: { query: args.query, locale: args.locale },
      metric: 'correction-new-v1',
    })
  }

  public fetchBannersV1(
    args: FetchBannersArgsV1
  ): Promise<FetchBannersResponse> {
    return this.http.get(`/api/intelligent-search/v1/banners/${args.path}`, {
      params: { query: args.query, locale: args.locale },
      metric: 'banners-new-v1',
    })
  }

  public productSearch(
    params: SearchResultArgs,
    path: string,
    shippingHeader?: string[]
  ): Promise<ProductSearchResponse> {
    const { query, leap, searchState } = params

    if (isPathTraversal(path)) {
      throw new Error('Malformed URL')
    }

    // The admin auth token is releavant for CallCenter users when the sales channel is private.
    const authToken =
      this.context.storeUserAuthToken ?? this.context.adminUserAuthToken

    return this.http.get(`/api/intelligent-search/v0/product-search/${path}`, {
      params: {
        query: query && decodeQuery(query),
        locale: this.locale,
        bgy_leap: leap ? true : undefined,
        ...parseState(searchState),
        ...params,
      },
      metric: 'product-search-new',
      headers: {
        'x-vtex-shipping-options': shippingHeader ?? '',
        ...(authToken ? { VtexIdclientAutCookie: authToken } : {}),
      },
    })
  }

  public facets(
    params: FacetsArgs,
    path: string,
    shippingHeader?: string[]
  ): Promise<FacetsResponse> {
    if (isPathTraversal(path)) {
      throw new Error('Malformed URL')
    }

    const { query, leap, searchState } = params

    // The admin auth token is releavant for CallCenter users when the sales channel is private.
    const authToken =
      this.context.storeUserAuthToken ?? this.context.adminUserAuthToken

    return this.http.get(`/api/intelligent-search/v0/facets/${path}`, {
      params: {
        ...params,
        query: query && decodeQuery(query),
        locale: this.locale,
        bgy_leap: leap ? true : undefined,
        ...parseState(searchState),
      },
      metric: 'facets-new',
      headers: {
        'x-vtex-shipping-options': shippingHeader ?? '',
        ...(authToken ? { VtexIdclientAutCookie: authToken } : {}),
      },
    })
  }
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-resolver/node/typings/@gocommerce/utils.d.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
declare module '@gocommerce/utils' {
  const Functions: any
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-resolver/node/resolvers/search/category.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { compose, last, prop, split } from 'ramda'

import { getCategoryInfo, logDegradedSearchError } from './utils'
import { formatTranslatableProp, shouldTranslateToBinding } from '../../utils/i18n'
import { Slugify } from '../../utils/slug'
import { APP_NAME } from './constants'

const lastSegment = compose<string, string[], string>(
  last,
  split('/')
)

function cleanUrl(url: string) {
  return url.replace(/https:\/\/[A-z0-9]+\.vtexcommercestable\.com\.br/, '').toLowerCase()
}

/** This type has to be created because the Catlog API to get category by ID does not return the url or children for now.
 * These fields only come if you get the category from the categroy tree api.
 */

type SafeCategory = CategoryByIdResponse | CategoryTreeResponse

export const resolvers = {
  Category: {
    name: formatTranslatableProp<SafeCategory, 'name', 'id'>(
      'name',
      'id'
    ),

    cacheId: prop('id'),

    href: async ({ url, id }: SafeCategory, _: unknown, ctx: Context) => {
      const settings: AppSettings = await ctx.clients.apps.getAppSettings(APP_NAME)

      if (shouldTranslateToBinding(ctx)) {
        try {
          const rewriterUrl = await ctx.clients.rewriter.getRoute(id.toString(), 'anyCategoryEntity', ctx.vtex.binding!.id!)
          if (rewriterUrl) {
            url = rewriterUrl
          }
        } catch (e) {
          logDegradedSearchError(ctx.vtex.logger, {
            service: 'Rewriter getRoute',
            error: `Rewriter getRoute query returned an error for category ${id}. Category href may be incorrect.`,
            errorStack: e,
          })
        }
      }
      const pathname = cleanUrl(url)

      return settings.slugifyLinks ? Slugify(pathname) : pathname
    },

    metaTagDescription: formatTranslatableProp<SafeCategory, 'MetaTagDescription', 'id'>(
      'MetaTagDescription',
      'id'
    ),

    titleTag: formatTranslatableProp<SafeCategory, 'Title', 'id'>(
      'Title',
      'id'
    ),

    slug: async ({ url, id }: SafeCategory, _: unknown, ctx: Context) => {
      if (shouldTranslateToBinding(ctx)) {
        try {
          const rewriterUrl = await ctx.clients.rewriter.getRoute(id.toString(), 'anyCategoryEntity', ctx.vtex.binding!.id!)
          if (rewriterUrl) {
            url = rewriterUrl
          }
        } catch (e) {
          logDegradedSearchError(ctx.vtex.logger, {
            service: 'Rewriter getRoute',
            error: `Rewriter getRoute query returned an error for category ${id}. Category slug may be incorrect.`,
            errorStack: e,
          })
        }
      }
      return url ? lastSegment(url) : null
    },

    children: async (
      { id, children }: SafeCategory,
      _: any,
      { clients: { search } }: Context
    ) => {
      if (children == null) {
        const category = await getCategoryInfo(search, id, 5)
        children = category.children
      }
      return children
    },
  },
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-resolver/node/resolvers/search/searchBreadcrumb.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { APP_NAME } from './constants'
import { Slugify } from '../../utils/slug'

export const resolvers = {
  SearchBreadcrumb: {
    href: async ({ href }: { href: string }, _: unknown, ctx: Context) => {
      const settings: AppSettings = await ctx.clients.apps.getAppSettings(APP_NAME)

      return settings.slugifyLinks ? Slugify(href) : href
    },
  },
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-resolver/node/resolvers/search/assemblyOption.test.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { resolvers } from './assemblyOption'

test('should return a TEXT type', async () => {
  const inputValues: InputValues = {
    'Line 1': {
      maximumNumberOfCharacters: 20,
      domain: []
    }
  }

  const result = await resolvers.AssemblyOption.inputValues(
    { inputValues } as AssemblyOption
  )

  expect(result).toHaveLength(1)
  expect(result[0].label).toBe('Line 1')
  expect(result[0].type).toBe('TEXT')
  expect(result[0].maxLength).toBe(20)
  expect(result[0].domain).not.toBeDefined()
  expect(result[0].defaultValue).toBe('')
})

test('should return an OPTIONS type', async () => {
  const inputValues: InputValues = {
    'Line 1': {
      maximumNumberOfCharacters: 10,
      domain: ['One option']
    },
    'Line 2': {
      maximumNumberOfCharacters: 7,
      domain: ['Two', 'Options']
    },
  }

  const result = await resolvers.AssemblyOption.inputValues(
    { inputValues } as AssemblyOption
  )

  expect(result).toHaveLength(2)

  const [line1, line2] = result
  expect(line1.label).toBe('Line 1')
  expect(line1.type).toBe('OPTIONS')
  expect(line1.maxLength).toBe(undefined)
  expect(line1.domain).toHaveLength(1)
  expect(line1.domain![0]).toBe('One option')
  expect(line1.defaultValue).toBe('One option')

  expect(line2.label).toBe('Line 2')
  expect(line2.type).toBe('OPTIONS')
  expect(line2.maxLength).toBe(undefined)
  expect(line2.domain).toHaveLength(2)
  expect(line2.defaultValue).toBe('Two')
})

test('should return a BOOLEAN type', async () => {
  const inputValues: InputValues = {
    'Use thing A': {
      maximumNumberOfCharacters: 5,
      domain: ['true', 'false']
    },
    'Use thing B': {
      maximumNumberOfCharacters: 5,
      domain: ['false', 'true']
    },
  }

  const result = await resolvers.AssemblyOption.inputValues(
    { inputValues } as AssemblyOption
  )

  expect(result).toHaveLength(2)

  const [thingA, thingB] = result
  expect(thingA.label).toBe('Use thing A')
  expect(thingA.type).toBe('BOOLEAN')
  expect(thingA.maxLength).toBe(undefined)
  expect(thingA.domain).not.toBeDefined()
  expect(thingA.defaultValue).toBe(true)

  expect(thingB.label).toBe('Use thing B')
  expect(thingB.type).toBe('BOOLEAN')
  expect(thingB.maxLength).toBe(undefined)
  expect(thingB.domain).not.toBeDefined()
  expect(thingB.defaultValue).toBe(false)
})

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-resolver/node/resolvers/search/brand.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { prop } from 'ramda'

import { formatTranslatableProp } from '../../utils/i18n'
import { searchSlugify } from '../../utils/slug'

export const resolvers = {
  Brand: {
    name: formatTranslatableProp<Brand, 'name', 'id'>(
      'name',
      'id'
    ),

    titleTag: formatTranslatableProp<Brand, 'title', 'id'>(
      'title',
      'id'
    ),

    metaTagDescription: formatTranslatableProp<Brand, 'metaTagDescription', 'id'>(
      'metaTagDescription',
      'id'
    ),

    active: prop('isActive'),

    cacheId: (brand: Brand) => searchSlugify(brand.name),

    slug: (brand: Brand) => searchSlugify(brand.name),
  },
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-resolver/node/resolvers/search/itemMetadataUnit.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { prop } from 'ramda'

export const resolvers = {
  ItemMetadataUnit: {
    imageUrl: prop('MainImage'),
    skuName: prop('Name'),
    name: prop('NameComplete'),
    productId: prop('ProductId'),
  },
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-resolver/node/resolvers/search/autocomplete.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { path, split } from 'ramda'

/**
 * It will extract the slug from the HREF in the item
 * passed as parameter.
 *
 * That is needed once the API provide only the old link
 * (from CMS portal) to access the product page, nothing
 * more.
 *
 * HREF provided:
 * https://portal.vtexcommercestable.com.br/:slug/p
 *
 * @param item The item to extract the information
 */
const extractSlug = (item: SearchAutocompleteUnit) => {
  const href = split('/', item.href)
  return item.criteria ? `${href[3]}/${href[4]}` : href[3]
}

export const resolvers = {
  Items: {
    slug: (root: SearchAutocompleteUnit) => extractSlug(root),

    productId: ({ items }: SearchAutocompleteUnit) =>
      items ? path([0, 'productId'], items) : null,
  },
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-resolver/node/resolvers/search/productSearch.test.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
/* eslint-disable jest/no-mocks-import */

import { queries } from './index'
import { resolvers } from './productSearch'
import { mockContext, resetContext } from '../../__mocks__/helpers'
import { getProduct } from '../../__mocks__/product'

describe('tests related to the searchMetadata query', () => {
  beforeEach(() => {
    jest.clearAllMocks()
    resetContext()
  })

  it('get search metadata from pageType for category', async () => {
    const args = { query: 'Department/Category', map: 'c,c' }

    const result = await queries.searchMetadata({}, args, mockContext as any)

    expect(result.titleTag).toBe('department/category-title')
    expect(result.metaTagDescription).toBe(
      'department/category-metaTagDescription (((1))) <<<pt-BR>>> [[[original]]]'
    )
    expect(mockContext.clients.search.pageType).toBeCalledTimes(1)
  })

  it('get search metadata from pageType for brand', async () => {
    const args = { query: 'Brand', map: 'b' }

    const result = await queries.searchMetadata({}, args, mockContext as any)

    expect(result.titleTag).toBe('Brand-title')
    expect(result.metaTagDescription).toBe(
      'Brand-metaTagDescription (((1))) <<<pt-BR>>> [[[original]]]'
    )
    expect(mockContext.clients.search.pageType).toBeCalledTimes(1)
  })

  it('get search metadata for ft search', async () => {
    const args = { query: 'Shoes', map: 'ft' }

    const result = await queries.searchMetadata({}, args, mockContext as any)

    expect(result.titleTag).toBe('Shoes')
    expect(result.metaTagDescription).toBeNull()
    expect(mockContext.clients.search.pageType).toBeCalledTimes(0)
  })

  it('get search metadata for specification filter search', async () => {
    const args = { query: 'Large', map: 'specificationFilter_10' }

    const result = await queries.searchMetadata({}, args, mockContext as any)

    expect(result.titleTag).toBe('Large')
    expect(result.metaTagDescription).toBeNull()
    expect(mockContext.clients.search.pageType).toBeCalledTimes(0)
  })

  it('get search metadata from pageType for category with brand', async () => {
    const args = { query: 'Department/Category/Brand', map: 'c,c,b' }

    const result = await queries.searchMetadata({}, args, mockContext as any)

    expect(result.titleTag).toBe('brand - department/category-title')
    expect(result.metaTagDescription).toBe(
      'department/category-metaTagDescription (((1))) <<<pt-BR>>> [[[original]]]'
    )
    expect(mockContext.clients.search.pageType).toBeCalledTimes(2)
  })

  it('get search metadata from pageType for brand with category', async () => {
    const args = { query: 'Brand/Department/Category', map: 'b,c,c' }

    const result = await queries.searchMetadata({}, args, mockContext as any)

    expect(result.titleTag).toBe('department/category - Brand-title')
    expect(result.metaTagDescription).toBe(
      'Brand-metaTagDescription (((1))) <<<pt-BR>>> [[[original]]]'
    )
    expect(mockContext.clients.search.pageType).toBeCalledTimes(2)
  })

  it('get search metadata from pageType for category with brand & specification filter', async () => {
    const args = {
      query: 'Department/Category/Brand/Large',
      map: 'c,c,b,specificationFilter_15',
    }

    const result = await queries.searchMetadata({}, args, mockContext as any)

    expect(result.titleTag).toBe('Large - brand - department/category-title')
    expect(result.metaTagDescription).toBe(
      'department/category-metaTagDescription (((1))) <<<pt-BR>>> [[[original]]]'
    )
    expect(mockContext.clients.search.pageType).toBeCalledTimes(2)
  })

  it('get search metadata from pageType for category with brand in the middle', async () => {
    const args = {
      query: 'Department/Category/Brand/SubCategory',
      map: 'c,c,b,c',
    }

    const result = await queries.searchMetadata({}, args, mockContext as any)

    expect(result.titleTag).toBe(
      'brand - department/category/subcategory-title'
    )
    expect(result.metaTagDescription).toBe(
      'department/category/subcategory-metaTagDescription (((1))) <<<pt-BR>>> [[[original]]]'
    )
    expect(mockContext.clients.search.pageType).toBeCalledTimes(2)
  })

  it('get search metadata for search ft with category', async () => {
    const args = {
      query: 'Shoes/Department/Category',
      map: 'ft,c,c',
    }

    const result = await queries.searchMetadata({}, args, mockContext as any)

    expect(result.titleTag).toBe('department/category - Shoes')
    expect(result.metaTagDescription).toBeNull()
    expect(mockContext.clients.search.pageType).toBeCalledTimes(1)
  })

  it('get search metadata for search ft with brand', async () => {
    const args = {
      query: 'Shoes/Brand',
      map: 'ft,b',
    }

    const result = await queries.searchMetadata({}, args, mockContext as any)

    expect(result.titleTag).toBe('brand - Shoes')
    expect(result.metaTagDescription).toBeNull()
    expect(mockContext.clients.search.pageType).toBeCalledTimes(1)
  })

  it('get search metadata for search ft with specification filter', async () => {
    const args = {
      query: 'Shoes/Large',
      map: 'ft,specificationFilter_10',
    }

    const result = await queries.searchMetadata({}, args, mockContext as any)

    expect(result.titleTag).toBe('Large - Shoes')
    expect(result.metaTagDescription).toBeNull()
    expect(mockContext.clients.search.pageType).toBeCalledTimes(0)
  })
  it('get search metadata from pageType for category with correct locale variables', async () => {
    const args = { query: 'Department/Category', map: 'c,c' }

    mockContext.vtex.locale = 'es-ES'
    mockContext.vtex.tenant.locale = 'fr-FR'

    const result = await queries.searchMetadata({}, args, mockContext as any)

    expect(result.titleTag).toBe('department/category-title-es-ES')
    expect(result.metaTagDescription).toBe(
      'department/category-metaTagDescription (((1))) <<<fr-FR>>> [[[original]]]'
    )
    expect(mockContext.clients.search.pageType).toBeCalledTimes(1)
    expect(mockContext.state.messagesBindingLanguage.loadMany).toBeCalledTimes(
      1
    )
  })

  it('get search metadata from pageType for category with translated flag true', async () => {
    const args = { query: 'Department/Category', map: 'c,c' }

    mockContext.translated = true

    const result = await queries.searchMetadata({}, args, mockContext as any)

    expect(result.titleTag).toBe('department/category-title')
    expect(result.metaTagDescription).toBe(
      'department/category-metaTagDescription (((1))) <<<pt-BR>>> [[[translated]]]'
    )
    expect(result.metaTagDescription).not.toContain('[[[original]]]')
    expect(mockContext.clients.search.pageType).toBeCalledTimes(1)
  })

  it('get search metadata from pageType for category with translated flag false', async () => {
    const args = { query: 'Department/Category', map: 'c,c' }

    mockContext.translated = false

    const result = await queries.searchMetadata({}, args, mockContext as any)

    expect(result.titleTag).toBe('department/category-title')
    expect(result.metaTagDescription).toBe(
      'department/category-metaTagDescription (((1))) <<<pt-BR>>> [[[original]]]'
    )
    expect(mockContext.clients.search.pageType).toBeCalledTimes(1)
  })
})

describe('tests for breadcrumb resolver', () => {
  beforeEach(() => {
    jest.clearAllMocks()
    resetContext()
  })
  it('create correct params for search with department', async () => {
    const products = [getProduct()]
    const args = {
      translatedArgs: {
        query: 'category',
        map: 'c',
      },
      productsRaw: { data: products },
    }

    const result = await resolvers.ProductSearch.breadcrumb(
      args as any,
      {},
      mockContext as any
    )

    expect(result).toHaveLength(1)
    const head = result[0]

    expect(head.queryUnit).toBe('category')
    expect(head.mapUnit).toBe('c')
    expect(head.categories).toHaveLength(0)
    expect(head.metadataMap).toMatchObject({})
    expect(head.categoriesSearched).toMatchObject(['category'])
    expect(head.hrefs).toBeNull()
    expect(head.index).toBe(0)
    expect(head.queryArray).toMatchObject(['category'])
    expect(head.mapArray).toMatchObject(['c'])
    expect(head.products).toBeDefined()
    expect(mockContext.clients.rewriter.getRoute).toBeCalledTimes(0)
  })
  it('create correct params for search with category', async () => {
    const products = [getProduct()]
    const args = {
      translatedArgs: {
        query: 'category/category2',
        map: 'c,c',
      },
      productsRaw: { data: products },
    }

    const result = await resolvers.ProductSearch.breadcrumb(
      args as any,
      {},
      mockContext as any
    )

    expect(result).toHaveLength(2)
    const head = result[0]
    const expectedQueryArray = ['category', 'category2']
    const expectedMapArray = ['c', 'c']

    expect(head.queryUnit).toBe('category')
    expect(head.mapUnit).toBe('c')
    expect(head.categories).toHaveLength(0)
    expect(head.metadataMap).toMatchObject({})
    expect(head.categoriesSearched).toMatchObject(['category', 'category2'])
    expect(head.hrefs).toBeNull()
    expect(head.index).toBe(0)
    expect(head.queryArray).toMatchObject(expectedQueryArray)
    expect(head.mapArray).toMatchObject(expectedMapArray)
    const tail = result[1]

    expect(tail.queryUnit).toBe('category2')
    expect(tail.mapUnit).toBe('c')
    expect(tail.categories).toHaveLength(0)
    expect(tail.metadataMap).toMatchObject({})
    expect(tail.categoriesSearched).toMatchObject(expectedQueryArray)
    expect(tail.hrefs).toBeNull()
    expect(tail.index).toBe(1)
    expect(tail.queryArray).toMatchObject(expectedQueryArray)
    expect(tail.mapArray).toMatchObject(expectedMapArray)
    expect(mockContext.clients.rewriter.getRoute).toBeCalledTimes(0)
  })

  it('create correct params for search with category binding translations', async () => {
    const products = [getProduct()]
    const args = {
      translatedArgs: {
        query: 'category/category2',
        map: 'c,c',
      },
      productsRaw: { data: products },
    }

    mockContext.vtex.binding.locale = 'es-ES'

    const result = await resolvers.ProductSearch.breadcrumb(
      args as any,
      {},
      mockContext as any
    )

    expect(result).toHaveLength(2)
    const head = result[0]
    const expectedQueryArray = ['category', 'category2']
    const expectedMapArray = ['c', 'c']

    expect(head.queryUnit).toBe('category')
    expect(head.mapUnit).toBe('c')
    expect(head.categories).toHaveLength(0)
    expect(head.categoriesSearched).toMatchObject(['category', 'category2'])
    expect(head.hrefs).toMatchObject([
      '/1-department-abc-es-ES',
      '/1-category-abc-es-ES',
    ])
    expect(head.index).toBe(0)
    expect(head.queryArray).toMatchObject(expectedQueryArray)
    expect(head.mapArray).toMatchObject(expectedMapArray)
    expect(head.metadataMap).toMatchObject({
      'category-c': { name: 'category', id: '1' },
      'category2-c': { name: 'category/category2', id: '1' },
    })
    const tail = result[1]

    expect(tail.queryUnit).toBe('category2')
    expect(tail.mapUnit).toBe('c')
    expect(tail.categories).toHaveLength(0)
    expect(tail.metadataMap).toMatchObject({
      'category-c': { name: 'category', id: '1' },
      'category2-c': { name: 'category/category2', id: '1' },
    })
    expect(tail.categoriesSearched).toMatchObject(expectedQueryArray)
    expect(tail.hrefs).toMatchObject([
      '/1-department-abc-es-ES',
      '/1-category-abc-es-ES',
    ])
    expect(tail.index).toBe(1)
    expect(tail.queryArray).toMatchObject(expectedQueryArray)
    expect(tail.mapArray).toMatchObject(expectedMapArray)
    expect(mockContext.clients.rewriter.getRoute).toBeCalledTimes(2)
  })
})

describe.skip('tests related to the productSearch query', () => {
  beforeEach(() => {
    jest.clearAllMocks()
    resetContext()
  })

  it('should not translate args that are not ft', async () => {
    const args = {
      query: 'shoes/sneakers',
      map: 'c,c',
    }

    const result = await queries.productSearch(
      {},
      args as any,
      mockContext as any
    )

    expect(result.translatedArgs).toMatchObject({
      query: 'shoes/sneakers',
      map: 'c,c',
    })
  })

  it('should not translate ft args if user locale matches tenant locale', async () => {
    const args = {
      query: 'tenis/shoes/sneakers',
      map: 'ft,c,c',
    }

    const result = await queries.productSearch(
      {},
      args as any,
      mockContext as any
    )

    expect(result.translatedArgs).toMatchObject({
      query: 'tenis/shoes/sneakers',
      map: 'ft,c,c',
    })
  })

  it('should translate ft args if user locale differs tenant locale', async () => {
    mockContext.vtex.locale = 'es-ES'
    const args = {
      query: 'tenis/shoes/sneakers',
      map: 'ft,c,c',
    }

    const result = await queries.productSearch(
      {},
      args as any,
      mockContext as any
    )

    expect(result.translatedArgs).toMatchObject({
      query: 'tenis-pt-BR/shoes/sneakers',
      map: 'ft,c,c',
    })
  })
})

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-resolver/node/resolvers/search/index.test.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { queries } from './index'
import { mockContext } from '../../__mocks__/helpers'
import { getProduct, getItem } from '../../__mocks__/product'

beforeEach(() => {
  jest.clearAllMocks()
})

describe('product recommendations query tests', () => {
  test('test if productRecommendations products have correct custom cacheId', async () => {
    const product1 = getProduct()
    const product2 = getProduct()

    const item1 = getItem()
    const item2 = getItem('2')

    product1.items = [item1 as any]
    product2.items = [item2 as any]
    mockContext.clients.search.crossSelling.mockImplementation(() => [
      product1,
      product2,
    ])
    const results = await queries.productRecommendations(
      {},
      { identifier: { field: 'id', value: '1' }, type: 'similars' as any },
      mockContext as any
    )
    expect(results.every(p => Boolean(p.cacheId))).toBeTruthy()
    expect(results[0].cacheId).toBe('classic-shoes-1')
    expect(results[1].cacheId).toBe('classic-shoes-2')
  })

  test('ensure correct types call the correct APIs', async () => {
    mockContext.clients.search.crossSelling.mockImplementation(() => [])
    await queries.productRecommendations(
      {},
      { identifier: { field: 'id', value: '1' }, type: 'similars' as any },
      mockContext as any
    )
    expect(mockContext.clients.search.crossSelling.mock.calls[0][1]).toBe(
      'similars'
    )

    await queries.productRecommendations(
      {},
      { identifier: { field: 'id', value: '1' }, type: 'view' as any },
      mockContext as any
    )
    expect(mockContext.clients.search.crossSelling.mock.calls[1][1]).toBe(
      'whosawalsosaw'
    )

    await queries.productRecommendations(
      {},
      { identifier: { field: 'id', value: '1' }, type: 'buy' as any },
      mockContext as any
    )
    expect(mockContext.clients.search.crossSelling.mock.calls[2][1]).toBe(
      'whoboughtalsobought'
    )

    await queries.productRecommendations(
      {},
      { identifier: { field: 'id', value: '1' }, type: 'viewAndBought' as any },
      mockContext as any
    )
    expect(mockContext.clients.search.crossSelling.mock.calls[3][1]).toBe(
      'whosawalsobought'
    )

    await queries.productRecommendations(
      {},
      { identifier: { field: 'id', value: '1' }, type: 'suggestions' as any },
      mockContext as any
    )
    expect(mockContext.clients.search.crossSelling.mock.calls[4][1]).toBe(
      'suggestions'
    )

    await queries.productRecommendations(
      {},
      { identifier: { field: 'id', value: '1' }, type: 'accessories' as any },
      mockContext as any
    )
    expect(mockContext.clients.search.crossSelling.mock.calls[5][1]).toBe(
      'accessories'
    )
  })
})

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-resolver/node/resolvers/search/product.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { compose, flatten, last, omit, pathOr, split } from 'ramda'

import {
  addContextToTranslatableString,
  formatTranslatableProp,
  shouldTranslateToBinding,
  shouldTranslateToUserLocale,
} from '../../utils/i18n'
import { getBenefits } from '../benefits'
import { buildCategoryMap, logDegradedSearchError } from './utils'

type DynamicKey<T> = Record<string, T>

const urlToSlug = (slug: string | undefined) => {
  if (!slug) {
    return slug
  }

  const erasedSlash = slug.replace(/^\//g, '') // removing starting / char
  const finalSlug = erasedSlash.replace(/(\/p)$/g, '') // remove ending /p chars

  return finalSlug
}

const objToNameValue = (
  keyName: string,
  valueName: string,
  record: Record<string, any> | null | undefined
) => {
  if (!record) {
    return []
  }

  return Object.keys(record).reduce((acc, key: any) => {
    const value = record[key]

    if (typeof value === 'string') {
      acc.push({ [keyName]: key, [valueName]: value })
    }

    return acc
  }, [] as Array<Record<string, string>>)
}

type SearchProductWithCache = SearchProduct & { cacheId?: string }
type ItemsFilter = 'ALL' | 'FIRST_AVAILABLE' | 'ALL_AVAILABLE'
type ItemArg = {
  filter?: ItemsFilter
}

const isSellerAvailable = (seller: Seller) =>
  pathOr(0, ['commertialOffer', 'AvailableQuantity'], seller) > 0

const isAvailable = (item: SearchItem): boolean => {
  return item.sellers.find(isSellerAvailable) !== undefined
}

const knownNotPG = [
  'allSpecifications',
  'brand',
  'categoriesIds',
  'categoryId',
  'clusterHighlights',
  'productClusters',
  'items',
  'productId',
  'link',
  'linkText',
  'productReference',
]

const removeTrailingSlashes = (str: string) =>
  str.endsWith('/') ? str.slice(0, str.length - 1) : str

const removeStartingSlashes = (str: string) =>
  str.startsWith('/') ? str.slice(1) : str

const getProductFilterIdMap = async (product: SearchProduct, ctx: Context) => {
  const filters = await ctx.clients.search.filtersInCategoryFromId(
    product.categoryId
  )

  const filterMapFromName = filters.reduce((acc, curr) => {
    acc[curr.Name] = curr.FieldId.toString()

    return acc
  }, {} as Record<string, string>)

  return filterMapFromName
}

const getLastCategory = compose<string, string, string[], string>(
  last,
  split('/'),
  removeTrailingSlashes
)

const treeStringToArray = compose(
  split('/'),
  removeTrailingSlashes,
  removeStartingSlashes
)

const findMainTree = (categoriesIds: string[], prodCategoryId: string) => {
  const mainTree = categoriesIds.find(
    (treeIdString) => getLastCategory(treeIdString) === prodCategoryId
  )

  if (mainTree) {
    return treeStringToArray(mainTree)
  }

  // If we are here, did not find the specified main category ID in given strings. It is probably a bug.
  // We will return the biggest tree we find

  const trees = categoriesIds.map(treeStringToArray)

  return trees.reduce(
    (acc, currTree) => (currTree.length > acc.length ? currTree : acc),
    []
  )
}

const productCategoriesToCategoryTree = async (
  { categories, categoriesIds, categoryId: prodCategoryId }: SearchProduct,
  _: any,
  { clients: { search }, vtex: { platform, logger } }: Context
) => {
  if (!categories || !categoriesIds) {
    return []
  }

  const mainTreeIds = findMainTree(categoriesIds, prodCategoryId)

  if (platform === 'vtex') {
    return mainTreeIds.map((categoryId) => search.category(Number(categoryId)))
  }

  logger.info({
    message: 'productCategoriesToCategoryTree: not vtex platform',
    platform,
  })
  const categoriesTree = await search.categories(mainTreeIds.length)
  const categoryMap = buildCategoryMap(categoriesTree)
  const mappedCategories = mainTreeIds
    .map((id) => categoryMap[id])
    .filter(Boolean)

  return mappedCategories.length ? mappedCategories : null
}

const addTranslationParamsToSpecification =
  (filterIdFromNameMap: Record<string, string>, ctx: Context) =>
  (specification: { name: string; values: string[] }) => {
    const { name, values } = specification
    const filterId = filterIdFromNameMap[name]

    return {
      originalName: name,
      name: addContextToTranslatableString(
        { content: name, context: filterId },
        ctx
      ),
      values: values.map((value) =>
        addContextToTranslatableString(
          { content: value, context: filterId },
          ctx
        )
      ),
    }
  }

export const resolvers = {
  Product: {
    brand: formatTranslatableProp<SearchProduct, 'brand', 'brandId'>(
      'brand',
      'brandId'
    ),

    benefits: async (
      { items, productId }: SearchProduct,
      _: any,
      ctx: Context
    ) => {
      const promises = items?.map((item) => getBenefits(item.itemId, ctx))
      const promisesResult = flatten(
        await Promise.all(promises.map((p) => p.catch((error) => error)))
      )

      const benefitsWithoutError = promisesResult.filter(
        (result) => !(result instanceof Error)
      )

      if (benefitsWithoutError.length !== promisesResult.length) {
        logDegradedSearchError(ctx.vtex.logger, {
          service: 'Checkout simulation',
          error: `Checkout simulation API returned an error for product ${productId}.
            Simulation will be skipped for one or more items and the benefits list may be incomplete.`,
        })
      }

      return benefitsWithoutError
    },

    categoryTree: productCategoriesToCategoryTree,

    cacheId: ({ linkText, cacheId }: SearchProductWithCache) =>
      cacheId ?? linkText,

    clusterHighlights: ({ origin, clusterHighlights }: SearchProduct) => {
      if (origin === 'intelligent-search') {
        return clusterHighlights
      }

      return objToNameValue('id', 'name', clusterHighlights)
    },

    jsonSpecifications: (product: SearchProduct) => {
      const { Specifications = [] } = product
      const specificationsMap = Specifications.reduce(
        (acc: Record<string, string>, key: string) => {
          acc[key] = (product as any)[key]

          return acc
        },
        {}
      )

      return JSON.stringify(specificationsMap)
    },

    productClusters: ({ origin, productClusters }: SearchProduct) => {
      if (origin === 'intelligent-search') {
        return productClusters
      }

      return objToNameValue('id', 'name', productClusters)
    },

    properties: async (product: SearchProduct, _: unknown, ctx: Context) => {
      let valuesUntranslated = []

      if (product.origin === 'intelligent-search') {
        valuesUntranslated = product.properties ?? []
      } else {
        valuesUntranslated = (product.allSpecifications ?? []).map(
          (name: string) => {
            const value = (product as unknown as DynamicKey<string[]>)[name]

            return { name, originalName: name, values: value }
          }
        )
      }

      if (!shouldTranslateToUserLocale(ctx)) {
        return valuesUntranslated
      }

      const filterIdFromNameMap = await getProductFilterIdMap(product, ctx)
      const valuesWithTranslations = valuesUntranslated.map(
        addTranslationParamsToSpecification(filterIdFromNameMap, ctx)
      )

      return valuesWithTranslations
    },

    propertyGroups: (product: SearchProduct) => {
      const { allSpecifications = [] } = product
      const notPG = knownNotPG.concat(allSpecifications)

      return objToNameValue('name', 'values', omit(notPG, product))
    },

    recommendations: (product: SearchProduct, args: any) => {
      const { groupBy } = args

      return { product, groupBy }
    },

    description: formatTranslatableProp<
      SearchProduct,
      'description',
      'productId'
    >('description', 'productId'),

    metaTagDescription: formatTranslatableProp<
      SearchProduct,
      'metaTagDescription',
      'productId'
    >('metaTagDescription', 'productId'),

    titleTag: (
      { productId, productTitle, productName }: SearchProduct,
      _: unknown,
      ctx: Context
    ) =>
      addContextToTranslatableString(
        {
          content: productTitle ?? productName ?? '',
          context: productId,
        },
        ctx
      ),

    productName: formatTranslatableProp<
      SearchProduct,
      'productName',
      'productId'
    >('productName', 'productId'),

    linkText: async (
      { productId, linkText, origin }: SearchProduct,
      _: unknown,
      ctx: Context
    ) => {
      const {
        clients: { rewriter },
        vtex: { binding },
      } = ctx

      if (origin === 'intelligent-search' || !shouldTranslateToBinding(ctx)) {
        return linkText
      }

      try {
        const route = await rewriter.getRoute(
          productId,
          'product',
          binding!.id!
        )

        return urlToSlug(route) ?? linkText
      } catch (e) {
        logDegradedSearchError(ctx.vtex.logger, {
          service: 'Rewriter getRoute',
          error: `Rewriter getRoute query returned an error for productId ${productId}. Linktext may be incorrect.`,
          errorStack: e,
        })
      }

      return linkText
    },

    specificationGroups: async (
      product: SearchProduct,
      _: unknown,
      ctx: Context
    ) => {
      if (product.origin === 'intelligent-search') {
        return product.specificationGroups
      }

      const allSpecificationsGroups = (
        product.allSpecificationsGroups ?? []
      ).concat(['allSpecifications'])

      const visibleSpecifications = product.completeSpecifications
        ? product.completeSpecifications.reduce<Record<string, boolean>>(
            (acc, specification) => {
              acc[specification.Name] = specification.IsOnProductDetails

              return acc
            },
            {}
          )
        : null

      let noTranslationSpecificationGroups = allSpecificationsGroups.map(
        (groupName: string) => {
          let groupSpecifications =
            (product as unknown as DynamicKey<string[]>)?.[groupName] ?? []

          groupSpecifications = groupSpecifications.filter(
            (specificationName) => {
              if (visibleSpecifications?.[specificationName] != null)
                return visibleSpecifications[specificationName]

              return true
            }
          )

          return {
            originalName: groupName,
            name: groupName,
            specifications: groupSpecifications.map((name) => {
              const values =
                (product as unknown as DynamicKey<string[]>)[name] || []

              return {
                originalName: name,
                name,
                values,
              }
            }),
          }
        }
      )

      noTranslationSpecificationGroups =
        noTranslationSpecificationGroups.filter(
          (group) => group.specifications.length > 0
        )

      if (!shouldTranslateToUserLocale(ctx)) {
        return noTranslationSpecificationGroups
      }

      const filterIdFromNameMap = await getProductFilterIdMap(product, ctx)
      const translatedGroups = noTranslationSpecificationGroups.map((group) => {
        return {
          originalName: group.name,
          name: addContextToTranslatableString(
            { content: group.name, context: product.productId },
            ctx
          ),
          specifications: group.specifications.map(
            addTranslationParamsToSpecification(filterIdFromNameMap, ctx)
          ),
        }
      })

      return translatedGroups
    },
    items: (
      { items: searchItems, skuSpecifications = [] }: SearchProduct,
      { filter }: ItemArg
    ) => {
      const searchItemsWithVariations = searchItems.map((item) => ({
        ...item,
        skuSpecifications,
      }))

      if (filter === 'ALL') {
        return searchItemsWithVariations
      }

      if (filter === 'FIRST_AVAILABLE') {
        const firstAvailable = searchItemsWithVariations.find(isAvailable)

        return firstAvailable
          ? [firstAvailable]
          : [searchItemsWithVariations[0]]
      }

      if (filter === 'ALL_AVAILABLE') {
        const onlyAvailable = searchItemsWithVariations.filter(isAvailable)

        return onlyAvailable.length > 0
          ? onlyAvailable
          : [searchItemsWithVariations[0]]
      }

      return searchItemsWithVariations
    },
    priceRange: ({ items: searchItems }: SearchProduct) => {
      const offers = searchItems.reduce<CommertialOffer[]>(
        (acc, currentItem) => {
          for (const seller of currentItem.sellers) {
            if (isSellerAvailable(seller)) {
              acc.push(seller.commertialOffer)
            }
          }

          return acc
        },
        []
      )

      return { offers }
    },

    itemMetadata: ({
      items,
      origin,
      itemMetadata,
      brand,
      categoryId,
      productId,
    }: SearchProduct) => {
      // Since the IS doesn't return the itemMetadata, we need to build it from the items.attachments
      if (origin === 'intelligent-search') {
        // Build itemMetadata from items.attachments
        const metadataItems = items.map((item) => {
          // Build assemblyOptions from attachments
          const assemblyOptions = (item.attachments || []).map((attachment) => {
            // Build inputValues from attachment fields
            const inputValues: Record<
              string,
              { maximumNumberOfCharacters: number; domain: string[] }
            > = {}

            if (attachment.fields) {
              attachment.fields.forEach((field) => {
                const domain = field.domainValues
                  ? field.domainValues
                      .split(',')
                      .map((v) => v.trim())
                      .filter(Boolean)
                  : []

                inputValues[field.fieldName] = {
                  maximumNumberOfCharacters:
                    parseInt(field.maxCharacters, 10) || 0,
                  domain,
                }
              })
            }

            return {
              id: attachment.name,
              name: attachment.name,
              required: attachment.required || attachment.isRequired || false,
              inputValues,
              composition: null,
            }
          })

          // Get the first available seller or default to '1'
          const firstSeller =
            item.sellers && item.sellers[0] ? item.sellers[0].sellerId : '1'

          // Get the main image URL
          const mainImage =
            item.images && item.images[0] ? item.images[0].imageUrl : ''

          return {
            Name: item.name,
            NameComplete: item.nameComplete,
            MainImage: mainImage,
            BrandName: brand,
            CategoryId: parseInt(categoryId, 10),
            ProductId: parseInt(productId, 10),
            id: item.itemId,
            seller: firstSeller,
            assemblyOptions,
          }
        })

        return {
          items: metadataItems,
        }
      }

      return itemMetadata
    },
  },
  OnlyProduct: {
    categoryTree: productCategoriesToCategoryTree,
    productClusters: ({ productClusters }: any) =>
      ([] as any[]).concat(productClusters ?? []),
    clusterHighlights: ({ clusterHighlights }: any) =>
      ([] as any[]).concat(clusterHighlights ?? []),
  },
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-resolver/node/resolvers/search/discount.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { has, prop } from 'ramda'

export const resolvers = {
  Discount: {
    name: (discount: any) =>
      has('name', discount)
        ? prop('name', discount)
        : prop('<Name>k__BackingField', discount),
  },
  Teaser: {
    name: (teaser: any) =>
      has('name', teaser)
        ? prop('name', teaser)
        : prop('<Name>k__BackingField', teaser),

    conditions: (teaser: any) =>
      has('conditions', teaser)
        ? prop('conditions', teaser)
        : prop('<Conditions>k__BackingField', teaser),

    effects: (teaser: any) =>
      has('effects', teaser)
        ? prop('effects', teaser)
        : prop('<Effects>k__BackingField', teaser),
  },
  TeaserCondition: {
    minimumQuantity: (teaserCondition: any) =>
      has('minimumQuantity', teaserCondition)
        ? prop('minimumQuantity', teaserCondition)
        : prop('<MinimumQuantity>k__BackingField', teaserCondition),

    parameters: (teaserCondition: any) =>
      has('parameters', teaserCondition)
        ? prop('parameters', teaserCondition)
        : prop('<Parameters>k__BackingField', teaserCondition),
  },
  TeaserEffects: {
    parameters: (teaserEffects: any) =>
      has('parameters', teaserEffects)
        ? prop('parameters', teaserEffects)
        : prop('<Parameters>k__BackingField', teaserEffects),
  },
  TeaserValue: {
    name: (teaserValue: any) =>
      has('name', teaserValue)
        ? prop('name', teaserValue)
        : prop('<Name>k__BackingField', teaserValue),

    value: (teaserValue: any) =>
      has('value', teaserValue)
        ? prop('value', teaserValue)
        : prop('<Value>k__BackingField', teaserValue),
  },
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-resolver/node/resolvers/search/skuSpecificationField.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { addContextToTranslatableString } from '../../utils/i18n'

// if it has `originalName`, the specification is already coming in the appropriate format and language
export const resolvers = {
  SKUSpecificationField: {
    originalName: (value: SKUSpecificationField) => {
      return value.originalName ? value.originalName : value.name
    },
    name: (field: SKUSpecificationField, _: any, ctx: Context) => {
      return field.originalName ? field.name : addContextToTranslatableString(
        {
          content: field.name,
          context: field.id
        },
        ctx
      )
    }
  }
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-resolver/node/resolvers/search/newURLs.test.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable new-cap */
/* eslint-disable no-await-in-loop */

import * as TypeMoq from 'typemoq'
import type { IOContext } from '@vtex/api'
import { VBase } from '@vtex/api'

import { Search } from '../../clients/search'
import { mountCompatibilityQuery } from './newURLs'
import { getCompatibilityArgs } from '.'
import { Clients } from '../../clients'

const contextMock = TypeMoq.Mock.ofType<IOContext>()
const categoryTreeResponseMock = TypeMoq.Mock.ofType<CategoryTreeResponse>()
const facetsMock = TypeMoq.Mock.ofType<SearchFacets>()
const vbaseTypeMock = TypeMoq.Mock.ofInstance(VBase)
const state = TypeMoq.Mock.ofType<State>()
const customContext = TypeMoq.Mock.ofType<CustomContext>()

describe('Search new URLs dicovery', () => {
  class VBaseMock extends vbaseTypeMock.object {
    private jsonData: any

    constructor() {
      super(contextMock.object)
      this.jsonData = {}
    }

    public getJSON = async <T>(
      bucket: string,
      file: string,
      nullOrUndefined?: boolean | undefined
    ): Promise<T> => {
      if (!this.jsonData[bucket]) {
        return (nullOrUndefined ? null : {}) as T
      }

      return Promise.resolve(this.jsonData[bucket][file] as T)
    }

    public saveJSON = async <T>(
      bucket: string,
      file: string,
      data: T
    ): Promise<any> => {
      if (!this.jsonData[bucket]) {
        this.jsonData[bucket] = {}
      }

      this.jsonData[bucket][file] = data
    }
  }

  const search = class SearchMock extends Search {
    private categoriesResponse: CategoryTreeResponse[]
    private categoryChildrenResponse: Record<number, Record<string, string>>
    private facetsResponse: SearchFacets

    constructor(
      categories: CategoryTreeResponse[],
      categoryChildren: Record<number, Record<string, string>>,
      facets: SearchFacets
    ) {
      super(contextMock.object)
      this.categoriesResponse = categories
      this.categoryChildrenResponse = categoryChildren
      this.facetsResponse = facets
    }

    public categories = async (_: number) => {
      return Promise.resolve(this.categoriesResponse)
    }

    public getCategoryChildren = (id: number) => {
      return Promise.resolve(this.categoryChildrenResponse[id])
    }

    public facets = (_?: string) => {
      return Promise.resolve(this.facetsResponse)
    }
  }

  it('Should transform /category in /category?map=c', async () => {
    const args = {
      query: 'category',
      map: '',
    }

    const categoryTree: CategoryTreeResponse[] = [
      {
        ...categoryTreeResponseMock.object,
        id: 1,
        name: 'category',
        hasChildren: false,
      },
    ]

    const facets = {
      ...facetsMock.object,
      SpecificationFilters: {},
    }

    const vbaseMock = new VBaseMock()
    const searchMock = new search(categoryTree, {}, facets)
    const result = await mountCompatibilityQuery({
      vbase: vbaseMock,
      search: searchMock,
      args,
    })

    expect(result).toStrictEqual({ query: 'category', map: 'c' })
  })

  it('Should transform /department/category/subcategory in /department/category/subcategory?map=c,c,c', async () => {
    const args = {
      query: 'department/category/subcategory',
      map: '',
    }

    const categoryTree: CategoryTreeResponse[] = [
      {
        ...categoryTreeResponseMock.object,
        id: 1,
        name: 'department',
        hasChildren: true,
      },
    ]

    const categoryChildren = {
      1: { '2': 'category' },
      2: { '3': 'subcategory' },
    }

    const facets = {
      ...facetsMock.object,
      SpecificationFilters: {},
    }

    const vbaseMock = new VBaseMock()
    const searchMock = new search(categoryTree, categoryChildren, facets)
    const result = await mountCompatibilityQuery({
      vbase: vbaseMock,
      search: searchMock,
      args,
    })

    expect(result).toStrictEqual({
      query: 'department/category/subcategory',
      map: 'c,c,c',
    })
  })

  it('Should transform /category/brand?map=b in /category?map=c,b', async () => {
    const args = {
      query: 'category/brand',
      map: 'b',
    }

    const categoryTree: CategoryTreeResponse[] = [
      {
        ...categoryTreeResponseMock.object,
        id: 1,
        name: 'category',
        hasChildren: false,
      },
    ]

    const categoryChildren = { 1: {} }

    const facets = {
      ...facetsMock.object,
      SpecificationFilters: {},
    }

    const vbaseMock = new VBaseMock()
    const searchMock = new search(categoryTree, categoryChildren, facets)
    const result = await mountCompatibilityQuery({
      vbase: vbaseMock,
      search: searchMock,
      args,
    })

    expect(result).toStrictEqual({ query: 'category/brand', map: 'c,b' })
  })

  it('Should transform /collection?map=productClusterIds in /collection?map=productClusterIds', async () => {
    const args = {
      query: 'collection',
      map: 'productClusterIds',
    }

    const categoryTree: CategoryTreeResponse[] = []

    const categoryChildren = {}

    const facets = {
      ...facetsMock.object,
      SpecificationFilters: {},
    }

    const vbaseMock = new VBaseMock()
    const searchMock = new search(categoryTree, categoryChildren, facets)
    const result = await mountCompatibilityQuery({
      vbase: vbaseMock,
      search: searchMock,
      args,
    })

    expect(result).toStrictEqual({
      query: 'collection',
      map: 'productClusterIds',
    })
  })

  it('Should transform /filterxpto?map=specificationFilter_0 in /filterxpto?map=specificationFilter_0', async () => {
    const args = {
      query: 'filterxpto',
      map: 'specificationFilter_0',
    }

    const categoryTree: CategoryTreeResponse[] = []

    const categoryChildren = {}

    const facets = {
      ...facetsMock.object,
      SpecificationFilters: {},
    }

    const vbaseMock = new VBaseMock()
    const searchMock = new search(categoryTree, categoryChildren, facets)
    const result = await mountCompatibilityQuery({
      vbase: vbaseMock,
      search: searchMock,
      args,
    })

    expect(result).toStrictEqual({
      query: 'filterxpto',
      map: 'specificationFilter_0',
    })
  })

  it('Should transform /department/style_1/color_2/size_3 in /department/1/2/3?map=c,specificationFilter_1,specificationFilter_2,specificationFilter_3', async () => {
    const args = {
      query: 'department/style_1/color_2/size_3',
      map: '',
    }

    const categoryTree: CategoryTreeResponse[] = [
      {
        ...categoryTreeResponseMock.object,
        id: 1,
        name: 'department',
        hasChildren: false,
      },
    ]

    const categoryChildren = { 1: {} }

    const facets = {
      ...facetsMock.object,
      SpecificationFilters: {
        style: [
          {
            Name: '1',
            Map: 'specificationFilter_1',
            Value: '1',
          },
        ],
        color: [
          {
            Name: '2',
            Map: 'specificationFilter_2',
            Value: '2',
          },
        ],
        size: [
          {
            Name: '3',
            Map: 'specificationFilter_3',
            Value: '3',
          },
        ],
      },
    }

    const vbaseMock = new VBaseMock()
    const searchMock = new search(categoryTree, categoryChildren, facets as any)
    const result = await mountCompatibilityQuery({
      vbase: vbaseMock,
      search: searchMock,
      args,
    })

    expect(result).toStrictEqual({
      query: 'department/1/2/3',
      map: 'c,specificationFilter_1,specificationFilter_2,specificationFilter_3',
    })
  })

  it('Should transform /department/style_1/brand?map=b in /department/style_1/brand?map=c,specificationFilter_1,b', async () => {
    const args = {
      query: 'department/style_1/brand',
      map: 'b',
    }

    const categoryTree: CategoryTreeResponse[] = [
      {
        ...categoryTreeResponseMock.object,
        id: 1,
        name: 'department',
        hasChildren: false,
      },
    ]

    const categoryChildren = { 1: {} }

    const facets = {
      ...facetsMock.object,
      SpecificationFilters: {
        style: [
          {
            Name: '1',
            Map: 'specificationFilter_1',
            Value: '1',
          },
        ],
      },
    }

    const vbaseMock = new VBaseMock()
    const searchMock = new search(categoryTree, categoryChildren, facets as any)
    const result = await mountCompatibilityQuery({
      vbase: vbaseMock,
      search: searchMock,
      args,
    })

    expect(result).toStrictEqual({
      query: 'department/1/brand',
      map: 'c,specificationFilter_1,b',
    })
  })

  it('Should not transform urls in the format query?map=c', async () => {
    const argsList = [
      {
        map: 'c',
        query: 'category',
      },
      {
        map: 'specificationFilter_1',
        query: 'filtertest',
      },
      {
        map: 'c,specificationFilter_1',
        query: 'category/filtertest',
      },
      {
        map: 'specificationFilter_1,c,b',
        query: 'filtertest/category/brand',
      },
    ]

    for (const args of argsList) {
      const ClientsImpl = class ClientsMock extends Clients {
        public get search() {
          try {
            return new search([], {}, { SpecificationFilters: {} } as any)
          } catch (error) {
            console.error('Error getting search client:', error)

            return {} as any
          }
        }

        public get vbase() {
          try {
            return new VBaseMock()
          } catch (error) {
            console.error('Error getting vbase client:', error)

            return {} as any
          }
        }
      }

      const context = {
        metrics: {} as any,
        clients: new ClientsImpl({}, contextMock.object),
        ...contextMock.object,
        ...customContext.object,
        state: {
          ...state.object,
        },
      } as any

      const result = await getCompatibilityArgs(context, args)

      expect(result).toStrictEqual(args)
    }
  })
})

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-resolver/node/resolvers/search/utils.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
import crypto from 'crypto'
import { compose, last, split, toLower, zip } from 'ramda'
import { Functions } from '@gocommerce/utils'

import { searchSlugify, Slugify } from '../../utils/slug'
import { Logger } from '@vtex/api'

export enum SearchCrossSellingTypes {
  whoboughtalsobought = 'whoboughtalsobought',
  similars = 'similars',
  whosawalsosaw = 'whosawalsosaw',
  whosawalsobought = 'whosawalsobought',
  accessories = 'accessories',
  suggestions = 'suggestions',
}

export enum GroupByCrossSellingTypes {
  PRODUCT = 'PRODUCT',
  NONE = 'NONE',
}

const pageTypeMapping: Record<string, string> = {
  Brand: 'brand',
  Department: 'department',
  Category: 'category',
  SubCategory: 'subcategory',
  NotFound: 'search',
  FullText: 'search',
  Search: 'search',
}

const lastSegment = compose<string, string[], string>(
  last,
  split('/')
)

export const zipQueryAndMap = (query?: string | null, map?: string | null) => {
  const cleanQuery = query || ''
  const cleanMap = map || ''
  return zip(
    cleanQuery
      .toLowerCase()
      .split('/')
      .map(decodeURIComponent),
    cleanMap.split(',')
  )
}

export function hashMD5(text: string) {
  const hash = crypto.createHash('md5')
  return hash.update(text).digest('hex')
}

export function findCategoryInTree(
  tree: CategoryTreeResponse[],
  values: string[],
  index = 0
): CategoryTreeResponse | null {
  for (const node of tree) {
    const slug = lastSegment(node.url)
    if (slug.toUpperCase() === values[index].toUpperCase()) {
      if (index === values.length - 1) {
        return node
      }
      return findCategoryInTree(node.children, values, index + 1)
    }
  }
  return null
}

export const getBrandFromSlug = async (
  brandSlug: string,
  search: Context['clients']['search']
) => {
  const brands = await search.brands()
  return brands.find(
    brand =>
      brand.isActive &&
      (toLower(searchSlugify(brand.name)) === brandSlug ||
        toLower(Slugify(brand.name)) === brandSlug)
  )
}

type CategoryMap = Record<string, CategoryTreeResponse>

/**
 * We are doing this because the `get category` API is not returning the values
 * for slug and href. So we get the whole category tree and get that info from
 * there instead until the Search team fixes this issue with the category API.
 */
export async function getCategoryInfo(
  search: Context['clients']['search'],
  id: number,
  levels: number
) {
  const categories = await search.categories(levels)
  const mapCategories = categories.reduce(appendToMap, {}) as CategoryMap

  const category = mapCategories[id] || { url: '' }

  return category
}

export function buildCategoryMap(categoryTree: CategoryTreeResponse[]) {
  return categoryTree.reduce(appendToMap, {}) as CategoryMap
}

/**
 * That's a recursive function to fill an object like { [categoryId]: Category }
 * It will go down the category.children appending its children and so on.
 */
function appendToMap(
  mapCategories: CategoryMap,
  category: CategoryTreeResponse
) {
  mapCategories[category.id] = category

  mapCategories = category.children.reduce(appendToMap, mapCategories)

  return mapCategories
}

export function translatePageType(searchPageType: string): string {
  return pageTypeMapping[searchPageType] || 'search'
}

export const searchEncodeURI = (account: string) => (str: string) => {
  if (!Functions.isGoCommerceAcc(account)) {
    return str.replace(/[%"'.()]/g, (c: string) => {
      switch (c) {
        case '%':
          return '@perc@'
        case '"':
          return '@quo@'
        case "'":
          return '@squo@'
        case '.':
          return '@dot@'
        case '(':
          return '@lpar@'
        case ')':
          return '@rpar@'
        default: {
          return c
        }
      }
    })
  }
  return str
}

export const searchDecodeURI = (str: string) => {
  return str.replace(/(\@perc\@|\@slash\@|\@quo\@|\@squo\@|\@dot\@|\@lpar\@|\@rpar\@)/g, (c: string) => {
    switch (c) {
      case '@perc@':
        return '%'
      case '@quo@':
        return "\""
      case '@squo@':
        return "\'"
      case '@dot@':
        return "."
      case '@lpar@':
        return "("
      case '@rpar@':
        return ")"
      case '@slash@':
        return "/"
      default: {
        return c
      }
    }
  })
}

export const getMapAndPriceRangeFromSelectedFacets = (
  selectedFacets: SelectedFacet[]
) => {
  const priceRangeIndex = selectedFacets.findIndex(
    facet => facet.key === 'priceRange'
  )
  const priceRange =
    priceRangeIndex > -1
      ? selectedFacets.splice(priceRangeIndex, 1)[0].value
      : undefined

  const map = selectedFacets.map(facet => facet.key).join(',')

  return [map, priceRange]
}

export const breadcrumbMapKey = (queryUnit: string, mapUnit: string) => {
  return `${queryUnit}-${mapUnit}`
}

export const validMapAndQuery = (query?: string, map?: string) => {
  const values = query?.split('/').filter(value => value)
  const facets = map?.split(',').filter(facetKey => facetKey)
  return values?.length === facets?.length
}

export const logDegradedSearchError = (
  logger: Logger,
  error: DegradedSearchError
) => {
  logger.warn({
    message: 'Degraded search',
    ...error,
  })
}

export const getShippingOptionsFromSelectedFacets = (
  selectedFacets: SelectedFacet[] = []
): [string[], SelectedFacet[]] => {
  const shippingOptions: string[] = []
  const filteredFacets: SelectedFacet[] = []

  selectedFacets.forEach((facet) => {
    if (facet.key === 'shipping-option') {
      shippingOptions.push(facet.value)
    } else {
      filteredFacets.push(facet)
    }
  })

  return [shippingOptions, filteredFacets]
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-resolver/node/resolvers/search/recommendation.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { GroupByCrossSellingTypes, SearchCrossSellingTypes } from './utils'

interface RecommendationParentProps {
  product: SearchProduct
  groupBy: string
}

export const resolvers = {
  Recommendation: {
    buy: (
      { product: {productId},
        groupBy = GroupByCrossSellingTypes.PRODUCT
      }: RecommendationParentProps,
      _: any,
      { clients: { search } }: Context
    ) =>
    {
      const groupByProduct =
        groupBy === GroupByCrossSellingTypes.PRODUCT ? true : false

      return search.crossSelling(
        productId,
        SearchCrossSellingTypes.whoboughtalsobought,
        groupByProduct
      )
    },

    similars: (
      { product: {productId},
        groupBy = GroupByCrossSellingTypes.PRODUCT
      }: RecommendationParentProps,
      _: any,
      { clients: { search } }: Context
    ) =>
    {
      const groupByProduct =
        groupBy === GroupByCrossSellingTypes.PRODUCT ? true : false

      return search.crossSelling(
        productId,
        SearchCrossSellingTypes.similars,
        groupByProduct
      )
    },

    view: (
      { product: {productId},
        groupBy = GroupByCrossSellingTypes.PRODUCT
      }: RecommendationParentProps,
      _: any,
      { clients: { search } }: Context
    ) =>
    {
      const groupByProduct =
        groupBy === GroupByCrossSellingTypes.PRODUCT ? true : false

      return search.crossSelling(
        productId,
        SearchCrossSellingTypes.whosawalsosaw,
        groupByProduct
      )
    },
  },
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-resolver/node/resolvers/search/skuSpecificationValue.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { addContextToTranslatableString } from '../../utils/i18n'

// if it has `originalName`, the specification is already coming in the appropriate format and language
export const resolvers = {
  SKUSpecificationValue: {
    originalName: (value: SKUSpecificationValue) => {
      return  value.originalName ? value.originalName : value.name
    },
    name: (value: SKUSpecificationValue, _: any, ctx: Context) => {
      return value.originalName ? value.name : addContextToTranslatableString(
        {
          content: value.name,
          context: value.fieldId
        },
        ctx
      )
    }
  }
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-resolver/node/resolvers/search/itemMetadata.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { Segment } from '@vtex/api'
import camelCase from 'camelcase'
import { both, pickBy } from 'ramda'
import { renameKeysWith } from '../../utils'

const isTruthy = (val?: string) => !!val
const isUtm = (_: any, key: string | number) => String(key).startsWith('utm')
const isValidUtm = both(isUtm, isTruthy)

interface Parent {
  items: SearchMetadataItem[]
}

interface SimulationPayload {
  countryCode: string
  marketingData: Record<string, string>
}

const getSimulationPayload = async (
  segment: Segment
): Promise<SimulationPayload> => {
  const segmentData = await segment.getSegment()

  let marketingData: Record<string, string> = {}
  try {
    marketingData = renameKeysWith(
      camelCase,
      pickBy(isValidUtm, segmentData)
    ) as Record<string, string>
  } catch (e) {
    // TODO: Log to Splunk
    console.error(e)
  }

  return {
    countryCode: segmentData.countryCode,
    marketingData,
  }
}

type PriceTableMap = Record<
  string,
  {
    compositionItem: CompositionItem
    simulationPayload: SimulationPayload
    items: SearchMetadataItem[]
    parent: SearchMetadataItem
    assemblyOption: AssemblyOption
  }[]
>

export const resolvers = {
  ItemMetadata: {
    priceTable: async (
      { items }: Parent,
      _: any,
      { clients: { segment } }: Context
    ) => {
      const simulationPayload = await getSimulationPayload(segment)

      const itemsWithAssembly = items.filter(
        item => item.assemblyOptions.length > 0
      )

      const priceTableMap: PriceTableMap = itemsWithAssembly.reduce<PriceTableMap>((acc, item) => {
        for (const assemblyOption of item.assemblyOptions) {
          if (!assemblyOption.composition || !assemblyOption.composition.items) {
            continue
          }

          for (const compItem of assemblyOption.composition.items) {
            const currentArray = acc[compItem.priceTable] || []
            currentArray.push({
              compositionItem: compItem,
              simulationPayload,
              items: itemsWithAssembly,
              parent: item,
              assemblyOption,
            })
            acc[compItem.priceTable] = currentArray
          }
        }

        return acc
      }, {})

      return Object.entries(priceTableMap).map(
        ([priceTableName, priceTableValues]) => ({
          type: priceTableName,
          values: priceTableValues,
        })
      )
    },
  },
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-resolver/node/resolvers/search/constants.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
export const SEARCH_URLS_BUCKET = 'newSearchURLs'
export const SELLERS_BUCKET = 'sellersByRegionId'
export const CATEGORY_TREE_ROOT_BUCKET = 'categoryTree'
export const CATEGORY_TREE_CHILDREN_BUCKET = 'categoryTreeChildren'
export const SPECIFICATION_BUCKET = 'specificationField'
export const LIST_SPECIFICATIONS_BY_CATEGORY_ID = 'specificationByCategoryId'
export const CATEGORY_TREE_ROOT_PATH = 'categoryTree.json'
export const FACETS_BUCKET = 'facets'
export const SPEC_FILTER = 'specificationFilter'
export const MAP_QUERY_KEY = 'map'
export const MAP_VALUES_SEP = ','
export const PATH_SEPARATOR = '/'
export const SPACE_REPLACER = '-'
export const FILTER_TITLE_SEP = '_'
export const CATEGORY_SEGMENT = 'c'
export const FULL_TEXT_SEGMENT = 'ft'

export const APP_NAME = 'vtex.search-resolver@1.x'

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-resolver/node/resolvers/search/newURLs.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { Search } from '../../clients/search'
import { VBase } from '@vtex/api'
import {
  SEARCH_URLS_BUCKET,
  CATEGORY_SEGMENT,
  FACETS_BUCKET,
  FULL_TEXT_SEGMENT,
} from './constants'
import { CategoryTreeSegmentsFinder, CategoryIdNamePair } from '../../utils/CategoryTreeSegmentsFinder'
import { staleFromVBaseWhileRevalidate } from '../../utils/vbase'
import { searchSlugify } from '../../utils/slug'
import { PATH_SEPARATOR, MAP_SEPARATOR } from '../stats/constants'

export const hasFacetsBadArgs = ({ query, map }: QueryArgs) => !query || !map

export const toCompatibilityArgs = async (vbase:VBase, search: Search, args: QueryArgs): Promise<QueryArgs|undefined> => {
  const {query} = args
  if(!query){
    return
  }
  const { query: compatibilityQuery, map: compatibilityMap } = await staleFromVBaseWhileRevalidate(
    vbase, SEARCH_URLS_BUCKET, query, mountCompatibilityQuery, {vbase, search, args} )
  return { query: compatibilityQuery, map: compatibilityMap }
}

export const mountCompatibilityQuery = async (params: {vbase: VBase, search: Search, args: any}) => {
  const {vbase, search, args} = params
  const { query, map } = args
  const querySegments = query.startsWith(PATH_SEPARATOR)? query.split(PATH_SEPARATOR).slice(1): query.split(PATH_SEPARATOR)

  const categoryTreeFinder = new CategoryTreeSegmentsFinder({vbase, search}, querySegments)
  const categories = await categoryTreeFinder.find()
  const facetsQuery = getFacetsQueryFromCategories(categories)

  const fieldsLookup = facetsQuery? await getCategoryFilters(vbase, search, facetsQuery): {}
  const mapSegments = fillCategoriesMapSegments(categories, map)

  const compatMapSegments = []
  const compatQuerySegments = []

  for(let segmentIndex = 0; segmentIndex < querySegments.length; segmentIndex++ ) {
    const querySegment = querySegments[segmentIndex]

    const [fieldName, fieldValue] = querySegment.split('_')
    const compatMapSegmentField = fieldsLookup[fieldName]

    const mapSegment = compatMapSegmentField || mapSegments.shift() || FULL_TEXT_SEGMENT
    compatMapSegments.push(mapSegment)
    compatQuerySegments.push(fieldValue || querySegment)
  }

  const compatibilityQuery = compatQuerySegments.join('/')
  const compatibilityMap = compatMapSegments.join(',')
  return { query: compatibilityQuery, map: compatibilityMap}
}

const normalizeName = (name: string): string => searchSlugify(name)

const fillCategoriesMapSegments = (categories: (CategoryIdNamePair|null)[], map: string): (string|undefined)[] => {
  const mapSegments = map.split(MAP_SEPARATOR).filter(segment=> segment !== CATEGORY_SEGMENT)
  const segmentsFound = []

  for( const category of categories){
    if(!category){
      segmentsFound.push(mapSegments.shift())
    }else{
      segmentsFound.push(CATEGORY_SEGMENT)
    }
  }
  return [...segmentsFound, ...mapSegments]
}

const getFacetsQueryFromCategories = (categories: (CategoryIdNamePair|null)[]) => {
  const queryArgs = categories.reduce((acc: QueryArgs, category) => {
    if(category){
      acc.query = acc.query? acc.query + PATH_SEPARATOR + category.name.toLocaleLowerCase(): category.name.toLocaleLowerCase()
      acc.map = acc.map? acc.map + MAP_SEPARATOR + CATEGORY_SEGMENT: CATEGORY_SEGMENT
    }
    return acc
  }, {query: '', map: ''} as QueryArgs)
  return !hasFacetsBadArgs(queryArgs)? `${queryArgs.query}?map=${queryArgs.map}`: null
}

const getCategoryFilters = async (vbase: VBase, search: Search, query: string) => {
  const facets = await staleFromVBaseWhileRevalidate<SearchFacets>(
    vbase, FACETS_BUCKET, query, search.facets, query)
  return normalizedFiltersFromFacets(facets)
}

const normalizedFiltersFromFacets = async (facets: SearchFacets) => {
  const specificationFilters = facets['SpecificationFilters']
  return Object.keys(specificationFilters).reduce((acc, filterKey) => {
    const facets = specificationFilters[filterKey]
    const normalizedFilterName = normalizeName(filterKey)
    acc[normalizedFilterName] = facets[0].Map
    return acc
  }, {} as Record<string, string>)
}

```