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

[Assistant]
Here's the implementation:

```typescript
/* eslint-disable @typescript-eslint/no-unused-vars */

import * as TypeMoq from 'typemoq'
import { VBase, IOContext } from "@vtex/api"
import { Search } from '../../clients/search'
import { mountCompatibilityQuery } from './newURLs'
import { getCompatibilityArgs } from '.'
import { Clients } from '../../clients'

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

describe('Search new URLs dicovery', () => {
  let context: any

  class VBaseMock extends vbaseTypeMock.object {
    private jsonData: any

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

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

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

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

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

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

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

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

  beforeEach(() => {
    // tslint:disable-next-line: max-classes-per-file
    const ClientsImpl = class ClientsMock extends Clients {
      public get vbase() {
        return this.getOrSet('vbase', VBaseMock)
      }
    }

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

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

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

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

    const vbaseMock = new VBaseMock()
    const searchMock = new search(categoryTree, {}, facets)
    const result = await mountCompatibilityQuery({vbase: vbaseMock, search: searchMock, args})
    expect(result).toStrictEqual({query: 'category', map: 'c'})
  })

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

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

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

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

    const vbaseMock = new VBaseMock()
    const searchMock = new search(categoryTree, categoryChildren, facets)
    const result = await mountCompatibilityQuery({vbase: vbaseMock, search: searchMock, args})
    expect(result).toStrictEqual({query: 'department/category/subcategory', map: 'c,c,c'})
  })

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

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

    const categoryChildren = { 1: {} }

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

    const vbaseMock = new VBaseMock()
    const searchMock = new search(categoryTree, categoryChildren, facets)
    const result = await mountCompatibilityQuery({vbase: vbaseMock, search: searchMock, args})
    expect(result).toStrictEqual({query: 'category/brand', map: 'c,b'})
  })

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

    const categoryTree: CategoryTreeResponse[] = []

    const categoryChildren = {}

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

    const vbaseMock = new VBaseMock()
    const searchMock = new search(categoryTree, categoryChildren, facets)
    const result = await mountCompatibilityQuery({vbase: vbaseMock, search: searchMock, args})
    expect(result).toStrictEqual({query: 'collection', map: 'productClusterIds'})
  })

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

    const categoryTree: CategoryTreeResponse[] = []

    const categoryChildren = {}

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

    const vbaseMock = new VBaseMock()
    const searchMock = new search(categoryTree, categoryChildren, facets)
    const result = await mountCompatibilityQuery({vbase: vbaseMock, search: searchMock, args})
    expect(result).toStrictEqual({query: 'filterxpto', map: 'specificationFilter_0'})
  })

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

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

    const categoryChildren = { 1: {} }

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

    const vbaseMock = new VBaseMock()
    const searchMock = new search(categoryTree, categoryChildren, facets)
    const result = await mountCompatibilityQuery({vbase: vbaseMock, search: searchMock, args})
    expect(result).toStrictEqual({query: 'department/1/2/3', map: 'c,specificationFilter_1,specificationFilter_2,specificationFilter_3'})
  })

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

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

    const categoryChildren = { 1: {} }

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

    const vbaseMock = new VBaseMock()
    const searchMock = new search(categoryTree, categoryChildren, facets)
    const result = await mountCompatibilityQuery({vbase: vbaseMock, search: searchMock, args})
    expect(result).toStrictEqual({query: 'department/1/brand', map: 'c,specificationFilter_1,b'})
  })

  it('Should not transform urls in the format query?map=c', async () => {

    const argsList = [
      {
        map: 'c',
        query: 'category'
      },
      {
        map: 'specificationFilter_1',
        query: 'filtertest'
      },
      {
        map: 'c,specificationFilter_1',
        query: 'category/filtertest'
      },
      {
        map: 'specificationFilter_1,c,b',
        query: 'filtertest/category/brand'
      }
    ]

    for(const args of argsList){
      const result = await getCompatibilityArgs(context, args)
      expect(result).toStrictEqual(args)
    }
  })
})
```


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

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

[Assistant]
Here's the implementation:

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

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

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

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

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

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

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

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

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

type CategoryMap = Record<string, CategoryTreeResponse>

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

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

  return category
}

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

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

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

  return mapCategories
}

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

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

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

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

  return [map, priceRange]
}

```


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

[Assistant]
Here's the implementation:

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

export const resolvers = {
  Recommendation: {
    buy: (
      { productId }: SearchProduct,
      _: any,
      { clients: { search } }: Context
    ) =>
      search.crossSelling(
        productId,
        SearchCrossSellingTypes.whoboughtalsobought
      ),

    similars: (
      { productId }: SearchProduct,
      _: any,
      { clients: { search } }: Context
    ) => search.crossSelling(productId, SearchCrossSellingTypes.similars),

    view: (
      { productId }: SearchProduct,
      _: any,
      { clients: { search } }: Context
    ) =>
      search.crossSelling(productId, SearchCrossSellingTypes.whosawalsosaw),
  },
}

```


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

[Assistant]
Here's the implementation:

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

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

interface Parent {
  items: SearchMetadataItem[]
}

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

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

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

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

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

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

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

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

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

        return acc
      }, {})

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

```


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

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

[Assistant]
Here's the implementation:

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


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

[Assistant]
Here's the implementation:

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

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

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

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

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

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

  const compatMapSegments = []
  const compatQuerySegments = []

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

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

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

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

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

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

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

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

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

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

```


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

[Assistant]
Here's the implementation:

```typescript
import { isEmpty, path } from 'ramda'
import { getItemChoiceType, CHOICE_TYPES } from '../../utils/attachments'

interface Params {
  compositionItem: CompositionItem
  simulationPayload: {
    marketingData: Record<string, string>
    countryCode: string
  }
  items: SearchMetadataItem[]
  parent: SearchMetadataItem
  assemblyOption: AssemblyOption
}

// Generate a valid simulation list with this child, considering its a single item type
const getSimulationPayloadItemsForSingleFromTree = (
  parent: SearchMetadataItem,
  childId: string,
  seller: string,
  parentAssemblyBinding: string
) => {
  return [
    { id: parent.id, quantity: 1, seller: parent.seller },
    {
      id: childId,
      seller,
      quantity: 1,
      parentItemIndex: 0,
      parentAssemblyBinding,
    },
  ]
}

// Generate a valid simulation list with this child, considering its a TOGGLE item type
const getSimulationPayloadItemsForToggleFromTree = (
  parent: SearchMetadataItem,
  childId: string,
  seller: string,
  assemblyOption: AssemblyOption,
  parentBasicTree: PayloadItem[]
) => {
  const parentPayloadItem = {
    id: parent.id,
    quantity: 1,
    seller: parent.seller,
  }
  const basicChildItem = {
    id: childId,
    seller,
    quantity: 1,
    parentItemIndex: 0,
    parentAssemblyBinding: assemblyOption.id,
  }
  const siblings = parentBasicTree.filter(
    ({ parentAssemblyBinding }) => parentAssemblyBinding === assemblyOption.id
  )
  const siblingCount = siblings.length
  // See if we can add this item, considering the group max quantity
  if (siblingCount < assemblyOption.composition!.maxQuantity) {
    return [parentPayloadItem, basicChildItem]
  }

  // If there are siblings on the basic tree, remove it and add this child
  const brotherToBeRemoved = siblings.find(item => {
    const brotherComposition = assemblyOption.composition!.items.find(
      comp => comp.id === item.id
    )
    return brotherComposition!.minQuantity === 0
  })
  if (!brotherToBeRemoved) {
    return []
  }
  const withoutBrother = parentBasicTree.filter(
    ({ id, parentAssemblyBinding }) =>
      id !== brotherToBeRemoved.id &&
      parentAssemblyBinding !== brotherToBeRemoved.parentAssemblyBinding
  )
  return [parentPayloadItem, ...withoutBrother, basicChildItem]
}

// Generate a valid simulation list with this child, considering its a MULTIPLE item type
const getSimulationPayloadItemsForMultipleFromTree = (
  parent: SearchMetadataItem,
  assemblyOption: AssemblyOption,
  parentBasicTree: PayloadItem[],
  childCompositionItem: CompositionItem
) => {
  const parentPayloadItem = {
    id: parent.id,
    quantity: 1,
    seller: parent.seller,
  }

  const basicItemQuantity = childCompositionItem.minQuantity || 1
  const familyMinimum = assemblyOption.composition!.minQuantity
  const basicChildItem = {
    id: childCompositionItem.id,
    seller: childCompositionItem.seller,
    quantity: basicItemQuantity,
    parentItemIndex: 0,
    parentAssemblyBinding: assemblyOption.id,
  }

  // Corner case that can be easily solved: when a single child can fill alone the whole group
  if (childCompositionItem.maxQuantity >= familyMinimum) {
    return [
      parentPayloadItem,
      { ...basicChildItem, quantity: familyMinimum || basicItemQuantity },
    ]
  }

  const siblings = parentBasicTree.filter(
    ({ parentAssemblyBinding, parentItemIndex }) =>
      parentAssemblyBinding === assemblyOption.id && parentItemIndex === 0
  )

  const siblingCount = siblings.reduce((sum, sib) => sum + sib.quantity, 0)
  const siblingsAllowed = assemblyOption.composition!.maxQuantity - siblingCount
  if (basicItemQuantity <= siblingsAllowed) {
    return [parentPayloadItem, ...siblings, basicChildItem]
  }

  let currentSiblingCount = siblingCount
  // We will iterate through the childs siblings and remove the minimum we should
  for (const brother of siblings) {
    const brotherComposition = assemblyOption.composition!.items.find(
      comp => comp.id === brother.id
    )
    const oldBrotherQuantity = brother.quantity
    brother.quantity = brotherComposition!.minQuantity
    currentSiblingCount -= oldBrotherQuantity - brotherComposition!.minQuantity
    const currentSiblingsAllowed =
      assemblyOption.composition!.maxQuantity - currentSiblingCount
    if (currentSiblingsAllowed >= basicItemQuantity) {
      // We can now break because we can add out chuld item safely
      break
    }
  }

  const nonEmptySiblings = siblings.filter(({ quantity }) => quantity > 0)
  return [parentPayloadItem, ...nonEmptySiblings, basicChildItem]
}

const simulateAndGetPrice = async (
  payload: SimulationPayload,
  checkout: Context['clients']['checkout'],
  itemId: string,
  assemblyId: string
) => {
  const simulation = await checkout.simulation(payload)
  const childInTree = simulation.items.find(
    item => itemId === item.id && assemblyId === item.parentAssemblyBinding
  )
  return childInTree ? childInTree.sellingPrice : 0
}

const getSimulationPayloadItems = (
  assemblyOption: AssemblyOption,
  compositionItem: CompositionItem,
  parent: SearchMetadataItem,
  fatherBasicTree: PayloadItem[]
) => {
  const assemblyType = getItemChoiceType(assemblyOption)

  if (assemblyType === CHOICE_TYPES.SINGLE) {
    return getSimulationPayloadItemsForSingleFromTree(
      parent,
      compositionItem.id,
      compositionItem.seller,
      assemblyOption.id
    )
  }
  if (assemblyType === CHOICE_TYPES.TOGGLE) {
    return getSimulationPayloadItemsForToggleFromTree(
      parent,
      compositionItem.id,
      compositionItem.seller,
      assemblyOption,
      fatherBasicTree
    )
  }
  return getSimulationPayloadItemsForMultipleFromTree(
    parent,
    assemblyOption,
    fatherBasicTree,
    compositionItem
  )
}

export const resolvers = {
  PriceTableItem: {
    assemblyId: path(['assemblyOption', 'id']),
    id: path(['compositionItem', 'id']),
    price: async (
      { parent, simulationPayload, assemblyOption, compositionItem }: Params,
      _: any,
      { clients: { checkout } }: Context
    ) => {
      const { id, priceTable } = compositionItem
      const { marketingData, countryCode } = simulationPayload
      const payload = {
        country: countryCode,
        isCheckedIn: false,
        priceTables: [priceTable],
        ...(isEmpty(marketingData) ? {} : { marketingData }),
      }

      const parentBasicTree = [
        { id: parent.id, seller: parent.seller, quantity: 1 },
      ] as PayloadItem[]

      for (const fatherAssemblyOption of parent.assemblyOptions) {
        const assemblyId = fatherAssemblyOption.id
        if (!fatherAssemblyOption.composition) {
          continue
        }
        const groupMinimum = fatherAssemblyOption.composition!.minQuantity
        const itemsInitialsSum = fatherAssemblyOption.composition!.items.reduce(
          (sum, item) => sum + item.initialQuantity,
          0
        )
        // The basic tree will not contain children of this group
        if (groupMinimum !== itemsInitialsSum) {
          continue
        }
        for (const compItem of fatherAssemblyOption.composition!.items) {
          if (compItem.initialQuantity > 0) {
            parentBasicTree.push({
              id: compItem.id,
              quantity: compItem.initialQuantity,
              seller: compItem.seller,
              parentItemIndex: 0,
              parentAssemblyBinding: assemblyId,
            })
          }
        }
      }

      const itemInTree = parentBasicTree.find(
        item =>
          id === item.id && assemblyOption.id === item.parentAssemblyBinding
      )
      // If item is already in tree, just simulate and return the price
      if (itemInTree) {
        return simulateAndGetPrice(
          { ...payload, items: parentBasicTree },
          checkout,
          id,
          assemblyOption.id
        )
      }

      // If not in tree, include it, then simulate and get price
      const newSimulationPayloadItems = getSimulationPayloadItems(
        assemblyOption,
        compositionItem,
        parent,
        parentBasicTree
      )
      return simulateAndGetPrice(
        { ...payload, items: newSimulationPayloadItems },
        checkout,
        id,
        assemblyOption.id
      )
    },
  },
}

```


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

[Assistant]
Here's the implementation:

```typescript
import { NotFoundError, UserInputError, createMessagesLoader } from '@vtex/api'
import { head, isEmpty, isNil, test, pathOr } from 'ramda'

import { resolvers as assemblyOptionResolvers } from './assemblyOption'
import { resolvers as autocompleteResolvers } from './autocomplete'
import { resolvers as brandResolvers } from './brand'
import { resolvers as categoryResolvers } from './category'
import { resolvers as discountResolvers } from './discount'
import { resolvers as itemMetadataResolvers } from './itemMetadata'
import { resolvers as itemMetadataPriceTableItemResolvers } from './itemMetadataPriceTableItem'
import { resolvers as itemMetadataUnitResolvers } from './itemMetadataUnit'
import { resolvers as offerResolvers } from './offer'
import { resolvers as productResolvers } from './product'
import { resolvers as recommendationResolvers } from './recommendation'
import { resolvers as skuResolvers } from './sku'
import { resolvers as productPriceRangeResolvers } from './productPriceRange'
import { getSearchMetaData } from './modules/metadata'
import {
  SearchCrossSellingTypes,
  getMapAndPriceRangeFromSelectedFacets,
} from './utils'
import { toCompatibilityArgs } from './newURLs'
import { PATH_SEPARATOR, MAP_VALUES_SEP } from './constants'
import {
  biggyAttributesToVtexFilters,
  buildAttributePath,
  convertOrderBy,
  buildBreadcrumb,
} from '../../commons/compatibility-layer'
import { productsCatalog, productsBiggy } from '../../commons/products'

interface ProductIndentifier {
  field: 'id' | 'slug' | 'ean' | 'reference' | 'sku'
  value: string
}

interface ProductArgs {
  slug?: string
  identifier?: ProductIndentifier
}

enum CrossSellingInput {
  view = 'view',
  buy = 'buy',
  similars = 'similars',
  viewAndBought = 'viewAndBought',
  suggestions = 'suggestions',
  accessories = 'accessories',
}

interface ProductRecommendationArg {
  identifier?: ProductIndentifier
  type?: CrossSellingInput
}

interface ProductsByIdentifierArgs {
  field: 'id' | 'ean' | 'reference' | 'sku'
  values: string[]
}

const inputToSearchCrossSelling = {
  [CrossSellingInput.buy]: SearchCrossSellingTypes.whoboughtalsobought,
  [CrossSellingInput.view]: SearchCrossSellingTypes.whosawalsosaw,
  [CrossSellingInput.similars]: SearchCrossSellingTypes.similars,
  [CrossSellingInput.viewAndBought]: SearchCrossSellingTypes.whosawalsobought,
  [CrossSellingInput.accessories]: SearchCrossSellingTypes.accessories,
  [CrossSellingInput.suggestions]: SearchCrossSellingTypes.suggestions,
}

/**
 * There is an URL pattern in VTEX where the number of mapSegments doesn't match the number of querySegments. This function deals with these cases.
 * Since this should not be a search concern, this function will be removed soon.
 *
 * @param {Context} ctx
 * @param {QueryArgs} args
 * @returns
 */
const getCompatibilityArgsFromSelectedFacets = async (
  ctx: Context,
  args: QueryArgs
) => {
  const { selectedFacets, query } = args

  if (!selectedFacets || selectedFacets.length === 0 || !query) {
    return args
  }

  const [map, priceRange] = getMapAndPriceRangeFromSelectedFacets([
    ...selectedFacets,
  ])

  args.map = map

  if (isLegacySearchFormat({ query: args.query, map: args.map })) {
    return args
  }

  const compatibilityArgs = (await getCompatibilityArgs(ctx, args)) as QueryArgs

  const mapSegments = compatibilityArgs.map!.split(MAP_VALUES_SEP)
  const querySegments = compatibilityArgs.query!.split(PATH_SEPARATOR)

  args.selectedFacets = mapSegments.map((map, index) => {
    return {
      key: map,
      value: querySegments[index],
    }
  })

  if (priceRange) {
    args.selectedFacets.push({
      key: 'priceRange',
      value: priceRange,
    })
  }

  return args
}

const translateToStoreDefaultLanguage = async (
  ctx: Context,
  term: string
): Promise<string> => {
  const {
    clients,
    state,
    vtex: { locale: from, tenant },
  } = ctx
  const { locale: to } = tenant!

  if (!from || from === to) {
    return term
  }

  if (!state.messages) {
    state.messages = createMessagesLoader(clients, to)
  }

  return state.messages!.load({
    from,
    content: term,
  })
}

const noop = () => {}

// Does prefetching and warms up cache for up to the 10 first elements of a search, so if user clicks on product page
const searchFirstElements = (
  products: SearchProduct[],
  from: number | null = 0,
  search: Context['clients']['search']
) => {
  if (from !== 0 || from == null) {
    // We do not want this for pages other than the first
    return
  }
  products
    .slice(0, Math.min(10, products.length))
    .forEach(product =>
      search.productById(product.productId, false).catch(noop)
    )
}

export const fieldResolvers = {
  ...autocompleteResolvers,
  ...brandResolvers,
  ...categoryResolvers,
  ...itemMetadataResolvers,
  ...itemMetadataUnitResolvers,
  ...itemMetadataPriceTableItemResolvers,
  ...offerResolvers,
  ...discountResolvers,
  ...productResolvers,
  ...recommendationResolvers,
  ...skuResolvers,
  ...assemblyOptionResolvers,
  ...productPriceRangeResolvers,
}

export const getCompatibilityArgs = async <T extends QueryArgs>(
  ctx: Context,
  args: T
) => {
  const {
    clients: { vbase, search },
  } = ctx
  const compatArgs = isLegacySearchFormat(args)
    ? args
    : await toCompatibilityArgs(vbase, search, args)
  return compatArgs ? { ...args, ...compatArgs } : args
}

// Legacy search format is our search with path?map=c,c,specificationFilter
// Where it has specificationFilters and all segments in path are mapped in `map` querystring
const isLegacySearchFormat = ({
  query,
  map,
}: {
  query: string
  map?: string
}) => {
  if (!map) {
    return false
  }
  return map.split(MAP_VALUES_SEP).length === query.split(PATH_SEPARATOR).length
}

const isValidProductIdentifier = (identifier: ProductIndentifier | undefined) =>
  !!identifier && !isNil(identifier.value) && !isEmpty(identifier.value)

export const queries = {
  autocomplete: async (
    _: any,
    args: { maxRows: number; searchTerm?: string },
    ctx: Context
  ) => {
    const {
      clients: { search },
    } = ctx

    if (!args.searchTerm) {
      throw new UserInputError('No search term provided')
    }
    const translatedTerm = await translateToStoreDefaultLanguage(
      ctx,
      args.searchTerm
    )
    const { itemsReturned } = await search.autocomplete({
      maxRows: args.maxRows,
      searchTerm: translatedTerm,
    })
    return {
      cacheId: args.searchTerm,
      itemsReturned,
    }
  },
  facets: async (_: any, args: FacetsInput, ctx: any) => {
    args = (await getCompatibilityArgsFromSelectedFacets(
      ctx,
      args
    )) as FacetsInput

    let { fullText } = args

    if (fullText) {
      fullText = await translateToStoreDefaultLanguage(ctx, args.fullText!)
    }

    const { customSearchEngine } = ctx.clients
    const { segment } = ctx.vtex

    const biggyArgs = {
      attributePath: buildAttributePath(args.selectedFacets),
      query: args.fullText,
      tradePolicy: segment && segment.channel,
    }

    const result = await customSearchEngine.facets(biggyArgs)

    return {
      facets: result.attributes
        ? biggyAttributesToVtexFilters(result.attributes)
        : [],
      queryArgs: {
        query: args.query,
        selectedFacets: args.selectedFacets,
      },
    }
  },

  product: async (_: any, rawArgs: ProductArgs, ctx: Context) => {
    const {
      clients: { search },
    } = ctx

    const args =
      rawArgs && isValidProductIdentifier(rawArgs.identifier)
        ? rawArgs
        : { identifier: { field: 'slug', value: rawArgs.slug! } }

    if (!args.identifier) {
      throw new UserInputError('No product identifier provided')
    }

    const { field, value } = args.identifier
    let products = [] as SearchProduct[]

    switch (field) {
      case 'id':
        products = await search.productById(value)
        break
      case 'slug':
        products = await search.product(value)
        break
      case 'ean':
        products = await search.productByEan(value)
        break
      case 'reference':
        products = await search.productByReference(value)
        break
      case 'sku':
        products = await search.productBySku([value])
        break
    }

    if (products.length > 0) {
      return head(products)
    }

    throw new NotFoundError(
      `No product was found with requested ${field} ${JSON.stringify(args)}`
    )
  },

  products: async (_: any, args: SearchArgs, ctx: Context) => {
    const {
      clients: { search },
    } = ctx
    const queryTerm = args.query
    if (queryTerm == null || test(/[?&[\]=]/, queryTerm)) {
      throw new UserInputError(
        `The query term contains invalid characters. query=${queryTerm}`
      )
    }

    if (args.to && args.to > 2500) {
      throw new UserInputError(
        `The maximum value allowed for the 'to' argument is 2500`
      )
    }
    const products = await search.products(args)
    searchFirstElements(products, args.from, ctx.clients.search)
    return products
  },

  productsByIdentifier: async (
    _: any,
    args: ProductsByIdentifierArgs,
    ctx: Context
  ) => {
    const {
      clients: { search },
    } = ctx

    let products = [] as SearchProduct[]
    const { field, values } = args

    switch (field) {
      case 'id':
        products = await search.productsById(values)
        break
      case 'ean':
        products = await search.productsByEan(values)
        break
      case 'reference':
        products = await search.productsByReference(values)
        break
      case 'sku':
        products = await search.productBySku(values)
        break
    }

    if (products.length > 0) {
      return products
    }

    throw new NotFoundError(`No products were found with requested ${field}`)
  },

  productSearch: async (_: any, args: ProductSearchInput, ctx: any) => {
    args = (await getCompatibilityArgsFromSelectedFacets(
      ctx,
      args
    )) as ProductSearchInput

    const { customSearchEngine } = ctx.clients
    const { segment } = ctx.vtex
    const { from, to, selectedFacets, fullText, fuzzy, operator } = args

    const count = to - from + 1
    const page = Math.round((to + 1) / count)

    const biggyArgs = {
      page,
      count,
      fuzzy,
      operator,
      attributePath: buildAttributePath(selectedFacets),
      query: fullText,
      tradePolicy: segment && segment.channel,
      sort: convertOrderBy(args.orderBy),
    }

    const result = await customSearchEngine.productSearch(biggyArgs)

    const productResolver = args.productOriginVtex
      ? productsCatalog
      : productsBiggy
    const convertedProducts = productResolver(result, ctx)

    return {
      products: convertedProducts,
      recordsFiltered: result.total,
      breadcrumb: buildBreadcrumb(selectedFacets),
      suggestion: result.suggestion,
      correction: result.correction,
      fuzzy: result.fuzzy,
      operator: result.operator,
      banners: result.banners,
    }
  },

  productRecommendations: async (
    _: any,
    { identifier, type }: ProductRecommendationArg,
    ctx: Context
  ) => {
    if (identifier == null || type == null) {
      throw new UserInputError('Wrong input provided')
    }
    const searchType = inputToSearchCrossSelling[type]
    let productId = identifier.value
    if (identifier.field !== 'id') {
      const product = await queries.product(_, { identifier }, ctx)
      productId = product!.productId
    }

    const products = await ctx.clients.search.crossSelling(
      productId,
      searchType
    )

    searchFirstElements(products, 0, ctx.clients.search)
    // We add a custom cacheId because these products are not exactly like the other products from search apis.
    // Each product is basically a SKU and you may have two products in response with same ID but each one representing a SKU.
    return products.map(product => {
      const skuId = pathOr('', ['items', '0', 'itemId'], product)
      return {
        ...product,
        cacheId: `${product.linkText}-${skuId}`,
      }
    })
  },

  searchMetadata: async (_: any, args: SearchMetadataArgs, ctx: Context) => {
    const queryTerm = args.query
    if (queryTerm == null || test(/[?&[\]=]/, queryTerm)) {
      throw new UserInputError(
        `The query term contains invalid characters. query=${queryTerm}`
      )
    }

    if (args.selectedFacets) {
      const [map] = getMapAndPriceRangeFromSelectedFacets(args.selectedFacets)
      args.map = map
    }

    const query = await translateToStoreDefaultLanguage(ctx, args.query || '')
    const translatedArgs = {
      ...args,
      query,
    }
    const compatibilityArgs = await getCompatibilityArgs<SearchArgs>(
      ctx,
      translatedArgs as SearchArgs
    )
    return getSearchMetaData(_, compatibilityArgs, ctx)
  },
  topSearches: () => null,
  searchSuggestions: () => null,
  productSuggestions:() => 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/assemblyOption.ts from the search-engine-example VTEX app.

[Assistant]
Here's the implementation:

```typescript
export const resolvers = {
  AssemblyOption: {
    inputValues: (
      { inputValues }: AssemblyOption,
    ): InputValue[] => {
      return Object.keys(inputValues).reduce<InputValue[]>((acc, label) => {
        const inputValue = inputValues[label]
        const type = defineInputValueType(inputValue)

        const maxLength = type === InputValueType.TEXT
          ? inputValue.maximumNumberOfCharacters : undefined

        const domain = type === InputValueType.OPTIONS
          ? inputValue.domain
          : undefined

        const defaultValue = type === InputValueType.OPTIONS
          ? inputValue.domain[0]
          : type === InputValueType.BOOLEAN
            ? stringToBoolean(inputValue.domain[0])
            : ''

        acc.push({
          label,
          type,
          maxLength,
          defaultValue,
          domain,
        })

        return acc
      }, [])
    }
  },
}

function defineInputValueType(inputValue: RawInputValue): InputValueType {
  const domain = inputValue.domain

  // If domain is something like ['true', 'false'] or ['false', 'true']
  if (
    domain && domain.length === 2 &&
    domain.indexOf('true') !== -1 && domain.indexOf('false') !== -1
  ) {
    return InputValueType.BOOLEAN
  }

  // If domain is something like ['Sans serif', 'Roman']
  if (domain && domain.length > 0) {
    return InputValueType.OPTIONS
  }

  return InputValueType.TEXT
}

function stringToBoolean(value: string) {
  return value === 'true'
    ? true
    : false
}

interface InputValue {
  label: string
  maxLength?: number
  type: InputValueType
  defaultValue: string|boolean
  domain?: string[]
}

enum InputValueType {
  TEXT = 'TEXT',
  BOOLEAN = 'BOOLEAN',
  OPTIONS = 'OPTIONS',
}
```


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

[Assistant]
Here's the implementation:

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

const InstallmentsCriteria = {
  ALL: 'ALL',
  MAX: 'MAX',
  MIN: 'MIN',
  MAX_WITHOUT_INTEREST: 'MAX_WITHOUT_INTEREST',
  MAX_WITH_INTEREST: 'MAX_WITH_INTEREST',
}

const gte = (a: number, b: number) => a > b
const lte = (a: number, b: number) => a < b

export const resolvers = {
  Offer: {
    Installments: (
      { Installments }: CommertialOffer,
      { criteria, rates }: { criteria?: string; rates?: boolean }
    ) => {
      if (criteria === InstallmentsCriteria.ALL || Installments.length === 0) {
        return Installments
      }

      /** TODO: Transforms arguments for backwards-compatibility. Should be cleaned up in the future */
      if (criteria === InstallmentsCriteria.MAX_WITH_INTEREST || criteria === InstallmentsCriteria.MAX_WITHOUT_INTEREST){
        rates = criteria === InstallmentsCriteria.MAX_WITHOUT_INTEREST
        criteria = InstallmentsCriteria.MAX
      }

      const filteredInstallments = !rates
        ? Installments
        : Installments.filter(({ InterestRate }) => !InterestRate)

      const compareFunc = criteria === InstallmentsCriteria.MAX ? gte : lte
      const value = filteredInstallments.reduce(
        (acc, currentValue) =>
          compareFunc(
            currentValue.NumberOfInstallments,
            acc.NumberOfInstallments
          )
            ? currentValue
            : acc,
        filteredInstallments[0]
      )
      return [value]
    },
    teasers: propOr([], 'Teasers'),
    giftSkuIds: propOr([], 'GiftSkuIds'),
    gifts: async ({ GiftSkuIds }: CommertialOffer, _: any, ctx: Context) => {
      if (GiftSkuIds.length === 0) {
        return []
      }

      const catalogGiftProducts = await ctx.clients.search.productBySku(GiftSkuIds).catch(() => null)

      if (!catalogGiftProducts || catalogGiftProducts.length === 0) {
        return []
      }

      const giftProducts = GiftSkuIds.map(skuId => {
        let giftSku = null
        let giftProduct = null

        for (const currGiftProduct of catalogGiftProducts) {
          const currGiftSku = currGiftProduct.items.find(item => item.itemId === skuId)
          if (currGiftSku) {
            giftSku = currGiftSku
            giftProduct = currGiftProduct
            break
          }
        }
        if (!giftProduct || !giftSku) {
          return null
        }
        const {
          productName,
          brand,
          linkText,
          description,
        } = giftProduct
        return {
          productName,
          brand,
          linkText,
          description,
          skuName: giftSku?.nameComplete ?? '',
          images: giftSku?.images.map(({ imageLabel, imageUrl, imageText }) => ({
            imageUrl,
            imageLabel,
            imageText,
          })) ?? [],
        }
      })
      const filteredGiftProducts = giftProducts.filter(Boolean)

      return filteredGiftProducts
    },
    discountHighlights: propOr([], 'DiscountHighLight'),
    spotPrice: (offer: CommertialOffer) => {
      const sellingPrice = offer.Price
      const spotPrice: number | undefined = offer.Installments.find(({NumberOfInstallments, Value}) => {
        return (NumberOfInstallments === 1 && Value < sellingPrice)
      })?.Value;
      return spotPrice || sellingPrice
    },
    taxPercentage: () => 0 // NOTE: We do not have offer.Tax 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/resolvers/search/productPriceRange.ts from the search-engine-example VTEX app.

[Assistant]
Here's the implementation:

```typescript
interface Root {
  offers: CommertialOffer[]
}

const getMaxAndMinForAttribute = (
  offers: CommertialOffer[],
  attribute: 'Price' | 'ListPrice'
) => {
  return offers.reduce(
    (acc, currentOffer) => {
      const highPrice =
        currentOffer[attribute] > acc.highPrice
          ? currentOffer[attribute]
          : acc.highPrice
      const lowPrice =
        currentOffer[attribute] < acc.lowPrice
          ? currentOffer[attribute]
          : acc.lowPrice
      return { highPrice, lowPrice }
    },
    { highPrice: 0, lowPrice: Infinity }
  )
}

export const resolvers = {
  ProductPriceRange: {
    sellingPrice: ({ offers }: Root) => {
      return getMaxAndMinForAttribute(offers, 'Price')
    },
    listPrice: ({ offers }: Root) => {
      return getMaxAndMinForAttribute(offers, 'ListPrice')
    },
  },
  PriceRange: {
    lowPrice: ({ lowPrice }: any) => lowPrice !== Infinity ? lowPrice : 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/productPriceRange.test.ts from the search-engine-example VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { resolvers } from './product'
import { resolvers as productPriceResolvers } from './productPriceRange'
import { mockContext } from '../../__mocks__/helpers'
import { productPriceRange } from '../../__mocks__/productPriceRange'
import { clone } from 'ramda'

describe('tests related to ProductPriceRange type', () => {
  beforeEach(() => {
    jest.clearAllMocks()
    mockContext.vtex.platform = 'vtex'
  })

  test('gets correct value for product with many skus and prices', () => {
    const { offers } = resolvers.Product.priceRange(
      productPriceRange as any
    ) as { offers: CommertialOffer[] }

    const sellingPrice = productPriceResolvers.ProductPriceRange.sellingPrice({
      offers,
    })
    expect(sellingPrice).toMatchObject({ highPrice: 100, lowPrice: 20 })

    const listPrice = productPriceResolvers.ProductPriceRange.listPrice({
      offers,
    })

    expect(listPrice).toMatchObject({ highPrice: 150, lowPrice: 30 })
  })
  test('works for products with many sellers', () => {
    const cloneProduct = clone(productPriceRange)
    cloneProduct.items[0].sellers.push({
      sellerId: '99',
      commertialOffer: {
        Price: 10,
        ListPrice: 160,
        AvailableQuantity: 10,
      },
    } as any)

    const { offers } = resolvers.Product.priceRange(cloneProduct as any) as {
      offers: CommertialOffer[]
    }
    const sellingPrice = productPriceResolvers.ProductPriceRange.sellingPrice({
      offers,
    })
    expect(sellingPrice).toMatchObject({ highPrice: 100, lowPrice: 10 })

    const listPrice = productPriceResolvers.ProductPriceRange.listPrice({
      offers,
    })

    expect(listPrice).toMatchObject({ highPrice: 160, lowPrice: 30 })
  })

  test('ignores sellers with no availability', () => {
    const cloneProduct = clone(productPriceRange)
    cloneProduct.items[0].sellers.push({
      sellerId: '99',
      commertialOffer: {
        Price: 10,
        ListPrice: 10,
        AvailableQuantity: 0,
      },
    } as any)

    const { offers } = resolvers.Product.priceRange(cloneProduct as any) as {
      offers: CommertialOffer[]
    }
    const sellingPrice = productPriceResolvers.ProductPriceRange.sellingPrice({
      offers,
    })
    expect(sellingPrice).toMatchObject({ highPrice: 100, lowPrice: 20 })

    const listPrice = productPriceResolvers.ProductPriceRange.listPrice({
      offers,
    })

    expect(listPrice).toMatchObject({ highPrice: 150, lowPrice: 30 })
  })
})

```


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

[Assistant]
Here's the implementation:

```typescript
import { resolvers } from './product'
import { mockContext } from '../../__mocks__/helpers'
import { getProduct } from '../../__mocks__/product'

describe('tests related to product resolver', () => {
  beforeEach(() => {
    jest.clearAllMocks()
    mockContext.vtex.platform = 'vtex'
  })
  describe('categoryTree resolver', () => {
    test('ensure that VTEX account never calls the category tree search API', async () => {
      const searchProduct = getProduct()
      await resolvers.Product.categoryTree(
        searchProduct as any,
        {},
        mockContext as any
      )
      expect(mockContext.clients.search.category).toBeCalledTimes(2)
      expect(mockContext.clients.search.categories).toBeCalledTimes(0)
    })

    test('get correct main category tree for product with only one tree', async () => {
      const searchProduct = getProduct()
      await resolvers.Product.categoryTree(
        searchProduct as any,
        {},
        mockContext as any
      )
      expect(mockContext.clients.search.category).toBeCalledTimes(2)
      expect(mockContext.clients.search.category.mock.calls[0][0]).toBe(25)
      expect(mockContext.clients.search.category.mock.calls[1][0]).toBe(10)
    })

    test('get correct main category tree for product with more than one tree', async () => {
      const categoriesIds = [
        '/101/101003/101003009/',
        '/101/101003/',
        '/101/',
        '/101/101019/101019004/',
        '/101/101019/',
        '/103/103023/103023003/',
        '/103/103023/',
        '/103/',
      ]
      const searchProduct = getProduct({
        categoriesIds,
        categoryId: '101003009',
      })

      await resolvers.Product.categoryTree(
        searchProduct as any,
        {},
        mockContext as any
      )
      expect(mockContext.clients.search.category).toBeCalledTimes(3)
      expect(mockContext.clients.search.category.mock.calls[0][0]).toBe(101)
      expect(mockContext.clients.search.category.mock.calls[1][0]).toBe(101003)
      expect(mockContext.clients.search.category.mock.calls[2][0]).toBe(
        101003009
      )
    })

    test('ensure that GC account calls the category tree API ', async () => {
      const searchProduct = getProduct()
      mockContext.vtex.platform = 'gocommerce'
      mockContext.clients.search.categories.mockImplementation(() => [
        {
          id: '10',
          name: 'a',
          url: 'a',
          children: [],
        },
        {
          id: '25',
          name: 'a',
          url: 'a',
          children: [],
        },
      ])
      const result = await resolvers.Product.categoryTree(
        searchProduct as any,
        {},
        mockContext as any
      )
      expect(mockContext.clients.search.category).toBeCalledTimes(0)
      expect(mockContext.clients.search.categories).toBeCalledTimes(1)
      expect(mockContext.clients.search.categories.mock.calls[0][0]).toBe(2) //ensure maximum level was correct
      expect(result!.length).toBe(2)
    })

    test('if categoryId does not match any id in categoriesIds, find biggest tree', async () => {
      const searchProduct = getProduct()
      searchProduct.categoryId = '1'
      searchProduct.categoriesIds = ['/2064927469/', '/2064927469/630877787/']
      await resolvers.Product.categoryTree(
        searchProduct as any,
        {},
        mockContext as any
      )
      expect(mockContext.clients.search.category).toBeCalledTimes(2)
      expect(mockContext.clients.search.category.mock.calls[0][0]).toBe(
        2064927469
      )
      expect(mockContext.clients.search.category.mock.calls[1][0]).toBe(
        630877787
      )
    })
  })

  test('if custom cacheId is given, it should be returned', async () => {
    const searchProduct = getProduct()
    const cacheAsSlug = await resolvers.Product.cacheId(searchProduct as any)
    expect(cacheAsSlug).toBe(searchProduct.linkText)

    searchProduct.cacheId = 'CUSTOM_CACHE'

    const customCache = await resolvers.Product.cacheId(searchProduct as any)
    expect(customCache).toBe('CUSTOM_CACHE')
  })

  test('productClusters should be properly formatted', () => {
    const product = getProduct()
    product.productClusters = {
      "140": 'Casual Footwear',
      "195": 'All products',
      "201": "Third"
    }
    const result = resolvers.Product.productClusters(product as any)
    expect(result).toMatchObject([
      { id: '140', name: 'Casual Footwear' },
      { id: '195', name: 'All products' },
      { id: '201', name: 'Third' }
    ])
  })

  test('productClusters should not break if value is null', () => {
    const product = getProduct()
    product.productClusters = null as any
    const result = resolvers.Product.productClusters(product as any)
    expect(result).toMatchObject([])
  })

  test('clusterHighlights should be properly formatted', () => {
    const product = getProduct()
    product.clusterHighlights = {
      "140": 'Casual Footwear',
      "195": 'All products',
      "201": "Third"
    }
    const result = resolvers.Product.clusterHighlights(product as any)
    expect(result).toMatchObject([
      { id: '140', name: 'Casual Footwear' },
      { id: '195', name: 'All products' },
      { id: '201', name: 'Third' }
    ])
  })

  test('clusterHighlights should not break if value is null', () => {
    const product = getProduct()
    product.clusterHighlights = null as any
    const result = resolvers.Product.clusterHighlights(product as any)
    expect(result).toMatchObject([])
  })

  test('clusterHighlights should be properly formatted', () => {
    const product = getProduct()
    product.clusterHighlights = {
      "140": 'Casual Footwear',
      "195": 'All products',
      "201": "Third"
    }
    const result = resolvers.Product.clusterHighlights(product as any)
    expect(result).toMatchObject([
      { id: '140', name: 'Casual Footwear' },
      { id: '195', name: 'All products' },
      { id: '201', name: 'Third' }
    ])
  })

  test('clusterHighlights should not break if value is null', () => {
    const product = getProduct()
    product.clusterHighlights = null as any
    const result = resolvers.Product.clusterHighlights(product as any)
    expect(result).toMatchObject([])
  })
})

```


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

[Assistant]
Here's the implementation:

```typescript
import { path } from 'ramda'
import { IOResponse } from '@vtex/api'
import { Functions } from '@gocommerce/utils'
import { zipQueryAndMap } from './utils'

interface ProductSearchParent {
  productsRaw: IOResponse<SearchProduct[]>
  translatedArgs: SearchArgs
  searchMetaData: {
    titleTag: string | null
    metaTagDescription: string | null
  }
}

export const resolvers = {
  ProductSearch: {
    titleTag: path(['searchMetaData', 'titleTag']),
    metaTagDescription: path(['searchMetaData', 'metaTagDescription']),
    recordsFiltered: ({ productsRaw }: ProductSearchParent) => {
      const {
        headers: { resources },
      } = productsRaw
      const quantity = resources.split('/')[1]
      return parseInt(quantity, 10)
    },
    products: path(['productsRaw', 'data']),
    breadcrumb: async (
      { translatedArgs, productsRaw: { data: products } }: ProductSearchParent,
      _: any,
      { vtex: { account }, clients: { search } }: Context
    ) => {
      const query = translatedArgs?.query || ''
      const map = translatedArgs?.map || ''
      const queryAndMap = zipQueryAndMap(
        translatedArgs?.query,
        translatedArgs?.map
      )
      const categoriesSearched = queryAndMap
        .filter(([_, m]) => m === 'c')
        .map(([q]) => q)
      const categoriesCount = map.split(',').filter(m => m === 'c').length
      const categories =
        !!categoriesCount && Functions.isGoCommerceAcc(account)
          ? await search.categories(categoriesCount)
          : []

      const queryArray = query.split('/')
      const mapArray = map.split(',')
      return queryAndMap.map(
        ([queryUnit, mapUnit]: [string, string], index: number) => ({
          queryUnit,
          mapUnit,
          index,
          queryArray,
          mapArray,
          categories,
          categoriesSearched,
          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/resolvers/search/sku.ts from the search-engine-example VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { find, head, map, replace, slice } from 'ramda'

export const resolvers = {
  SKU: {
    attachments: ({ attachments = [] }: SearchItem) =>
      map(
        (attachment: any) => ({
          ...attachment,
          domainValues: JSON.parse(attachment.domainValues),
        }),
        attachments
      ),

    images: (
      { images = [] }: SearchItem,
      { quantity }: { quantity: number }
    ) =>
      map(
        image => ({
          cacheId: image.imageId,
          ...image,
          imageUrl: replace('http://', 'https://', image.imageUrl),
        }),
        quantity > 0 ? slice(0, quantity, images) : images
      ),

    kitItems: (
      { kitItems }: SearchItem,
      _: any,
      { clients: { search } }: Context
    ) =>
      !kitItems
        ? []
        : kitItems.map(async kitItem => {
            const products = await search.productBySku([kitItem.itemId])
            const { items: skus = [], ...product } = head(products) || {}
            const sku = find(({ itemId }) => itemId === kitItem.itemId, skus)
            return { ...kitItem, product, sku }
          }),

    variations: (sku: SearchItem) =>
      sku &&
      map(
        (name: string) => ({ name, values: (sku as any)[name] }),
        sku.variations || []
      ),

    videos: ({ Videos }: SearchItem) =>
      map(
        (video: string) => ({
          videoUrl: video,
        }),
        Videos
      ),
  },
}

```


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

[Assistant]
Here's the implementation:

```typescript
import { flatten, indexOf, path } from 'ramda'

const SKU_SEPARATOR = ','
const CATALOG = 'Catalog'
const DEFAULT_SELLER = '1'
const DEFAULT_QUANTITY = 1

export const fieldResolvers = {
  Benefit: {
    items: async (benefit: any, _: any, { clients: { search } }: Context) => {
      const { teaserType, conditions, effects } = benefit

      if (teaserType === CATALOG) {
        const {
          parameters: conditionsParameters,
          minimumQuantity = DEFAULT_QUANTITY,
        } = conditions

        const { parameters: effectsParameters } = effects

        const items = await Promise.all(
          conditionsParameters.map(
            async (conditionsParameter: any, index: any) => {
              const skuIds: string[] = conditionsParameter.value.split(
                SKU_SEPARATOR
              )
              const discount = effectsParameters[index].value
              const products = await search.productBySku(skuIds)

              return products.map((product: any) => {
                const benefitSKUIds: any = []

                product.items.map((item: any) => {
                  if (indexOf(item.itemId, skuIds) > -1) {
                    benefitSKUIds.push(item.itemId)
                  }
                })

                return {
                  benefitProduct: product,
                  benefitSKUIds,
                  discount,
                  minQuantity: minimumQuantity,
                }
              })
            }
          )
        )
        return flatten(items)
      }
      return
    },
  },
}

export const getBenefits = async (
  itemId: string,
  { clients: { checkout } }: Context
) => {
  const requestBody = {
    items: [
      {
        id: itemId,
        quantity: DEFAULT_QUANTITY,
        seller: DEFAULT_SELLER,
      },
    ],
  }
  const benefitsData = await checkout.simulation(requestBody)
  return path(['ratesAndBenefitsData', 'teaser'], benefitsData) || []
}

```


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

[Assistant]
Here's the implementation:

```typescript
import { VBase } from '@vtex/api'
import { VBASE_BUCKET, URL_COUNT_FILE_PATH } from './constants'
import { SearchStatsUrlCount } from './searchStats'

export enum SORT {
  ASC = 'ASC',
  DESC = 'DESC',
}

export const searchURLsCount = async (
  vbase: VBase,
  limit: number,
  sort: SORT = SORT.DESC
) => {
  const data = (await vbase.getJSON<SearchStatsUrlCount>(
    VBASE_BUCKET,
    URL_COUNT_FILE_PATH,
    true
  )) as SearchStatsUrlCount
  let comparator = urlsCountDESCComparator

  if (sort === SORT.ASC) {
    comparator = urlsCountASCComparator
  }

  return (
    (data &&
      Object.entries(data.searchUrlCount)
        .sort(comparator)
        .slice(0, limit)
        .map(searchUrlStat => {
          return { path: searchUrlStat[0], count: searchUrlStat[1] }
        })) ||
    []
  )
}

const urlsCountASCComparator = (
  urlStatsA: [string, number],
  urlStatsB: [string, number]
) => {
  return urlStatsA[1] - urlStatsB[1]
}

const urlsCountDESCComparator = (
  urlStatsA: [string, number],
  urlStatsB: [string, number]
) => {
  return urlStatsB[1] - urlStatsA[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/stats/searchStats.ts from the search-engine-example VTEX app.

[Assistant]
Here's the implementation:

```typescript
import {
  PATH_SEPARATOR,
  MAP_QUERY_PARAM,
  MAP_SEPARATOR,
  VBASE_BUCKET,
  URL_COUNT_FILE_PATH,
  INDEXING_UPDATE_FREQUENCY,
  INDEXING_EVENT_ROUTE_NAME,
} from './constants'

export interface SearchStatsUrlCount {
  updatedCount: number
  searchUrlCount: Record<string, number>
}

export const count = async (ctx: Context, args: SearchArgs) => {
  const {
    clients: { vbase },
  } = ctx

  const searchURLPath = `${PATH_SEPARATOR}${args.query}?${MAP_QUERY_PARAM}=${args.map}`
  const pathSegments = args.query && args.query.split(PATH_SEPARATOR)
  const mapSegments = args.map && args.map.split(MAP_SEPARATOR)

  if (pathSegments && mapSegments && pathSegments.length === mapSegments.length) {
    const data = ((await vbase.getJSON<SearchStatsUrlCount>(
      VBASE_BUCKET,
      URL_COUNT_FILE_PATH,
      true
    )) || { updatedCount: 0, searchUrlCount: {} }) as SearchStatsUrlCount

    data.searchUrlCount[searchURLPath] = (data.searchUrlCount[searchURLPath] || 0) + 1
    data.updatedCount = data.updatedCount + 1
    if (data.updatedCount === INDEXING_UPDATE_FREQUENCY) {
      data.updatedCount = 0
      sendIndexSearchUrlsEvent(ctx)
    }

    vbase.saveJSON<SearchStatsUrlCount>(VBASE_BUCKET, URL_COUNT_FILE_PATH, data)
  }
}

const sendIndexSearchUrlsEvent = async (ctx: Context) => {
  const { clients: { events }, vtex: { locale, tenant, binding } } = ctx
  events.sendEvent('', INDEXING_EVENT_ROUTE_NAME, {locale, tenant, binding})
}
```


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

[Assistant]
Here's the implementation:

```typescript
export const VBASE_BUCKET = 'searchStats'
export const URL_COUNT_FILE_PATH = 'searchUrlsCount'

export const INDEXING_UPDATE_FREQUENCY = 500
export const INDEXING_EVENT_ROUTE_NAME = 'searchUrlsCountIndex'

export const MAP_QUERY_PARAM = 'map'
export const PATH_SEPARATOR = '/'
export const MAP_SEPARATOR = ','
```


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

[Assistant]
Here's the implementation:

```typescript
import { searchURLsCount, SORT } from "./searchURLsCount"

export const queries = {
  searchURLsCount: async (
    _: any,
    args: { limit: number, sort?: SORT },
    ctx: Context
  ) => {
    const { clients: { vbase } } = ctx
    const count = searchURLsCount(vbase, args.limit, args.sort)
    return count
  },
}

```


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

[Assistant]
Here's the implementation:

```typescript
import {
  tail,
  head,
  compose,
  equals,
  prop,
  join,
  map,
  filter,
  findLastIndex,
  path,
  split,
  toLower,
  reverse,
  complement,
  isNil,
} from 'ramda'
import { Functions } from '@gocommerce/utils'

import { zipQueryAndMap, findCategoryInTree, getBrandFromSlug } from '../utils'
import { toTitleCase } from '../../../utils/string'

type TupleString = [string, string]

const isTupleMap = compose<TupleString, string, boolean>(
  equals('c'),
  prop('1')
)

const getLastCategoryIndex = findLastIndex(isTupleMap)

const categoriesOnlyQuery = compose<
  TupleString[],
  TupleString[],
  string[],
  string
>(
  join('/'),
  map(prop('0')),
  filter(isTupleMap)
)

const getAndParsePagetype = async (path: string, ctx: Context) => {
  const pagetype = await ctx.clients.search.pageType(path).catch(() => null)
  if (!pagetype) {
    return emptyTitleTag
  }
  return {
    titleTag: pagetype.title || pagetype.name,
    metaTagDescription: pagetype.metaTagDescription,
  }
}

const getCategoryMetadata = async (
  { map, query }: SearchMetadataArgs,
  ctx: Context
): Promise<SearchMetadata> => {
  const {
    vtex: { account },
    clients: { search },
  } = ctx
  const queryAndMap = zipQueryAndMap(query, map)
  const cleanQuery = categoriesOnlyQuery(queryAndMap)

  if (Functions.isGoCommerceAcc(account)) {
    // GoCommerce does not have pagetype query implemented yet
    const category =
      findCategoryInTree(
        await search.categories(cleanQuery.split('/').length),
        cleanQuery.split('/')
      ) || {}
    return {
      metaTagDescription: path(['MetaTagDescription'], category),
      titleTag: path(['Title'], category) || path(['Name'], category),
    }
  }

  return getAndParsePagetype(cleanQuery, ctx)
}

const getBrandMetadata = async (
  query: SearchMetadataArgs['query'],
  ctx: Context
): Promise<SearchMetadata> => {
  const {
    vtex: { account },
    clients: { search },
  } = ctx
  const cleanQuery = head(split('/', query || '')) || ''

  if (Functions.isGoCommerceAcc(account)) {
    const brand = (await getBrandFromSlug(toLower(cleanQuery), search)) || {}
    return {
      metaTagDescription: path(['metaTagDescription'], brand),
      titleTag: path(['title'], brand) || path(['name'], brand),
    }
  }
  return getAndParsePagetype(cleanQuery, ctx)
}

export const getSpecificationFilterName = (name: string) => {
  return toTitleCase(decodeURI(name))
}

const getPrimaryMetadata = (
  args: SearchMetadataArgs,
  ctx: Context
): Promise<SearchMetadata> | SearchMetadata => {
  const map = args.map || ''
  const firstMap = head(map.split(','))
  if (firstMap === 'c') {
    return getCategoryMetadata(args, ctx)
  }
  if (firstMap === 'b') {
    return getBrandMetadata(args.query, ctx)
  }
  if (firstMap && firstMap.includes('specificationFilter')) {
    const cleanQuery = args.query || ''
    const name = head(cleanQuery.split('/')) || ''
    return {
      titleTag: getSpecificationFilterName(name),
      metaTagDescription: null,
    }
  }
  if (firstMap === 'ft') {
    const cleanQuery = args.query || ''
    const term = head(cleanQuery.split('/')) || ''
    return {
      titleTag: decodeURI(term),
      metaTagDescription: null,
    }
  }
  return emptyTitleTag
}

const getNameForRemainingMaps = async (
  remainingTuples: [string, string][],
  ctx: Context
) => {
  const {
    vtex: { account },
    clients: { search },
  } = ctx
  const lastCategoryIndex = getLastCategoryIndex(remainingTuples)
  const isGC = Functions.isGoCommerceAcc(account)
  const names = await Promise.all(
    remainingTuples.map(async ([query, map], index) => {
      if (map === 'c' && index === lastCategoryIndex && !isGC) {
        const cleanQuery = categoriesOnlyQuery(remainingTuples)
        const pagetype = await search.pageType(cleanQuery).catch(() => null)
        if (pagetype) {
          return pagetype.name
        }
      }
      if (map === 'b' && !isGC) {
        const brand = await search.pageType(decodeURI(query)).catch(() => null)
        if (brand) {
          return brand.name
        }
      }
      if (map.includes('specificationFilter')) {
        return getSpecificationFilterName(query)
      }
      return null
    })
  )
  return names
}

export const emptyTitleTag = {
  titleTag: null,
  metaTagDescription: null,
}

type StringNull = string | null | undefined

const isNotNil = complement(isNil)
const joinNames = compose<StringNull[], string[], string[], string>(
  join(' - '),
  reverse,
  filter(isNotNil) as any
)

/**
 * Get metadata of category/brand APIs
 *
 * @param _
 * @param args
 * @param ctx
 */
export const getSearchMetaData = async (
  _: any,
  args: SearchMetadataArgs,
  ctx: Context
) => {
  const queryAndMap = zipQueryAndMap(args.query, args.map)
  if (queryAndMap.length === 0) {
    return emptyTitleTag
  }

  const isFirstCategory = queryAndMap[0][1] === 'c'
  const tailTuples = tail(queryAndMap)

  const validTuples = tailTuples.filter(
    ([_, m]) =>
      m === 'b' ||
      m.includes('specificationFilter') ||
      (m === 'c' && !isFirstCategory)
  )
  const [metadata, otherNames] = await Promise.all([
    getPrimaryMetadata(args, ctx),
    getNameForRemainingMaps(validTuples, ctx),
  ])

  return {
    titleTag: joinNames([metadata.titleTag, ...otherNames]),
    metaTagDescription: metadata.metaTagDescription,
  }
}

```


[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 vtex-logger-react/messages/pt.json from the vtex-logger-react VTEX app.

[Assistant]
Here's the implementation:

```json
{
  "admin/test": "teste",
  "store/test": "teste"
}

```


[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 vtex-logger-react/messages/en.json from the vtex-logger-react VTEX app.

[Assistant]
Here's the implementation:

```json
{
  "admin/test": "test",
  "store/test": "test"
}

```


[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 vtex-logger-react/messages/es.json from the vtex-logger-react VTEX app.

[Assistant]
Here's the implementation:

```json
{
  "admin/test": "teste",
  "store/test": "teste"
}

```


[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 vtex-logger-react/docs/README.md from the vtex-logger-react VTEX app.

[Assistant]
Here's the implementation:

```markdown
# VTEX Logger React

<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->

[![All Contributors](https://img.shields.io/badge/all_contributors-1-orange.svg?style=flat-square)](#contributors-)

<!-- ALL-CONTRIBUTORS-BADGE:END -->

##  ⚠ Maintenance ⚠
*Starting June 1st 2023, this application will no longer be maintained by VTEX.*

---

The VTEX Logger React app will help to make log entries in the VTEX IO Logging Service on React Apps.

## Configuration

1. Add the `vtex-logger-react` app as a dependency in you theme's `manifest.json` file:

```diff
  "dependencies": {
+   "vtex.vtex-logger-react": "1.x"
  }
```

Now, you can import any of the exported components and hooks from the app. Here's an example of a component that use the `useLog` hook from the `useLoggerVtex` to log some relevant info:

```tsx
// Notice that this is TypeScript, and this code should be in a .tsx file
import React, { FC } from 'react'
import { useLoggerVtex } from 'vtex.vtex-logger-react'
import { Button } from 'vtex.styleguide'

const MyComponent: FC = () => {
  const { useLog } = useLoggerVtex()
  const exampleDetail = {
    id: '1',
    name: 'Test Name',
    description: 'Test Description',
    price: '100',
    quantity: '1',
    brand: 'Test Brand',
    category: 'Test Category',
  }

  const handleClickLog = async () => {
    const appName = 'my-app'
    const message = 'Test log'
    await useLog({ app: appName, message: message, detail: exampleDetail })
  }

  return (
    <Fragment>
      <Button onClick={handleClickLog} />
    </Fragment>
  )
}

export default MyComponent
```

:warning: _Be sure to run `vtex setup --typings` in your project to install the correct TypeScript types exported by this app._

### Hooks

#### `useLog`

This is the most useful export from this app. The `useLog` hook can be used to log messages to the VTEX IO Logging Service.

It needs to recieve:

- `app`: The app name that want to log the message.
- `message`: The message to be logged.
- `detail`: An object with the details of the log. It can be any JSON-serializable object.

```ts
interface VtexLoggerProps {
  app: string
  message: string
  detail: any
}
```

You should expect an object that looks like this:

```json
{
  "logger": {
    "status": 200
  }
}
```

Always check the `status` property to know if the log was successful.

:information*source: \_To have the full type definition in your development environment, be sure to run `vtex setup` in your project to install all TypeScript types exported by this app.*

<!-- DOCS-IGNORE:start -->

## Review the Logs (Splunk)

With this query you can retrieve the logs from Splunk, filtering by `app`:

```
index=io_vtex_logs app="vtex.vtex-logger-graphql@0.2.0" account={account} workspace={workspace} | spath "data.app" | search "data.app"={app}
```

Example:

```
index=io_vtex_logs app="vtex.vtex-logger-graphql@0.2.0" account=gbonacchi workspace=customstockexport | spath "data.app" | search "data.app"="my-app"
```

![image](https://user-images.githubusercontent.com/55905671/146546432-2a1df845-5d46-4f5d-8ba5-049fed2b0efc.png)

## Contributors ✨

Thanks goes to these wonderful people:

<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
  <tr>
    <td align="center"><a href="https://github.com/germanBonacchi"><img src="https://avatars.githubusercontent.com/u/55905671?v=4" width="100px;" alt=""/><br /><sub><b>Germán Bonacchi</b></sub></a><br /><a href="https://github.com/vtex-apps/vtex-logger-react/commits?author=germanBonacchi" title="Code">💻</a></td>
    <td align="center"><a href="https://github.com/tomymehdi"><img src="https://avatars.githubusercontent.com/u/774112?v=4" width="100px;" alt=""/><br /><sub><b>Tomás Alfredo Mehdi</b></sub></a><br /><a href="https://github.com/vtex-apps/vtex-logger-react/commits?author=tomymehdi" title="Code">💻</a></td>
  </tr>
</table>

<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->

<!-- ALL-CONTRIBUTORS-LIST:END -->

This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!

<!-- DOCS-IGNORE:end -->

```


[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 vtex-logger-react/react/useLoggerVtex.ts from the vtex-logger-react VTEX app.

[Assistant]
Here's the implementation:

```typescript
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useMutation } from 'react-apollo'

import logger from './graphql/logger.gql'

interface VtexLoggerProps {
  app: string
  message: string
  detail: any
}

const useLoggerVtex = () => {
  const [loggerMutation] = useMutation(logger)

  const useLog = async ({ app, message, detail }: VtexLoggerProps) => {
    const loggerResponse = await loggerMutation({
      variables: { app, message, detail: JSON.stringify(detail) },
    })

    return loggerResponse
  }

  return { useLog }
}

export default useLoggerVtex

```


[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 vtex-logger-react/react/package.json from the vtex-logger-react VTEX app.

[Assistant]
Here's the implementation:

```json
{
  "name": "CHANGE_ME",
  "version": "1.0.2",
  "scripts": {
    "test": "vtex-test-tools test"
  },
  "dependencies": {
    "apollo-client": "^2.6.8",
    "react": "^16.12.0",
    "react-apollo": "^3.1.3",
    "react-dom": "^16.12.0",
    "react-intl": "^3.12.0"
  },
  "devDependencies": {
    "@apollo/react-testing": "^3.1.3",
    "@types/jest": "^25.1.4",
    "@types/node": "^13.9.8",
    "@types/react": "^16.9.31",
    "@vtex/test-tools": "^3.3.2",
    "@vtex/tsconfig": "^0.4.4",
    "apollo-cache-inmemory": "^1.6.5",
    "graphql": "^14.6.0",
    "typescript": "3.9.7",
    "vtex.render-runtime": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.render-runtime@8.132.3/public/@types/vtex.render-runtime",
    "vtex.vtex-logger-graphql": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.vtex-logger-graphql@0.2.0/public/@types/vtex.vtex-logger-graphql"
  }
}

```


[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 vtex-logger-react/react/typings/gql.d.ts from the vtex-logger-react VTEX app.

[Assistant]
Here's the implementation:

```typescript
declare module '*.gql' {
  import { DocumentNode } from 'graphql'

  const Schema: DocumentNode

  export default Schema
}

declare module '*.graphql' {
  import { DocumentNode } from 'graphql'

  const schema: DocumentNode

  export default Schema
}

```


[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 vtex-logger-react/react/typings/css.d.ts from the vtex-logger-react VTEX app.

[Assistant]
Here's the implementation:

```typescript
declare module '*.css' {
  type Styles = {
    [selector: string]: string
  }

  const styles: Styles

  export default styles
}

```


[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 vtex-logger-react/react/graphql/logger.gql from the vtex-logger-react VTEX app.

[Assistant]
Here's the implementation:

```graphql
mutation logger($app: String, $message: String, $detail: String) {
  logger(app: $app, message: $message, detail: $detail)
    @context(provider: "vtex.vtex-logger-graphql") {
    status
  }
}

```


[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 cross-device-cart/messages/pt.json from the cross-device-cart VTEX app.

[Assistant]
Here's the implementation:

```json
{
  "store/crossCart.challenge.cta": "Faça",
  "store/crossCart.challenge.text": "Parece que você deixou alguns itens em outro dispositivo. ¿Quer recuperá-los?",
  "store/crossCart.toast.success": "Itens adicionados com sucesso ao seu carrinho",
  "store/crossCart.toast.error": "Ocorreu um erro ao adicionar seus itens"
}

```


[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 cross-device-cart/messages/en.json from the cross-device-cart VTEX app.

[Assistant]
Here's the implementation:

```json
{
  "store/crossCart.challenge.cta": "Do it",
  "store/crossCart.challenge.text": "It seems you left your cart in another device. Do you want to recover it?",
  "store/crossCart.toast.success": "Your last cart has been successfully added",
  "store/crossCart.toast.error": "An error occurred while retrieving your cart",
  "admin/cross-device-cart.title": "Cross Device Cart",
  "admin/cross-device-cart.subtitle": "App Settings",
  "admin/cross-device-cart.error": "There was a problem loading the application settings",
  "admin/cross-device-cart.automatic-toggle-automatic": "Automatic",
  "admin/cross-device-cart.automatic-toggle-manual": "Manual",
  "admin/cross-device-cart.automatic-toggle-help-text": "Toggle between Automatic and Manual mode",
  "admin/cross-device-cart.automatic-toggle-explanation": "When the 'Automatic' mode is selected the user will not be prompted to take any action. If 'Manual' mode is selected the user will need to confirm what they want to do with the items that were previously in the cart.",
  "admin/cross-device-cart.strategy-explanation.replace": "Replace all items on current cart with the items on the saved Cart.",
  "admin/cross-device-cart.strategy-explanation.combine": "Combine items in the current cart with the saved Cart, adding items quantities.",
  "admin/cross-device-cart.strategy-explanation.add": "Add missing items from the current cart to the saved Cart, doesn't add duplicate quantities."
}

```


[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 cross-device-cart/messages/ca.json from the cross-device-cart VTEX app.

[Assistant]
Here's the implementation:

```json
{
  "store/crossCart.challenge.cta": "Recuperar",
  "store/crossCart.challenge.text": "Sembla que has deixat productes a la cistella d'un altre dispositiu. Vols recuperar-los?",
  "store/crossCart.toast.success": "Els productes s'han afegit a la cistella",
  "store/crossCart.toast.error": "Hi ha hagut un error a l'afegir els productes"
}

```


[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 cross-device-cart/messages/context.json from the cross-device-cart VTEX app.

[Assistant]
Here's the implementation:

```json
{
  "store/crossCart.challenge.cta": "Challenge's call to action",
  "store/crossCart.challenge.text": "Description of service provided",
  "store/crossCart.toast.success": "Toast message for added items",
  "store/crossCart.toast.error": "Toast message for error while adding items",
  "admin/cross-device-cart.title": "Admin panel title",
  "admin/cross-device-cart.subtitle": "Admin panel subtitle",
  "admin/cross-device-cart.error": "Alert on error fetching the app settings",
  "admin/cross-device-cart.automatic-toggle-automatic": "Automatic toggle label when toggle is on",
  "admin/cross-device-cart.automatic-toggle-manual": "Automatic toggle label when toggle is off",
  "admin/cross-device-cart.automatic-toggle-help-text": "Automatic toggle help text",
  "admin/cross-device-cart.automatic-toggle-explanation": "Automatic toggle modes explanation",
  "admin/cross-device-cart.strategy-explanation.replace": "Replace strategy explanation",
  "admin/cross-device-cart.strategy-explanation.combine": "Combine strategy explanation",
  "admin/cross-device-cart.strategy-explanation.add": "Add strategy explanation"
}

```


[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 cross-device-cart/messages/es.json from the cross-device-cart VTEX app.

[Assistant]
Here's the implementation:

```json
{
  "store/crossCart.challenge.cta": "Hazlo",
  "store/crossCart.challenge.text": "Parece que has dejado un carrito en otro dispositivo. ¿Quieres recuperarlo?",
  "store/crossCart.toast.success": "Tu ultimo carrito ha sido recuperado",
  "store/crossCart.toast.error": "Un error ocurrio al recuperar tu carrito",
  "admin/cross-device-cart.title": "Cross Device Cart",
  "admin/cross-device-cart.subtitle": "Configuración de la aplicación",
  "admin/cross-device-cart.error": "Ha ocurrido un problema cargando la configuración de la aplicación",
  "admin/cross-device-cart.automatic-toggle-automatic": "Automático",
  "admin/cross-device-cart.automatic-toggle-manual": "Manual",
  "admin/cross-device-cart.automatic-toggle-help-text": "Elija entre el modo Automático o Manual para el cross device cart.",
  "admin/cross-device-cart.automatic-toggle-explanation": "Cuando se selecciona el modo 'Automático' no se le pedirá al usuario que realice ninguna acción. Si se selecciona el modo 'Manual' el usuario deberá confirmar qué quiere hacer con los artículos que estaban previamente en el carrito."
}

```


[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 cross-device-cart/admin/navigation.json from the cross-device-cart VTEX app.

[Assistant]
Here's the implementation:

```json
{
  "section": "marketplace",
  "titleId": "admin/cross-device-cart.title",
  "path": "/admin/cross-device-cart"
}

```


[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 cross-device-cart/admin/routes.json from the cross-device-cart VTEX app.

[Assistant]
Here's the implementation:

```json
{
  "admin.app.cross-device-cart": {
    "component": "AdminPanel",
    "path": "/admin/app/cross-device-cart"
  }
}

```


[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 cross-device-cart/docs/README.md from the cross-device-cart VTEX app.

[Assistant]
Here's the implementation:

```markdown
📢 Use this project, [contribute](https://github.com/vtex-apps/cross-device-cart) to it or open issues to help evolve it using [Store Discussion](https://github.com/vtex-apps/store-discussion).

# CROSS DEVICE CART

##  ⚠ Maintenance ⚠
*Starting June 1st 2023, this application will no longer be maintained by VTEX.*

---

<!-- DOCS-IGNORE:start -->
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->

[![All Contributors](https://img.shields.io/badge/all_contributors-2-orange.svg?style=flat-square)](#contributors-)

<!-- ALL-CONTRIBUTORS-BADGE:END -->
<!-- DOCS-IGNORE:end -->

The main feature users are looking for is to keep an up-to-date shopping cart through different devices; one of the most important experiences of a truly unified commerce.

To do so, this app was created to enable `logged in` users to retrieve their cart from their last session.

## Configuration

1.  Install the app and then import it to your theme's peer dependencies in `manifest.json`,

```json
  "peerDependencies": {
    // ...
    "vtex.cross-device-cart": "1.x"
  }
```

2. Add the `cross-device-cart` block as a children of your store header, desktop and mobile, for i.e:

   ```diff
   "header-layout.desktop": {
       "children": [
   +     "cross-device-cart",
         "flex-layout.row#1-desktop",
         "flex-layout.row#2-desktop",
         "flex-layout.row#3-desktop",
         "sticky-layout#4-desktop"
       ]
     },
     "header-layout.mobile": {
       "children": [
   +     "cross-device-cart",
         "flex-layout.row#1-mobile",
         "sticky-layout#2-mobile"
       ]
     },
   ```

3. (Optional) You can tailor the default experience by accesing the admin app settings.
   By default, the app handles the replacement automatically. But it you set it to manual, a challenge block will be rendered as an action bar, for the user to interact with.

## Customization

| CSS Handles     |
| --------------- |
| `actionBar`     |
| `challengeText` |

<!-- DOCS-IGNORE:start -->

## Contributors

Thanks goes to these wonderful people:

<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<!-- markdownlint-enable -->
<!-- prettier-ignore-end -->

<!-- ALL-CONTRIBUTORS-LIST:END -->

This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind are welcome!

<!-- DOCS-IGNORE:end -->

```


[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 cross-device-cart/graphql/schema.graphql from the cross-device-cart VTEX app.

[Assistant]
Here's the implementation:

```graphql
type Query {
  """
  Retrieves a cross cart OrderForm ID. Will return NULL if no reference was stored.
  """
  getSavedCart(userId: String!, nullOnEmpty: Boolean, userType: String): String
    @cacheControl(scope: PUBLIC, maxAge: ZERO)

  """
  Retrieve app settings
  """
  getAppSettings: CrossDeviceCartSettings
    @cacheControl(scope: PUBLIC, maxAge: SHORT)
}

type Mutation {
  """
  Stores an OrderForm ID for a specific user, as a cross cart reference.
  """
  saveCurrentCart(userId: String!, orderFormId: String, userType: String): String
  """
  Replaces current cart with a saved cart. Optionally, you can request that both list of items be merged.
  """
  replaceCart(
    savedCart: String!
    currentCart: String!
    strategy: Strategy!
    userType: String
  ): OrderForm

  """
  Save app settings
  """
  saveAppSettings(settings: SaveSettings): Boolean
}

```