@graphcommerce/algolia-products 9.1.0-canary.28 → 9.1.0-canary.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -78,7 +78,7 @@ query GetAlgoliaSettings($indexName: String!) {
78
78
  }
79
79
  }
80
80
  advancedSyntax
81
- optionalWords
81
+ # optionalWords
82
82
  disableExactOnAttributes
83
83
  exactOnSingleWordQuery
84
84
  alternativesAsExact
package/index.ts CHANGED
@@ -1,8 +1,3 @@
1
- export * from './mesh/algoliaFacetsToAggregations'
2
- export * from './mesh/algoliaHitToMagentoProduct'
3
- export * from './mesh/getSearchResults'
4
- export * from './mesh/getSearchResultsInput'
5
- export * from './mesh/getSearchSuggestions'
6
- export * from './mesh/getSearchSuggestionsInput'
1
+ export * from './mesh'
7
2
  export * from './hooks/useAlgoliaIndexName'
8
3
  export * from './hooks/useAlgoliaQueryContext'
@@ -6,6 +6,7 @@ import type {
6
6
  MeshContext,
7
7
  } from '@graphcommerce/graphql-mesh'
8
8
  import type { AttributeList } from './getAttributeList'
9
+ import { getIndexName } from './getIndexName'
9
10
  import type { GetStoreConfigReturn } from './getStoreConfig'
10
11
 
11
12
  type AlgoliaFacets = { [facetName: string]: AlgoliaFacetOption }
@@ -42,72 +43,24 @@ function compare(a, b) {
42
43
  }
43
44
 
44
45
  /** @public */
45
- export function algoliaPricesToPricesAggregations(pricesList: {
46
- [key: string]: number
47
- }): AggregationOption[] {
46
+ export function algoliaPricesToPricesAggregations(
47
+ pricesList: AlgoliaFacetOption,
48
+ ): AggregationOption[] {
48
49
  const priceArraylist: { value: number; count: number }[] = Object.entries(pricesList)
49
50
  .sort(compare)
50
- .map((price) => {
51
- const value: number = +price[0]
52
- return { value, count: price[1] }
53
- })
54
-
55
- const interval = Math.round(
56
- (priceArraylist[priceArraylist.length - 1].value - priceArraylist[0].value) / 2,
57
- )
58
-
59
- const pricesBucket: { [key: number]: { count: number; value: string; label: string } } = {}
60
- let increasingInterval = interval
61
- priceArraylist.forEach((price) => {
62
- if (price.value <= increasingInterval) {
63
- if (!pricesBucket[increasingInterval]) {
64
- pricesBucket[increasingInterval] = {
65
- count: price.count,
66
- value:
67
- increasingInterval === interval
68
- ? `0_${interval}`
69
- : `${increasingInterval - interval}_${increasingInterval}`,
70
- label:
71
- increasingInterval === interval
72
- ? `0_${interval}`
73
- : `${increasingInterval - interval}-${increasingInterval}`,
74
- }
75
- } else {
76
- pricesBucket[increasingInterval].count += price.count
77
- }
78
- } else {
79
- increasingInterval += interval
80
- pricesBucket[increasingInterval] = {
81
- count: price.count,
82
- value:
83
- increasingInterval === interval
84
- ? `0_${interval}`
85
- : `${increasingInterval - interval}_${increasingInterval}`,
86
- label:
87
- increasingInterval === interval
88
- ? `0-${interval}`
89
- : `${increasingInterval - interval}-${increasingInterval}`,
90
- }
91
- }
92
- if (
93
- price.value === increasingInterval &&
94
- priceArraylist[priceArraylist.length - 1].value !== price.value
95
- ) {
96
- increasingInterval += interval
97
- pricesBucket[increasingInterval] = {
98
- count: price.count,
99
- value:
100
- increasingInterval === interval
101
- ? `0_${interval}`
102
- : `${increasingInterval - interval}_${increasingInterval}`,
103
- label:
104
- increasingInterval === interval
105
- ? `0_${interval}`
106
- : `${increasingInterval - interval}-${increasingInterval}`,
107
- }
108
- }
109
- })
110
- return Object.values(pricesBucket)
51
+ .map((price) => ({ value: Number(+price[0]), count: price[1] }))
52
+
53
+ let minValue = priceArraylist[0].value
54
+ const maxValue = priceArraylist[priceArraylist.length - 1].value
55
+ if (minValue === maxValue) minValue = 0
56
+
57
+ return [
58
+ {
59
+ value: `${minValue}_${maxValue}`,
60
+ label: `${minValue}-${maxValue}`,
61
+ count: priceArraylist.reduce((acc, price) => acc + price.count, 0),
62
+ },
63
+ ]
111
64
  }
112
65
 
113
66
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -117,8 +70,6 @@ function assertAlgoliaFacets(facets: any): facets is AlgoliaFacets {
117
70
 
118
71
  /**
119
72
  * Map algolia facets to aggregations format
120
- *
121
- * TODO: Make sure the aggregations are sorted correctly:
122
73
  * https://magento-247-git-canary-graphcommerce.vercel.app/men/photography, through position
123
74
  */
124
75
  export function algoliaFacetsToAggregations(
@@ -169,31 +120,41 @@ export function algoliaFacetsToAggregations(
169
120
  position,
170
121
  })
171
122
  } else {
172
- // Fallback to code if no label is found
173
- aggregations.push({
174
- label,
175
- attribute_code,
176
- options: Object.entries(facet).map(([filter, count]) => ({
177
- label: filter,
178
- count,
179
- value: filter,
180
- })),
181
- position,
182
- })
123
+ /** @todo: We probably need to modify the render-side of this to make it work properly. Magento by default doesn't really support range filters that aren't prices. */
124
+ const isNumericFacet = false
125
+
126
+ if (isNumericFacet) {
127
+ aggregations.push({
128
+ label,
129
+ attribute_code,
130
+ options: algoliaPricesToPricesAggregations(facet),
131
+ position,
132
+ })
133
+ } else {
134
+ aggregations.push({
135
+ label,
136
+ attribute_code,
137
+ options: Object.entries(facet).map(([filter, count]) => ({
138
+ label: filter,
139
+ count,
140
+ // @see productFilterInputToAlgoliafacetFiltersInput for the other side.
141
+ value: filter.replaceAll('/', '_OR_').replaceAll(',', '_AND_'),
142
+ })),
143
+ position,
144
+ })
145
+ }
183
146
  }
184
147
  })
185
148
 
186
149
  return aggregations
187
150
  }
188
151
 
189
- let categoryListCache: CategoryResult | null = null
190
-
191
152
  export async function getCategoryList(context: MeshContext) {
192
- if (categoryListCache) return categoryListCache
153
+ const cacheKey = `algolia_getCategoryList_${getIndexName(context)}`
154
+ const categoryListCached = context.cache.get(cacheKey)
193
155
 
194
- // context.cache.get('algolia_getCategoryList')
195
-
196
- categoryListCache = await context.m2.Query.categories({
156
+ if (categoryListCached) return categoryListCached as CategoryResult
157
+ const categoryListCache = await context.m2.Query.categories({
197
158
  args: { filters: {} },
198
159
  selectionSet: /* GraphQL */ `
199
160
  {
@@ -208,5 +169,8 @@ export async function getCategoryList(context: MeshContext) {
208
169
  context,
209
170
  })
210
171
 
211
- return categoryListCache!
172
+ if (!categoryListCache) throw new Error('Category list not found')
173
+ await context.cache.set(cacheKey, categoryListCache)
174
+
175
+ return categoryListCache
212
176
  }
@@ -81,9 +81,14 @@ function mapPriceRange(
81
81
  }
82
82
  }
83
83
 
84
- export function algoliaUrlToUrlKey(url?: string | null, base?: string | null): string | null {
85
- if (!url || !base) return null
86
- return url.replace(base, '')
84
+ export function algoliaUrlToUrlKey(url?: string | null, urlSuffix?: string | null): string | null {
85
+ if (!url) return null
86
+ const path = new URL(url).pathname.split('/')
87
+ // The URL key is the last part of the URL.
88
+ const urlKey = path[path.length - 1]
89
+
90
+ // The last part of the URL might end with the urlSuffix (something like `.html`), remove from the end of the URL.
91
+ return urlSuffix ? urlKey.replace(new RegExp(`${urlSuffix}$`), '') : urlKey
87
92
  }
88
93
 
89
94
  /**
@@ -132,11 +137,12 @@ export function algoliaHitToMagentoProduct(
132
137
  created_at,
133
138
  image_url,
134
139
  is_stock,
135
-
136
140
  price,
137
141
  thumbnail_url,
138
142
  type_id,
139
143
  url,
144
+ description,
145
+ short_description,
140
146
 
141
147
  // not used
142
148
  ordered_qty,
@@ -148,12 +154,13 @@ export function algoliaHitToMagentoProduct(
148
154
  ...rest
149
155
  } = additionalProperties
150
156
 
151
- // Some custom attributes are returned as array while they need to be a string. Flatten those arrays
152
- const flattenedCustomAttributes = {}
157
+ /**
158
+ * We flatten any custom attribute array values to a string as 95% of the time they are an array
159
+ * because the product is a configurable.
160
+ */
153
161
  for (const [key, value] of Object.entries(rest)) {
154
- if (value !== null && Array.isArray(value) && value?.length > 0) {
155
- flattenedCustomAttributes[key] = value.toString()
156
- delete rest[key]
162
+ if (value !== null && Array.isArray(value)) {
163
+ rest[key] = value.join(' ')
157
164
  }
158
165
  }
159
166
 
@@ -170,6 +177,8 @@ export function algoliaHitToMagentoProduct(
170
177
  review_count: 0,
171
178
  rating_summary: Number(rating_summary),
172
179
  reviews: { items: [], page_info: {} },
180
+ description: description ? { html: description } : null,
181
+ short_description: short_description ? { html: short_description } : null,
173
182
  // canonical_url: null,
174
183
  // categories: [],
175
184
  // country_of_manufacture: null,
@@ -187,18 +196,14 @@ export function algoliaHitToMagentoProduct(
187
196
  // options_container: null,
188
197
  // price_tiers: [],
189
198
  // product_links: [],
190
- // related_products: null,
191
- // short_description: null,
192
- // small_image: null,
193
199
  // special_price: null,
194
200
  // special_to_date: null,
195
201
  small_image: { url: getOriginalImage(thumbnail_url) },
196
202
  swatch_image: getOriginalImage(image_url),
197
203
  thumbnail: { url: getOriginalImage(thumbnail_url) },
198
204
  // upsell_products: [],
199
- url_key: algoliaUrlToUrlKey(url, storeConfig?.base_link_url),
205
+ url_key: algoliaUrlToUrlKey(url, storeConfig?.product_url_suffix),
200
206
  url_suffix: storeConfig?.product_url_suffix,
201
207
  ...rest,
202
- ...flattenedCustomAttributes,
203
208
  }
204
209
  }
@@ -1,21 +1,36 @@
1
1
  import type { AlgoliasettingsResponse, MeshContext } from '@graphcommerce/graphql-mesh'
2
+ import type { GraphQLError } from 'graphql'
2
3
  import { getIndexName } from './getIndexName'
3
4
 
4
- let settingsCache: AlgoliasettingsResponse | null = null
5
+ function isGraphQLError(err: unknown): err is GraphQLError {
6
+ return !!(err as GraphQLError)?.message
7
+ }
8
+
9
+ export type GetAlgoliaSettingsReturn = Pick<
10
+ AlgoliasettingsResponse,
11
+ 'attributesForFaceting' | 'replicas'
12
+ >
13
+
14
+ export async function getAlgoliaSettings(context: MeshContext): Promise<GetAlgoliaSettingsReturn> {
15
+ const cacheKey = `algolia_getSettings_${getIndexName(context)}`
16
+ const settingsCached = context.cache.get(cacheKey)
17
+ if (settingsCached) return settingsCached
18
+
19
+ const settings = await context.algolia.Query.algolia_getSettings({
20
+ args: { indexName: getIndexName(context) },
21
+ selectionSet: /* GraphQL */ `
22
+ {
23
+ replicas
24
+ attributesForFaceting
25
+ }
26
+ `,
27
+ context,
28
+ })
29
+
30
+ if (isGraphQLError(settings)) throw settings
31
+ if (!settings) throw Error('No settings found')
5
32
 
6
- export async function getAlgoliaSettings(context: MeshContext): Promise<AlgoliasettingsResponse> {
7
- if (!settingsCache) {
8
- settingsCache = await context.algolia.Query.algolia_getSettings({
9
- args: { indexName: getIndexName(context) },
10
- selectionSet: /* GraphQL */ `
11
- {
12
- replicas
13
- }
14
- `,
15
- context,
16
- })
17
- }
33
+ await context.cache.set(cacheKey, settings)
18
34
 
19
- if (!settingsCache) throw Error('No settings found')
20
- return settingsCache
35
+ return settings
21
36
  }
@@ -1,13 +1,14 @@
1
1
  import type { MeshContext } from '@graphcommerce/graphql-mesh'
2
2
  // eslint-disable-next-line import/no-extraneous-dependencies
3
3
  import { filterNonNullableKeys } from '@graphcommerce/next-ui/RenderType/filterNonNullableKeys'
4
+ import { getIndexName } from './getIndexName'
4
5
 
5
6
  export type AttributeList = { label: string; code: string }[]
6
7
 
7
- let attributeListCache: AttributeList | null = null
8
-
9
8
  export async function getAttributeList(context: MeshContext): Promise<AttributeList> {
10
- if (attributeListCache) return attributeListCache
9
+ const cacheKey = `algolia_getAttributeList_${getIndexName(context)}`
10
+ const attributeListCached = context.cache.get(cacheKey)
11
+ if (attributeListCached) return attributeListCached
11
12
 
12
13
  if (
13
14
  import.meta.graphCommerce.magentoVersion >= 247 &&
@@ -22,7 +23,9 @@ export async function getAttributeList(context: MeshContext): Promise<AttributeL
22
23
  selectionSet: '{ items{ code label } }',
23
24
  context,
24
25
  }).then((res) => res?.items)) as { label?: string; code: string }[]
25
- attributeListCache = filterNonNullableKeys(items, ['label'])
26
+
27
+ if (!items) throw new Error('Attribute list not found')
28
+ await context.cache.set(cacheKey, filterNonNullableKeys(items, ['label']))
26
29
 
27
30
  return filterNonNullableKeys(items, ['label'])
28
31
  }
@@ -2,7 +2,7 @@ import type { MeshContext, QueryproductsArgs } from '@graphcommerce/graphql-mesh
2
2
  import type { GraphQLResolveInfo } from 'graphql'
3
3
  import { getAlgoliaSettings } from './getAlgoliaSettings'
4
4
  import { getSearchResultsInput } from './getSearchResultsInput'
5
- import { getSortedIndex } from './sortOptions'
5
+ import { getSortedIndex } from './getSortedIndex'
6
6
 
7
7
  export async function getSearchResults(
8
8
  args: QueryproductsArgs,
@@ -27,6 +27,13 @@ export async function getSearchResults(
27
27
  additionalProperties
28
28
  }
29
29
  facets
30
+ renderingContent {
31
+ facetOrdering {
32
+ facets {
33
+ order
34
+ }
35
+ }
36
+ }
30
37
  }
31
38
  `,
32
39
  context,
@@ -3,6 +3,7 @@ import type {
3
3
  MeshContext,
4
4
  QueryproductsArgs,
5
5
  } from '@graphcommerce/graphql-mesh'
6
+ import { getAlgoliaSettings } from './getAlgoliaSettings'
6
7
  import { getStoreConfig } from './getStoreConfig'
7
8
  import {
8
9
  productFilterInputToAlgoliaFacetFiltersInput,
@@ -20,11 +21,16 @@ export async function getSearchResultsInput(
20
21
  facets: ['*'],
21
22
  hitsPerPage: args.pageSize ? args.pageSize : 10,
22
23
  page: args.currentPage ? args.currentPage - 1 : 0,
23
- facetFilters: productFilterInputToAlgoliaFacetFiltersInput(filters),
24
+ facetFilters: productFilterInputToAlgoliaFacetFiltersInput(
25
+ await getAlgoliaSettings(context),
26
+ filters,
27
+ args.search ?? '',
28
+ ),
24
29
  numericFilters: await productFilterInputToAlgoliaNumericFiltersInput(
25
30
  getStoreConfig(context),
26
31
  filters,
27
32
  ),
28
33
  analytics: true,
34
+ ruleContexts: typeof args.search === 'string' ? ['Search'] : ['Catalog'],
29
35
  }
30
36
  }
@@ -1,6 +1,7 @@
1
1
  import type { MeshContext, SearchSuggestion } from '@graphcommerce/graphql-mesh'
2
2
  import { filterNonNullableKeys } from '@graphcommerce/next-ui'
3
- import { getSearchSuggestionsInput, getSuggestionsIndexName } from './getSearchSuggestionsInput'
3
+ import { getSearchSuggestionsIndexName } from './getSearchSuggestionsIndexName'
4
+ import { getSearchSuggestionsInput } from './getSearchSuggestionsInput'
4
5
 
5
6
  export async function getSearchSuggestions(
6
7
  search: string,
@@ -8,7 +9,7 @@ export async function getSearchSuggestions(
8
9
  ): Promise<SearchSuggestion[]> {
9
10
  const suggestions = await context.algolia.Query.algolia_searchSingleIndex({
10
11
  args: {
11
- indexName: getSuggestionsIndexName(context),
12
+ indexName: getSearchSuggestionsIndexName(context),
12
13
  input: await getSearchSuggestionsInput(search, context),
13
14
  },
14
15
  selectionSet: /* GraphQL */ `
@@ -23,5 +24,7 @@ export async function getSearchSuggestions(
23
24
  context,
24
25
  })
25
26
 
26
- return filterNonNullableKeys(suggestions?.hits, []).map((hit) => ({ search: hit.objectID }))
27
+ return filterNonNullableKeys(suggestions?.hits, [])
28
+ .filter((hit) => hit.objectID !== search)
29
+ .map((hit) => ({ search: hit.objectID }))
27
30
  }
@@ -0,0 +1,6 @@
1
+ import type { MeshContext } from '@graphcommerce/graphql-mesh'
2
+ import { getIndexName } from './getIndexName'
3
+
4
+ export function getSearchSuggestionsIndexName(context: MeshContext) {
5
+ return `${getIndexName(context)}${import.meta.graphCommerce.algolia.suggestionsSuffix}`
6
+ }
@@ -1,14 +1,9 @@
1
1
  import type { MeshContext, Queryalgolia_searchSingleIndexArgs } from '@graphcommerce/graphql-mesh'
2
- import { getIndexName } from './getIndexName'
3
2
 
4
3
  export function isSuggestionsEnabled() {
5
4
  return Boolean(import.meta.graphCommerce.algolia.suggestionsSuffix)
6
5
  }
7
6
 
8
- export function getSuggestionsIndexName(context: MeshContext) {
9
- return `${getIndexName(context).replace('_products', import.meta.graphCommerce.algolia.suggestionsSuffix ?? '')}`
10
- }
11
-
12
7
  // eslint-disable-next-line @typescript-eslint/require-await
13
8
  export async function getSearchSuggestionsInput(
14
9
  search: string,
@@ -0,0 +1,54 @@
1
+ import type { MeshContext, ProductAttributeSortInput } from '@graphcommerce/graphql-mesh'
2
+ import { nonNullable } from '@graphcommerce/magento-customer'
3
+ import type { GetAlgoliaSettingsReturn } from './getAlgoliaSettings'
4
+ import { getGroupId } from './getGroupId'
5
+ import { getIndexName } from './getIndexName'
6
+
7
+ function stripVirtual(index: string) {
8
+ return index.slice(8, -1)
9
+ }
10
+
11
+ export async function getSortedIndex(
12
+ context: MeshContext,
13
+ settings: Promise<GetAlgoliaSettingsReturn>,
14
+ sortInput: ProductAttributeSortInput | null = {},
15
+ ): Promise<string> {
16
+ const baseIndex = getIndexName(context)
17
+ // const availableSorting = Object.values(sortOptions)
18
+ const [attr, dirEnum] = Object.entries(sortInput ?? {}).filter(nonNullable)?.[0] ?? []
19
+ if (!attr || !dirEnum) return baseIndex
20
+
21
+ const dir = dirEnum.toLowerCase()
22
+ const candidates = ((await settings).replicas ?? [])
23
+ .filter(nonNullable)
24
+ .filter((r) => r.startsWith(`virtual(${baseIndex}_${attr}`) && r.endsWith(`_${dir})`))
25
+
26
+ if (candidates.length === 0) {
27
+ console.warn(
28
+ `[@graphcommerce/algolia-products] WARNING: Expecting virtual replica but couldn't find the expected index for attr ${attr} with ${dir}, please add to the Algolia settings in the Magento Admin Panel. Falling back to baseIndex.`,
29
+ )
30
+ return baseIndex
31
+ }
32
+
33
+ if (attr === 'price') {
34
+ const enabled = import.meta.graphCommerce.algolia.customerGroupPricingEnabled
35
+
36
+ const groupId = enabled ? getGroupId(context) : 'default'
37
+ const found = candidates.find((r) => r.endsWith(`${groupId}_${dir})`))
38
+
39
+ if (!found) {
40
+ console.warn(
41
+ '[@graphcommerce/algolia-products] WARNING: Can not find correct price index.',
42
+ 'It should look something like:',
43
+ `virtual(${baseIndex}_${attr}_${groupId}_${dir})`,
44
+ 'Available indexes (using the first one):',
45
+ candidates,
46
+ )
47
+ return stripVirtual(candidates[0])
48
+ }
49
+
50
+ return stripVirtual(found)
51
+ }
52
+
53
+ return stripVirtual(candidates[0])
54
+ }
@@ -1,4 +1,5 @@
1
1
  import type { Maybe, MeshContext, StoreConfig } from '@graphcommerce/graphql-mesh'
2
+ import { getIndexName } from './getIndexName'
2
3
 
3
4
  export type GetStoreConfigReturn =
4
5
  | Maybe<
@@ -12,22 +13,24 @@ export type GetStoreConfigReturn =
12
13
  >
13
14
  | undefined
14
15
 
15
- let configCache: Promise<StoreConfig>
16
+ export async function getStoreConfig(context: MeshContext): Promise<StoreConfig> {
17
+ const cacheKey = `algolia_getStoreConfig_${getIndexName(context)}`
18
+ const configCached = context.cache.get(cacheKey)
19
+ if (configCached) return configCached as StoreConfig
16
20
 
17
- export function getStoreConfig(context: MeshContext): Promise<StoreConfig> {
18
- // eslint-disable-next-line @typescript-eslint/no-misused-promises
19
- if (!configCache) {
20
- configCache = context.m2.Query.storeConfig({
21
- context,
22
- selectionSet: /* GraphQL */ `
23
- {
24
- root_category_uid
25
- default_display_currency_code
26
- base_link_url
27
- product_url_suffix
28
- }
29
- `,
30
- })
31
- }
21
+ const configCache = await context.m2.Query.storeConfig({
22
+ context,
23
+ selectionSet: /* GraphQL */ `
24
+ {
25
+ root_category_uid
26
+ default_display_currency_code
27
+ base_link_url
28
+ product_url_suffix
29
+ }
30
+ `,
31
+ })
32
+
33
+ if (!configCache) throw new Error('Store config not found')
34
+ await context.cache.set(cacheKey, configCache)
32
35
  return configCache
33
36
  }
package/mesh/index.ts ADDED
@@ -0,0 +1,16 @@
1
+ export * from './algoliaFacetsToAggregations'
2
+ export * from './algoliaHitToMagentoProduct'
3
+ export * from './getAlgoliaSettings'
4
+ export * from './getAttributeList'
5
+ export * from './getGroupId'
6
+ export * from './getIndexName'
7
+ export * from './getSearchResults'
8
+ export * from './getSearchResultsInput'
9
+ export * from './getSearchSuggestions'
10
+ export * from './getSearchSuggestionsInput'
11
+ export * from './getSortedIndex'
12
+ export * from './getStoreConfig'
13
+ export * from './productFilterInputToAlgoliafacetFiltersInput'
14
+ export * from './sortAggregations'
15
+ export * from './sortFieldOptions'
16
+ export * from './utils'
@@ -8,6 +8,7 @@ import {
8
8
  isFilterTypeRange,
9
9
  } from '@graphcommerce/magento-product'
10
10
  import type { InputMaybe } from '@graphcommerce/next-config'
11
+ import type { GetAlgoliaSettingsReturn } from './getAlgoliaSettings'
11
12
  import type { GetStoreConfigReturn } from './getStoreConfig'
12
13
  import { nonNullable } from './utils'
13
14
 
@@ -17,24 +18,44 @@ import { nonNullable } from './utils'
17
18
  * https://www.algolia.com/doc/api-reference/api-parameters/facetFilters/
18
19
  */
19
20
  export function productFilterInputToAlgoliaFacetFiltersInput(
21
+ settings: GetAlgoliaSettingsReturn,
20
22
  filters?: InputMaybe<ProductAttributeFilterInput>,
23
+ query?: string,
21
24
  ) {
22
25
  const filterArray: (string | string[])[] = []
23
26
  if (!filters) {
24
27
  return []
25
28
  }
26
29
 
30
+ const hasVisibility = settings?.attributesForFaceting?.some(
31
+ (attr) => attr === 'visibility' || attr?.includes('filterOnly(visibility)'),
32
+ )
33
+
34
+ if (hasVisibility) {
35
+ if (typeof query === 'string') {
36
+ filterArray.push(['visibility:Catalog, Search', 'visibility:Search'])
37
+ } else {
38
+ filterArray.push(['visibility:Catalog, Search', 'visibility:Catalog'])
39
+ }
40
+ }
41
+
42
+ // @see algoliaFacetsToAggregations for the other side.
43
+ const maybeDecode = (value: string) => value.replaceAll('_OR_', '/').replaceAll('_AND_', ',')
44
+
27
45
  Object.entries(filters).forEach(([key, value]) => {
28
46
  if (isFilterTypeEqual(value)) {
29
47
  if (value.in) {
30
48
  const values = value.in.filter(nonNullable)
31
- if (key === 'category_uid') filterArray.push(values.map((v) => `categoryIds:${atob(v)}`))
32
- else filterArray.push(values.map((v) => `${key}:${v}`))
49
+ if (key === 'category_uid') {
50
+ filterArray.push(values.map((v) => `categoryIds:${atob(v)}`))
51
+ } else {
52
+ filterArray.push(values.map((v) => `${key}:${maybeDecode(v)}`))
53
+ }
33
54
  }
34
55
 
35
56
  if (value.eq) {
36
57
  if (key === 'category_uid') filterArray.push(`categoryIds:${atob(value.eq)}`)
37
- else filterArray.push(`${key}:${value.eq}`)
58
+ else filterArray.push(`${key}:${maybeDecode(value.eq)}`)
38
59
  }
39
60
  }
40
61
 
@@ -42,7 +63,6 @@ export function productFilterInputToAlgoliaFacetFiltersInput(
42
63
  throw Error('Match filters are not supported')
43
64
  }
44
65
  })
45
-
46
66
  return filterArray
47
67
  }
48
68