[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 store-sitemap/node/middlewares/sitemap.test.ts from the store-sitemap VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { Binding, IOContext, Logger, VBase } from '@vtex/api'
import * as TypeMoq from 'typemoq'

import {
  APPS_ROUTES_INDEX,
  PRODUCT_ROUTES_INDEX,
  REWRITER_ROUTES_INDEX,
} from './generateMiddlewares/utils'

import { Clients } from '../clients'
import { EXTENDED_INDEX_FILE } from '../utils'
import { sitemap } from './sitemap'

const vbaseTypeMock = TypeMoq.Mock.ofInstance(VBase)
const contextMock = TypeMoq.Mock.ofType<Context>()
const ioContext = TypeMoq.Mock.ofType<IOContext>()
const state = TypeMoq.Mock.ofType<State>()
const loggerMock = TypeMoq.Mock.ofType<Logger>()

const removeSpaces = (str: string) => str.replace(/(\r\n|\n|\r|\s)/gm, '')

describe('Test sitemap middleware', () => {
  let context: Context
  let hasExtendedFiles: boolean

  const vbase = class VBaseMock extends vbaseTypeMock.object {
    constructor() {
      super(ioContext.object)
    }

    public getJSON = async <T>(
      _: string,
      file: string,
      __?: boolean | undefined
    ): Promise<T> => {
      switch (file) {
        case APPS_ROUTES_INDEX:
          return ({
            index: ['appsRoutes-0'],
            lastUpdated: '2019-12-04',
          } as unknown) as T
        case REWRITER_ROUTES_INDEX:
          return ({
            index: ['brand-0', 'department-0'],
            lastUpdated: '2019-12-04',
          } as unknown) as T
        case PRODUCT_ROUTES_INDEX:
          return ({
            index: ['product-0'],
            lastUpdated: '2019-12-04',
          } as unknown) as T
        case EXTENDED_INDEX_FILE:
          return ((hasExtendedFiles
            ? {
                index: ['extra-0'],
                lastUpdated: '2019-12-04',
              }
            : null) as unknown) as T
        default:
          return (null as unknown) as T
      }
    }
  }

  const next = jest.fn()

  const matchingBindings = [
    {
      canonicalBaseAddress: 'www.host.com',
      defaultLocale: 'en-US',
      id: '1',
    },
    {
      canonicalBaseAddress: 'www.host.com/br',
      defaultLocale: 'pt-BR',
      id: '2',
    },
    {
      canonicalBaseAddress: 'www.host.com/de',
      defaultLocale: 'de-DE',
      id: '3',
    },
  ] as Binding[]

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

    hasExtendedFiles = false
    context = {
      ...contextMock.object,
      clients: new ClientsImpl({}, ioContext.object),
      state: {
        ...state.object,
        binding: {
          id: '1',
        } as Binding,
        bucket: 'bucket',
        enabledIndexFiles: [
          APPS_ROUTES_INDEX,
          REWRITER_ROUTES_INDEX,
          PRODUCT_ROUTES_INDEX,
        ],
        forwardedHost: 'www.host.com',
        forwardedPath: '/sitemap/file1.xml',
        matchingBindings: [matchingBindings[0]],
        rootPath: '',
        settings: {
          disableRoutesTerm: '',
          enableAppsRoutes: true,
          enableNavigationRoutes: true,
          enableProductRoutes: true,
          ignoreBindings: false,
        },
      },
      vtex: {
        ...ioContext.object,
        logger: loggerMock.object,
      },
    }
  })

  it('Should return binding index if it has multiple bindings', async () => {
    const thisContext = {
      ...context,
      state: {
        ...context.state,
        matchingBindings,
      },
    }
    await sitemap(thisContext, next)
    expect(
      thisContext.body.includes(
        '<loc>https://www.host.com/sitemap.xml?__bindingAddress=www.host.com</loc>'
      )
    ).toBeTruthy()
    expect(
      thisContext.body.includes(
        '<loc>https://www.host.com/sitemap.xml?__bindingAddress=www.host.com/br</loc>'
      )
    ).toBeTruthy()
    expect(
      thisContext.body.includes(
        '<loc>https://www.host.com/sitemap.xml?__bindingAddress=www.host.com/de</loc>'
      )
    ).toBeTruthy()
  })

  it('Should return binding index with canonical if it has multiple bindings in production', async () => {
    const thisContext = {
      ...context,
      state: {
        ...context.state,
        matchingBindings,
      },
      vtex: {
        ...context.vtex,
        production: true,
      },
    }

    await sitemap(thisContext, next)
    expect(
      thisContext.body.includes('<loc>https://www.host.com/sitemap.xml</loc>')
    ).toBeTruthy()
    expect(
      thisContext.body.includes(
        '<loc>https://www.host.com/br/sitemap.xml</loc>'
      )
    ).toBeTruthy()
    expect(
      thisContext.body.includes(
        '<loc>https://www.host.com/de/sitemap.xml</loc>'
      )
    ).toBeTruthy()
  })

  it('Should return index if it doesnt have multiple bindings', async () => {
    await sitemap(context, next)
    expect(removeSpaces(context.body)).toStrictEqual(
      removeSpaces(
        `<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
          <sitemap>
            <loc>https://www.host.com/sitemap/appsRoutes-0.xml</loc>
            <lastmod>2019-12-04</lastmod>
          </sitemap>
          <sitemap>
            <loc>https://www.host.com/sitemap/brand-0.xml</loc>
            <lastmod>2019-12-04</lastmod>
          </sitemap>
          <sitemap>
            <loc>https://www.host.com/sitemap/department-0.xml</loc>
            <lastmod>2019-12-04</lastmod>
          </sitemap>
          <sitemap>
            <loc>https://www.host.com/sitemap/product-0.xml</loc>
            <lastmod>2019-12-04</lastmod>
          </sitemap>
        </sitemapindex>`
      )
    )
  })

  it('Should return only enabled index', async () => {
    context.state.enabledIndexFiles = [APPS_ROUTES_INDEX, PRODUCT_ROUTES_INDEX]
    await sitemap(context, next)
    expect(removeSpaces(context.body)).toStrictEqual(
      removeSpaces(
        `<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
          <sitemap>
            <loc>https://www.host.com/sitemap/appsRoutes-0.xml</loc>
            <lastmod>2019-12-04</lastmod>
          </sitemap>
          <sitemap>
            <loc>https://www.host.com/sitemap/product-0.xml</loc>
            <lastmod>2019-12-04</lastmod>
          </sitemap>
        </sitemapindex>`
      )
    )

    context.state.enabledIndexFiles = [
      REWRITER_ROUTES_INDEX,
      'non-existant-index',
    ]
    await sitemap(context, next)
    expect(removeSpaces(context.body)).toStrictEqual(
      removeSpaces(
        `<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
          <sitemap>
            <loc>https://www.host.com/sitemap/brand-0.xml</loc>
            <lastmod>2019-12-04</lastmod>
          </sitemap>
          <sitemap>
            <loc>https://www.host.com/sitemap/department-0.xml</loc>
            <lastmod>2019-12-04</lastmod>
          </sitemap>
        </sitemapindex>`
      )
    )
  })

  it('Should return binding index with bindingAddress', async () => {
    const thisContext = {
      ...context,
      state: {
        ...context.state,
        bindingAddress: 'www.host.com/en',
      },
    }
    await sitemap(thisContext, next)
    expect(removeSpaces(thisContext.body)).toStrictEqual(
      removeSpaces(
        `<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
        <sitemap>
          <loc>https://www.host.com/sitemap/appsRoutes-0.xml?__bindingAddress=www.host.com/en</loc>
          <lastmod>2019-12-04</lastmod>
        </sitemap>
        <sitemap>
          <loc>https://www.host.com/sitemap/brand-0.xml?__bindingAddress=www.host.com/en</loc>
          <lastmod>2019-12-04</lastmod>
        </sitemap>
        <sitemap>
          <loc>https://www.host.com/sitemap/department-0.xml?__bindingAddress=www.host.com/en</loc>
          <lastmod>2019-12-04</lastmod>
        </sitemap>
        <sitemap>
          <loc>https://www.host.com/sitemap/product-0.xml?__bindingAddress=www.host.com/en</loc>
          <lastmod>2019-12-04</lastmod>
        </sitemap>
      </sitemapindex>`
      )
    )
  })

  it('Should return bindinig index with rootPath', async () => {
    const thisContext = {
      ...context,
      state: {
        ...context.state,
        rootPath: '/en',
      },
    }
    await sitemap(thisContext, next)
    expect(removeSpaces(thisContext.body)).toStrictEqual(
      removeSpaces(
        `<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
        <sitemap>
          <loc>https://www.host.com/en/sitemap/appsRoutes-0.xml</loc>
          <lastmod>2019-12-04</lastmod>
        </sitemap>
        <sitemap>
          <loc>https://www.host.com/en/sitemap/brand-0.xml</loc>
          <lastmod>2019-12-04</lastmod>
        </sitemap>
        <sitemap>
          <loc>https://www.host.com/en/sitemap/department-0.xml</loc>
          <lastmod>2019-12-04</lastmod>
        </sitemap>
        <sitemap>
          <loc>https://www.host.com/en/sitemap/product-0.xml</loc>
          <lastmod>2019-12-04</lastmod>
        </sitemap>
      </sitemapindex>`
      )
    )
  })

  it('Should return extra index if any', async () => {
    hasExtendedFiles = true
    await sitemap(context, next)
    expect(removeSpaces(context.body)).toStrictEqual(
      removeSpaces(
        `<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
        <sitemap>
          <loc>https://www.host.com/sitemap/appsRoutes-0.xml</loc>
          <lastmod>2019-12-04</lastmod>
        </sitemap>
        <sitemap>
          <loc>https://www.host.com/sitemap/brand-0.xml</loc>
          <lastmod>2019-12-04</lastmod>
        </sitemap>
        <sitemap>
          <loc>https://www.host.com/sitemap/department-0.xml</loc>
          <lastmod>2019-12-04</lastmod>
        </sitemap>
        <sitemap>
          <loc>https://www.host.com/sitemap/product-0.xml</loc>
          <lastmod>2019-12-04</lastmod>
        </sitemap>
        <sitemap>
          <loc>https://www.host.com/sitemap/extra-0.xml</loc>
          <lastmod>2019-12-04</lastmod>
        </sitemap>
      </sitemapindex>`
      )
    )
  })
})

```


[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 store-sitemap/node/middlewares/settings.test.ts from the store-sitemap VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { Apps, IOContext, Logger, RequestTracingConfig } from '@vtex/api'
import * as TypeMoq from 'typemoq'

import { Clients } from '../clients'
import { APPS_ROUTES_INDEX, PRODUCT_ROUTES_INDEX, REWRITER_ROUTES_INDEX } from './generateMiddlewares/utils'
import { settings } from './settings'

const appsTypeMock = TypeMoq.Mock.ofInstance(Apps)
const contextMock = TypeMoq.Mock.ofType<Context>()
const ioContext = TypeMoq.Mock.ofType<IOContext>()
const state = TypeMoq.Mock.ofType<State>()
const loggerMock = TypeMoq.Mock.ofType<Logger>()

describe('Test settings middleware', () => {
  let context: Context

  const apps = class AppsMock extends appsTypeMock.object {
    public settings = {
      enableAppsRoutes: true,
      enableNavigationRoutes: true,
      enableProductRoutes: true,
    }

    constructor() {
      super(ioContext.object)
    }

    public getAppSettings = async (_: string, __?: RequestTracingConfig) => this.settings
  }

  const next = jest.fn()

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

    context = {
      ...contextMock.object,
      clients: new ClientsImpl({}, ioContext.object),
      state: {
        ...state.object,
      },
      vtex: {
        ...ioContext.object,
        logger: loggerMock.object,
      },
    }
  })

  it('Should get correct apps index files', async () => {
    await settings(context, next)
    expect(context.state.enabledIndexFiles).toStrictEqual([APPS_ROUTES_INDEX, REWRITER_ROUTES_INDEX, PRODUCT_ROUTES_INDEX])

    let appClient = context.clients.apps as any
    appClient.settings = {
      enableAppsRoutes: true,
      enableNavigationRoutes: true,
      enableProductRoutes: false,
    }
    await settings(context, next)
    expect(context.state.enabledIndexFiles).toStrictEqual([APPS_ROUTES_INDEX, REWRITER_ROUTES_INDEX])

    appClient = context.clients.apps as any
    appClient.settings = {
      enableAppsRoutes: false,
      enableNavigationRoutes: false,
      enableProductRoutes: true,
    }
    await settings(context, next)
    expect(context.state.enabledIndexFiles).toStrictEqual([PRODUCT_ROUTES_INDEX])

    appClient = context.clients.apps as any
    appClient.settings = {
      enableAppsRoutes: false,
      enableNavigationRoutes: false,
      enableProductRoutes: false,
    }
    await settings(context, next)
    expect(context.state.enabledIndexFiles).toStrictEqual([])
  })
})

```


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

[User] Show me the implementation of store-sitemap/node/middlewares/cache.ts from the store-sitemap VTEX app.

[Assistant]
Here's the implementation:

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

const TEN_SECONDS_S = 10
const TEN_MINUTES_S = 10 * 60
const FIVE_MINUTES_S = 5 * 60
const THIRTY_SECONDS = 30
const TEN_SECONDS = 10
const TWENTY_HOURS = 20 * 60 * 60
const ONE_DAY = 24 * 60 * 60

const publicEndpoint = `.${process.env.VTEX_PUBLIC_ENDPOINT ?? 'myvtex.com'}`

const isPrivateHost = (host: string) => endsWith(publicEndpoint, host)

const from0To30 = () => Math.round(Math.random() * 30)

export async function cache(ctx: Context, next: () => Promise<void>) {
  const {
    vtex: { production, logger },
    request: {
      headers: { 'x-forwarded-host': originalHost },
    },
    query: { __disableSSR: disableSSR },
  } = ctx

  try {
    await next()
  } catch (err) {
    logger.error(err)
    ctx.body =
      ctx.body || `Something exploded: operationId=${ctx.vtex.operationId}`
    ctx.status = 500
  } finally {
    const shouldCache = 200 <= ctx.status && ctx.status < 300 && production
    const maxAge = isPrivateHost(originalHost ?? '')
      ? TEN_SECONDS
      : THIRTY_SECONDS + from0To30()

    if (shouldCache) {
      if (ctx.state.useLongCacheControl) {
        ctx.set(
          'cache-control',
          `public, max-age=${TWENTY_HOURS}, stale-while-revalidate=${ONE_DAY}`
        )
      } else {
        ctx.set(
          'cache-control',
          `public, max-age=${maxAge}, stale-while-revalidate=${TEN_SECONDS_S}, stale-if-error=${TEN_MINUTES_S}`
        )
      }
    } else {
      ctx.set('cache-control', 'no-store, no-cache')
    }

    if (!disableSSR) {
      ctx.set('x-vtex-etag-control', `public, max-age=${FIVE_MINUTES_S}`)
    }
  }
}

```


[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 store-sitemap/node/middlewares/sitemap.ts from the store-sitemap VTEX app.

[Assistant]
Here's the implementation:

```typescript
import * as cheerio from 'cheerio'

import { MultipleSitemapGenerationError } from '../errors'
import {
  EXTENDED_INDEX_FILE,
  xmlTruncateNodes,
  getBucket,
  hashString,
  SitemapNotFound,
  startSitemapGeneration,
} from '../utils'
import { currentDate, SitemapIndex } from './generateMiddlewares/utils'

const sitemapIndexEntry = (
  forwardedHost: string,
  rootPath: string,
  entry: string,
  lastUpdated: string,
  bindingAddress?: string
) => {
  const querystring = bindingAddress
    ? `?__bindingAddress=${bindingAddress}`
    : ''
  return `<sitemap>
      <loc>https://${forwardedHost}${rootPath}/sitemap/${entry}.xml${querystring}</loc>
      <lastmod>${lastUpdated}</lastmod>
    </sitemap>`
}

const sitemapBindingEntry = (
  host: string,
  lastUpdated: string,
  bindingAddress?: string
) => {
  const querystring = bindingAddress
    ? `?__bindingAddress=${bindingAddress}`
    : ''
  return `<sitemap>
      <loc>https://${host}/sitemap.xml${querystring}</loc>
      <lastmod>${lastUpdated}</lastmod>
    </sitemap>`
}

const sitemapIndex = async (ctx: Context) => {
  const {
    state: {
      enabledIndexFiles,
      forwardedHost,
      binding,
      bucket,
      rootPath,
      bindingAddress,
    },
    clients: { vbase },
  } = ctx

  const $ = cheerio.load(
    '<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
    {
      xmlMode: true,
    }
  )

  const rawIndexFiles = await Promise.all([
    ...enabledIndexFiles.map(indexFile =>
      vbase.getJSON<SitemapIndex>(bucket, indexFile, true)
    ),
    vbase.getJSON<SitemapIndex>(
      getBucket('', hashString(binding.id)),
      EXTENDED_INDEX_FILE,
      true
    ),
  ])

  const indexFiles = rawIndexFiles.filter(Boolean)

  if (indexFiles.length === 0) {
    throw new SitemapNotFound('Sitemap not found')
  }

  const index = [
    ...new Set(
      indexFiles.reduce(
        (acc, { index: fileIndex }) => acc.concat(fileIndex),
        [] as string[]
      )
    ),
  ]

  const lastUpdated = indexFiles[0].lastUpdated

  const indexXML = index.map(entry =>
    sitemapIndexEntry(
      forwardedHost,
      rootPath,
      entry,
      lastUpdated,
      bindingAddress
    )
  )
  $('sitemapindex').append(xmlTruncateNodes(indexXML))
  return $
}

const sitemapBindingIndex = async (ctx: Context) => {
  const {
    state: { forwardedHost, matchingBindings: bindings },
    vtex: { production },
  } = ctx

  const $ = cheerio.load(
    '<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
    {
      xmlMode: true,
    }
  )

  const date = currentDate()
  const bindingsIndexXML = bindings.map(binding =>
    sitemapBindingEntry(
      production ? binding.canonicalBaseAddress : forwardedHost,
      date,
      production ? '' : binding.canonicalBaseAddress
    )
  )
  $('sitemapindex').append(xmlTruncateNodes(bindingsIndexXML))
  return $
}

export async function sitemap(ctx: Context, next: () => Promise<void>) {
  const {
    state: { isCrossBorder },
  } = ctx

  if (isCrossBorder) {
    await legacySitemap(ctx)
  } else {
    await catalogSitemap(ctx)
  }

  next()
}

async function legacySitemap(ctx: Context) {
  const {
    state: {
      isCrossBorder,
      matchingBindings,
      bindingAddress,
      rootPath,
      settings,
    },
    vtex: { logger },
  } = ctx

  logger.info({
    message: 'Fetching legacy sitemap',
    payload: {
      isCrossBorder,
      matchingBindings,
      bindingAddress,
      rootPath,
      ignoreBindings: settings.ignoreBindings,
    },
  })

  const hasBindingIdentifier = rootPath || bindingAddress
  let $: any
  try {
    if (hasBindingIdentifier || settings.ignoreBindings) {
      $ = await sitemapIndex(ctx)
    } else {
      const hasMultipleMatchingBindings = matchingBindings.length > 1
      $ = hasMultipleMatchingBindings
        ? await sitemapBindingIndex(ctx)
        : await sitemapIndex(ctx)
    }
  } catch (err) {
    if (err instanceof SitemapNotFound) {
      ctx.status = 404
      ctx.body = 'Generating sitemap...'
      ctx.vtex.logger.error(err.message)
      await startSitemapGeneration(ctx).catch(err => {
        if (!(err instanceof MultipleSitemapGenerationError)) {
          throw err
        }
      })
    }
    throw err
  }

  ctx.body = $.xml()
}

async function catalogSitemap(ctx: Context) {
  const {
    clients: { catalog },
    headers: { 'x-forwarded-host': forwardedHost },
    state: { isCrossBorder, forwardedPath },
    vtex: { logger },
  } = ctx

  logger.info({
    message: 'Fetching catalog sitemap',
    payload: {
      forwardedHost,
      forwardedPath,
      isCrossBorder,
    },
  })

  const sitemapData = await catalog.getSitemap(forwardedHost, forwardedPath)
  ctx.body = sitemapData
}

```


[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 store-sitemap/node/middlewares/sitemapEntry.ts from the store-sitemap VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { Binding } from '@vtex/api'
import * as cheerio from 'cheerio'
import RouteParser from 'route-parser'

import { SITEMAP_URL } from '../utils'
import { SitemapEntry } from './generateMiddlewares/utils'

const getBinding = (bindingId: string, bindings: Binding[]) =>
  bindings.find(binding => binding.id === bindingId)

export const URLEntry = (
  ctx: Context,
  route: Route,
  lastUpdated: string
): string => {
  const { state: {
    binding,
    bindingAddress,
    forwardedHost,
    rootPath,
    matchingBindings,
  },
  } = ctx
  const querystring = bindingAddress
    ? `?__bindingAddress=${bindingAddress}`
    : ''
  const loc = `https://${forwardedHost}${rootPath}${route.path}${querystring}`
  const localization = route.alternates && route.alternates.length > 1
    ? route.alternates.map(
      ({ bindingId, path }) => {
        const alternateBinding = getBinding(bindingId, matchingBindings)
        if (bindingId === binding.id || !alternateBinding) {
            return ''
          }
          const { canonicalBaseAddress, defaultLocale: locale } = alternateBinding
          const href = querystring
            ? `https://${forwardedHost}${path}?__bindingAddress=${canonicalBaseAddress}`
            : `https://${canonicalBaseAddress}${path}`
          return `<xhtml:link rel="alternate" hreflang="${locale}" href="${href}"/>`
        }
       )
      .join('\n')
    : ''
  let entry = `
      <loc>${loc}</loc>
      ${localization}
      <lastmod>${lastUpdated}</lastmod>
    `
  if (route.imagePath && route.imageTitle) {
    // add image metainfo
    entry = `
    <image:image>
      <image:loc>${route.imagePath}</image:loc>
      <image:title>${route.imageTitle}</image:title>
    </image:image>
    ${entry}`
  }
  return `<url>${entry}</url>`
}

export async function sitemapEntry(ctx: Context, next: () => Promise<void>) {
  const {
    state: { isCrossBorder },
  } = ctx

  if (!isCrossBorder) {
    await catalogSitemapEntry(ctx)
  } else {
    await legacySitemapEntry(ctx)
    next()
  }
}

async function legacySitemapEntry(ctx: Context) {
  const {
    clients: { vbase },
    state: { forwardedPath, bucket },
    vtex: { logger },
  } = ctx

  const sitemapRoute = new RouteParser(SITEMAP_URL)
  const sitemapParams = sitemapRoute.match(forwardedPath)

  logger.info({
    message: 'Fetching legacy sitemap entry',
    payload: {
      forwardedPath,
      bucket,
    },
  })

  if (!sitemapParams) {
    ctx.status = 404
    ctx.body = `Sitemap not found the URL must be: ${SITEMAP_URL}`
    throw new Error(`URL differs from the expected, ${forwardedPath}`)
  }

  const { path } = sitemapParams

  const $: any = cheerio.load(
    '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">',
    {
      xmlMode: true,
    }
  )

  const fileName = path.split('.')[0]
  const maybeRoutesInfo = await vbase.getJSON<SitemapEntry>(
    bucket,
    fileName,
    true
  )

  if (!maybeRoutesInfo) {
    ctx.status = 404
    ctx.body = 'Sitemap entry not found'
    ctx.vtex.logger.error({
      message: 'Sitemap entry not found',
      payload: {
        bucket,
        fileName,
      },
    })

    return
  }

  const { routes, lastUpdated } = maybeRoutesInfo as SitemapEntry
  const entryXML = routes.map((route: Route) =>
    URLEntry(ctx, route, lastUpdated)
  )

  $('urlset').append(entryXML.join('\n'))

  ctx.body = $.xml()
}

async function catalogSitemapEntry(ctx: Context) {
  const {
    clients: { catalog },
    headers: { 'x-forwarded-host': forwardedHost },
    state: { forwardedPath },
    vtex: { logger },
  } = ctx

  logger.info({
    message: 'Fetching catalog sitemap entry',
    payload: {
      forwardedHost,
      forwardedPath,
    },
  })

  ctx.body = await catalog.getSitemap(forwardedHost, forwardedPath)
  ctx.status = 200
}

```


[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 store-sitemap/node/middlewares/customRoutes.ts from the store-sitemap VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { Logger, VBase } from '@vtex/api'

import { MultipleCustomRoutesGenerationError } from '../errors'
import { generateCustomRoutes } from './generateMiddlewares/generateCustomRoutes'
import {
  CONFIG_BUCKET,
  CUSTOM_ROUTES_BUCKET,
  CUSTOM_ROUTES_FILENAME,
  CUSTOM_ROUTES_GENERATION_LOCK_FILENAME,
  validDate,
} from '../utils'

const ONE_DAY_MS = 24 * 60 * 60 * 1000
const TWENTY_THREE_HOURS_MS = 23 * 60 * 60 * 1000

const twentyThreeHoursFromNowMS = () =>
  `${new Date(Date.now() + TWENTY_THREE_HOURS_MS)}`

const metricsConfig = {
  customRoutes: {
    failed: 'customRoutes-failed',
    success: 'customRoutes-success',
    cached: 'customRoutes-cached',
    notFound: 'customRoutes-notFound',
    generating: 'customRoutes-generating',
  },
}

const startCustomRoutesGeneration = async (ctx: Context) => {
  const {
    clients: { vbase },
    vtex: { logger, account },
  } = ctx

  logger.info({
    message: 'Checking for existing custom routes generation lock',
    type: 'custom-routes-lock-check',
    lockFile: CUSTOM_ROUTES_GENERATION_LOCK_FILENAME,
    bucket: CONFIG_BUCKET,
  })

  const lockFile = await vbase.getJSON<GenerationConfig>(
    CONFIG_BUCKET,
    CUSTOM_ROUTES_GENERATION_LOCK_FILENAME,
    true
  )

  if (lockFile && validDate(lockFile.endDate)) {
    logger.warn({
      message: 'Custom routes generation lock found - generation skipped',
      type: 'custom-routes-lock-found',
      existingGenerationId: lockFile.generationId,
      lockExpiresAt: lockFile.endDate,
      lockFile: CUSTOM_ROUTES_GENERATION_LOCK_FILENAME,
    })
    throw new MultipleCustomRoutesGenerationError(lockFile.endDate, account)
  }

  if (lockFile) {
    logger.info({
      message: 'Lock found but expired - proceeding with generation',
      type: 'custom-routes-lock-expired',
      expiredLock: lockFile,
    })
  } else {
    logger.info({
      message: 'No lock found - proceeding with generation',
      type: 'custom-routes-no-lock',
      lockFile: CUSTOM_ROUTES_GENERATION_LOCK_FILENAME,
    })
  }

  const generationId = (Math.random() * 10000).toString()
  const caller = ctx.request?.header?.['x-vtex-caller'] || 'unknown'
  const endDate = twentyThreeHoursFromNowMS()

  logger.info({
    message: `Custom routes generation started by ${caller}`,
    type: 'custom-routes-generation-started',
    generationId,
    caller,
    lockExpiresAt: endDate,
  })

  try {
    await vbase.saveJSON<GenerationConfig>(
      CONFIG_BUCKET,
      CUSTOM_ROUTES_GENERATION_LOCK_FILENAME,
      {
        endDate,
        generationId,
      }
    )
  } catch (error) {
    throw error
  }

  logger.info({
    message: 'Generation lock file created',
    type: 'custom-routes-lock-created',
    lockFile: CUSTOM_ROUTES_GENERATION_LOCK_FILENAME,
    generationId,
    expiresAt: endDate,
  })

  // Execute generation in background - don't await
  generateCustomRoutes(ctx).catch(error => {
    logger.error({
      message: 'Background custom routes generation failed',
      type: 'custom-routes-background-error',
      generationId,
      error,
    })
  })

  logger.info({
    message: 'Custom routes generation started in background',
    type: 'custom-routes-background-started',
    generationId,
  })
}

export const clearCustomRoutesGenerationLock = async (
  vbase: VBase,
  logger: Logger
) => {
  logger.info({
    message: 'Attempting to clear custom routes generation lock',
    type: 'custom-routes-lock-clear-attempt',
    lockFile: CUSTOM_ROUTES_GENERATION_LOCK_FILENAME,
    bucket: CONFIG_BUCKET,
  })

  try {
    await vbase.deleteFile(
      CONFIG_BUCKET,
      CUSTOM_ROUTES_GENERATION_LOCK_FILENAME
    )
    logger.info({
      message: 'Custom routes generation lock cleared successfully',
      type: 'custom-routes-lock-cleared',
      lockFile: CUSTOM_ROUTES_GENERATION_LOCK_FILENAME,
    })
  } catch (error) {
    // File might not exist, ignore error but log it
    logger.info({
      message: 'Lock file not found or already cleared',
      type: 'custom-routes-lock-clear-skipped',
      lockFile: CUSTOM_ROUTES_GENERATION_LOCK_FILENAME,
      error: error.message || error,
    })
  }
}

async function triggerCustomRoutesGeneration(ctx: Context) {
  const {
    vtex: { logger },
  } = ctx

  try {
    await startCustomRoutesGeneration(ctx)
    logger.info({
      message: 'Custom routes generation event triggered',
      type: 'custom-routes-trigger',
    })
  } catch (error) {
    if (error instanceof MultipleCustomRoutesGenerationError) {
      logger.info({
        message: 'Custom routes generation already in progress',
        type: 'custom-routes-already-generating',
      })
      throw error
    }
    logger.error({
      message: 'Failed to trigger custom routes generation',
      error,
      type: 'custom-routes-trigger-error',
    })
    throw error
  }
}

export async function customRoutes(ctx: Context, next: () => Promise<void>) {
  const startTime = process.hrtime()

  const {
    vtex: { logger },
    clients: { vbase },
  } = ctx

  try {
    // Get pre-compiled custom routes from VBase
    const cachedData = await vbase.getJSON<CustomRoutesData>(
      CUSTOM_ROUTES_BUCKET,
      CUSTOM_ROUTES_FILENAME,
      true
    )

    if (!cachedData) {
      // No cached data exists - trigger generation and return 404
      logger.info({
        message: 'No cached custom routes found, triggering generation',
        type: 'custom-routes-not-found',
        fileName: CUSTOM_ROUTES_FILENAME,
      })

      try {
        await triggerCustomRoutesGeneration(ctx)

        ctx.status = 404
        ctx.body = {
          message:
            'Custom routes not available. Generation has been triggered.',
        }

        const diffTime = process.hrtime(startTime)
        metrics.batch(metricsConfig.customRoutes.notFound, diffTime)
        logger.info({
          type: metricsConfig.customRoutes.notFound,
        })
      } catch (error) {
        if (error instanceof MultipleCustomRoutesGenerationError) {
          // Generation already in progress - return 404
          ctx.status = 404
          ctx.body = {
            message: error.message,
          }

          const diffTime = process.hrtime(startTime)
          metrics.batch(metricsConfig.customRoutes.generating, diffTime)
          logger.info({
            type: metricsConfig.customRoutes.generating,
          })
        } else {
          throw error
        }
      }

      await next()
      return
    }

    // Check if data is older than 1 day
    const dataAge = Date.now() - cachedData.timestamp
    const isOld = dataAge >= ONE_DAY_MS

    if (isOld) {
      // Data exists but is old - trigger regeneration in background
      logger.info({
        message: 'Cached custom routes are old, triggering regeneration',
        type: 'custom-routes-stale',
        ageInHours: Math.floor(dataAge / (60 * 60 * 1000)),
      })

      // Fire and forget - don't await
      triggerCustomRoutesGeneration(ctx)
    }

    // Return cached data
    ctx.status = 200
    ctx.body = cachedData.data
    ctx.state.useLongCacheControl = true

    const diffTime = process.hrtime(startTime)
    metrics.batch(metricsConfig.customRoutes.cached, diffTime)
    logger.info({
      message: 'Custom routes served from cache',
      type: metricsConfig.customRoutes.cached,
      ageInHours: Math.floor(dataAge / (60 * 60 * 1000)),
      isStale: isOld,
    })
  } catch (error) {
    ctx.status = 500
    ctx.body = {
      success: false,
      error: 'Failed to retrieve custom routes',
    }

    const diffTime = process.hrtime(startTime)
    metrics.batch(metricsConfig.customRoutes.failed, diffTime)
    logger.error({
      message: 'Error retrieving custom routes',
      error,
      type: metricsConfig.customRoutes.failed,
    })
  }

  await next()
}

```


[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 store-sitemap/node/middlewares/sitemapErrors.test.ts from the store-sitemap VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { CatalogSitemapError } from '../errors/CatalogSitemapError'
import { sitemapErrors } from './sitemapErrors'

// Mock a minimal context for testing
const createTestContext = (): Context => {
  return ({
    method: 'GET',
    path: '/sitemap.xml',
    vtex: {
      logger: {
        debug: jest.fn(),
        error: jest.fn(),
        info: jest.fn(),
        warn: jest.fn(),
      },
    },
  } as unknown) as Context
}

describe('sitemapErrors middleware', () => {
  let context: Context
  let next: jest.Mock

  beforeEach(() => {
    context = createTestContext()
    next = jest.fn()
  })

  it('should call next when no error occurs', async () => {
    await sitemapErrors(context, next)

    expect(next).toHaveBeenCalledTimes(1)
    expect(context.status).toBeUndefined()
    expect(context.type).toBeUndefined()
    expect(context.body).toBeUndefined()
  })

  it('should handle CatalogSitemapError with default 500 status', async () => {
    const originalError = new Error('Catalog API error')
    const error = new CatalogSitemapError(
      'Failed to fetch catalog sitemap',
      originalError
    )
    next.mockRejectedValue(error)

    await sitemapErrors(context, next)

    expect(context.status).toBe(500)
    expect(context.type).toBe('text/plain; charset=utf-8')
    expect(context.body).toBe('Error fetching sitemap data')
    expect(context.vtex.logger.error).toHaveBeenCalledWith({
      message: 'Sitemap pipeline error',
      payload: {
        error: error.message,
        method: 'GET',
        originalError: originalError.message,
        path: '/sitemap.xml',
        stack: error.stack,
        statusCode: 500,
      },
    })
  })

  it('should handle CatalogSitemapError with custom HTTP status code', async () => {
    const originalError = {
      message: 'Not Found',
      response: { status: 404 },
    }
    const error = new CatalogSitemapError(
      'Failed to fetch catalog sitemap',
      (originalError as unknown) as Error
    )
    next.mockRejectedValue(error)

    await sitemapErrors(context, next)

    expect(context.status).toBe(404)
    expect(context.type).toBe('text/plain; charset=utf-8')
    expect(context.body).toBe('Error fetching sitemap data')
    expect(context.vtex.logger.error).toHaveBeenCalledWith({
      message: 'Sitemap pipeline error',
      payload: {
        error: error.message,
        method: 'GET',
        originalError: originalError.message,
        path: '/sitemap.xml',
        stack: error.stack,
        statusCode: 404,
      },
    })
  })

  it('should extract status code from error response object', async () => {
    const originalError = {
      message: 'Bad Request',
      response: { status: 400 },
    }
    const error = new CatalogSitemapError(
      'Failed to fetch catalog sitemap',
      (originalError as unknown) as Error
    )
    next.mockRejectedValue(error)

    await sitemapErrors(context, next)

    expect(context.status).toBe(400)
    expect(context.type).toBe('text/plain; charset=utf-8')
    expect(context.body).toBe('Error fetching sitemap data')
    expect(context.vtex.logger.error).toHaveBeenCalledWith({
      message: 'Sitemap pipeline error',
      payload: {
        error: error.message,
        method: 'GET',
        originalError: originalError.message,
        path: '/sitemap.xml',
        stack: error.stack,
        statusCode: 400,
      },
    })
  })

  it('should handle generic errors with 500 status', async () => {
    const error = new Error('Some unexpected error')
    next.mockRejectedValue(error)

    await sitemapErrors(context, next)

    expect(context.status).toBe(500)
    expect(context.type).toBe('text/plain; charset=utf-8')
    expect(context.body).toBe('Internal Server Error')
    expect(context.vtex.logger.error).toHaveBeenCalledWith({
      message: 'Sitemap pipeline error',
      payload: {
        error: error.message,
        method: 'GET',
        path: '/sitemap.xml',
        stack: error.stack,
      },
    })
  })

  it('should log error details with correct structure', async () => {
    const error = new Error('Test error')
    error.stack = 'Error: Test error\n    at Object.<anonymous> (/test.js:1:1)'
    next.mockRejectedValue(error)

    await sitemapErrors(context, next)

    expect(context.vtex.logger.error).toHaveBeenCalledWith({
      message: 'Sitemap pipeline error',
      payload: {
        error: 'Test error',
        method: 'GET',
        path: '/sitemap.xml',
        stack: 'Error: Test error\n    at Object.<anonymous> (/test.js:1:1)',
      },
    })
  })

  it('should set correct content type for CatalogSitemapError', async () => {
    const error = new CatalogSitemapError(
      'Catalog error',
      new Error('Original')
    )
    const testContext = createTestContext()
    const testNext = jest.fn().mockRejectedValue(error)

    await sitemapErrors(testContext, testNext)

    expect(testContext.type).toBe('text/plain; charset=utf-8')
  })

  it('should set correct content type for generic errors', async () => {
    const error = new Error('Generic error')
    const testContext = createTestContext()
    const testNext = jest.fn().mockRejectedValue(error)

    await sitemapErrors(testContext, testNext)

    expect(testContext.type).toBe('text/plain; charset=utf-8')
  })

  it('should set correct content type for TypeError', async () => {
    const error = new TypeError('Type error')
    const testContext = createTestContext()
    const testNext = jest.fn().mockRejectedValue(error)

    await sitemapErrors(testContext, testNext)

    expect(testContext.type).toBe('text/plain; charset=utf-8')
  })
})

```


[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 store-sitemap/node/middlewares/methods.ts from the store-sitemap VTEX app.

[Assistant]
Here's the implementation:

```typescript
const TEN_SECONDS_S = 10

export function methodNotAllowed(ctx: Context) {
  ctx.status = 405
  ctx.set('cache-control', `max-age=${TEN_SECONDS_S}`)
}

```


[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 store-sitemap/node/middlewares/tenant.ts from the store-sitemap VTEX app.

[Assistant]
Here's the implementation:

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

export async function tenant(ctx: EventContext, next: () => Promise<void>) {
  const {
    clients: { tenant: tenantClient },
  } = ctx
  const tenantInfo = await tenantClient.info({
    forceMaxAge: TENANT_CACHE_TTL_S,
  })

  const locale = tenantInfo.defaultLocale

  ctx.vtex.locale = locale
  ctx.vtex.tenant = { locale }

  await next()
}

```


[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 store-sitemap/node/resolvers/mutations.ts from the store-sitemap VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { CVBase } from '../clients/vbase'
import { EXTENDED_INDEX_FILE, getBucket, hashString } from '../utils'
import {
  currentDate,
  SitemapIndex,
} from './../middlewares/generateMiddlewares/utils'
import { getDefaultStoreBinding } from './../resources/bindings'

const getBucketName = async (ctx: Context, binding?: string) => {
  if (!binding) {
    binding = await getDefaultStoreBinding(ctx)
  }
  return getBucket('', hashString(binding))
}

const getExtendedIndexes = async (bucket: string, vbase: CVBase) => {
  const { index } = (await vbase.getJSON<SitemapIndex>(
    bucket,
    EXTENDED_INDEX_FILE,
    true
  )) || { index: [] }

  return index
}

const saveBucket = (
  bucket: string,
  extendedIndexes: string[],
  vbase: CVBase
) => {
  return vbase
    .saveJSON(bucket, EXTENDED_INDEX_FILE, {
      index: [...new Set(extendedIndexes)],
      lastUpdated: currentDate(),
    })
    .then(() => true)
    .catch(() => false)
}

export const saveIndex = async (
  _: {},
  { index, binding }: { index: string; binding?: string },
  ctx: Context
) => {
  const {
    clients: { vbase },
  } = ctx
  const bucket = await getBucketName(ctx, binding)
  const extendedIndexes = await getExtendedIndexes(bucket, vbase)

  // check if index is already in the extendedIndexes
  if (!extendedIndexes.includes(index)) {
    extendedIndexes.push(index)
  }

  return saveBucket(bucket, extendedIndexes, vbase)
}

export const deleteIndex = async (
  _: {},
  { index, binding }: { index: string; binding?: string },
  ctx: Context
) => {
  const {
    clients: { vbase },
  } = ctx
  const bucket = await getBucketName(ctx, binding)
  const extendedIndexes = await getExtendedIndexes(bucket, vbase)

  // if index is not in the extendedIndexes, treat as success
  if (!extendedIndexes.includes(index)) {
    return true
  }

  // remove index from extendedIndexes
  // and use set to eliminate any duplicates that may have been added previously
  const newExtendedIndexes = extendedIndexes.filter(i => i !== index)

  return saveBucket(bucket, newExtendedIndexes, vbase)
}

```


[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 store-sitemap/node/resolvers/index.ts from the store-sitemap VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { startSitemapGeneration } from '../utils'
import { deleteIndex, saveIndex } from './mutations'

export const resolvers = {
  Mutation: {
    deleteIndex,
    saveIndex,
  },
  Query: {
    generateSitemap: async (
      _: {},
      { force }: { force?: boolean },
      ctx: Context
    ) => {
      await startSitemapGeneration(ctx, force)
      return true
    },
  },
}

```


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

[User] Show me the implementation of store-sitemap/node/errors/CatalogSitemapError.ts from the store-sitemap VTEX app.

[Assistant]
Here's the implementation:

```typescript
interface ErrorWithResponse {
  response?: {
    status?: number
  }
}

export class CatalogSitemapError extends Error {
  public statusCode: number

  constructor(message: string, public originalError?: Error) {
    super(message)
    this.name = 'CatalogSitemapError'

    this.statusCode = this.extractStatusCodeFromError(originalError) ?? 500
  }

  private extractStatusCodeFromError(error?: Error): number | null {
    if (!error) {
      return null
    }

    // Type guard to check if error has response property with status
    if (this.hasResponseWithStatus(error)) {
      return error.response?.status ?? null
    }

    return null
  }

  private hasResponseWithStatus(error: unknown): error is ErrorWithResponse {
    if (!error || typeof error !== 'object') {
      return false
    }

    const errorObj = error as Record<string, unknown>
    const hasResponseProperty = 'response' in errorObj
    if (!hasResponseProperty) {
      return false
    }

    const responseValue = (errorObj as { response?: unknown }).response
    const responseIsNonNullObject =
      typeof responseValue === 'object' && responseValue !== null

    if (!responseIsNonNullObject) {
      return false
    }

    const hasStatusProperty =
      'status' in (responseValue as Record<string, unknown>)

    if (!hasStatusProperty) {
      return false
    }

    const statusValue = (responseValue as { status?: unknown }).status
    const statusIsNumber = typeof statusValue === 'number'

    return statusIsNumber
  }
}

```


[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 store-sitemap/node/errors/CatalogSitemapError.test.ts from the store-sitemap VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { CatalogSitemapError } from './CatalogSitemapError'

describe('CatalogSitemapError', () => {
  it('should create error with default 500 status code', () => {
    const error = new CatalogSitemapError('Test error')

    expect(error.message).toBe('Test error')
    expect(error.name).toBe('CatalogSitemapError')
    expect(error.statusCode).toBe(500)
    expect(error.originalError).toBeUndefined()
  })

  it('should use default status code when no original error provided', () => {
    const originalError = new Error('Original error')
    const error = new CatalogSitemapError('Test error', originalError)

    expect(error.message).toBe('Test error')
    expect(error.name).toBe('CatalogSitemapError')
    expect(error.statusCode).toBe(500)
    expect(error.originalError).toBe(originalError)
  })

  it('should extract status code from original error response', () => {
    const originalError = {
      message: 'API Error',
      response: { status: 403 },
    }
    const error = new CatalogSitemapError(
      'Test error',
      (originalError as unknown) as Error
    )

    expect(error.message).toBe('Test error')
    expect(error.name).toBe('CatalogSitemapError')
    expect(error.statusCode).toBe(403)
    expect(error.originalError).toBe(originalError)
  })

  it('should default to 500 when original error has no response', () => {
    const originalError = new Error('Simple error')
    const error = new CatalogSitemapError('Test error', originalError)

    expect(error.message).toBe('Test error')
    expect(error.name).toBe('CatalogSitemapError')
    expect(error.statusCode).toBe(500)
    expect(error.originalError).toBe(originalError)
  })

  it('should default to 500 when original error response has no status', () => {
    const originalError = {
      message: 'API Error',
      response: {},
    }
    const error = new CatalogSitemapError(
      'Test error',
      (originalError as unknown) as Error
    )

    expect(error.message).toBe('Test error')
    expect(error.name).toBe('CatalogSitemapError')
    expect(error.statusCode).toBe(500)
    expect(error.originalError).toBe(originalError)
  })
})

```


[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 store-sitemap/node/services/routes.ts from the store-sitemap VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { Internal, ListInternalsResponse } from 'vtex.rewriter'
import { flatten } from 'ramda'

import { SitemapIndex } from '../middlewares/generateMiddlewares/utils'
import { EXTENDED_INDEX_FILE, getBucket, hashString } from '../utils'

const STORE_SITEMAP_BUILD_FILE = '/dist/vtex.store-sitemap/build.json'

const LIST_LIMIT = 300
const MAX_PAGES = 50

const isValidRoute = (internalRoute: Internal) =>
  !internalRoute.disableSitemapEntry &&
  !internalRoute.type.startsWith('notFound') &&
  internalRoute.type !== 'product'

async function fetchInternalRoutes(ctx: Context, limit: number) {
  const {
    clients: { rewriter },
    vtex: { logger },
  } = ctx

  const internalRoutes = []
  let nextCursor
  let pageCount = 0

  do {
    pageCount++
    // eslint-disable-next-line no-await-in-loop
    const response: ListInternalsResponse = await rewriter.listInternalsWithRetry(
      limit,
      nextCursor
    )
    internalRoutes.push(...(response.routes?.filter(isValidRoute) ?? []))
    nextCursor = response.next

    if (pageCount >= MAX_PAGES && nextCursor) {
      logger.warn({
        message: 'Maximum page limit reached for internal routes',
        type: 'internal-routes-max-pages',
        pageCount,
        totalRoutes: internalRoutes.length,
        hasMorePages: true,
      })
      break
    }
  } while (nextCursor)

  return internalRoutes
}

async function fetchExtendedRoutes(ctx: Context) {
  const {
    state: { binding },
    clients: { vbase },
    vtex: { logger },
  } = ctx

  // Extended routes require a binding context
  if (!binding) {
    logger.info({
      message: 'Skipping extended routes fetch - no binding context available',
      type: 'extended-routes-skip',
    })
    return []
  }

  const extendedIndex = await vbase.getJSON<SitemapIndex>(
    getBucket('', hashString(binding.id)),
    EXTENDED_INDEX_FILE,
    true
  )

  const extendedEntries = extendedIndex?.index.map(
    entry => `/sitemap/${entry.replace(/^\//, '')}.xml`
  )

  return extendedEntries || []
}

export async function getUserRoutes(ctx: Context) {
  const [internalRoutes, extendedRoutes] = await Promise.all([
    fetchInternalRoutes(ctx, LIST_LIMIT),
    fetchExtendedRoutes(ctx),
  ])

  const validInternalRoutes = internalRoutes
    .filter(isValidRoute)
    .map(route => route.from)

  const userRoutes = [...validInternalRoutes, ...extendedRoutes]

  return userRoutes
}

export async function getAppsRoutes(ctx: Context) {
  const {
    clients: { apps },
  } = ctx

  const deps = await apps.getAppsMetaInfos()
  const routes = await Promise.all(
    deps.map(async dep => {
      const build = await apps.getAppJSON<{ entries: string[] }>(
        dep.id,
        STORE_SITEMAP_BUILD_FILE,
        true
      )

      return build?.entries || []
    })
  )

  return flatten<string>(routes)
}

```


[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 store-sitemap/node/jest/setEnvVars.js from the store-sitemap VTEX app.

[Assistant]
Here's the implementation:

```javascript
process.env.VTEX_APP_ID = 'vtex.store-sitemap@2.3.5'
process.env.VTEX_APP_VERSION = '2.3.5'
process.env.VTEX_APP_NAME = 'vtex.store-sitemap'
process.env.VTEX_APP_VENDOR = 'vtex'

```


[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 store-sitemap/node/middlewares/generateMiddlewares/generateProductRoutes.ts from the store-sitemap VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { Binding, Logger, VBase } from '@vtex/api'
import { isEmpty, zipObj } from 'ramda'
import { Product } from 'vtex.catalog-graphql'

import {
  getBucket,
  getStoreBindings,
  hashString,
  TENANT_CACHE_TTL_S,
} from '../../utils'
import { GraphQLServer, ProductNotFound } from '../../clients/graphqlServer'
import {
  createFileName,
  createTranslator,
  currentDate,
  filterBindingsBySalesChannel,
  GENERATE_PRODUCT_ROUTES_EVENT,
  getAccountSalesChannels,
  GROUP_ENTRIES_EVENT,
  initializeSitemap,
  Message,
  PRODUCT_ROUTES_INDEX,
  RAW_DATA_PREFIX,
  SitemapEntry,
  SitemapIndex,
  slugify,
  Translator,
} from './utils'

const PRODUCT_QUERY = `query Product($identifier: ProductUniqueIdentifier) {
  product(identifier: $identifier) @context(provider: "vtex.search-graphql") {
		productId
  }
}`

type ProductInfo = Array<[Binding, Product]>
type MessagesByBinding = Record<
  string,
  { bindingLocale: string; messages: Message[] }
>

const isProductSearchResponseEmpty = async (
  productId: string,
  graphqlServer: GraphQLServer,
  logger: Logger
) => {
  const searchResponse = await graphqlServer
    .query(
      PRODUCT_QUERY,
      { identifier: { field: 'id', value: productId } },
      {
        persistedQuery: {
          provider: 'vtex.search-graphql@0.x',
          sender: 'vtex.store-sitemap@2.x',
        },
      }
    )
    .catch(error => {
      if (error instanceof ProductNotFound) {
        return null
      }
      logger.error({
        error,
        message: 'Error in product search',
        productId,
      })
      return null
    })
  return searchResponse !== null
}

const getProductInfo = (
  storeBindings: Binding[],
  hasSalesChannels: boolean,
  ctx: EventContext
) => async (productId: string): Promise<ProductInfo | undefined> => {
  const {
    clients: { catalogGraphQL, graphqlServer },
    vtex: { logger },
  } = ctx
  const [catalogResponse, hasSearchResponse] = await Promise.all([
    catalogGraphQL.product(productId),
    isProductSearchResponseEmpty(productId, graphqlServer, logger),
  ])
  const product = catalogResponse?.product
  if (!product || !product.isActive || !hasSearchResponse) {
    return
  }

  const bindings = hasSalesChannels
    ? filterBindingsBySalesChannel(
        storeBindings,
        product.salesChannel as Product['salesChannel']
      )
    : storeBindings

  return bindings.map(binding => [binding, product] as [Binding, Product])
}

const getMessagesFromProductsInfo = (
  productsInfo: Array<ProductInfo | undefined>
) =>
  productsInfo.reduce(
    (acc, productInfo) => {
      const { messagesByBinding } = acc
      if (!productInfo) {
        acc.currentInvalidProducts++
        return acc
      }
      acc.currentProcessedProducts++
      productInfo.forEach(([binding, product]) => {
        const message: Message = {
          content: product.linkId!,
          context: product.id,
        }
        if (messagesByBinding[binding.id]) {
          messagesByBinding[binding.id].messages = messagesByBinding[
            binding.id
          ].messages.concat(message)
        } else {
          messagesByBinding[binding.id] = {
            bindingLocale: binding.defaultLocale,
            messages: [message],
          }
        }
      })
      return acc
    },
    {
      currentInvalidProducts: 0,
      currentProcessedProducts: 0,
      messagesByBinding: {} as MessagesByBinding,
    }
  )

const createRoutes = (
  messagesByBinding: MessagesByBinding,
  tenantLocale: string,
  pathsDictionary: Record<string, string>,
  translate: Translator
) => async (bindingId: string) => {
  const { messages, bindingLocale } = messagesByBinding[bindingId]
  const translatedSlugs = await translate(tenantLocale, bindingLocale, messages)
  const ids = messages.map(message => message.context)
  const pathById = zipObj(ids, translatedSlugs)
  const routes: Route[] = Object.keys(pathById).map(id => {
    const slug = pathById[id]
    const path = `/${slugify(slug).toLowerCase()}/p`
    const key = `${bindingId}_${id}`
    pathsDictionary[key] = path
    return { path, id }
  })
  return routes
}

const completeRoutes = (
  pathsDictionary: Record<string, string>,
  bindingIds: string[]
) => (route: Route): Route => {
  const alternates: AlternateRoute[] = bindingIds.map(id => ({
    bindingId: id,
    path: pathsDictionary[`${id}_${route.id}`],
  }))
  return {
    ...route,
    alternates,
  }
}

const saveRoutes = (
  routesByBinding: Record<string, Route[]>,
  pathsDictionary: Record<string, string>,
  bindingsIds: string[],
  entry: string,
  vbase: VBase
) => async (bindingId: string) => {
  const routes = routesByBinding[bindingId].map(
    completeRoutes(pathsDictionary, bindingsIds)
  )
  const bucket = getBucket(RAW_DATA_PREFIX, hashString(bindingId))
  const { index } = await vbase.getJSON<SitemapIndex>(
    bucket,
    PRODUCT_ROUTES_INDEX
  )
  index.push(entry)
  const lastUpdated = currentDate()
  await vbase.saveJSON<SitemapEntry>(bucket, entry, {
    lastUpdated,
    routes,
  })
  await vbase.saveJSON<SitemapIndex>(bucket, PRODUCT_ROUTES_INDEX, {
    index,
    lastUpdated,
  })
}

const pageWasProcessed = async (
  entry: string,
  bindingId: string,
  vbase: VBase
) => {
  const bucket = getBucket(RAW_DATA_PREFIX, hashString(bindingId))
  const { index } = await vbase.getJSON<SitemapIndex>(
    bucket,
    PRODUCT_ROUTES_INDEX
  )
  if (index.includes(entry)) {
    return true
  }
  return false
}

export async function generateProductRoutes(
  ctx: EventContext,
  next: () => Promise<void>
) {
  if (ctx.body.page === 1) {
    await initializeSitemap(ctx, PRODUCT_ROUTES_INDEX)
  }
  const {
    clients: { catalog, vbase, messages: messagesClient, tenant },
    body,
    vtex: { logger },
  } = ctx

  const tenantInfo = await tenant.info({
    forceMaxAge: TENANT_CACHE_TTL_S,
  })

  const {
    page,
    generationId,
    processedProducts,
    invalidProducts,
  }: ProductRoutesGenerationEvent = body!

  const entry = createFileName('product', page)
  const storeBindings = await getStoreBindings(tenant)
  const salesChannels = getAccountSalesChannels(storeBindings)
  const hasSalesChannels = !!salesChannels?.length

  if (await pageWasProcessed(entry, storeBindings[0].id, vbase)) {
    logger.error({ message: 'Page already processed', page })
    return
  }

  const {
    items,
    paging: { pages: totalPages, total },
  } = await catalog.getProductsIds(page, salesChannels)

  const productsWithError: Array<{
    productId: string
    error: unknown
  }> = []

  const getProductInfoFn = getProductInfo(storeBindings, hasSalesChannels, ctx)
  const productsInfo = await Promise.all(
    items.map((productId: { toString: () => string }) =>
      getProductInfoFn(productId.toString()).catch(error => {
        productsWithError.push({ productId: productId.toString(), error })
        return undefined
      })
    )
  )

  if (!isEmpty(productsWithError)) {
    logger.error({
      message: `Error in product search, ${productsWithError.length} failed when adding into sitemap`,
      productsWithError,
    })
  }

  const {
    currentInvalidProducts,
    currentProcessedProducts,
    messagesByBinding,
  } = getMessagesFromProductsInfo(productsInfo)

  const translate = createTranslator(messagesClient)
  const tenantLocale = tenantInfo.defaultLocale

  const pathsDictionary: Record<string, string> = {}
  const routesList = await Promise.all(
    Object.keys(messagesByBinding).map(
      createRoutes(messagesByBinding, tenantLocale, pathsDictionary, translate)
    )
  )

  const bindingsIds = Object.keys(messagesByBinding)
  const routesByBinding: Record<string, Route[]> = zipObj(
    bindingsIds,
    routesList
  )
  await Promise.all(
    Object.keys(routesByBinding).map(
      saveRoutes(routesByBinding, pathsDictionary, bindingsIds, entry, vbase)
    )
  )

  const payload: ProductRoutesGenerationEvent = {
    generationId,
    invalidProducts: invalidProducts + currentInvalidProducts,
    page: page + 1,
    processedProducts: processedProducts + currentProcessedProducts,
  }
  ctx.state.nextEvent = {
    event: GENERATE_PRODUCT_ROUTES_EVENT,
    payload,
  }
  if (page >= totalPages) {
    logger.info({
      invalidProducts: payload.invalidProducts,
      message: `Product routes complete`,
      page,
      processedProducts: payload.processedProducts,
      total,
      totalPages,
      type: 'product-routes',
    })
    ctx.state.nextEvent = {
      event: GROUP_ENTRIES_EVENT,
      payload: {
        from: 0,
        generationId,
        indexFile: PRODUCT_ROUTES_INDEX,
      },
    }
  }

  await next()
}

```


[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 store-sitemap/node/middlewares/generateMiddlewares/prepare.ts from the store-sitemap VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { CONFIG_BUCKET, GENERATION_CONFIG_FILE } from '../../utils'

export async function prepare(ctx: EventContext, next: () => Promise<void>) {
  const {
    body,
    vtex: { logger },
    clients: { vbase },
    state: { isCrossBorder },
  } = ctx

  const { generationId } = body

  if (!isCrossBorder) {
    logger.info({
      message: 'Skipping generation for non-cross-border tenant',
      payload: body,
    })

    return
  }

  if (!generationId) {
    logger.error({ message: 'Missing generation id', payload: body })
    return
  }
  const { generationId: currentGenerationId } = await vbase.getJSON<GenerationConfig>(CONFIG_BUCKET, GENERATION_CONFIG_FILE, true)
    || { generationId: null }
  if (generationId !== currentGenerationId) {
    logger.debug({ message: 'Invalid generation id', payload: body })
    return
  }
  await next()
}

```


[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 store-sitemap/node/middlewares/generateMiddlewares/generateRewriterRoutes.ts from the store-sitemap VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { Binding } from '@vtex/api'
import { path as Rpath, startsWith } from 'ramda'
import { Internal } from 'vtex.rewriter'

import { getBucket, getStoreBindings, hashString } from '../../utils'
import { Clients } from '../../clients/index'
import { Rewriter } from '../../clients/rewriter'
import {
  createFileName,
  currentDate,
  GENERATE_REWRITER_ROUTES_EVENT,
  GROUP_ENTRIES_EVENT,
  initializeSitemap,
  RAW_DATA_PREFIX,
  REWRITER_ROUTES_INDEX,
  SitemapEntry,
  SitemapIndex,
} from './utils'

const LIST_LIMIT = 300

type RoutesByBinding = Record<string, Record<string, Route[]>>

const createRoutesByBinding = (routes: Internal[], report: Record<string, number>, storeBindings: Binding[], disableRoutesTerm: string) => {
  const storeBindingsIds = storeBindings.map(({ id }) => id)
  return routes.reduce(
    (acc, internal) => {
      report[internal.type] = (report[internal.type] || 0) + 1
      let validRoute =
        !startsWith('notFound', internal.type) &&
        internal.type !== 'product' &&
        !internal.disableSitemapEntry &&
        storeBindingsIds.includes(internal.binding)
      if (disableRoutesTerm) {
        validRoute = validRoute && !internal.from.includes(disableRoutesTerm)
      }
      if (validRoute) {
        const { binding } = internal
        const bindingRoutes: Route[] = Rpath([binding, internal.type], acc) || []
        const route: Route = {
          id: internal.id,
          imagePath: internal.imagePath || undefined,
          imageTitle: internal.imageTitle || undefined,
          path: internal.from,
        }
        acc[binding] = {
          ...acc[binding] || {},
          [internal.type]: bindingRoutes.concat(route),
        }
      }
      return acc
    },
    {} as RoutesByBinding
  )
}

const completeRoute = (rewriter: Rewriter, type: string) => async (route: Route) => {
  const routesById = await rewriter.routesById({
    id: route.id,
    type,
  })
  const alternates = routesById.map(({ route: path, binding: bindingId }) => ({ path, bindingId }))
  return {
    ...route,
    alternates,
  }
}

const saveRoutes = (routesByBinding: RoutesByBinding, count: number, clients: Clients) => async (bindingId: string) => {
  const { vbase, rewriter } = clients
  const bucket = getBucket(RAW_DATA_PREFIX, hashString(bindingId))
  const groupedRoutes = routesByBinding[bindingId]
  const newEntries = await Promise.all(
    Object.keys(groupedRoutes).map(async entityType => {
      const entityRoutes = await Promise.all(
        routesByBinding[bindingId][entityType].map(completeRoute(rewriter, entityType))
      )
      const entry = createFileName(entityType, count)
      const lastUpdated = currentDate()
      await vbase.saveJSON<SitemapEntry>(bucket, entry, {
        lastUpdated,
        routes: entityRoutes,
      })
      return entry
    })
  )
  const { index } = await vbase.getJSON<SitemapIndex>(bucket, REWRITER_ROUTES_INDEX, true)
  await vbase.saveJSON<SitemapIndex>(bucket, REWRITER_ROUTES_INDEX, {
    index: [...index, ...newEntries],
    lastUpdated: currentDate(),
  })
}

export async function generateRewriterRoutes(
  ctx: EventContext,
  nextMiddleware: () => Promise<void>
) {
  if (!ctx.body.count) {
    await initializeSitemap(ctx, REWRITER_ROUTES_INDEX)
  }
  const {
    clients: { rewriter, tenant },
    body,
  } = ctx
  const {
    count,
    disableRoutesTerm,
    generationId,
    next,
    report,
  }: RewriterRoutesGenerationEvent = body!

  const response = await rewriter.listInternalsWithRetry(LIST_LIMIT, next)
  const routes: Internal[] = response.routes || []
  const responseNext = response.next

  const storeBindings = await getStoreBindings(tenant)
  const routesByBinding = createRoutesByBinding(routes, report, storeBindings, disableRoutesTerm)

  await Promise.all(
    Object.keys(routesByBinding).map(saveRoutes(routesByBinding, count, ctx.clients))
  )

  if (responseNext) {
    const payload: RewriterRoutesGenerationEvent = {
      count: count + 1,
      disableRoutesTerm,
      generationId,
      next: responseNext,
      report,
    }
    ctx.state.nextEvent = {
      event: GENERATE_REWRITER_ROUTES_EVENT,
      payload,
    }
  } else {
    ctx.vtex.logger.info({
      message: 'Rewriter routes complete',
      report,
      type: GENERATE_REWRITER_ROUTES_EVENT,
    })
    ctx.state.nextEvent = {
      event: GROUP_ENTRIES_EVENT,
      payload: {
        from: 0,
        generationId,
        indexFile: REWRITER_ROUTES_INDEX,
      },
    }
  }
  await nextMiddleware()
}

```


[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 store-sitemap/node/middlewares/generateMiddlewares/generateSitemap.ts from the store-sitemap VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { startSitemapGeneration } from '../../utils'
import { MultipleSitemapGenerationError } from './../../errors'

import {
  GENERATE_APPS_ROUTES_EVENT,
  GENERATE_PRODUCT_ROUTES_EVENT,
  GENERATE_REWRITER_ROUTES_EVENT,
} from './utils'

export async function generateSitemapFromREST(ctx: Context) {
  ctx.status = 200
  try {
    await startSitemapGeneration(ctx, ctx.query.__force !== undefined)
  } catch (err) {
    if (err instanceof MultipleSitemapGenerationError) {
      ctx.status = 202
      ctx.body = err.message
      return
    }
    throw err
  }
}

const DEFAULT_REWRITER_ROUTES_PAYLOAD = {
  count: 0,
  next: null,
  report: {},
}

export async function generateSitemap(ctx: EventContext) {
  const { clients: { events }, body: { generationId }, state: { settings } }  = ctx
  const disableRoutesTerm = settings.disableRoutesTerm
  if (settings.enableNavigationRoutes) {
    events.sendEvent('', GENERATE_REWRITER_ROUTES_EVENT, {
      ...DEFAULT_REWRITER_ROUTES_PAYLOAD,
      disableRoutesTerm,
      generationId,
    } as RewriterRoutesGenerationEvent)
  }

  if (settings.enableProductRoutes) {
    events.sendEvent('', GENERATE_PRODUCT_ROUTES_EVENT, {
      generationId,
      invalidProducts: 0,
      page: 1,
      processedProducts: 0,
    } as ProductRoutesGenerationEvent)
  }

  if (settings.enableAppsRoutes) {
    events.sendEvent('', GENERATE_APPS_ROUTES_EVENT, { generationId })
  }
}

```


[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 store-sitemap/node/middlewares/generateMiddlewares/generateAppsRoutes.ts from the store-sitemap VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { Apps, VBase } from '@vtex/api'
import { flatten, splitEvery } from 'ramda'

import {
  CONFIG_BUCKET,
  CONFIG_FILE,
  getBucket,
  hashString,
  TENANT_CACHE_TTL_S
} from '../../utils'
import {
  APPS_ROUTES_INDEX,
  completeRoutes,
  createFileName,
  currentDate,
  DEFAULT_CONFIG,
  SitemapEntry,
  SitemapIndex,
} from './utils'

const APP_ROUTES_ENTITY = 'appsRoutes'
const STORE_SITEMAP_BUILD_FILE =`/dist/vtex.store-sitemap/build.json`
const FILE_LIMIT = 5000

interface SitemapBuild {
  entries: string[]
}

const getRoutes = async (apps: Apps): Promise<string[]> => {
  const deps = await apps.getAppsMetaInfos()
  const routes = await Promise.all(
    deps.map(async dep => {
      const build = await apps.getAppJSON<SitemapBuild | null>(dep.id, STORE_SITEMAP_BUILD_FILE, true)
      return build ? build.entries || [] : []
    }
    ))
  return flatten<string>(routes)
}

const saveRoutes = async (routes: string[], idx: number, bucket: string, vbase: VBase) => {
  const sitemapRoutes = routes.map(route => ({ id: route, path: route }))
  const entry = createFileName(APP_ROUTES_ENTITY, idx)
  await vbase.saveJSON<SitemapEntry>(bucket, entry, {
    lastUpdated: currentDate(),
    routes: sitemapRoutes,
  })
  return entry
}

export async function generateAppsRoutes(ctx: EventContext) {
  const { clients: { apps, tenant, vbase }, vtex: { logger } } = ctx

  const { bindings } = await tenant.info({
    forceMaxAge: TENANT_CACHE_TTL_S,
  })
  const { generationPrefix } = await vbase.getJSON<Config>(CONFIG_BUCKET, CONFIG_FILE, true) || DEFAULT_CONFIG

  const appsRoutes = await getRoutes(apps)

  await Promise.all(bindings.map(async binding => {
    const bucket = getBucket(generationPrefix, hashString(binding.id))
    const splittedRoutes = splitEvery(FILE_LIMIT, appsRoutes)
    const index = await Promise.all(splittedRoutes.map((routes, idx) => saveRoutes(routes, idx, bucket, vbase)))
    await vbase.saveJSON<SitemapIndex>(bucket, APPS_ROUTES_INDEX, {
      index,
      lastUpdated: currentDate(),
    })
  }))

  await completeRoutes(APPS_ROUTES_INDEX, vbase)
  logger.info({
    message: 'Apps routes complete',
    numberOfroutes: appsRoutes.length,
    type: 'apps-routes',
  })
}

```


[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 store-sitemap/node/middlewares/generateMiddlewares/utils.ts from the store-sitemap VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { Binding, LINKED, Logger, Tenant, VBase } from '@vtex/api'
import { uniqBy } from 'ramda'
import { Product, SalesChannel } from 'vtex.catalog-graphql'

import { Messages } from '../../clients/messages'
import { CONFIG_BUCKET, getBucket, hashString, TENANT_CACHE_TTL_S } from '../../utils'

export const RAW_DATA_PREFIX = `${LINKED ? 'L' : ''}C`

export const REWRITER_ROUTES_INDEX = 'rewriterRoutesIndex.json'
export const PRODUCT_ROUTES_INDEX = 'productRoutesIndex.json'
export const APPS_ROUTES_INDEX = 'appsRoutesIndex.json'

export const GENERATE_SITEMAP_EVENT = 'sitemap.generate'
export const GENERATE_REWRITER_ROUTES_EVENT = 'sitemap.generate:rewriter-routes'
export const GENERATE_PRODUCT_ROUTES_EVENT = 'sitemap.generate:product-routes'
export const GENERATE_APPS_ROUTES_EVENT = 'sitemap.generate:apps-routes'
export const GROUP_ENTRIES_EVENT = 'sitemap.generate:group-entries'

export const DEFAULT_CONFIG: Config = {
  generationPrefix: `${LINKED ? 'L' : ''}B`,
  productionPrefix: `${LINKED ? 'L' : ''}A`,
}

export interface SitemapIndex {
  index: string[]
  lastUpdated: string
}

export interface SitemapEntry {
  routes: Route[]
  lastUpdated: string
}

export interface Message {
  content: string
  context: string
}

export const uniq = <T>(array: T[]) => array.filter((value, idx) => array.indexOf(value) === idx)

export const createFileName = (entity: string, count: number) => `${entity}-${count}`

export const splitFileName = (file: string) => file.split('-')

export const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))

export const currentDate = (): string => new Date().toISOString()

export const initializeSitemap = async (ctx: EventContext, indexFile: string) => {
  const { tenant, vbase } = ctx.clients
  const { bindings } = await tenant.info({
    forceMaxAge: TENANT_CACHE_TTL_S,
  })

  await Promise.all(bindings.map(
    binding => vbase.saveJSON<SitemapIndex>(getBucket(RAW_DATA_PREFIX, hashString(binding.id)), indexFile, {
      index: [] as string[],
      lastUpdated: '',
    })
  ))
}

export const getAccountSalesChannels = (
  bindings: Binding[]
): string[] | undefined => {
  const salesChannels = bindings.reduce((acc, binding) => {
    const bindingSC: number | undefined =
      binding.extraContext.portal?.salesChannel
    if (bindingSC) {
      acc.push(bindingSC.toString())
    }
    return acc
  }, [] as string[])
  // Has one binding with one sales channel
  if (bindings.length === 1 && salesChannels.length === 1) {
    return
  }
  return uniqBy(i => i, salesChannels)
}

export const filterBindingsBySalesChannel = (
  storeBindings: Binding[],
  salesChannels: Product['salesChannel']
): Tenant['bindings'] => {
  const salesChannelsSet = salesChannels?.reduce((acc: Set<string>, sc: Maybe<SalesChannel>) => {
    if (sc?.id) {
      acc.add(sc.id)
    }
    return acc
  }, new Set<string>())

  return storeBindings.filter(binding => {
    const bindingSC: number | undefined =
      binding.extraContext.portal?.salesChannel
    const productActiveInBindingSC =
      bindingSC && salesChannelsSet?.has(bindingSC.toString())
    if (productActiveInBindingSC || !salesChannelsSet) {
      return true
    }
    return false
  })
}

export const slugify = (str: string) =>
  str
    .toLowerCase()
    .normalize('NFD')
    .replace(/[\u0300-\u036f]/g, '')
    .replace(/[*+~.()'"!:@&\[\]`,/ %$#?{}|><=_^]/g, '-')

export type Translator = (from: string, to: string, messages: Message[]) => Promise<string[]>

export const createTranslator = (service: Messages) => async (
  from: string,
  to: string,
  messages: Message[]
): Promise<string[]> => {
  if (from.toLowerCase() === to.toLowerCase()) {
    return messages.map(({ content }) => content)
  }
  const translations = await service.translateNoCache({
    indexedByFrom: [
      {
        from,
        messages,
      },
    ],
    to,
  })
  return translations
}

export const isSitemapComplete = async (enabledIndexFiles: string[], vbase: VBase, logger: Logger) => {
  const indexFiles = await Promise.all(enabledIndexFiles.map(
    indexFile =>
      vbase.getJSON(CONFIG_BUCKET, indexFile, true)
  ))
  logger.debug({
    enabledIndexFiles,
    indexFiles,
  })
  return allTruthy(indexFiles)
}

export const completeRoutes = async (file: string, vbase: VBase) =>
  vbase.saveJSON(CONFIG_BUCKET, file, 'OK')

export const cleanConfigBucket = async (enabledIndexFiles: string[], vbase: VBase) =>
  Promise.all([
    ...enabledIndexFiles.map(
      indexFile => vbase.deleteFile(CONFIG_BUCKET, indexFile)),
  ])

const allTruthy = <T>(array: T[]) => !array.some(e => !e)

```


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

[User] Show me the implementation of store-sitemap/node/middlewares/generateMiddlewares/generateSitemap.test.ts from the store-sitemap VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { Events, IOContext, Logger } from '@vtex/api'
import * as TypeMoq from 'typemoq'

import { Clients } from '../../clients'
import { generateSitemap } from './generateSitemap'
import {
  GENERATE_APPS_ROUTES_EVENT,
  GENERATE_PRODUCT_ROUTES_EVENT,
  GENERATE_REWRITER_ROUTES_EVENT,
} from './utils'

const eventsTypeMock = TypeMoq.Mock.ofInstance(Events)
const contextMock = TypeMoq.Mock.ofType<EventContext>()
const ioContext = TypeMoq.Mock.ofType<IOContext>()
const state = TypeMoq.Mock.ofType<State>()
const loggerMock = TypeMoq.Mock.ofType<Logger>()

const eventSent = jest.fn()

const DEFAULT_APPS_ROUTES_PAYLOAD = {
  generationId: '1',
}

const DEFAULT_REWRITER_ROUTES_PAYLOAD = {
  count: 0,
  generationId: '1',
  next: null,
  report: {},
}

const DEFAULT_PRODUCT_ROUTES_PAYLOAD: ProductRoutesGenerationEvent = {
  generationId: '1',
  invalidProducts: 0,
  page: 1,
  processedProducts: 0,
}

describe('Test generate sitemap', () => {
  let context: EventContext
  const events = class EventsMock extends eventsTypeMock.object {
    constructor() {
      super(ioContext.object)
    }

    public sendEvent = async (
      _: any,
      route: string,
      message?: any,
      __?: any
    ) => {
      eventSent(_, route, message)
    }
  }

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

    jest.clearAllMocks()

    context = {
      ...contextMock.object,
      body: {
        generationId: '1',
      },
      clients: new ClientsImpl({}, ioContext.object),
      state: {
        ...state.object,
        settings: {
          disableRoutesTerm: '',
          enableAppsRoutes: true,
          enableNavigationRoutes: true,
          enableProductRoutes: true,
          ignoreBindings: false,
        },
      },
      vtex: {
        ...ioContext.object,
        logger: loggerMock.object,
      },
    }
  })

  it('Should send both events', async () => {
    await generateSitemap(context)
    expect(eventSent).toHaveBeenCalledWith(
      '',
      GENERATE_REWRITER_ROUTES_EVENT,
      DEFAULT_REWRITER_ROUTES_PAYLOAD
    )
    expect(eventSent).toHaveBeenCalledWith(
      '',
      GENERATE_PRODUCT_ROUTES_EVENT,
      DEFAULT_PRODUCT_ROUTES_PAYLOAD
    )
    expect(eventSent).toHaveBeenCalledWith(
      '',
      GENERATE_APPS_ROUTES_EVENT,
      DEFAULT_APPS_ROUTES_PAYLOAD
    )
  })

  it('Should send only enabled events', async () => {
    context.state.settings = {
      disableRoutesTerm: '',
      enableAppsRoutes: true,
      enableNavigationRoutes: true,
      enableProductRoutes: false,
      ignoreBindings: false,
    }

    await generateSitemap(context)
    expect(eventSent).toHaveBeenCalledWith(
      '',
      GENERATE_REWRITER_ROUTES_EVENT,
      DEFAULT_REWRITER_ROUTES_PAYLOAD
    )
    expect(eventSent).toHaveBeenCalledWith(
      '',
      GENERATE_APPS_ROUTES_EVENT,
      DEFAULT_APPS_ROUTES_PAYLOAD
    )
    expect(eventSent).toHaveBeenCalledTimes(2)

    jest.clearAllMocks()
    context.state.settings = {
      disableRoutesTerm: '',
      enableAppsRoutes: false,
      enableNavigationRoutes: false,
      enableProductRoutes: true,
      ignoreBindings: false,
    }

    await generateSitemap(context)
    expect(eventSent).toHaveBeenCalledWith(
      '',
      GENERATE_PRODUCT_ROUTES_EVENT,
      DEFAULT_PRODUCT_ROUTES_PAYLOAD
    )
    expect(eventSent).toHaveBeenCalledTimes(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 store-sitemap/node/middlewares/generateMiddlewares/sendNextEvent.ts from the store-sitemap VTEX app.

[Assistant]
Here's the implementation:

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

export async function sendNextEvent(ctx: EventContext) {
  const { clients: { events }, state: { nextEvent } } = ctx
  const { payload, event } = nextEvent
  const timeToSleep = Math.ceil(Math.random() * 100)
  await sleep(timeToSleep)
  events.sendEvent('', event, payload)
}

```


[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 store-sitemap/node/middlewares/generateMiddlewares/isCrossBorder.ts from the store-sitemap VTEX app.

[Assistant]
Here's the implementation:

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

export async function isCrossBorder(
  ctx: Context | EventContext,
  next: () => Promise<void>
) {
  const {
    clients: { tenant },
    state,
  } = ctx

  const storefrontsBindings = await getStoreBindings(tenant)
  state.isCrossBorder = storefrontsBindings?.length > 1

  await next()
}

```


[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 store-sitemap/node/middlewares/generateMiddlewares/prepare.test.ts from the store-sitemap VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { IOContext, Logger, VBase, VBaseSaveResponse } from '@vtex/api'
import * as TypeMoq from 'typemoq'
import { Clients } from '../../clients'
import { CONFIG_BUCKET, GENERATION_CONFIG_FILE } from '../../utils'
import { prepare } from './prepare'

const vbaseTypeMock = TypeMoq.Mock.ofInstance(VBase)
const contextMock = TypeMoq.Mock.ofType<EventContext>()
const ioContext = TypeMoq.Mock.ofType<IOContext>()
const state = TypeMoq.Mock.ofType<State>()
const loggerMock = TypeMoq.Mock.ofType<Logger>()

const next = jest.fn()

describe('Test generation prepare', () => {
  let context: EventContext

  const vbase = class VBaseMock extends vbaseTypeMock.object {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    private jsonData: Record<string, any> = {}

    constructor() {
      super(ioContext.object)
    }

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

    public saveJSON = async <T>(
      bucket: string,
      file: string,
      data: T
    ): Promise<VBaseSaveResponse> => {
      if (!this.jsonData[bucket]) {
        this.jsonData[bucket] = {}
      }
      this.jsonData[bucket][file] = data
      return Promise.resolve([{ path: file, hash: 'mocked-hash' }])
    }
  }
  beforeEach(() => {
    // tslint:disable-next-line:max-classes-per-file
    const ClientsImpl = class ClientsMock extends Clients {
      get vbase() {
        return this.getOrSet('vbase', vbase)
      }
    }
    context = {
      ...contextMock.object,
      body: {
        generationId: '1',
      },
      clients: new ClientsImpl({}, ioContext.object),
      state: {
        ...state.object,
      },
      vtex: {
        ...ioContext.object,
        logger: loggerMock.object,
      },
    }
  })

   it('Shouldnt continue if genereationId isnt found', async () => {
     const thisContext = {
       ...context,
       body: {
         generationId: undefined,
       },
     }
     await prepare(thisContext, next)
     expect(next).toHaveBeenCalledTimes(0)
   })

   it('Shouldnt continue if generationId is not the one in VBae', async () => {
    const { vbase: vbaseClient } = context.clients
    await vbaseClient.saveJSON(CONFIG_BUCKET, GENERATION_CONFIG_FILE, {
      generationId: '2',
    })
    await prepare(context, next)
    expect(next).toHaveBeenCalledTimes(0)
  })

  it('Shouldnt continue if no config file is found', async () => {
    await prepare(context, next)
    expect(next).toHaveBeenCalledTimes(0)
  })

  it('Should continue if generationId matches the config', async () => {
    const { vbase: vbaseClient } = context.clients
    await vbaseClient.saveJSON(CONFIG_BUCKET, GENERATION_CONFIG_FILE, {
      generationId: '1',
    })
    await prepare(context, next)
    expect(next).toBeCalled()
  })

})

```


[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 store-sitemap/node/middlewares/generateMiddlewares/generateCustomRoutes.ts from the store-sitemap VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { getDefaultStoreBinding } from '../../resources/bindings'
import { clearCustomRoutesGenerationLock } from '../customRoutes'
import { getAppsRoutes, getUserRoutes } from '../../services/routes'
import { CUSTOM_ROUTES_BUCKET, CUSTOM_ROUTES_FILENAME } from '../../utils'

export async function generateCustomRoutes(ctx: Context) {
  const {
    clients: { vbase, tenant },
    vtex: { logger },
  } = ctx

  const startTime = process.hrtime()

  try {
    logger.info({
      message: 'Starting custom routes generation',
      type: 'custom-routes-generation',
    })

    // Get default store binding for the account
    const defaultBindingId = await getDefaultStoreBinding(ctx)

    // Get tenant info to construct binding object
    const tenantInfo = await tenant.info()
    const binding = tenantInfo.bindings?.find(b => b.id === defaultBindingId)

    // Create a minimal context-like object for the route functions
    // Always include binding in state, even if null (extended routes require it)
    const routeCtx = {
      clients: ctx.clients,
      state: {
        ...ctx.state,
        binding: binding ?? null,
      },
      vtex: ctx.vtex,
    } as Context

    const [appsRoutes, userRoutes] = await Promise.all([
      getAppsRoutes(routeCtx),
      getUserRoutes(routeCtx),
    ])

    logger.info({
      message: 'Routes fetched',
      appsRoutesCount: appsRoutes.length,
      userRoutesCount: userRoutes.length,
    })

    const customRoutesData: CustomRoutesData = {
      timestamp: Date.now(),
      data: [
        { name: 'apps-routes', routes: appsRoutes },
        { name: 'user-routes', routes: userRoutes },
      ],
    }

    // Save to VBase using custom-routes bucket
    await vbase.saveJSON<CustomRoutesData>(
      CUSTOM_ROUTES_BUCKET,
      CUSTOM_ROUTES_FILENAME,
      customRoutesData
    )

    logger.info({
      message: 'Custom routes saved',
      appsRoutesCount: appsRoutes.length,
      userRoutesCount: userRoutes.length,
      fileName: CUSTOM_ROUTES_FILENAME,
    })

    // Clear generation lock after successful completion
    await clearCustomRoutesGenerationLock(vbase, logger)

    const timeDiff = process.hrtime(startTime)
    logger.info({
      message: 'Custom routes generation complete',
      type: 'custom-routes-generation-complete',
      duration: timeDiff,
    })
  } catch (error) {
    // Clear generation lock on error as well
    await clearCustomRoutesGenerationLock(vbase, logger)

    const timeDiff = process.hrtime(startTime)
    logger.error({
      message: 'Error generating custom routes',
      error,
      type: 'custom-routes-generation-error',
      duration: timeDiff,
    })
    throw error
  }
}

```


[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 store-sitemap/node/middlewares/generateMiddlewares/groupEntries.test.ts from the store-sitemap VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { IOContext, Logger, RequestConfig, Tenant, TenantClient, VBase, VBaseSaveResponse } from '@vtex/api'
import * as TypeMoq from 'typemoq'

import { Clients } from '../../clients'
import {
  CONFIG_BUCKET,
  GENERATION_CONFIG_FILE,
  getBucket,
  hashString,
  STORE_PRODUCT
} from './../../utils'
import { FILE_LIMIT, FILE_PROCESS_LIMIT, groupEntries } from './groupEntries'
import {
  DEFAULT_CONFIG,
  GROUP_ENTRIES_EVENT,
  PRODUCT_ROUTES_INDEX,
  RAW_DATA_PREFIX,
  REWRITER_ROUTES_INDEX,
  SitemapEntry,
  SitemapIndex
} from './utils'

const vbaseTypeMock = TypeMoq.Mock.ofInstance(VBase)
const contextMock = TypeMoq.Mock.ofType<EventContext>()
const ioContext = TypeMoq.Mock.ofType<IOContext>()
const state = TypeMoq.Mock.ofType<State>()
const loggerMock = TypeMoq.Mock.ofType<Logger>()
const tenantTypeMock = TypeMoq.Mock.ofInstance(TenantClient)

const BANANA_PRODUCT_ROUTE = {
  alternates: [
    { bindingId: '1', path: '/banana/p' },
  ],
  id: '1',
  path: '/banana/p',
}

const APPLE_PRODUCT_ROUTE = {
  alternates: [
    { bindingId: '1', path: '/apple/p' },
  ],
  id: '2',
  path: '/apple/p',
}

let next: any

describe('Test group entries', () => {
  let context: EventContext
  let jsonData: Record<string, any> = {}

  const vbase = class VBaseMock extends vbaseTypeMock.object {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any

    constructor() {
      super(ioContext.object)
    }

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

    public saveJSON = async <T>(
      bucket: string,
      file: string,
      data: T
    ): Promise<VBaseSaveResponse> => {
      if (!jsonData[bucket]) {
       jsonData[bucket] = {}
      }
      jsonData[bucket][file] = data
      return Promise.resolve(({
        updated: true,
      } as unknown) as VBaseSaveResponse)
    }

    public deleteFile = async (bucket: string, file: string, _: any, __: any): Promise<any> => {
      jsonData[bucket][file] = undefined
      return
    }
  }

  // tslint:disable-next-line:max-classes-per-file
  const tenant = class TenantMock extends tenantTypeMock.object {
    constructor() {
      super(ioContext.object)
    }

    public info = async (_?: RequestConfig) => {
      return {
        bindings: [
          {
            id: '1',
            targetProduct: STORE_PRODUCT,
          },
        ],
      } as Tenant
    }
  }

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

      get vbaseWithCache() {
        return this.getOrSet('vbaseWithCache', vbase)
      }

      get tenant() {
        return this.getOrSet('tenant', tenant)
      }
    }

    jest.clearAllMocks()
    next = jest.fn()
    jsonData = {}

    context = {
      ...contextMock.object,
      clients: new ClientsImpl({}, ioContext.object),
      state: {
        ...state.object,
        enabledIndexFiles: [REWRITER_ROUTES_INDEX, PRODUCT_ROUTES_INDEX],
      },
      vtex: {
        ...ioContext.object,
        logger: loggerMock.object,
      },
    }
    const rawBucket = getBucket(RAW_DATA_PREFIX, hashString('1'))
    const { vbase: vbaseClient } = context.clients
    vbaseClient.saveJSON(rawBucket, PRODUCT_ROUTES_INDEX, { index: [] })
    vbaseClient.saveJSON(rawBucket, REWRITER_ROUTES_INDEX, { index: [] })
  })

  it('Should complete', async () => {
    const { vbase: vbaseClient } = context.clients

    context.body = { from: 0, indexFile: PRODUCT_ROUTES_INDEX }
    await groupEntries(context, next)

    let productCompleteFile = await vbaseClient.getJSON(CONFIG_BUCKET, PRODUCT_ROUTES_INDEX)
    expect(productCompleteFile).toBe('OK')

    context.body = { from: 0, indexFile: REWRITER_ROUTES_INDEX }
    await groupEntries(context, next)

    productCompleteFile = await vbaseClient.getJSON(CONFIG_BUCKET, PRODUCT_ROUTES_INDEX, true)
    const rewriterCompleteFile = await vbaseClient.getJSON(CONFIG_BUCKET, REWRITER_ROUTES_INDEX, true)
    const configCompleteFile = await vbaseClient.getJSON(CONFIG_BUCKET, GENERATION_CONFIG_FILE, true)
    expect(productCompleteFile).toBeNull()
    expect(rewriterCompleteFile).toBeNull()
    expect(configCompleteFile).toBeNull()
  })

  it('Should complete if enabled files were processed', async () => {
    const { vbase: vbaseClient } = context.clients
    context.state.enabledIndexFiles = [PRODUCT_ROUTES_INDEX]

    context.body = {  from: 0, indexFile: PRODUCT_ROUTES_INDEX }
    await groupEntries(context, next)

    const productCompleteFile = await vbaseClient.getJSON(CONFIG_BUCKET, PRODUCT_ROUTES_INDEX, true)
    let configCompleteFile = await vbaseClient.getJSON(CONFIG_BUCKET, GENERATION_CONFIG_FILE, true)
    expect(productCompleteFile).toBeNull()
    expect(configCompleteFile).toBeNull()

    context.body = { from: 0, indexFile: REWRITER_ROUTES_INDEX }
    context.state.enabledIndexFiles = [REWRITER_ROUTES_INDEX]
    await groupEntries(context, next)

    const rewriterCompleteFile = await vbaseClient.getJSON(CONFIG_BUCKET, REWRITER_ROUTES_INDEX, true)
    configCompleteFile = await vbaseClient.getJSON(CONFIG_BUCKET, GENERATION_CONFIG_FILE, true)
    expect(rewriterCompleteFile).toBeNull()
    expect(configCompleteFile).toBeNull()
  })

  it('Should group data', async () => {
    const { vbase: vbaseClient } = context.clients
    context.body = {  from: 0, indexFile: PRODUCT_ROUTES_INDEX }
    const { generationPrefix } = DEFAULT_CONFIG

    // Saves two product routes in different files
    const rawBucket = getBucket(RAW_DATA_PREFIX, hashString('1'))
    await vbaseClient.saveJSON(rawBucket, 'product-0', { routes: [BANANA_PRODUCT_ROUTE] })
    await vbaseClient.saveJSON(rawBucket, 'product-1', { routes: [APPLE_PRODUCT_ROUTE] })
    await vbaseClient.saveJSON(rawBucket, PRODUCT_ROUTES_INDEX, { index: ['product-0', 'product-1'] })

    await groupEntries(context, next)
    const bucket = getBucket(generationPrefix, hashString('1'))
    const { index } = await vbaseClient.getJSON<SitemapIndex>(bucket, PRODUCT_ROUTES_INDEX, true)
    const expectedIndex = ['product-0']
    expect(index).toStrictEqual(expectedIndex)
    const { routes } = await vbaseClient.getJSON<SitemapEntry>(bucket, expectedIndex[0])
    expect(routes).toStrictEqual([BANANA_PRODUCT_ROUTE, APPLE_PRODUCT_ROUTE])
  })

  it('Should create new file if one gets too big', async () => {
    const { vbase: vbaseClient } = context.clients
    context.body = { from: 0, indexFile: PRODUCT_ROUTES_INDEX }
    const { generationPrefix } = DEFAULT_CONFIG

    // Saves two product routes in different files
    const rawBucket = getBucket(RAW_DATA_PREFIX, hashString('1'))
    const tooManyRoutes = []
    for (let i = 0; i <= FILE_LIMIT; i++) {
      tooManyRoutes.push(
        {
          alternates: [
            { bindingId: '1', path: `/banana-${i}/p` },
          ],
          id: i,
          path: `/banana-${i}/p`,
        }
      )
    }
    await vbaseClient.saveJSON(rawBucket, 'product-0', { routes: tooManyRoutes })
    await vbaseClient.saveJSON(rawBucket, PRODUCT_ROUTES_INDEX, { index: ['product-0'] })

    await groupEntries(context, next)

    const bucket = getBucket(generationPrefix, hashString('1'))
    const { index } = await vbaseClient.getJSON<SitemapIndex>(bucket, PRODUCT_ROUTES_INDEX, true)
    const expectedIndex = ['product-0', 'product-1']
    expect(index).toStrictEqual(expectedIndex)

    const { routes: routes0 } = await vbaseClient.getJSON<SitemapEntry>(bucket, expectedIndex[0])
    expect(routes0.length).toEqual(FILE_LIMIT)

    const { routes: routes1 } = await vbaseClient.getJSON<SitemapEntry>(bucket, expectedIndex[1])
    expect(routes1.length).toEqual(1)
  })

  it('Next event is sent', async () => {
    const { vbase: vbaseClient } = context.clients

    // Saves a lot of routes
    const index = []
    const rawBucket = getBucket(RAW_DATA_PREFIX, hashString('1'))
    for (let i = 0; i <= FILE_PROCESS_LIMIT + 1; i++) {
      const file = `product-${i}`
      await vbaseClient.saveJSON(rawBucket, file, { routes: [BANANA_PRODUCT_ROUTE] })
      index.push(file)
    }
    await vbaseClient.saveJSON(rawBucket, PRODUCT_ROUTES_INDEX, { index })

    context.body = { from: 0, indexFile: PRODUCT_ROUTES_INDEX }
    await groupEntries(context, next)
    expect(next).toBeCalledWith()
    const { event, payload } = context.state.nextEvent
    expect(event).toEqual(GROUP_ENTRIES_EVENT)
    expect((payload as any)).toEqual({ from: FILE_PROCESS_LIMIT, indexFile: PRODUCT_ROUTES_INDEX })
  })

  it('Groups correctly over mutiple events', async () => {
    const { vbase: vbaseClient } = context.clients
    const { generationPrefix } = DEFAULT_CONFIG

    // Saves a lot of routes
    const index = []
    const rawBucket = getBucket(RAW_DATA_PREFIX, hashString('1'))
    for (let i = 0; i <= FILE_PROCESS_LIMIT + 1; i++) {
      const file = `product-${i}`
      await vbaseClient.saveJSON(rawBucket, file, { routes: [BANANA_PRODUCT_ROUTE] })
      index.push(file)
    }
    await vbaseClient.saveJSON(rawBucket, PRODUCT_ROUTES_INDEX, { index })

    context.body = { from: 0, indexFile: PRODUCT_ROUTES_INDEX }
    await groupEntries(context, next)
    context.body = { from: FILE_PROCESS_LIMIT , indexFile: PRODUCT_ROUTES_INDEX }
    await groupEntries(context, next)

    const productCompleteFile = await vbaseClient.getJSON(CONFIG_BUCKET, PRODUCT_ROUTES_INDEX)
    expect(productCompleteFile).toBe('OK')

    const bucket = getBucket(generationPrefix, hashString('1'))
    const { index: finalIndex } = await vbaseClient.getJSON<SitemapIndex>(bucket, PRODUCT_ROUTES_INDEX, true)
    const expectedIndex = ['product-0']
    expect(finalIndex).toStrictEqual(expectedIndex)
  })

  it('Should initialize', async () => {
    const { vbase: vbaseClient } = context.clients
    context.body = { from: 0, indexFile: PRODUCT_ROUTES_INDEX }
    const { generationPrefix } = DEFAULT_CONFIG

    const bucket = getBucket(generationPrefix, hashString('1'))
    await vbaseClient.saveJSON(bucket, PRODUCT_ROUTES_INDEX, { index: ['product-0', 'product-1'] })

    // Saves two product routes in different files
    const rawBucket = getBucket(RAW_DATA_PREFIX, hashString('1'))
    await vbaseClient.saveJSON(rawBucket, 'product-0', { routes: [BANANA_PRODUCT_ROUTE] })
    await vbaseClient.saveJSON(rawBucket, 'product-1', { routes: [APPLE_PRODUCT_ROUTE] })
    await vbaseClient.saveJSON(rawBucket, PRODUCT_ROUTES_INDEX, { index: ['product-0', 'product-1'] })

    await groupEntries(context, next)

    const { index } = await vbaseClient.getJSON<SitemapIndex>(bucket, PRODUCT_ROUTES_INDEX, true)
    const expectedIndex = ['product-0']
    expect(index).toStrictEqual(expectedIndex)
  })
})

```