[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-engine-example/node/globals.ts from the search-engine-example VTEX app.

[Assistant]
Here's the implementation:

```typescript
import {
  IOContext,
  MetricsAccumulator,
  ParamsContext,
  RecorderState,
  SegmentData,
  ServiceContext,
  MessagesLoaderV2,
} from '@vtex/api'

import { Clients } from './clients'

if (!global.metrics) {
  console.error('No global.metrics at require time')
  global.metrics = new MetricsAccumulator()
}

declare global {
  type Context = ServiceContext<Clients, State, CustomContext>

  interface State extends RecorderState {
    messages?: MessagesLoaderV2
  }

  interface StaleRevalidateData<T>{
    ttl: Date
    data: T
  }

  interface CustomContext extends ParamsContext {
    cookie: string
    originalPath: string
    vtex: CustomIOContext
  }

  interface CustomIOContext extends IOContext {
    segment?: SegmentData
  }

  interface Property {
    name: string
    values: [string]
  }

  interface TranslatableMessage {
    content: string
    from: string
    id: string
  }

  interface Reference {
    Key: string
    Value: string
  }
}

```


[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-engine-example/node/index.ts from the search-engine-example VTEX app.

[Assistant]
Here's the implementation:

```typescript
import './globals'

import { Cached, LRUCache, RecorderState, Service } from '@vtex/api'
import schema from 'vtex.search-graphql/graphql'

import { Clients } from './clients'
import { schemaDirectives } from './directives'
import { resolvers } from './resolvers'

const TWO_SECONDS_MS = 2 * 1000
const THREE_SECONDS_MS = 3 * 1000
const SIX_SECONDS_MS = 6 * 1000

// Segments are small and immutable.
const MAX_SEGMENT_CACHE = 10000
const segmentCache = new LRUCache<string, Cached>({ max: MAX_SEGMENT_CACHE })
const searchCache = new LRUCache<string, Cached>({ max: 3000 })
const customSearchEngineCache = new LRUCache<string, Cached>({ max: 3000 })
const messagesCache = new LRUCache<string, Cached>({ max: 3000 })
const vbaseCache = new LRUCache<string, Cached>({ max: 3000 })

metrics.trackCache('segment', segmentCache)
metrics.trackCache('search', searchCache)
metrics.trackCache('customSearchEngine', customSearchEngineCache)
metrics.trackCache('messages', messagesCache)
metrics.trackCache('vbase', vbaseCache)

export default new Service<Clients, RecorderState, CustomContext>({
  clients: {
    implementation: Clients,
    options: {
      default: {
        retries: 2,
        timeout: THREE_SECONDS_MS,
      },
      messagesGraphQL: {
        concurrency: 10,
        memoryCache: messagesCache,
        timeout: TWO_SECONDS_MS,
      },
      segment: {
        concurrency: 10,
        memoryCache: segmentCache,
        timeout: THREE_SECONDS_MS,
      },
      search: {
        concurrency: 10,
        memoryCache: searchCache,
        timeout: SIX_SECONDS_MS,
      },
      customSearchEngine: {
        concurrency: 10,
        memoryCache: customSearchEngineCache,
        timeout: SIX_SECONDS_MS,
      },
      vbase: {
        concurrency: 2,
        memoryCache: vbaseCache,
        timeout: TWO_SECONDS_MS,
      },
    },
  },
  graphql: {
    resolvers,
    schema,
    schemaDirectives,
  },
})

```


[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-engine-example/node/directives/withSegment.ts from the search-engine-example VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { defaultFieldResolver, GraphQLField } from 'graphql'
import { SchemaDirectiveVisitor } from 'graphql-tools'
import atob from 'atob'

/**
 * Warning: we stopped using the segment client to decode the segment token for us because it added unnecessary overhead for now.
 * If the getSegment API evolves to do more than a decode we must stop decoding it here and start calling the API once again.
 */
export class WithSegment extends SchemaDirectiveVisitor {
  public visitFieldDefinition(field: GraphQLField<any, any>) {
    const { resolve = defaultFieldResolver } = field
    field.resolve = async (root, args, ctx: Context, info) => {
      const {
        vtex: { segmentToken },
        clients: {segment}
      } = ctx
      ctx.vtex.segment = segmentToken
        ? JSON.parse(atob(segmentToken))
        : (await segment.getSegment())
      return resolve(root, args, ctx, info)
    }
  }
}

```


[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-engine-example/node/directives/toVtexAssets.ts from the search-engine-example VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { defaultFieldResolver, GraphQLField } from 'graphql'
import { SchemaDirectiveVisitor } from 'graphql-tools'

import { flags } from '../featureFlags'

export class ToVtexAssets extends SchemaDirectiveVisitor {
  public visitFieldDefinition(field: GraphQLField<any, any>) {
    const { resolve = defaultFieldResolver } = field
    field.resolve = async (root, args, ctx: Context, info) => {
      const result = resolve(root, args, ctx, info)
      return flags.VTEX_ASSETS_URL && result
        ? result.replace('vteximg.com.br', 'vtexassets.com')
        : result
    }
  }
}

```


[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-engine-example/node/directives/index.ts from the search-engine-example VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { WithSegment } from './withSegment'
import { ToVtexAssets } from './toVtexAssets'

export const schemaDirectives = {
  withSegment: WithSegment,
  toVtexAssets: ToVtexAssets,
}

```


[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-engine-example/node/clients/search.ts from the search-engine-example VTEX app.

[Assistant]
Here's the implementation:

```typescript
import {
  AppClient,
  InstanceOptions,
  IOContext,
  RequestConfig,
  SegmentData,
  CacheType,
} from '@vtex/api'
import { stringify } from 'qs'

import { searchEncodeURI, SearchCrossSellingTypes } from '../resolvers/search/utils'

interface AutocompleteArgs {
  maxRows: number | string
  searchTerm: string
}

enum SimulationBehavior {
  SKIP = 'skip',
  DEFAULT = 'default'
}

const inflightKey = ({ baseURL, url, params, headers }: RequestConfig) => {
  return (
    baseURL! +
    url! +
    stringify(params, { arrayFormat: 'repeat', addQueryPrefix: true }) +
    `&segmentToken=${headers['x-vtex-segment']}`
  )
}

interface SearchPageTypeResponse {
  id: string
  pageType: string
  name: string
  url: string
  title: string | null
  metaTagDescription: string | null
}

/** Search API
 * Docs: https://documenter.getpostman.com/view/845/catalogsystem-102/Hs44
 */
export class Search extends AppClient {
  private searchEncodeURI: (x: string) => string

  public constructor(ctx: IOContext, opts?: InstanceOptions) {
    super('vtex.catalog-api-proxy@0.x', ctx, opts)

    this.searchEncodeURI = searchEncodeURI(ctx.account)
  }

  public pageType = (path: string, query: string = '') => {
    const pageTypePath = path.startsWith('/') ? path.substr(1) : path

    const pageTypeQuery = !query || query.startsWith('?') ? query : `?${query}`

    return this.get<SearchPageTypeResponse>(
      `/pub/portal/pagetype/${pageTypePath}${pageTypeQuery}`,
      { metric: 'search-pagetype' }
    )
  }

  public product = (slug: string) =>
    this.get<SearchProduct[]>(
      `/pub/products/search/${this.searchEncodeURI(slug && slug.toLowerCase())}/p`,
      { metric: 'search-product' }
    )

  public productByEan = (id: string) =>
    this.get<SearchProduct[]>(
      `/pub/products/search?fq=alternateIds_Ean:${id}`,
      {
        metric: 'search-productByEan',
      }
    )

  public productsByEan = (ids: string[]) =>
    this.get<SearchProduct[]>(
      `/pub/products/search?${ids
        .map(id => `fq=alternateIds_Ean:${id}`)
        .join('&')}`,
      { metric: 'search-productByEan' }
    )

  public productById = (id: string, cacheable: boolean = true) => {
    const isVtex = this.context.platform === 'vtex'
    const url = isVtex ? '/pub/products/search?fq=productId:' : '/products/'
    return this.get<SearchProduct[]>(`${url}${id}`, {
      metric: 'search-productById',
      ...(cacheable ? {} : { cacheable: CacheType.None })
    })
  }

  public productsById = (ids: string[]) =>
    this.get<SearchProduct[]>(
      `/pub/products/search?${ids.map(id => `fq=productId:${id}`).join('&')}`,
      { metric: 'search-productById' }
    )

  public productByReference = (id: string) =>
    this.get<SearchProduct[]>(
      `/pub/products/search?fq=alternateIds_RefId:${id}`,
      {
        metric: 'search-productByReference',
      }
    )

  public productsByReference = (ids: string[]) =>
    this.get<SearchProduct[]>(
      `/pub/products/search?${ids
        .map(id => `fq=alternateIds_RefId:${id}`)
        .join('&')}`,
      { metric: 'search-productByReference' }
    )

  public productBySku = (skuIds: string[]) =>
    this.get<SearchProduct[]>(
      `/pub/products/search?${skuIds
        .map(skuId => `fq=skuId:${skuId}`)
        .join('&')}`,
      { metric: 'search-productBySku' }
    )

  public productsRaw = (args: SearchArgs) => {
    return this.getRaw<SearchProduct[]>(this.productSearchUrl(args), {
      metric: 'search-products',
    })
  }

  public products = (args: SearchArgs) => {
    return this.get<SearchProduct[]>(this.productSearchUrl(args), {
      metric: 'search-products',
    })
  }

  public productsQuantity = async (args: SearchArgs) => {
    const {
      headers: { resources },
    } = await this.getRaw(this.productSearchUrl(args))
    const quantity = resources.split('/')[1]
    return parseInt(quantity, 10)
  }

  public brands = () =>
    this.get<Brand[]>('/pub/brand/list', { metric: 'search-brands' })

  public brand = (id: number) =>
    this.get<Brand[]>(`/pub/brand/${id}`, { metric: 'search-brands' })

  public categories = (treeLevel: number) =>
    this.get<CategoryTreeResponse[]>(`/pub/category/tree/${treeLevel}/`, {
      metric: 'search-categories',
    })

  public getCategoryChildren = (id: number) =>
  this.get<Record<string, string>>(`/pub/category/categories/children?id=${id}`, {
    metric: 'search-category-children'
  })

  public facets = (facets: string = '') => {
    const [path, options] = decodeURI(facets).split('?')
    return this.get<SearchFacets>(
      `/pub/facets/search/${encodeURI(this.searchEncodeURI(
        `${path.trim()}${options ? '?' + options : ''}`
      ))}`,
      { metric: 'search-facets' }
    )
  }

  public category = (id: string | number) =>
    this.get<CategoryByIdResponse>(`/pub/category/${id}`, {
      metric: 'search-category',
    })

  public crossSelling = (id: string, type: SearchCrossSellingTypes) =>
    this.get<SearchProduct[]>(`/pub/products/crossselling/${type}/${id}`, {
      metric: 'search-crossSelling',
    })

  public autocomplete = ({ maxRows, searchTerm }: AutocompleteArgs) =>
    this.get<{ itemsReturned: SearchAutocompleteUnit[] }>(
      `/buscaautocomplete?maxRows=${maxRows}&productNameContains=${
        encodeURIComponent(this.searchEncodeURI(searchTerm))
      }`,
      { metric: 'search-autocomplete' }
    )

  private get = <T = any>(url: string, config: RequestConfig = {}) => {
    const segmentData: SegmentData | undefined = (this
      .context! as CustomIOContext).segment
    const { channel: salesChannel = '' } = segmentData || {}

    config.params = {
      ...config.params,
      ...(!!salesChannel && { sc: salesChannel }),
    }
    config.inflightKey = inflightKey

    return this.http.get<T>(`/proxy/catalog${url}`, config)
  }

  public getField = (id: number) =>
    this.get<FieldResponseAPI>(
      `/pub/specification/fieldGet/${id}`,
      { metric: 'catalog-get-field-by-id' }
    )

  private getRaw = <T = any>(url: string, config: RequestConfig = {}) => {
    const segmentData: SegmentData | undefined = (this
      .context! as CustomIOContext).segment
    const { channel: salesChannel = '' } = segmentData || {}

    config.params = {
      ...config.params,
      ...(!!salesChannel && { sc: salesChannel }),
    }
    config.inflightKey = inflightKey

    return this.http.getRaw<T>(`/proxy/catalog${url}`, config)
  }

  private productSearchUrl = ({
    query = '',
    category = '',
    specificationFilters,
    priceRange = '',
    collection = '',
    salesChannel = '',
    orderBy = '',
    from = 0,
    to = 9,
    map = '',
    hideUnavailableItems = false,
    simulationBehavior = SimulationBehavior.DEFAULT,
  }: SearchArgs) => {
    const sanitizedQuery = encodeURIComponent(
      this.searchEncodeURI(decodeURIComponent(query || '').trim())
    )
    if (hideUnavailableItems) {
      const segmentData = (this.context as CustomIOContext).segment
      salesChannel = (segmentData && segmentData.channel.toString()) || ''
    }
    let url = `/pub/products/search/${sanitizedQuery}?`
    if (category && !query) {
      url += `&fq=C:/${category}/`
    }
    if (specificationFilters && specificationFilters.length > 0) {
      url += specificationFilters.map(filter => `&fq=${filter}`)
    }
    if (priceRange) {
      url += `&fq=P:[${priceRange}]`
    }
    if (collection) {
      url += `&fq=productClusterIds:${collection}`
    }
    if (salesChannel) {
      url += `&fq=isAvailablePerSalesChannel_${salesChannel}:1`
    }
    if (orderBy) {
      url += `&O=${orderBy}`
    }
    if (map) {
      url += `&map=${map}`
    }
    if (from != null && from > -1) {
      url += `&_from=${from}`
    }
    if (to != null && to > -1) {
      url += `&_to=${to}`
    }
    if (simulationBehavior === SimulationBehavior.SKIP) {
      url += `&simulation=false`
    }
    return url
  }
}

```


[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-engine-example/node/clients/checkout.ts from the search-engine-example VTEX app.

[Assistant]
Here's the implementation:

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

import { statusToError } from '../utils'

/**
 * Checkout client to connect with VTEX Core Commerce APIs about Sales Channel and Checkout Simulation
 *
 */
export class Checkout extends JanusClient {
  public constructor(ctx: IOContext, options?: InstanceOptions) {
    super(ctx, {
      ...options,
      headers: {
        ...(options && options.headers),
      },
    })
  }

  private getChannelQueryString = () => {
    const { segment } = this.context as CustomIOContext
    const channel = segment && segment.channel
    const queryString = channel ? `?sc=${channel}` : ''
    return queryString
  }

  public simulation = (simulation: SimulationPayload) =>
    this.post<OrderForm>(
      this.routes.simulation(this.getChannelQueryString()),
      simulation,
      {
        metric: 'checkout-simulation',
      }
    )

  protected post = <T>(url: string, data?: any, config: RequestConfig = {}) => {
    return this.http.post<T>(url, data, config).catch(statusToError) as Promise<
      T
    >
  }

  private get routes() {
    const base = '/api/checkout/pub'
    return {
      simulation: (queryString: string) =>
        `${base}/orderForms/simulation${queryString}`,
    }
  }
}

```


[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-engine-example/node/clients/index.ts from the search-engine-example VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { IOClients } from '@vtex/api'

import { Search } from './search'
import { Checkout } from './checkout'
import { customSearchEngineClient } from './customEngine'

/**
 * On this file you'll name and export the custom clients created by this app.
 *
 * The Search and Checkout clients will be used to connect with VTEX APIs, and the customSearchEngine with the API to be integrated.
 */
export class Clients extends IOClients {
  public get search() {
    return this.getOrSet('search', Search)
  }
  public get checkout() {
    return this.getOrSet('checkout', Checkout)
  }
  public get customSearchEngine() {
    return this.getOrSet('customSearchEngine', customSearchEngineClient)
  }
}

```


[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-engine-example/node/clients/customEngine.ts from the search-engine-example VTEX app.

[Assistant]
Here's the implementation:

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

/**
 * The client used for connecting with the Search Engine external API.
 *
 * Don't forget to also add the host on the manifest.json's policies as well!
 */
export class customSearchEngineClient extends ExternalClient {
  private store: string

  public constructor(context: IOContext, options?: InstanceOptions) {
    super('https://api.customsearchengine.com/v1', context, options)

    const { account } = context
    this.store = account
  }

  /**
   *  This is where will be implemented the appropriate methods to retrieve data from the search engine API.
   *
   *  Clients on VTEX IO may have retries, timeouts and cache configured. Check it on our documentation about Clients.
   *
   * Also, don't forget to add the `metric` option when fetching HTTP. It may be used for billing purposes or to track problematic connections.
   *
   * Example method:
   */

  public async suggestionSearches(args: SuggestionSearchesArgs): Promise<any> {
    const { term } = args

    const result = await this.http.get<any>(
      `${this.store}/api/suggestion_searches`,
      {
        params: {
          term,
        },
        metric: 'suggestion-searches',
      }
    )

    return result
  }

}

```


[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-engine-example/node/commons/compatibility-layer.ts from the search-engine-example VTEX app.

[Assistant]
Here's the implementation:

```typescript
import VtexSeller from './models/VtexSeller'
import unescape from 'unescape'

export enum IndexingType {
  API = 'API',
  XML = 'XML',
}

// Probably all code here should change acccordingly to the search engine to be integrated. Keeping it here for reference.
export const convertBiggyProduct = (
  product: any,
  tradePolicy?: string,
  indexingType?: IndexingType
) => {
  const categories: string[] = product.categories
    ? product.categories.map((_: any, index: number) => {
        const subArray = product.categories.slice(0, index)
        return `/${subArray.join('/')}/`
      })
    : []

  const skus: any[] = (product.skus || []).map(
    convertSKU(product, indexingType, tradePolicy)
  )

  return {
    categories,
    cacheId: product.link,
    productId: product.product || product.id,
    productName: product.name,
    productReference: product.reference || product.product || product.id,
    linkText: product.link,
    brand:
      product.brand ||
      product.extraInfo['marca'] ||
      product.extraInfo['brand'] ||
      '',
    brandId: -1,
    link: product.url,
    description: product.description,
    items: skus,
    sku: skus.find(sku => sku.sellers && sku.sellers.length > 0),
  }
}

const getSellersIndexedByApi = (
  product: any,
  sku: any,
  tradePolicy?: string
) => {
  const selectedPolicy = tradePolicy
    ? sku.policies.find((policy: any) => policy.id === tradePolicy)
    : sku.policies[0]

  const biggySellers = (selectedPolicy && selectedPolicy.sellers) || []

  return biggySellers.map((seller: any) => {
    const price = seller.price || sku.price || product.price
    const oldPrice = seller.oldPrice || sku.oldPrice || product.oldPrice
    const installment = seller.installment || product.installment

    return new VtexSeller(seller.id, price, oldPrice, installment)
  })
}

const getSellersIndexedByXML = (product: any) => {
  const { installment, price, oldPrice } = product
  return [new VtexSeller('1', price, oldPrice, installment)]
}

const getImageId = (imageUrl: string) => {
  const baseUrlRegex = new RegExp(/.+ids\/(\d+)/)
  return baseUrlRegex.test(imageUrl)
    ? baseUrlRegex.exec(imageUrl)![1]
    : undefined
}

const elasticImageToVtexImage = (image: ElasticImage, imageId: string) => {
  return {
    imageId,
    cacheId: imageId,
    imageLabel: image.name,
    imageText: image.name,
    imageUrl: image.value,
  }
}

const convertImages = (images: ElasticImage[], indexingType?: IndexingType) => {
  const vtexImages: VtexImage[] = []

  if (indexingType && indexingType === IndexingType.XML) {
    const selectedImage: ElasticImage = images[0]
    const imageId = getImageId(selectedImage.value)

    return imageId ? [elasticImageToVtexImage(selectedImage, imageId)] : []
  }

  images.forEach(image => {
    const imageId = getImageId(image.value)
    imageId ? vtexImages.push(elasticImageToVtexImage(image, imageId)) : []
  })

  return vtexImages
}

const convertSKU = (
  product: any,
  indexingType?: IndexingType,
  tradePolicy?: string
) => (sku: any) => {
  const images = convertImages(product.images, indexingType)

  const sellers =
    indexingType === IndexingType.XML
      ? getSellersIndexedByXML(product)
      : getSellersIndexedByApi(product, sku, tradePolicy)

  return {
    sellers,
    images,
    seller: sellers[0],
    itemId: sku.id,
    name: product.name,
    nameComplete: product.name,
    complementName: product.name,
    referenceId: [
      {
        Key: 'RefId',
        Value: sku.reference,
      },
    ],
  }
}

export const biggyAttributesToVtexFilters = (attributes: any) =>
  attributes.map((attribute: any) => {
    const isNumber = attribute.type === 'number'

    return {
      name: attribute.label,
      type: isNumber ? 'PRICERANGE' : attribute.type.toUpperCase(),
      values: isNumber
        ? [
            {
              quantity: attribute.values.reduce(
                (acum: number, value: any) => acum + value.count,
                0
              ),
              name: unescape(attribute.label),
              key: attribute.key,
              value: attribute.key,
              range: {
                from: attribute.minValue,
                to: attribute.maxValue,
              },
            },
          ]
        : attribute.values.map((value: any) => {
            return {
              quantity: value.count,
              name: unescape(value.label),
              key: attribute.key,
              value: value.key,
              selected: value.active,
            }
          }),
    }
  })

/**
 * Convert from VTEX OrderBy into Biggy's sort.
 *
 * @export
 * @param {OrderBy} orderBy VTEX OrderBy.
 * @returns {string} Biggy's sort.
 */
export const convertOrderBy = (orderBy?: string): string => {
  switch (orderBy) {
    case 'OrderByPriceDESC':
      return 'price:desc'
    case 'OrderByPriceASC':
      return 'price:asc'
    case 'OrderByTopSaleDESC':
      return 'orders:desc'
    case 'OrderByReviewRateDESC':
      return '' // TODO: Not Supported
    case 'OrderByNameDESC':
      return 'name:desc'
    case 'OrderByNameASC':
      return 'name:asc'
    case 'OrderByReleaseDateDESC':
      return 'fields.release:desc'
    case 'OrderByBestDiscountDESC':
      return 'discount:desc'
    default:
      return ''
  }
}

export const buildBreadcrumb = (selectedFacets: SelectedFacet[]) => {
  const pivotValue: string[] = []
  const pivotMap: string[] = []

  return selectedFacets
    .filter(selectedFacet => selectedFacet.key !== 'priceRange')
    .map(selectedFacet => {
      pivotValue.push(selectedFacet.value)
      pivotMap.push(selectedFacet.key)

      return {
        name: decodeURIComponent(selectedFacet.value),
        href: `/${pivotValue.join('/')}?map=${pivotMap.join(',')}`,
      }
    })
}

export const buildAttributePath = (selectedFacets: SelectedFacet[]) => {
  return selectedFacets.reduce(
    (attributePath, facet) => {
      if (facet.key === 'priceRange') {
        facet.key = 'price'
        facet.value = facet.value.replace(` TO `, ':')
      }

      return facet.key !== 'ft'
        ? `${attributePath}${facet.key}/${facet.value}/`
        : attributePath
    },

    ''
  )
}

```


[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-engine-example/node/commons/products.ts from the search-engine-example VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { convertBiggyProduct } from './compatibility-layer'
import { map, prop, isEmpty, sort, indexOf } from 'ramda'
import { queries } from '../resolvers/search'

// Probably all code here should change acccordingly to the search engine to be integrated. Keeping it here for reference.
export const productsBiggy = async (searchResult: any, ctx: any) => {
  const { segment } = ctx.vtex
  const products: any[] = []

  searchResult.products.forEach((product: any) => {
    try {
      products.push(convertBiggyProduct(product, segment && segment.channel))
    } catch (e) {
      // TODO: add logging
    }
  })

  return products
}

export const productsCatalog = async (searchResult: any, ctx: any) => {
  let products: any[] = searchResult.products
  const productIds = map<any, string>((product: any) => {
    return prop('product', product) || prop('id', product) || ''
  }, products)

  if (!isEmpty(productIds)) {
    // Get products' model from VTEX search API
    products = await queries.productsByIdentifier(
      undefined,
      {
        field: 'id',
        values: productIds,
      },
      ctx
    )

    // Maintain customSearchEngine's order.
    products = sort(
      (a, b) =>
        indexOf(a.productId, productIds) - indexOf(b.productId, productIds),
      products
    )
  }

  return products
}

```


[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-engine-example/node/typings/unescape.ts from the search-engine-example VTEX app.

[Assistant]
Here's the implementation:

```typescript
declare module 'unescape' {
  export default function(codedString: string): string
}

```


[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-engine-example/node/typings/Search.ts from the search-engine-example VTEX app.

[Assistant]
Here's the implementation:

```typescript
interface SegmentData {
  campaigns?: any
  channel: number
  priceTables?: any
  utm_campaign: string
  regionId?: string
  utm_source: string
  utmi_campaign: string
  currencyCode: string
  currencySymbol: string
  countryCode: string
  cultureInfo: string
  [key: string]: any
}

interface ElasticImage {
  name: string
  value: string
}

interface VtexImage {
  cacheId: string
  imageId: string
  imageLabel: string
  imageUrl: string
  imageText: string
}

enum IndexingType {
  API = 'API',
  XML = 'XML',
}

interface SearchResultArgs {
  attributePath: string
  query: string
  page: number
  count: number
  sort: string
  operator: string
  fuzzy: string
  leap?: boolean
  tradePolicy?: number
  segment?: SegmentData
  indexingType?: IndexingType
}

interface SuggestionProductsArgs {
  term: string
  attributeKey?: string
  attributeValue?: string
  tradePolicy?: string
  segment?: SegmentData
  indexingType?: IndexingType
}

interface SuggestionSearchesArgs {
  term: string
}

interface SelectedFacet {
  value: string
  key: string
}

interface FacetsInput {
  selectedFacets: SelectedFacet[]
  fullText: string
  query: string
}

interface ProductSearchInput {
  query: string
  from: number
  to: number
  selectedFacets: SelectedFacet[]
  fullText: string
  fuzzy: string
  operator: string
  orderBy: string
  productOriginVtex: boolean
}

```


[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-engine-example/node/typings/Checkout.ts from the search-engine-example VTEX app.

[Assistant]
Here's the implementation:

```typescript
interface OrderFormMarketingData {
  utmCampaign?: string
  utmMedium?: string
  utmSource?: string
  utmiCampaign?: string
  utmiPart?: string
  utmipage?: string
  marketingTags?: string
}

interface CheckoutAddress {
  addressType: string
  receiverName: string
  addressId: string
  postalCode: string
  city: string
  state: string
  country: string
  street: string
  number: string
  neighborhood: string
  complement: string
  reference: string | null
  geoCoordinates: [number, number]
}
interface OrderFormItem {
  id: string
  name: string
  detailUrl: string
  imageUrl: string
  productRefId: string
  skuName: string
  quantity: number
  uniqueId: string
  productId: string
  refId: string
  ean: string
  priceValidUntil: string
  price: number
  tax: number
  listPrice: number
  sellingPrice: number
  rewardValue: number
  isGift: boolean
  parentItemIndex: number | null
  parentAssemblyBinding: string | null
  productCategoryIds: string
  priceTags: string[]
  measurementUnit: string
  additionalInfo: {
    brandName: string
    brandId: string
    offeringInfo: any | null
    offeringType: any | null
    offeringTypeId: any | null
  }
  productCategories: Record<string, string>
  seller: string
  sellerChain: string[]
  availability: string
  unitMultiplier: number
}
interface OrderForm {
  orderFormId: string
  salesChannel: string
  loggedIn: boolean
  isCheckedIn: boolean
  storeId: string | null
  checkedInPickupPointId: string | null
  allowManualPrice: boolean
  canEditData: boolean
  userProfileId: string | null
  userType: string | null
  ignoreProfileData: boolean
  value: number
  messages: any[]
  items: OrderFormItem[]
  selectableGifts: any[]
  totalizers: { id: string; name: string; value: number }[]
  shippingData: {
    address: CheckoutAddress
    logisticsInfo: {
      itemIndex: number
      selectedSla: string
      selectedDeliveryChannel: string
      addressId: string
      slas: {
        id: string
        deliveryChannel: string
        name: string
        deliveryIds: {
          courierId: string
          warehouseId: string
          dockId: string
          courierName: string
          quantity: number
        }[]
        shippingEstimate: string
        shippingEstimateDate: string | null
        lockTTL: string | null
        availableDeliveryWindows: any[]
        deliveryWindow: string | null
        price: number
        listPrice: number
        tax: number
        pickupStoreInfo: {
          isPickupStore: boolean
          friendlyName: string | null

          address: CheckoutAddress | null
          additionalInfo: any | null
          dockId: string | null
        }
        pickupPointId: string | null
        pickupDistance: number
        polygonName: string | null
      }[]
      shipsTo: string[]
      itemId: string
      deliveryChannels: { id: string }[]
    }[]
    selectedAddresses: CheckoutAddress[]
    availableAddresses: CheckoutAddress[]
    pickupPoints: {
      friendlyName: string
      address: CheckoutAddress
      additionalInfo: string
      id: string
      businessHours: {
        DayOfWeek: number
        OpeningTime: string
        ClosingTime: string
      }[]
    }[]
  }
  clientProfileData: any | null
  paymentData: {
    installmentOptions: {
      paymentSystem: string
      bin: string | null
      paymentName: string | null
      paymentGroupName: string | null
      value: number
      installments: {
        count: number
        hasInterestRate: false
        interestRate: number
        value: number
        total: number
        sellerMerchantInstallments: {
          count: number
          hasInterestRate: false
          interestRate: number
          value: number
          total: number
        }[]
      }[]
    }[]
    paymentSystems: {
      id: string
      name: string
      groupName: string
      validator: {
        regex: string
        mask: string
        cardCodeRegex: string
        cardCodeMask: string
        weights: number[]
        useCvv: boolean
        useExpirationDate: boolean
        useCardHolderName: boolean
        useBillingAddress: boolean
      }
      stringId: string
      template: string
      requiresDocument: boolean
      isCustom: boolean
      description: string | null
      requiresAuthentication: boolean
      dueDate: string
      availablePayments: any | null
    }[]
    payments: any[]
    giftCards: any[]
    giftCardMessages: any[]
    availableAccounts: any[]
    availableTokens: any[]
  }
  marketingData: OrderFormMarketingData | null
  sellers: {
    id: string
    name: string
    logo: string
  }[]
  clientPreferencesData: {
    locale: string
    optinNewsLetter: any | null
  }
  commercialConditionData: any | null
  storePreferencesData: {
    countryCode: string
    saveUserData: boolean
    timeZone: string
    currencyCode: string
    currencyLocale: number
    currencySymbol: string
    currencyFormatInfo: {
      currencyDecimalDigits: number
      currencyDecimalSeparator: string
      currencyGroupSeparator: string
      currencyGroupSize: number
      startsWithCurrencySymbol: boolean
    }
  }
  giftRegistryData: any | null
  openTextField: any | null
  invoiceData: any | null
  customData: any | null
  itemMetadata: {
    items: MetadataItem[]
  }
  hooksData: any | null
  ratesAndBenefitsData: {
    rateAndBenefitsIdentifiers: any[]
    teaser: any[]
  }
  subscriptionData: any | null
  itemsOrdination: any | null
}

interface PayloadItem {
  id: string
  quantity: number
  seller: string
  parentItemIndex?: number | null
  parentAssemblyBinding?: string | null
}

interface SimulationPayload {
  country?: string
  items: PayloadItem[]
  postalCode?: string
  isCheckedIn?: boolean
  priceTables?: string[]
  marketingData?: Record<string, string>
}

```


[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-engine-example/node/typings/SearchMetadata.ts from the search-engine-example VTEX app.

[Assistant]
Here's the implementation:

```typescript
interface SearchMetadataArgs {
  query?: string | null
  map?: string | null
  selectedFacets?: SelectedFacet[]
}

interface SearchMetadata {
  titleTag?: string | null
  metaTagDescription?: string | 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-engine-example/node/typings/ItemMetadata.ts from the search-engine-example VTEX app.

[Assistant]
Here's the implementation:

```typescript
/**
 * Assembly options related types
 */

interface CompositionItem {
  id: string
  minQuantity: number
  maxQuantity: number
  initialQuantity: number
  priceTable: string
  seller: string
}

interface Composition {
  minQuantity: number
  maxQuantity: number
  items: CompositionItem[]
}

interface AssemblyOption {
  id: string
  name: string
  composition: Composition | null
  inputValues: InputValues
}

interface InputValues {
  [key: string]: RawInputValue
}

interface RawInputValue {
  maximumNumberOfCharacters: number
  domain: string[]
}

interface SearchMetadataItem {
  Name: string
  NameComplete: string
  MainImage: string
  BrandName: string
  CategoryId: number
  ProductId: number
  id: string
  seller: string
  assemblyOptions: AssemblyOption[]
}

interface MetadataItem {
  id: string
  name: string
  imageUrl: string
  detailUrl: string
  seller: string
  assemblyOptions: AssemblyOption[]
  skuName: string
  productId: string
  refId: string
  ean: string | null
}
interface AddedItem {
  choiceType: string
  compositionItem: CompositionItem
  extraQuantity: number
  item: OrderFormItem
  normalizedQuantity: number
}

interface RemovedItem {
  initialQuantity: number
  name: string
  removedQuantity: number
}

```


[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-engine-example/node/typings/Catalog.ts from the search-engine-example VTEX app.

[Assistant]
Here's the implementation:

```typescript
interface Range {
  from: number
  to: number
}

interface SelectedFacet {
  key: string
  value: string
}

interface QueryArgs {
  query: string
  map?: string
  selectedFacets?: SelectedFacet[]
}

interface SearchArgs extends QueryArgs {
  category: string | null
  specificationFilters: string[] | null
  priceRange?: string | null
  collection: string | null
  salesChannel: string | null
  orderBy: string | null
  from: number | null
  to: number | null
  hideUnavailableItems: boolean | null
  simulationBehavior: 'skip' | 'default' | null
}

interface Metadata {
  metaTagDescription?: string
  titleTag?: string
}

interface Brand {
  id: string
  name: string
  isActive: boolean
  title: string | null
  metaTagDescription: string | null
  imageUrl: string | null
}

interface CategoryTreeResponse {
  id: number
  name: string
  hasChildren: boolean
  url: string
  children: CategoryTreeResponse[]
  Title: string
  MetaTagDescription: string
}

interface CategoryByIdResponse {
  parentId: number | null
  GlobalCategoryId: number
  GlobalCategoryName: string
  position: number
  slug: string
  id: number
  name: string
  hasChildren: boolean
  url: string
  children: null
  Title: string
  MetaTagDescription: string
}

interface FacetsArgs extends QueryArgs {
  hideUnavailableItems?: boolean
  behavior?: FacetsBehavior
}

enum FacetsBehavior {
  STATIC = 'Static',
  DYNAMIC = 'Dynamic',
}

interface SearchProduct {
  productId: string
  productName: string
  brand: string
  brandId: number
  linkText: string
  productReference: string
  categoryId: string
  productTitle: string
  metaTagDescription: string
  clusterHighlights: Record<string, string>
  productClusters: Record<string, string>
  searchableClusters: Record<string, string>
  categories: string[]
  categoriesIds: string[]
  link: string
  description: string
  items: SearchItem[]
  itemMetadata: {
    items: SearchMetadataItem[]
  }
  titleTag: string
  Specifications?: string[]
  allSpecifications?: string[]
  allSpecificationsGroups?: string[]
}

interface SearchItem {
  itemId: string
  name: string
  nameComplete: string
  complementName: string
  ean: string
  referenceId: { Key: string; Value: string }[]
  measurementUnit: string
  unitMultiplier: number
  modalType: any | null
  images: SearchImage[]
  Videos: string[]
  variations: string[]
  sellers: Seller[]
  attachments: {
    id: number
    name: string
    required: boolean
    domainValues: string
  }[]
  isKit: boolean
  kitItems?: {
    itemId: string
    amount: number
  }[]
}

interface SearchImage {
  imageId: string
  imageLabel: string | null
  imageTag: string
  imageUrl: string
  imageText: string
}

interface SearchInstallment {
  Value: number
  InterestRate: number
  TotalValuePlusInterestRate: number
  NumberOfInstallments: number
  PaymentSystemName: string
  PaymentSystemGroupName: string
  Name: string
}

interface CommertialOffer {
  DeliverySlaSamplesPerRegion: Record<
    string,
    { DeliverySlaPerTypes: any[]; Region: any | null }
  >
  Installments: SearchInstallment[]
  DiscountHighLight: any[]
  GiftSkuIds: string[]
  Teasers: any[]
  BuyTogether: any[]
  ItemMetadataAttachment: any[]
  Price: number
  ListPrice: number
  PriceWithoutDiscount: number
  RewardValue: number
  PriceValidUntil: string
  AvailableQuantity: number
  Tax: number
  DeliverySlaSamples: {
    DeliverySlaPerTypes: any[]
    Region: any | null
  }[]
  GetInfoErrorMessage: any | null
  CacheVersionUsedToCallCheckout: string
}

interface Seller {
  sellerId: string
  sellerName: string
  addToCartLink: string
  sellerDefault: boolean
  commertialOffer: CommertialOffer
}

interface SearchFacet {
  Quantity: number
  Name: string
  Link: string
  LinkEncoded: string
  Map: string
  Value: string
}

interface SearchFacetCategory {
  Id: number
  Quantity: number
  Name: string
  Link: string
  LinkEncoded: string
  Map: string
  Value: string
  Children: SearchFacetCategory[]
}

interface SummaryItem {
  DisplayedItems: number
  TotalItems: number
}

interface SearchFacets {
  Departments: SearchFacet[]
  Brands: SearchFacet[]
  SpecificationFilters: Record<string, SearchFacet[]>
  CategoriesTrees: SearchFacetCategory[]
  PriceRanges: {
    Slug: string
    Quantity: number
    Name: string
    Link: string
    LinkEncoded: string
    Map: null
    Value: string
  }[]
  Summary: {
    Departments: SummaryItem
    CategoriesTrees: SummaryItem
    Brands: SummaryItem
    PriceRanges: SummaryItem
    SpecificationFilters: Record<string, SummaryItem>
  }
}

interface SearchAutocompleteItem {
  productId: string
  itemId: string
  name: string
  nameComplete: string
  imageUrl: string
}

interface SearchAutocompleteUnit {
  items: SearchAutocompleteItem[]
  thumb: string
  thumbUrl: string | null
  name: string
  href: string
  criteria: string
}

interface FieldTreeResponseAPI {
  Name: string
  CategoryId: number
  FieldId: number
  IsActive: boolean
  IsStockKeepingUnit: boolean
}

interface FieldResponseAPI {
  Name: string
  CategoryId: number | null
  FieldId: number
  IsActive: boolean
  IsRequired: boolean
  FieldTypeId: number
  FieldTypeName: string
  FieldValueId: string | null
  Description: string | null
  IsStockKeepingUnit: boolean
  IsFilter: boolean
  IsOnProductDetails: boolean
  Position: number
  IsWizard: boolean
  IsTopMenuLinkActive: boolean
  IsSideMenuLinkActive: boolean
  DefaultValue: string | null
  FieldGroupId: number
  FieldGroupName: string
}

```


[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-engine-example/node/utils/categoryTreeSearch.ts from the search-engine-example VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { searchSlugify } from "./slug"

export const categoryTreeSearch = (categoryTree: CategoryTreeResponse[], query: string) => {
  const querySegments = query.startsWith('/')? query.split('/').slice(1): query.split('/')
  return bfsCategory(categoryTree, querySegments, 0)
}

const bfsCategory = (categoryTree: CategoryTreeResponse[], segments: string[], depth: number): CategoryTreeResponse[]  => {
  if(segments.length === 0 || segments.length === depth){
    return []
  }

  for (const categoryNode of categoryTree){
    if(searchSlugify(categoryNode.name) === segments[depth]){
      return [categoryNode, ...bfsCategory(categoryNode.children, segments, depth + 1)]
    }
  }

  segments.shift()
  return []
}
```


[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-engine-example/node/utils/slug.ts from the search-engine-example VTEX app.

[Assistant]
Here's the implementation:

```typescript
import slugify from 'slugify'
import { toLower } from 'ramda'

export function Slugify(str: any) {
  return slugify(str, { lower: true, remove: /[*+~.()'"!:@]/g })
}

const from =
  'ÁÄÂÀÃÅČÇĆĎÉĚËÈÊẼĔȆÍÌÎÏŇÑÓÖÒÔÕØŘŔŠŤÚŮÜÙÛÝŸŽáäâàãåčçćďéěëèêẽĕȇíìîïňñóöòôõøðřŕšťúůüùûýÿžþÞĐđßÆa·/_,:;'
const to =
  'AAAAAACCCDEEEEEEEEIIIINNOOOOOORRSTUUUUUYYZaaaaaacccdeeeeeeeeiiiinnooooooorrstuuuuuyyzbBDdBAa------'
const removeAccents = (str: string) => {
  let newStr = str.slice(0)
  for (let i = 0; i < from.length; i++) {
    newStr = newStr.replace(new RegExp(from.charAt(i), 'g'), to.charAt(i))
  }
  return newStr
}

// Parameter "S. Coifman" should output "s--coifman"
export function searchSlugify(str: string) {
  // According to Bacelar, the search API uses a legacy method for slugifying strings.
  // replaces special characters with dashes, remove accents and lower cases everything
  // eslint-disable-next-line no-useless-escape
  const noCommas = str.replace(/,/g, '')
  const replaced = noCommas.replace(/[*+~.()'"!:@&\[\]`/ %$#?{}|><=_^]/g, '-')
  return toLower(removeAccents(replaced))
}

```


[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-engine-example/node/utils/string.ts from the search-engine-example VTEX app.

[Assistant]
Here's the implementation:

```typescript
export const toTitleCase = (str: string) => {
  return str
    .toLowerCase()
    .split(' ')
    .map(word => word.charAt(0).toUpperCase() + word.slice(1))
    .join(' ')
}

```


[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-engine-example/node/utils/vbase.ts from the search-engine-example VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { VBase } from '@vtex/api'
import { createHash } from 'crypto'

export const staleFromVBaseWhileRevalidate = async <T>(
  vbase: VBase,
  bucket: string,
  filePath: string,
  validateFunction: (params?: any) => Promise<T>,
  params?: any,
  options?: { expirationInMinutes?: number}
): Promise<T> => {
    const normalizedFilePath = normalizedJSONFile(filePath)
    const cachedData = await vbase.getJSON<StaleRevalidateData<T>>(bucket, normalizedFilePath, true).catch() as StaleRevalidateData<T>
    if(!cachedData){
      const endDate = getTTL(options?.expirationInMinutes)
      return await revalidate<T>(vbase, bucket, normalizedFilePath, endDate, validateFunction, params)
    }

    const { data, ttl} = cachedData as StaleRevalidateData<T>

    const today = new Date()
    const ttlDate = new Date(ttl)
    if(today < ttlDate){
      return data
    }
    const endDate = getTTL(options?.expirationInMinutes)
    revalidate<T>(vbase, bucket, normalizedFilePath, endDate, validateFunction, params)
    return data
}

const getTTL = (expirationInMinutes?: number) => {
  const ttl = new Date()
  ttl.setMinutes(ttl.getMinutes() + (expirationInMinutes || 30 ))
  return ttl
}

const revalidate = async<T> (
  vbase: VBase, bucket: string, filePath: string,
  endDate: Date, validateFunction: (params?: any) => Promise<T>, params?: any) => {
  const data = await validateFunction(params)
  const revalidatedData = { data, ttl: endDate }
  vbase.saveJSON<StaleRevalidateData<T>>(bucket, filePath, revalidatedData).catch()
  return data
}

const normalizedJSONFile = (filePath: string) => createHash('md5').update(filePath).digest('hex') + '.json'
```


[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-engine-example/node/utils/attachments.ts from the search-engine-example VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { all, propEq } from 'ramda'

export const CHOICE_TYPES = {
  MULTIPLE: 'MULTIPLE',
  SINGLE: 'SINGLE',
  TOGGLE: 'TOGGLE',
}

const isParentOptionSingleChoice = ({ composition }: AssemblyOption) => {
  if (!composition) {
    return false
  }
  const { minQuantity, maxQuantity } = composition
  return minQuantity === 1 && maxQuantity === 1
}

const isParentOptionToggleChoice = ({ composition }: AssemblyOption) => {
  if (!composition) {
    return false
  }
  const { items } = composition
  return all(propEq('maxQuantity', 1))(items)
}

export const getItemChoiceType = (childAssemblyData?: AssemblyOption) => {
  if (!childAssemblyData) {
    return CHOICE_TYPES.MULTIPLE
  }
  const isSingle = isParentOptionSingleChoice(childAssemblyData!)
  if (isSingle) {
    return CHOICE_TYPES.SINGLE
  }
  const isToggle = isParentOptionToggleChoice(childAssemblyData)
  if (isToggle) {
    return CHOICE_TYPES.TOGGLE
  }

  return CHOICE_TYPES.MULTIPLE
}

```


[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-engine-example/node/utils/cookie.ts from the search-engine-example VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { parse } from 'cookie'
import { keys } from 'ramda'

const isUserLoggedIn = (ctx: Context) => {
  const { vtex: { account } } = ctx
  return !!ctx.cookies.get(`VtexIdclientAutCookie_${account}`)
}

const parseCookie = (cookie: string) => {
  const parsed = parse(cookie)
  const cookieName = keys(parsed)[0]
  const cookieValue = parsed[cookieName]

  const extraOptions = {
    path: parsed.path,
    domain: parsed.domain,
    expires: parsed.expires ? new Date(parsed.expires) : undefined,
  }
  return {
    name: cookieName,
    value: cookieValue,
    options: extraOptions,
  }
}

/** Checkout cookie methods */
const CHECKOUT_COOKIE = 'checkout.vtex.com'

const checkoutCookieFormat = (orderFormId: string) => `${CHECKOUT_COOKIE}=__ofid=${orderFormId};`

const getOrderFormIdFromCookie = (cookies: Context['cookies']) => {
  const cookie = cookies.get(CHECKOUT_COOKIE)
  return cookie && cookie.split('=')[1]
}

export { isUserLoggedIn, CHECKOUT_COOKIE, checkoutCookieFormat, getOrderFormIdFromCookie, parseCookie }

```


[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-engine-example/node/utils/CategoryTreeSegmentsFinder.ts from the search-engine-example VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { searchSlugify } from './slug'
import { VBase } from '@vtex/api'
import { Search } from '../clients/search'
import { CATEGORY_TREE_CHILDREN_BUCKET, CATEGORY_TREE_ROOT_BUCKET, CATEGORY_TREE_ROOT_PATH } from '../resolvers/search/constants'
import { staleFromVBaseWhileRevalidate } from './vbase'
interface Clients{
  vbase: VBase
  search: Search
}

interface LazyCategoryTreeNode{
  id: number
  name: string
  hasChildren: boolean
}

export interface CategoryIdNamePair{
  id: string
  name: string
}

export class CategoryTreeSegmentsFinder {

  private clients: Clients
  private segments: string[]
  protected categoryTreeRoot: Record<string, LazyCategoryTreeNode>

  public constructor(clients: Clients, segments: string[]){
    this.clients = clients
    this.segments = segments
    this.categoryTreeRoot = {}
  }

  public find = async () => {
    const { segments } = this
    const result: (CategoryIdNamePair|null)[] = []
    await this.initCategoryTreeRoot()

    const rootCategorySegment = this.findRootCategorySegment()
    if(!rootCategorySegment){
      return []
    }

    const {category, index} = rootCategorySegment

    result[index] = {id: category.id.toString(), name: category.name}
    const segmentsTail = segments.slice(index+1)
    const categorySegmentsFromChildren = await this.findCategoriesFromChildren(category.id, segmentsTail)
    return result.concat(categorySegmentsFromChildren)
  }

  /**
   * Fetches a category children from vbase or search
   * clients: {vbase: VBase, search: Search}
   */
  protected lazyFetchChildren = async (id: number) => {
    const { search } = this.clients
    return await this.staleWhileRevalidate<Record<string, string>>(CATEGORY_TREE_CHILDREN_BUCKET, id.toString(), this.fetchChildrenFromSearch, { search, id })
  }

  protected staleWhileRevalidate = async <T>(bucket: string, path: string,validateFunction: (params?: any) => Promise<T>, params?: any) => {
    return await staleFromVBaseWhileRevalidate<T>(this.clients.vbase, bucket, path, validateFunction, params)
  }

  protected getCategoryTreeRoot = async () => {
    const categoryTree = await this.clients.search.categories(0)
    return categoryTree.reduce((acc, categoryTreeNode) => {
      const categorySlug = searchSlugify(categoryTreeNode.name)
      const lazyCategoryTreeNode = {
        id: categoryTreeNode.id,
        name: categoryTreeNode.name,
        hasChildren: categoryTreeNode.hasChildren,
      }
      acc[categorySlug] = lazyCategoryTreeNode
      return acc
    }, {} as Record<string, LazyCategoryTreeNode>)
  }

  private initCategoryTreeRoot = async () => {
    this.categoryTreeRoot = await this.staleWhileRevalidate<Record<string, LazyCategoryTreeNode>>(
      CATEGORY_TREE_ROOT_BUCKET, CATEGORY_TREE_ROOT_PATH, this.getCategoryTreeRoot)
  }

  // Returns {id: categoryId, name: categorySlug }
  private findCategoriesFromChildren = async (categoryId: number, segments: string[]) => {
    const result: (CategoryIdNamePair|null)[] = []
    for(const segment of segments){
      const children = await this.getChildren(categoryId)
      const childCategoryId = children[segment]
      if(childCategoryId){
        categoryId = Number(childCategoryId)
        result.push({ id: childCategoryId, name: segment })
      }else{
        result.push(null)
      }
    }
    return result
  }

  private findRootCategorySegment = () => {
    const { segments, categoryTreeRoot } = this
    const segmentIndex = segments.findIndex(segment => !!categoryTreeRoot[segment])
    return segmentIndex !== -1? {index: segmentIndex, category: categoryTreeRoot[segments[segmentIndex]]}: null
  }

  private getChildren = async (id: number) => {
    return await this.lazyFetchChildren(id)
  }

  // Returns { categorySlug: categoryId }
  private fetchChildrenFromSearch = async (params: { search: Search, id: number }): Promise<Record<string, string>> => {
    const {search, id} = params
    const categoryChildren = await search.getCategoryChildren(id)
    const categoryChildrenBySlug = Object.keys(categoryChildren).reduce((acc, categoryChildId: string) => {
      const categoryChildName = categoryChildren[categoryChildId]
      const categoryChildSlug = searchSlugify(categoryChildName)
      acc[categoryChildSlug] = categoryChildId
      return acc
    }, {} as Record<string, string>)
    return categoryChildrenBySlug
  }
}
```


[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-engine-example/node/utils/object.ts from the search-engine-example VTEX app.

[Assistant]
Here's the implementation:

```typescript
import {
  adjust,
  curry,
  fromPairs,
  map,
  mergeAll,
  pipe,
  toPairs,
  zipObj,
} from 'ramda'

/**
 * Creates a new object with the own properties of the provided object, but the
 * keys renamed according to logic of renaming function.
 *
 * Keep in mind that in the case of keys conflict is behaviour undefined and
 * the result may vary between various JS engines!
 *
 * @func renameKeysWith
 * @category Object
 * @param {Function} func Function that renames the keys
 * @param {!Object} object Provided object
 * @return {!Object} New object with renamed keys
 * @see {@link https://github.com/ramda/ramda/wiki/Cookbook#rename-keys-of-an-object-by-a-function|Ramda Cookbook}
 * @example
 *
 * renameKeysWith(concat('a'), { A: 1, B: 2, C: 3 }) //=> { aA: 1, aB: 2, aC: 3 }
 */
type Tuple = [string, any]
const renameKeysWith = curry((func: any, object: any) =>
  pipe<any, Tuple[], any, any>(
    toPairs,
    map(adjust(0, func)),
    fromPairs
  )(object)
)

/*
 * Convert a list of fields like [ {key: 'propertyName', value: 'String'}, ... ]
 * to a JSON format.
 */
const parseFieldsToJson = (fields: any) =>
  mergeAll(fields.map((field: any) => zipObj([field.key], [field.value])))

export { renameKeysWith, parseFieldsToJson }

```


[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-engine-example/node/utils/index.ts from the search-engine-example VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { AxiosError } from 'axios'
import { AuthenticationError, ForbiddenError, UserInputError } from '@vtex/api'

export * from './object'
export * from './cookie'

export function generateRandomName() {
  return (1 + Math.random()).toString(36).substring(2)
}

export function getFileExtension(fileName: string) {
  return fileName.match(/\.[0-9a-z]+$/i)
    ? (fileName.match(/\.[0-9a-z]+$/i) as any[])[0]
    : undefined
}

export function statusToError(e: any) {
  if (!e.response) {
    throw e
  }

  const { response } = e as AxiosError
  const { status } = response!

  if (status === 401) {
    throw new AuthenticationError(e)
  }
  if (status === 403) {
    throw new ForbiddenError(e)
  }
  if (status === 400) {
    throw new UserInputError(e)
  }

  throw e
}

```


[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-engine-example/node/utils/categoryTreeSearch.test.ts from the search-engine-example VTEX app.

[Assistant]
Here's the implementation:

```typescript
import * as TypeMoq from 'typemoq'
import { CategoryTreeSegmentsFinder } from "./CategoryTreeSegmentsFinder"
import { VBase, IOContext } from "@vtex/api"
import { Search } from "../clients/search"

/*  CategoryTree
 *         c0
 *        / \
 *      c1   c2
 *     / \
 *   c3   c4
 *         \
 *          c5
 */

const context = TypeMoq.Mock.ofType<IOContext>()
const vbaseMock = TypeMoq.Mock.ofInstance(VBase)
const searchMock = TypeMoq.Mock.ofInstance(Search)

const c5 = {'c5': '5'}
const c4 = {'c4': '4'}
const c3 = {'c3': '3'}
const c2 = {'c2': '2'}
const c1 = { 'c1': '1'}
const c0 = { 'c0': '0' }
const root = {'c0': {id: 0, name: 'c0', hasChildren: true}}

const childrenTree = {
  "0": {...c1, ...c2},
  "1": {...c3, ...c4},
  "2": {},
  "3": {},
  "4": {...c5},
  "5": {}
} as any

const clients = {vbase: new vbaseMock.object(context.object), search: new searchMock.object(context.object) }

const toPair = (categories: any[]) =>
  categories.map(c => {
    const [key, value] = c && Object.entries(c)[0] || []
    return key? { id: value, name: key }: null
  })

describe('Category Tree Search tests', () => {
  const finder = class CategoryTreeSegmentsFinderMock extends CategoryTreeSegmentsFinder {
    public constructor(segments: string[]){
      super(clients, segments)
      this.categoryTreeRoot = root
    }

    protected lazyFetchChildren = async (id: number) => {
      const children = childrenTree[id.toString()]
      return children
    }

    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    protected staleWhileRevalidate = async <T>(_: string, __: string, func: (params?: any) => Promise<T>, params?: any) => {
      return func(params)
    }

    protected getCategoryTreeRoot = () => {
      return this.categoryTreeRoot as any
    }
  }
  test('It should find a complete tree of categories', async () => {
    const segments = 'c0/c1/c4'.split('/')
    const treeFinder = new finder(segments)
    const result = await treeFinder.find()
    expect(result).toStrictEqual(toPair([c0, c1, c4]))
  })

  test('It should find the maximum categories possible ', async () => {
    const segments = 'c0/c1/c2'.split('/')
    const treeFinder = new finder(segments)
    const result = await treeFinder.find()
    expect(result).toStrictEqual(toPair([c0, c1, null]))
  })

  test('It should not find when doesnt exist', async () => {
    const segments = 'x/c1/c4'.split('/')
    const treeFinder = new finder(segments)
    const result = await treeFinder.find()
    expect(result).toStrictEqual([])
  })
})
```


[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-engine-example/node/resolvers/index.ts from the search-engine-example VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { fieldResolvers as benefitsFieldResolvers } from './benefits'
import {
  fieldResolvers as searchFieldResolvers,
  queries as searchQueries,
} from './search'

import { queries as statsQueries } from './stats'

export const resolvers = {
  ...searchFieldResolvers,
  ...benefitsFieldResolvers,
  Query: {
    ...searchQueries,
    ...statsQueries
  },
}

```


[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-engine-example/node/commons/models/VtexSeller.ts from the search-engine-example VTEX app.

[Assistant]
Here's the implementation:

```typescript
interface Installment {
  Value: number
  InterestRate: number
  TotalValuePlusInterestRate: number
  NumberOfInstallments: number
  Name: string
}

interface CommertialOffer {
  AvailableQuantity: number
  discountHighlights: any[]
  teasers: any[]
  Installments: Installment[]
  Price: number
  ListPrice: number
  PriceWithoutDiscount: number
}

class VtexSeller {
  public sellerId: string
  public sellerName: string
  public commertialOffer: CommertialOffer

  public constructor(
    sellerId: string,
    price: number,
    oldPrice: number,
    installment: { value: number; count: number }
  ) {
    this.sellerId = sellerId
    this.sellerName = ''

    this.commertialOffer = {
      AvailableQuantity: 10000,
      discountHighlights: [],
      teasers: [],
      Installments: installment
        ? [
            {
              Value: installment.value,
              InterestRate: 0,
              TotalValuePlusInterestRate: price,
              NumberOfInstallments: installment.count,
              Name: '',
            },
          ]
        : [],
      Price: price,
      ListPrice: oldPrice,
      PriceWithoutDiscount: price,
    }
  }
}

export default VtexSeller

```


[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-engine-example/node/typings/@gocommerce/utils.d.ts from the search-engine-example 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-engine-example/node/resolvers/search/category.ts from the search-engine-example VTEX app.

[Assistant]
Here's the implementation:

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

import { getCategoryInfo } from './utils'

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

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

/** 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: {
    cacheId: prop('id'),

    href: async ({ url }: SafeCategory) => {
      return cleanUrl(url)
    },

    metaTagDescription: prop('MetaTagDescription'),

    titleTag: prop('Title'),

    slug: async ({ url }: SafeCategory) => {
      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-engine-example/node/resolvers/search/searchBreadcrumb.ts from the search-engine-example VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { Functions } from '@gocommerce/utils'
import { equals, includes, toLower } from 'ramda'

import { getSpecificationFilterName } from './modules/metadata'
import { findCategoryInTree, getBrandFromSlug } from './utils'

interface BreadcrumbParams {
  queryUnit: string
  mapUnit: string
  index: number
  queryArray: string[]
  mapArray: string[]
  categories: CategoryTreeResponse[]
  categoriesSearched: string[]
  products: SearchProduct[]
}

const findClusterNameFromId = (
  products: SearchProduct[],
  clusterId: string
) => {
  const productWithCluster = products.find(
    ({ productClusters }) => !!productClusters[clusterId]
  )
  return productWithCluster && productWithCluster.productClusters[clusterId]
}

const findSellerFromSellerId = (
  products: SearchProduct[],
  sellerId: string
) => {
  for (const product of products) {
    for (const item of product.items) {
      const seller = item.sellers.find(sel => sel.sellerId === sellerId)
      if (seller) {
        return seller.sellerName
      }
    }
  }
  return null
}

const sliceAndJoin = (array: string[], max: number, joinChar: string) =>
  array.slice(0, max).join(joinChar)

const isCategoryMap = equals('c')
const isBrandMap = equals('b')
const isProductClusterMap = equals('productClusterIds')
const isSellerMap = equals('sellerIds')
const isSpecificationFilter = includes('specificationFilter')

const getCategoryInfo = (
  { categoriesSearched, queryUnit, categories }: BreadcrumbParams,
  isVtex: boolean,
  ctx: Context
) => {
  const queryPosition = categoriesSearched.findIndex(cat => cat === queryUnit)
  if (!isVtex) {
    return findCategoryInTree(
      categories,
      categoriesSearched.slice(0, queryPosition + 1)
    )
  }
  return ctx.clients.search
    .pageType(categoriesSearched.slice(0, queryPosition + 1).join('/'))
    .catch(() => null)
}

const getBrandInfo = async (
  { queryUnit }: BreadcrumbParams,
  isVtex: boolean,
  { clients: { search } }: Context
) => {
  if (!isVtex) {
    return getBrandFromSlug(toLower(queryUnit), search)
  }
  return search.pageType(queryUnit).catch(() => null)
}

export const resolvers = {
  SearchBreadcrumb: {
    name: async (obj: BreadcrumbParams, _: any, ctx: Context) => {
      const {
        vtex: { account },
      } = ctx
      const { queryUnit, mapUnit, index, queryArray, products } = obj
      const defaultName = queryArray[index]
      const isVtex = !Functions.isGoCommerceAcc(account)
      if (isProductClusterMap(mapUnit)) {
        const clusterName = findClusterNameFromId(products, queryUnit)
        if (clusterName) {
          return clusterName
        }
      }
      if (isCategoryMap(mapUnit)) {
        const categoryData = await getCategoryInfo(obj, isVtex, ctx)
        if (categoryData) {
          return categoryData.name
        }
      }
      if (isSellerMap(mapUnit)) {
        const sellerName = findSellerFromSellerId(products, queryUnit)
        if (sellerName) {
          return sellerName
        }
      }
      if (isBrandMap(mapUnit)) {
        const brandData = await getBrandInfo(obj, isVtex, ctx)
        return brandData ? brandData.name : defaultName
      }
      if (isSpecificationFilter(mapUnit)) {
        return getSpecificationFilterName(queryUnit)
      }
      return defaultName && decodeURI(defaultName)
    },
    href: ({
      index,
      queryArray,
      mapArray,
      mapUnit,
      queryUnit,
    }: BreadcrumbParams) => {
      if (index === 0 && (isCategoryMap(mapUnit) || isBrandMap(mapUnit))) {
        return `/${queryUnit}`
      }
      if (mapArray.every(isCategoryMap)) {
        return `/${sliceAndJoin(queryArray, index + 1, '/')}`
      }
      return `/${sliceAndJoin(queryArray, index + 1, '/')}?map=${sliceAndJoin(
        mapArray,
        index + 1,
        ','
      )}`
    },
  },
}

```


[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-engine-example/node/resolvers/search/assemblyOption.test.ts from the search-engine-example 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-engine-example/node/resolvers/search/brand.ts from the search-engine-example VTEX app.

[Assistant]
Here's the implementation:

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

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

export const resolvers = {
  Brand: {

    titleTag: prop('title'),

    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-engine-example/node/resolvers/search/itemMetadataUnit.ts from the search-engine-example 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-engine-example/node/resolvers/search/autocomplete.ts from the search-engine-example 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-engine-example/node/resolvers/search/productSearch.test.ts from the search-engine-example VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { queries } from './index'
import { mockContext } from '../../__mocks__/helpers'

describe('tests related to the searchMetadata query', () => {
  beforeEach(() => {
    jest.clearAllMocks()
  })
  test('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'
    )
    expect(mockContext.clients.search.pageType).toBeCalledTimes(1)
  })

  test('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')
    expect(mockContext.clients.search.pageType).toBeCalledTimes(1)
  })

  test('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).toBe(null)
    expect(mockContext.clients.search.pageType).toBeCalledTimes(0)
  })

  test('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).toBe(null)
    expect(mockContext.clients.search.pageType).toBeCalledTimes(0)
  })

  test('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'
    )
    expect(mockContext.clients.search.pageType).toBeCalledTimes(2)
  })

  test('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')
    expect(mockContext.clients.search.pageType).toBeCalledTimes(2)
  })

  test('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'
    )
    expect(mockContext.clients.search.pageType).toBeCalledTimes(2)
  })

  test('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'
    )
    expect(mockContext.clients.search.pageType).toBeCalledTimes(2)
  })

  test('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).toBe(null)
    expect(mockContext.clients.search.pageType).toBeCalledTimes(1)
  })

  test('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).toBe(null)
    expect(mockContext.clients.search.pageType).toBeCalledTimes(1)
  })

  test('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).toBe(null)
    expect(mockContext.clients.search.pageType).toBeCalledTimes(0)
  })
})

```


[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-engine-example/node/resolvers/search/index.test.ts from the search-engine-example 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-engine-example/node/resolvers/search/product.ts from the search-engine-example VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { formatTranslatableStringV2 } from '@vtex/api'
import { compose, last, map, omit, pathOr, propOr, split } from 'ramda'

import { getBenefits } from '../benefits'
import { buildCategoryMap } from './utils'

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 Record<string, string>[]
  )
}

type SearchProductWithCache = SearchProduct & { cacheId?: string }
enum ItemsFilterEnum {
  ALL = 'ALL',
  FIRST_AVAILABLE = 'FIRST_AVAILABLE',
  ALL_AVAILABLE = 'ALL_AVAILABLE',
}
interface ItemArg {
  filter?: ItemsFilterEnum
}

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 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 } }: Context
) => {
  if (!categories || !categoriesIds) {
    return []
  }

  const mainTreeIds = findMainTree(categoriesIds, prodCategoryId)

  if (platform === 'vtex') {
    return mainTreeIds.map(categoryId => search.category(Number(categoryId)))
  }
  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
}

export const resolvers = {
  Product: {
    benefits: ({ productId }: SearchProduct, _: any, ctx: Context) =>
      getBenefits(productId, ctx),

    categoryTree: productCategoriesToCategoryTree,

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

    clusterHighlights: ({ clusterHighlights = {} }: SearchProduct) =>
      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: ({ productClusters = {} }: SearchProduct) =>
      objToNameValue('id', 'name', productClusters),

    properties: (product: SearchProduct) =>
      map(
        (name: string) => ({ name, values: (product as any)[name] }),
        product.allSpecifications || []
      ),

    propertyGroups: (product: SearchProduct) => {
      const { allSpecifications = [] } = product
      const notPG = knownNotPG.concat(allSpecifications)
      return objToNameValue('name', 'values', omit(notPG, product))
    },

    recommendations: (product: SearchProduct) => product,

    description: ({ productId, description }: SearchProduct) => formatTranslatableStringV2({
      content: description,
      context: productId,
    }),

    metaTagDescription: ({ productId, metaTagDescription }: SearchProduct) => formatTranslatableStringV2({
      content: metaTagDescription,
      context: productId,
    }),

    titleTag: ({ productId, productTitle }: SearchProduct) => formatTranslatableStringV2({
      content: productTitle,
      context: productId,
    }),

    productName: ({ productId, productName }: SearchProduct) => formatTranslatableStringV2({
      content: productName,
      context: productId,
    }),

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

      if (!binding || !tenant || binding.locale === tenant.locale) {
        return linkText
      }

      const messages = [{
        context: productId,
        content: linkText
      }]

      const translations = await messagesGraphQL.translate({
        to: binding.locale,
        indexedByFrom: [{
          from: tenant.locale,
          messages
        }]
      })

      return translations[0]
    },

    specificationGroups: (product: SearchProduct) => {
      const allSpecificationsGroups = propOr<[], SearchProduct, string[]>(
        [],
        'allSpecificationsGroups',
        product
      ).concat(['allSpecifications'])
      const specificationGroups = allSpecificationsGroups.map(
        (groupName: string) => ({
          name: groupName,
          specifications: ((product as any)[groupName] || []).map(
            (name: string) => ({
              name,
              values: (product as any)[name] || [],
            })
          ),
        })
      )
      return specificationGroups || []
    },
    items: ({ items: searchItems }: SearchProduct, { filter }: ItemArg) => {
      if (filter === ItemsFilterEnum.ALL) {
        return searchItems
      }
      if (filter === ItemsFilterEnum.FIRST_AVAILABLE) {
        const firstAvailable = searchItems.find(isAvailable)
        return firstAvailable ? [firstAvailable] : [searchItems[0]]
      }
      if (filter === ItemsFilterEnum.ALL_AVAILABLE) {
        const onlyAvailable = searchItems.filter(isAvailable)
        return onlyAvailable.length > 0 ? onlyAvailable : [searchItems[0]]
      }
      return searchItems
    },
    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 }
    },
  },
  OnlyProduct: {
    categoryTree: productCategoriesToCategoryTree,
  },
}

```


[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-engine-example/node/resolvers/search/discount.ts from the search-engine-example VTEX app.

[Assistant]
Here's the implementation:

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

export const resolvers = {
  Discount: {
    name: prop('<Name>k__BackingField'),
  },
  Teaser: {
    name: prop('<Name>k__BackingField'),
    conditions: prop('<Conditions>k__BackingField'),
    effects: prop('<Effects>k__BackingField'),
  },
  TeaserCondition: {
    minimumQuantity: prop('<MinimumQuantity>k__BackingField'),
    parameters: prop('<Parameters>k__BackingField'),
  },
  TeaserEffects: {
    parameters: prop('<Parameters>k__BackingField'),
  },
  TeaserValue: {
    name: prop('<Name>k__BackingField'),
    value: prop('<Value>k__BackingField'),
  }
}

```