@graphcommerce/algolia-products 9.0.0-canary.100

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.
Files changed (40) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/Config.graphqls +54 -0
  3. package/README.md +79 -0
  4. package/algolia-spec.yaml +4418 -0
  5. package/graphql/CustomerGroupId.graphql +3 -0
  6. package/graphql/GetAlgoliaSettings.graphql +122 -0
  7. package/graphql/ProductListItems_Algolia.graphql +3 -0
  8. package/hooks/useAlgoliaIndexName.ts +5 -0
  9. package/hooks/useAlgoliaQueryContext.ts +11 -0
  10. package/index.ts +8 -0
  11. package/link/customerGroupIdLink.ts +20 -0
  12. package/mesh/algoliaFacetsToAggregations.ts +209 -0
  13. package/mesh/algoliaHitToMagentoProduct.ts +201 -0
  14. package/mesh/getAlgoliaSettings.ts +21 -0
  15. package/mesh/getAttributeList.ts +31 -0
  16. package/mesh/getGroupId.ts +7 -0
  17. package/mesh/getIndexName.ts +11 -0
  18. package/mesh/getSearchResults.ts +35 -0
  19. package/mesh/getSearchResultsInput.ts +30 -0
  20. package/mesh/getSearchSuggestions.ts +27 -0
  21. package/mesh/getSearchSuggestionsInput.ts +21 -0
  22. package/mesh/getStoreConfig.ts +33 -0
  23. package/mesh/productFilterInputToAlgoliafacetFiltersInput.ts +81 -0
  24. package/mesh/resolvers.ts +126 -0
  25. package/mesh/sortOptions.ts +76 -0
  26. package/mesh/utils.ts +3 -0
  27. package/next-env.d.ts +4 -0
  28. package/package.json +32 -0
  29. package/plugins/GraphQLProviderAlgoliaCustomerGroupId.tsx +14 -0
  30. package/plugins/ProductListItemsBaseAlgolia.tsx +21 -0
  31. package/plugins/magentoProductApplyAlgoliaEngine.ts +25 -0
  32. package/plugins/magentoSearchApplyAlgoliaEngine.ts +26 -0
  33. package/plugins/meshConfigAlgolia.ts +66 -0
  34. package/schema/AlgoliaSchema.graphqls +60 -0
  35. package/schema/CustomerAlgoliaGroupId.graphqls +9 -0
  36. package/scripts/base-schema-filter.mts +45 -0
  37. package/scripts/generate-spec.mts +69 -0
  38. package/tsconfig.json +5 -0
  39. package/utils/applyCategoryEngineVariable.ts +11 -0
  40. package/utils/applyEngineVariable.ts +11 -0
@@ -0,0 +1,3 @@
1
+ fragment CustomerGroupId on Customer @inject(into: ["CustomerInfo"]) {
2
+ group_id
3
+ }
@@ -0,0 +1,122 @@
1
+ query GetAlgoliaSettings($indexName: String!) {
2
+ algolia_getSettings(indexName: $indexName) {
3
+ attributesForFaceting
4
+ replicas
5
+ paginationLimitedTo
6
+ unretrievableAttributes
7
+ disableTypoToleranceOnWords
8
+ attributesToTransliterate
9
+ camelCaseAttributes
10
+ decompoundedAttributes {
11
+ de
12
+ }
13
+ indexLanguages
14
+ disablePrefixOnAttributes
15
+ allowCompressionOfIntegerArray
16
+ numericAttributesForFiltering
17
+ separatorsToIndex
18
+ searchableAttributes
19
+ userData {
20
+ settingID
21
+ pluginVersion
22
+ }
23
+ customNormalization
24
+ attributeForDistinct
25
+ attributesToRetrieve
26
+ ranking
27
+ customRanking
28
+ relevancyStrictness
29
+ attributesToHighlight
30
+ attributesToSnippet
31
+ highlightPreTag
32
+ highlightPostTag
33
+ snippetEllipsisText
34
+ restrictHighlightAndSnippetArrays
35
+ hitsPerPage
36
+ minWordSizefor1Typo
37
+ minWordSizefor2Typos
38
+ typoTolerance {
39
+ __typename
40
+ ... on AlgoliaBoolean_container {
41
+ Boolean
42
+ }
43
+ ... on Algoliatypo_tolerance_container {
44
+ typo_tolerance
45
+ }
46
+ }
47
+ allowTyposOnNumericTokens
48
+ disableTypoToleranceOnAttributes
49
+ ignorePlurals {
50
+ __typename
51
+ ... on AlgoliasupportedLanguage_container {
52
+ supportedLanguage
53
+ }
54
+ ... on AlgoliaBoolean_container {
55
+ Boolean
56
+ }
57
+ }
58
+ removeStopWords {
59
+ __typename
60
+ }
61
+ keepDiacriticsOnCharacters
62
+ queryLanguages
63
+ decompoundQuery
64
+ enableRules
65
+ enablePersonalization
66
+ queryType
67
+ removeWordsIfNoResults
68
+ mode
69
+ semanticSearch {
70
+ eventSources {
71
+ __typename
72
+ ... on AlgoliaString_container {
73
+ String
74
+ }
75
+ ... on AlgoliaVoid_container {
76
+ Void
77
+ }
78
+ }
79
+ }
80
+ advancedSyntax
81
+ optionalWords
82
+ disableExactOnAttributes
83
+ exactOnSingleWordQuery
84
+ alternativesAsExact
85
+ advancedSyntaxFeatures
86
+ distinct
87
+ replaceSynonymsInHighlight
88
+ minProximity
89
+ responseFields
90
+ maxFacetHits
91
+ maxValuesPerFacet
92
+ sortFacetValuesBy
93
+ attributeCriteriaComputedByMinProximity
94
+ renderingContent {
95
+ facetOrdering {
96
+ facets {
97
+ order
98
+ }
99
+ values {
100
+ additionalProperties {
101
+ key
102
+ value {
103
+ order
104
+ sortRemainingBy
105
+ hide
106
+ }
107
+ }
108
+ }
109
+ }
110
+ }
111
+ enableReRanking
112
+ reRankingApplyFilter {
113
+ __typename
114
+ ... on AlgoliaString_container {
115
+ String
116
+ }
117
+ ... on AlgoliaVoid_container {
118
+ Void
119
+ }
120
+ }
121
+ }
122
+ }
@@ -0,0 +1,3 @@
1
+ fragment ProductListItems_Algolia on Products @inject(into: ["ProductListItems"]) {
2
+ algolia_queryID
3
+ }
@@ -0,0 +1,5 @@
1
+ import { useStorefrontConfig } from '@graphcommerce/next-ui'
2
+
3
+ export function useAlgoliaIndexName() {
4
+ return `${import.meta.graphCommerce.algolia.indexNamePrefix}${useStorefrontConfig().magentoStoreCode}_products`
5
+ }
@@ -0,0 +1,11 @@
1
+ import React, { useContext } from 'react'
2
+
3
+ export type AlgoliaQueryContextType = {
4
+ queryID?: string | null
5
+ }
6
+
7
+ export const AlgoliaQueryContext = React.createContext<AlgoliaQueryContextType>({})
8
+
9
+ export function useAlgoliaQuery() {
10
+ return useContext(AlgoliaQueryContext)
11
+ }
package/index.ts ADDED
@@ -0,0 +1,8 @@
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'
7
+ export * from './hooks/useAlgoliaIndexName'
8
+ export * from './hooks/useAlgoliaQueryContext'
@@ -0,0 +1,20 @@
1
+ import { ApolloCache, setContext } from '@graphcommerce/graphql'
2
+ import { CustomerDocument } from '@graphcommerce/magento-customer'
3
+
4
+ declare module '@apollo/client' {
5
+ interface DefaultContext {
6
+ cache?: ApolloCache<unknown>
7
+ headers?: Record<string, string>
8
+ }
9
+ }
10
+
11
+ export const customerGroupIdLink = setContext((_, context) => {
12
+ if (!context.headers) context.headers = {}
13
+ try {
14
+ const group_id = context.cache?.readQuery({ query: CustomerDocument })?.customer?.group_id
15
+ if (group_id) context.headers['x-magento-group-id'] = `${group_id}`
16
+ return context
17
+ } catch (error) {
18
+ return context
19
+ }
20
+ })
@@ -0,0 +1,209 @@
1
+ import type {
2
+ Aggregation,
3
+ AggregationOption,
4
+ AlgoliasearchResponse,
5
+ CategoryResult,
6
+ MeshContext,
7
+ } from '@graphcommerce/graphql-mesh'
8
+ import { AttributeList } from './getAttributeList'
9
+ import { GetStoreConfigReturn } from './getStoreConfig'
10
+
11
+ type AlgoliaFacets = { [facetName: string]: AlgoliaFacetOption }
12
+ type AlgoliaFacetOption = { [facetOption: string]: number }
13
+
14
+ function categoryMapping(
15
+ categoryList: CategoryResult | null | undefined,
16
+ facetList: AlgoliaFacetOption,
17
+ ): AggregationOption[] {
18
+ if (!categoryList?.items) {
19
+ return []
20
+ }
21
+
22
+ return categoryList?.items
23
+ ?.map((category) => {
24
+ const count = category?.id ? facetList[category?.id] : 0
25
+ return { label: category?.name, value: category?.uid ?? '', count }
26
+ })
27
+ .filter((category) => category.count > 0)
28
+ }
29
+
30
+ function compare(a, b) {
31
+ const numberA: number = +a[0]
32
+ const numberB: number = +b[0]
33
+
34
+ if (numberA < numberB) {
35
+ return -1
36
+ }
37
+ if (numberA > numberB) {
38
+ return 1
39
+ }
40
+
41
+ return 0
42
+ }
43
+
44
+ export function algoliaPricesToPricesAggregations(pricesList: {
45
+ [key: string]: number
46
+ }): AggregationOption[] {
47
+ const priceArraylist: { value: number; count: number }[] = Object.entries(pricesList)
48
+ .sort(compare)
49
+ .map((price) => {
50
+ const value: number = +price[0]
51
+ return { value, count: price[1] }
52
+ })
53
+
54
+ const interval = Math.round(
55
+ (priceArraylist[priceArraylist.length - 1].value - priceArraylist[0].value) / 2,
56
+ )
57
+
58
+ const pricesBucket: { [key: number]: { count: number; value: string; label: string } } = {}
59
+ let increasingInterval = interval
60
+ priceArraylist.forEach((price) => {
61
+ if (price.value <= increasingInterval) {
62
+ if (!pricesBucket[increasingInterval]) {
63
+ pricesBucket[increasingInterval] = {
64
+ count: price.count,
65
+ value:
66
+ increasingInterval === interval
67
+ ? `0_${interval}`
68
+ : `${increasingInterval - interval}_${increasingInterval}`,
69
+ label:
70
+ increasingInterval === interval
71
+ ? `0_${interval}`
72
+ : `${increasingInterval - interval}-${increasingInterval}`,
73
+ }
74
+ } else {
75
+ pricesBucket[increasingInterval].count += price.count
76
+ }
77
+ } else {
78
+ increasingInterval += interval
79
+ pricesBucket[increasingInterval] = {
80
+ count: price.count,
81
+ value:
82
+ increasingInterval === interval
83
+ ? `0_${interval}`
84
+ : `${increasingInterval - interval}_${increasingInterval}`,
85
+ label:
86
+ increasingInterval === interval
87
+ ? `0-${interval}`
88
+ : `${increasingInterval - interval}-${increasingInterval}`,
89
+ }
90
+ }
91
+ if (
92
+ price.value === increasingInterval &&
93
+ priceArraylist[priceArraylist.length - 1].value !== price.value
94
+ ) {
95
+ increasingInterval += interval
96
+ pricesBucket[increasingInterval] = {
97
+ count: price.count,
98
+ value:
99
+ increasingInterval === interval
100
+ ? `0_${interval}`
101
+ : `${increasingInterval - interval}_${increasingInterval}`,
102
+ label:
103
+ increasingInterval === interval
104
+ ? `0_${interval}`
105
+ : `${increasingInterval - interval}-${increasingInterval}`,
106
+ }
107
+ }
108
+ })
109
+ return Object.values(pricesBucket)
110
+ }
111
+
112
+ function assertAlgoliaFacets(facets: any): facets is AlgoliaFacets {
113
+ return true
114
+ }
115
+
116
+ /**
117
+ * Map algolia facets to aggregations format
118
+ *
119
+ * TODO: Make sure the aggregations are sorted correctly: https://magento-247-git-canary-graphcommerce.vercel.app/men/photography, through position
120
+ */
121
+ export function algoliaFacetsToAggregations(
122
+ algoliaFacets: AlgoliasearchResponse['facets'],
123
+ attributes: AttributeList,
124
+ storeConfig: GetStoreConfigReturn,
125
+ categoryList?: null | CategoryResult,
126
+ groupId?: number,
127
+ ): Aggregation[] {
128
+ if (!storeConfig?.default_display_currency_code) throw new Error('Currency is required')
129
+ const aggregations: Aggregation[] = []
130
+
131
+ if (!assertAlgoliaFacets(algoliaFacets)) throw Error('these are not facets')
132
+
133
+ Object.entries(algoliaFacets).forEach(([facetIndex, facet]) => {
134
+ let attribute_code = facetIndex
135
+
136
+ if (facetIndex.startsWith('categories.level')) return
137
+ if (facetIndex.startsWith('price')) {
138
+ attribute_code = 'price'
139
+ }
140
+
141
+ const position = 0
142
+
143
+ const label =
144
+ attributes?.find((attribute) => attribute?.code === attribute_code)?.label ?? attribute_code
145
+ if (facetIndex === 'categoryIds') {
146
+ aggregations.push({
147
+ label,
148
+ attribute_code: 'category_uid',
149
+ options: categoryMapping(categoryList, algoliaFacets[facetIndex]),
150
+ position,
151
+ })
152
+ } else if (facetIndex.startsWith('price')) {
153
+ if (!groupId && facetIndex !== `price.${storeConfig.default_display_currency_code}.default`) {
154
+ return
155
+ }
156
+ if (
157
+ groupId &&
158
+ facetIndex !== `price.${storeConfig.default_display_currency_code}.group_${groupId}`
159
+ ) {
160
+ return
161
+ }
162
+ aggregations.push({
163
+ label,
164
+ attribute_code,
165
+ options: algoliaPricesToPricesAggregations(algoliaFacets[facetIndex]),
166
+ position,
167
+ })
168
+ } else {
169
+ // Fallback to code if no label is found
170
+ aggregations.push({
171
+ label,
172
+ attribute_code,
173
+ options: Object.entries(facet).map(([filter, count]) => ({
174
+ label: filter,
175
+ count,
176
+ value: filter,
177
+ })),
178
+ position,
179
+ })
180
+ }
181
+ })
182
+
183
+ return aggregations
184
+ }
185
+
186
+ let categoryListCache: CategoryResult | null = null
187
+
188
+ export async function getCategoryList(context: MeshContext) {
189
+ if (categoryListCache) return categoryListCache
190
+
191
+ // context.cache.get('algolia_getCategoryList')
192
+
193
+ categoryListCache = await context.m2.Query.categories({
194
+ args: { filters: {} },
195
+ selectionSet: /* GraphQL */ `
196
+ {
197
+ items {
198
+ uid
199
+ name
200
+ id
201
+ position
202
+ }
203
+ }
204
+ `,
205
+ context,
206
+ })
207
+
208
+ return categoryListCache!
209
+ }
@@ -0,0 +1,201 @@
1
+ import type {
2
+ AlgoliaPrice,
3
+ AlgoliaProductHitAdditionalProperties,
4
+ Algoliahit,
5
+ CurrencyEnum,
6
+ MeshContext,
7
+ PriceRange,
8
+ QueryproductsArgs,
9
+ RequireFields,
10
+ ResolverFn,
11
+ ResolversParentTypes,
12
+ ResolversTypes,
13
+ } from '@graphcommerce/graphql-mesh'
14
+ // eslint-disable-next-line import/no-extraneous-dependencies
15
+ import { GraphQLResolveInfo } from 'graphql'
16
+ import { GetStoreConfigReturn } from './getStoreConfig'
17
+
18
+ export function assertAdditional(
19
+ additional: unknown,
20
+ ): additional is AlgoliaProductHitAdditionalProperties {
21
+ return true
22
+ }
23
+
24
+ const algoliaTypeToTypename = {
25
+ bundle: 'BundleProduct',
26
+ simple: 'SimpleProduct',
27
+ configurable: 'ConfigurableProduct',
28
+ downloadable: 'DownloadableProduct',
29
+ virtual: 'VirtualProduct',
30
+ grouped: 'GroupedProduct',
31
+ giftcard: 'GiftCardProduct',
32
+ } as const
33
+
34
+ function mapPriceRange(
35
+ price: AlgoliaProductHitAdditionalProperties['price'],
36
+ storeConfig: GetStoreConfigReturn,
37
+ customerGroup = 0,
38
+ ): PriceRange {
39
+ if (!storeConfig?.default_display_currency_code) throw new Error('Currency is required')
40
+
41
+ const key = storeConfig.default_display_currency_code as keyof AlgoliaPrice
42
+ const currency = storeConfig.default_display_currency_code as CurrencyEnum
43
+
44
+ const maxRegular = price?.[key]?.default_max ?? 0
45
+ const maxFinal = price?.[key]?.[`group_${customerGroup}_max`] ?? price?.[key]?.default_max ?? 0
46
+
47
+ const minRegular = price?.[key]?.default ?? 0
48
+ const minFinal = price?.[key]?.[`group_${customerGroup}`] ?? price?.[key]?.default
49
+
50
+ return {
51
+ maximum_price: {
52
+ regular_price: {
53
+ currency,
54
+ value: maxRegular,
55
+ },
56
+ final_price: {
57
+ currency,
58
+ value: maxFinal,
59
+ },
60
+ discount: {
61
+ percent_off:
62
+ maxRegular !== maxFinal && maxRegular > 0 ? 1 - (maxFinal / maxRegular) * 100 : 0,
63
+ amount_off: maxRegular - maxFinal,
64
+ },
65
+ // fixed_product_taxes
66
+ },
67
+ minimum_price: {
68
+ regular_price: {
69
+ currency,
70
+ value: price?.[key]?.default,
71
+ },
72
+ final_price: {
73
+ currency,
74
+ value: minFinal,
75
+ },
76
+ discount: {
77
+ percent_off:
78
+ minRegular !== minFinal && minRegular > 0 ? 1 - (minFinal / minRegular) * 100 : 0,
79
+ amount_off: minRegular - minFinal,
80
+ },
81
+ // fixed_product_taxes
82
+ },
83
+ }
84
+ }
85
+
86
+ export function algoliaUrlToUrlKey(url?: string | null, base?: string | null): string | null {
87
+ if (!url || !base) return null
88
+ return url.replace(base, '')
89
+ }
90
+
91
+ /**
92
+ * For the URL https://configurator.reachdigital.dev/media/catalog/product/cache/d911de87cf9e562637815cc5a14b1b05/1/0/1087_1_3.jpg
93
+ * Remove /cache/HASH from the URL but only if the url contains media/catalog/product
94
+ * @param url
95
+ */
96
+ function getOriginalImage(url?: string | undefined | null) {
97
+ if (!url || !url.includes('media/catalog/product')) return url
98
+ return url.replace(/\/cache\/[a-z0-9]+/, '')
99
+ }
100
+
101
+ export type ProductsItemsItem = NonNullable<
102
+ Awaited<
103
+ ReturnType<
104
+ ResolverFn<
105
+ ResolversTypes['Products'],
106
+ ResolversParentTypes['Query'],
107
+ MeshContext,
108
+ RequireFields<QueryproductsArgs, 'pageSize' | 'currentPage'>
109
+ >
110
+ >
111
+ >['items']
112
+ >[number] & {
113
+ __typename: (typeof algoliaTypeToTypename)[keyof typeof algoliaTypeToTypename]
114
+ }
115
+
116
+ /**
117
+ * Mapping function to map Algolia hit to Magento product.
118
+ *
119
+ * You can create a FunctionPlugin to modify the behavior of this function or implement brand specific code.
120
+ */
121
+ export function algoliaHitToMagentoProduct(
122
+ hit: Algoliahit,
123
+ storeConfig: GetStoreConfigReturn,
124
+ customerGroup: number,
125
+ ): ProductsItemsItem | null {
126
+ const { objectID, additionalProperties } = hit
127
+ if (!assertAdditional(additionalProperties)) return null
128
+
129
+ const {
130
+ sku,
131
+ created_at,
132
+ image_url,
133
+ is_stock,
134
+
135
+ price,
136
+ thumbnail_url,
137
+ type_id,
138
+ url,
139
+
140
+ // not used
141
+ ordered_qty,
142
+ visibility_catalog,
143
+ visibility_search,
144
+ rating_summary,
145
+
146
+ // The rest will be spread into the product
147
+ ...rest
148
+ } = additionalProperties
149
+
150
+ // Some custom attributes are returned as array while they need to be a string. Flatten those arrays
151
+ const flattenedCustomAttributes = {}
152
+ for (const [key, value] of Object.entries(rest)) {
153
+ if (value !== null && Array.isArray(value) && value?.length > 0) {
154
+ flattenedCustomAttributes[key] = value.toString()
155
+ delete rest[key]
156
+ }
157
+ }
158
+
159
+ return {
160
+ redirect_code: 0,
161
+ __typename: algoliaTypeToTypename[type_id as keyof typeof algoliaTypeToTypename],
162
+ uid: btoa(objectID),
163
+ sku: Array.isArray(sku) ? sku[0] : `${sku}`,
164
+ price_range: mapPriceRange(price, storeConfig, customerGroup),
165
+ created_at: created_at ? new Date(created_at).toISOString() : null,
166
+ stock_status: is_stock ? 'IN_STOCK' : 'OUT_OF_STOCK',
167
+ review_count: 0,
168
+ rating_summary: Number(rating_summary),
169
+ reviews: { items: [], page_info: {} },
170
+ // canonical_url: null,
171
+ // categories: [],
172
+ // country_of_manufacture: null,
173
+ // crosssell_products: [],
174
+ // custom_attributesV2: null,
175
+ // description: null,
176
+ // gift_message_available: null,
177
+ image: { url: getOriginalImage(image_url) },
178
+ // media_gallery: [],
179
+ // meta_keyword: null,
180
+ // meta_title: null,
181
+ // new_from_date: null,
182
+ // new_to_date: null,
183
+ // only_x_left_in_stock: null,
184
+ // options_container: null,
185
+ // price_tiers: [],
186
+ // product_links: [],
187
+ // related_products: null,
188
+ // short_description: null,
189
+ // small_image: null,
190
+ // special_price: null,
191
+ // special_to_date: null,
192
+ small_image: { url: getOriginalImage(thumbnail_url) },
193
+ swatch_image: getOriginalImage(image_url),
194
+ thumbnail: { url: getOriginalImage(thumbnail_url) },
195
+ // upsell_products: [],
196
+ url_key: algoliaUrlToUrlKey(url, storeConfig?.base_link_url),
197
+ url_suffix: storeConfig?.product_url_suffix,
198
+ ...rest,
199
+ ...flattenedCustomAttributes,
200
+ }
201
+ }
@@ -0,0 +1,21 @@
1
+ import type { AlgoliasettingsResponse, MeshContext } from '@graphcommerce/graphql-mesh'
2
+ import { getIndexName } from './getIndexName'
3
+
4
+ let settingsCache: AlgoliasettingsResponse | null = null
5
+
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
+ }
18
+
19
+ if (!settingsCache) throw Error('No settings found')
20
+ return settingsCache
21
+ }
@@ -0,0 +1,31 @@
1
+ import type { MeshContext } from '@graphcommerce/graphql-mesh'
2
+ // eslint-disable-next-line import/no-extraneous-dependencies
3
+ import { filterNonNullableKeys } from '@graphcommerce/next-ui/RenderType/filterNonNullableKeys'
4
+
5
+ export type AttributeList = { label: string; code: string }[]
6
+
7
+ let attributeListCache: AttributeList | null = null
8
+
9
+ export async function getAttributeList(context: MeshContext): Promise<AttributeList> {
10
+ if (attributeListCache) return attributeListCache
11
+
12
+ if (
13
+ import.meta.graphCommerce.magentoVersion >= 247 &&
14
+ 'attributesList' in context.m2.Query &&
15
+ typeof context.m2.Query.attributesList === 'function'
16
+ ) {
17
+ const items = (await context.m2.Query.attributesList({
18
+ args: {
19
+ entityType: 'CATALOG_PRODUCT',
20
+ filters: {},
21
+ },
22
+ selectionSet: `{ items{ code label } }`,
23
+ context,
24
+ }).then((res) => res?.items)) as { label?: string; code: string }[]
25
+ attributeListCache = filterNonNullableKeys(items, ['label'])
26
+
27
+ return filterNonNullableKeys(items, ['label'])
28
+ }
29
+
30
+ return []
31
+ }
@@ -0,0 +1,7 @@
1
+ import type { MeshContext } from '@graphcommerce/graphql-mesh'
2
+
3
+ export function getGroupId(context: MeshContext): number {
4
+ const { headers } = context as MeshContext & { headers?: Record<string, string | undefined> }
5
+ if (!headers?.authorization) return 0
6
+ return parseInt(headers?.['x-magento-group-id'] || '0', 10)
7
+ }
@@ -0,0 +1,11 @@
1
+ import type { MeshContext } from '@graphcommerce/graphql-mesh'
2
+ import { storefrontConfigDefault } from '@graphcommerce/next-ui'
3
+
4
+ function getStoreHeader(context: MeshContext) {
5
+ return (context as MeshContext & { headers: Record<string, string | undefined> }).headers.store
6
+ }
7
+
8
+ export function getIndexName(context: MeshContext) {
9
+ const storeCode = getStoreHeader(context) ?? storefrontConfigDefault().magentoStoreCode
10
+ return `${import.meta.graphCommerce.algolia.indexNamePrefix}${storeCode}_products`
11
+ }
@@ -0,0 +1,35 @@
1
+ import type { MeshContext, QueryproductsArgs } from '@graphcommerce/graphql-mesh'
2
+ import type { GraphQLResolveInfo } from 'graphql'
3
+ import { getAlgoliaSettings } from './getAlgoliaSettings'
4
+ import { getSearchResultsInput } from './getSearchResultsInput'
5
+ import { getSortedIndex } from './sortOptions'
6
+
7
+ export async function getSearchResults(
8
+ args: QueryproductsArgs,
9
+ context: MeshContext,
10
+ info: GraphQLResolveInfo,
11
+ ) {
12
+ return context.algolia.Query.algolia_searchSingleIndex({
13
+ args: {
14
+ indexName: await getSortedIndex(context, getAlgoliaSettings(context), args.sort),
15
+ input: await getSearchResultsInput(args, context),
16
+ },
17
+ selectionSet: /* GraphQL */ `
18
+ {
19
+ nbPages
20
+ hitsPerPage
21
+ page
22
+ queryID
23
+ nbHits
24
+ hits {
25
+ __typename
26
+ objectID
27
+ additionalProperties
28
+ }
29
+ facets
30
+ }
31
+ `,
32
+ context,
33
+ info,
34
+ })
35
+ }