@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.
- package/CHANGELOG.md +36 -0
- package/Config.graphqls +3 -5
- package/README.md +1 -77
- package/algolia-spec.yaml +317 -210
- package/graphql/GetAlgoliaSettings.graphql +1 -1
- package/index.ts +1 -6
- package/mesh/algoliaFacetsToAggregations.ts +48 -84
- package/mesh/algoliaHitToMagentoProduct.ts +19 -14
- package/mesh/getAlgoliaSettings.ts +30 -15
- package/mesh/getAttributeList.ts +7 -4
- package/mesh/getSearchResults.ts +8 -1
- package/mesh/getSearchResultsInput.ts +7 -1
- package/mesh/getSearchSuggestions.ts +6 -3
- package/mesh/getSearchSuggestionsIndexName.ts +6 -0
- package/mesh/getSearchSuggestionsInput.ts +0 -5
- package/mesh/getSortedIndex.ts +54 -0
- package/mesh/getStoreConfig.ts +19 -16
- package/mesh/index.ts +16 -0
- package/mesh/productFilterInputToAlgoliafacetFiltersInput.ts +24 -4
- package/mesh/resolvers.ts +20 -34
- package/mesh/sortAggregations.ts +23 -0
- package/mesh/{sortOptions.ts → sortFieldOptions.ts} +7 -31
- package/package.json +9 -9
- package/schema/AlgoliaSchema.graphqls +4 -0
- package/scripts/generate-spec.mts +3 -3
package/index.ts
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
export * from './mesh
|
|
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(
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
label
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
153
|
+
const cacheKey = `algolia_getCategoryList_${getIndexName(context)}`
|
|
154
|
+
const categoryListCached = context.cache.get(cacheKey)
|
|
193
155
|
|
|
194
|
-
|
|
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
|
-
|
|
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,
|
|
85
|
-
if (!url
|
|
86
|
-
|
|
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
|
-
|
|
152
|
-
|
|
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)
|
|
155
|
-
|
|
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?.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
20
|
-
return settingsCache
|
|
35
|
+
return settings
|
|
21
36
|
}
|
package/mesh/getAttributeList.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/mesh/getSearchResults.ts
CHANGED
|
@@ -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 './
|
|
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(
|
|
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 {
|
|
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:
|
|
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, [])
|
|
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
|
+
}
|
package/mesh/getStoreConfig.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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')
|
|
32
|
-
|
|
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
|
|