@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.
- package/CHANGELOG.md +105 -0
- package/Config.graphqls +54 -0
- package/README.md +79 -0
- package/algolia-spec.yaml +4418 -0
- package/graphql/CustomerGroupId.graphql +3 -0
- package/graphql/GetAlgoliaSettings.graphql +122 -0
- package/graphql/ProductListItems_Algolia.graphql +3 -0
- package/hooks/useAlgoliaIndexName.ts +5 -0
- package/hooks/useAlgoliaQueryContext.ts +11 -0
- package/index.ts +8 -0
- package/link/customerGroupIdLink.ts +20 -0
- package/mesh/algoliaFacetsToAggregations.ts +209 -0
- package/mesh/algoliaHitToMagentoProduct.ts +201 -0
- package/mesh/getAlgoliaSettings.ts +21 -0
- package/mesh/getAttributeList.ts +31 -0
- package/mesh/getGroupId.ts +7 -0
- package/mesh/getIndexName.ts +11 -0
- package/mesh/getSearchResults.ts +35 -0
- package/mesh/getSearchResultsInput.ts +30 -0
- package/mesh/getSearchSuggestions.ts +27 -0
- package/mesh/getSearchSuggestionsInput.ts +21 -0
- package/mesh/getStoreConfig.ts +33 -0
- package/mesh/productFilterInputToAlgoliafacetFiltersInput.ts +81 -0
- package/mesh/resolvers.ts +126 -0
- package/mesh/sortOptions.ts +76 -0
- package/mesh/utils.ts +3 -0
- package/next-env.d.ts +4 -0
- package/package.json +32 -0
- package/plugins/GraphQLProviderAlgoliaCustomerGroupId.tsx +14 -0
- package/plugins/ProductListItemsBaseAlgolia.tsx +21 -0
- package/plugins/magentoProductApplyAlgoliaEngine.ts +25 -0
- package/plugins/magentoSearchApplyAlgoliaEngine.ts +26 -0
- package/plugins/meshConfigAlgolia.ts +66 -0
- package/schema/AlgoliaSchema.graphqls +60 -0
- package/schema/CustomerAlgoliaGroupId.graphqls +9 -0
- package/scripts/base-schema-filter.mts +45 -0
- package/scripts/generate-spec.mts +69 -0
- package/tsconfig.json +5 -0
- package/utils/applyCategoryEngineVariable.ts +11 -0
- package/utils/applyEngineVariable.ts +11 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AlgoliasearchParamsObject_Input,
|
|
3
|
+
MeshContext,
|
|
4
|
+
QueryproductsArgs,
|
|
5
|
+
} from '@graphcommerce/graphql-mesh'
|
|
6
|
+
import { getStoreConfig } from './getStoreConfig'
|
|
7
|
+
import {
|
|
8
|
+
productFilterInputToAlgoliaFacetFiltersInput,
|
|
9
|
+
productFilterInputToAlgoliaNumericFiltersInput,
|
|
10
|
+
} from './productFilterInputToAlgoliafacetFiltersInput'
|
|
11
|
+
|
|
12
|
+
export async function getSearchResultsInput(
|
|
13
|
+
args: QueryproductsArgs,
|
|
14
|
+
context: MeshContext,
|
|
15
|
+
): Promise<AlgoliasearchParamsObject_Input> {
|
|
16
|
+
const { engine, ...filters } = args.filter ?? {}
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
query: args.search ?? '',
|
|
20
|
+
facets: ['*'],
|
|
21
|
+
hitsPerPage: args.pageSize ? args.pageSize : 10,
|
|
22
|
+
page: args.currentPage ? args.currentPage - 1 : 0,
|
|
23
|
+
facetFilters: productFilterInputToAlgoliaFacetFiltersInput(filters),
|
|
24
|
+
numericFilters: await productFilterInputToAlgoliaNumericFiltersInput(
|
|
25
|
+
getStoreConfig(context),
|
|
26
|
+
filters,
|
|
27
|
+
),
|
|
28
|
+
analytics: true,
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { MeshContext, SearchSuggestion } from '@graphcommerce/graphql-mesh'
|
|
2
|
+
import { filterNonNullableKeys } from '@graphcommerce/next-ui'
|
|
3
|
+
import { getSearchSuggestionsInput, getSuggestionsIndexName } from './getSearchSuggestionsInput'
|
|
4
|
+
|
|
5
|
+
export async function getSearchSuggestions(
|
|
6
|
+
search: string,
|
|
7
|
+
context: MeshContext,
|
|
8
|
+
): Promise<SearchSuggestion[]> {
|
|
9
|
+
const suggestions = await context.algolia.Query.algolia_searchSingleIndex({
|
|
10
|
+
args: {
|
|
11
|
+
indexName: getSuggestionsIndexName(context),
|
|
12
|
+
input: await getSearchSuggestionsInput(search, context),
|
|
13
|
+
},
|
|
14
|
+
selectionSet: /* GraphQL */ `
|
|
15
|
+
{
|
|
16
|
+
hits {
|
|
17
|
+
__typename
|
|
18
|
+
objectID
|
|
19
|
+
additionalProperties
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
`,
|
|
23
|
+
context,
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
return filterNonNullableKeys(suggestions?.hits, []).map((hit) => ({ search: hit.objectID }))
|
|
27
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { MeshContext, Queryalgolia_searchSingleIndexArgs } from '@graphcommerce/graphql-mesh'
|
|
2
|
+
import { getIndexName } from './getIndexName'
|
|
3
|
+
|
|
4
|
+
export function isSuggestionsEnabled() {
|
|
5
|
+
return Boolean(import.meta.graphCommerce.algolia.suggestionsSuffix)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function getSuggestionsIndexName(context: MeshContext) {
|
|
9
|
+
return `${getIndexName(context).replace('_products', import.meta.graphCommerce.algolia.suggestionsSuffix ?? '')}`
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
13
|
+
export async function getSearchSuggestionsInput(
|
|
14
|
+
search: string,
|
|
15
|
+
context: MeshContext,
|
|
16
|
+
): Promise<Queryalgolia_searchSingleIndexArgs['input']> {
|
|
17
|
+
return {
|
|
18
|
+
query: search,
|
|
19
|
+
hitsPerPage: 5,
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { MeshContext, Maybe, StoreConfig } from '@graphcommerce/graphql-mesh'
|
|
2
|
+
|
|
3
|
+
export type GetStoreConfigReturn =
|
|
4
|
+
| Maybe<
|
|
5
|
+
Pick<
|
|
6
|
+
StoreConfig,
|
|
7
|
+
| 'root_category_uid'
|
|
8
|
+
| 'default_display_currency_code'
|
|
9
|
+
| 'base_link_url'
|
|
10
|
+
| 'product_url_suffix'
|
|
11
|
+
>
|
|
12
|
+
>
|
|
13
|
+
| undefined
|
|
14
|
+
|
|
15
|
+
let configCache: Promise<StoreConfig>
|
|
16
|
+
|
|
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
|
+
}
|
|
32
|
+
return configCache
|
|
33
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ProductAttributeFilterInput,
|
|
3
|
+
AlgolianumericFilters_Input,
|
|
4
|
+
} from '@graphcommerce/graphql-mesh'
|
|
5
|
+
import {
|
|
6
|
+
isFilterTypeEqual,
|
|
7
|
+
isFilterTypeRange,
|
|
8
|
+
isFilterTypeMatch,
|
|
9
|
+
} from '@graphcommerce/magento-product'
|
|
10
|
+
import { InputMaybe } from '@graphcommerce/next-config'
|
|
11
|
+
import { GetStoreConfigReturn } from './getStoreConfig'
|
|
12
|
+
import { nonNullable } from './utils'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Map filters recieved from arguments to algolia facetfilter format
|
|
16
|
+
*
|
|
17
|
+
* https://www.algolia.com/doc/api-reference/api-parameters/facetFilters/
|
|
18
|
+
*/
|
|
19
|
+
export function productFilterInputToAlgoliaFacetFiltersInput(
|
|
20
|
+
filters?: InputMaybe<ProductAttributeFilterInput>,
|
|
21
|
+
) {
|
|
22
|
+
const filterArray: (string | string[])[] = []
|
|
23
|
+
if (!filters) {
|
|
24
|
+
return []
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
Object.entries(filters).forEach(([key, value]) => {
|
|
28
|
+
if (isFilterTypeEqual(value)) {
|
|
29
|
+
if (value.in) {
|
|
30
|
+
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}`))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (value.eq) {
|
|
36
|
+
if (key === 'category_uid') filterArray.push(`categoryIds:${atob(value.eq)}`)
|
|
37
|
+
else filterArray.push(`${key}:${value.eq}`)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (isFilterTypeMatch(value)) {
|
|
42
|
+
throw Error('Match filters are not supported')
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
return filterArray
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Map filters recieved from arguments to algolia facetfilter format
|
|
51
|
+
*
|
|
52
|
+
* https://www.algolia.com/doc/api-reference/api-parameters/numericFilters/#examples
|
|
53
|
+
*/
|
|
54
|
+
export async function productFilterInputToAlgoliaNumericFiltersInput(
|
|
55
|
+
storeConfig: Promise<GetStoreConfigReturn>,
|
|
56
|
+
filters?: InputMaybe<ProductAttributeFilterInput>,
|
|
57
|
+
) {
|
|
58
|
+
if (!filters) return []
|
|
59
|
+
|
|
60
|
+
const filterArray: AlgolianumericFilters_Input[] = []
|
|
61
|
+
|
|
62
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
63
|
+
if (isFilterTypeRange(value)) {
|
|
64
|
+
if (key === 'price') {
|
|
65
|
+
// eslint-disable-next-line no-await-in-loop
|
|
66
|
+
const currencyCode = (await storeConfig)?.default_display_currency_code
|
|
67
|
+
filterArray.push(
|
|
68
|
+
{ numericFilters_Input: { String: `${key}.${currencyCode}.default >= ${value.from}` } },
|
|
69
|
+
{ numericFilters_Input: { String: `${key}.${currencyCode}.default <= ${value.to}` } },
|
|
70
|
+
)
|
|
71
|
+
} else {
|
|
72
|
+
filterArray.push(
|
|
73
|
+
{ numericFilters_Input: { String: `${key} >= ${value.from}` } },
|
|
74
|
+
{ numericFilters_Input: { String: `${key} <= ${value.to}` } },
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return filterArray
|
|
81
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AlgoliasearchResponse,
|
|
3
|
+
hasSelectionSetPath,
|
|
4
|
+
type Resolvers,
|
|
5
|
+
} from '@graphcommerce/graphql-mesh'
|
|
6
|
+
import type { GraphQLError, GraphQLResolveInfo } from 'graphql'
|
|
7
|
+
import { algoliaFacetsToAggregations, getCategoryList } from './algoliaFacetsToAggregations'
|
|
8
|
+
import { algoliaHitToMagentoProduct, ProductsItemsItem } from './algoliaHitToMagentoProduct'
|
|
9
|
+
import { getAlgoliaSettings } from './getAlgoliaSettings'
|
|
10
|
+
import { getAttributeList } from './getAttributeList'
|
|
11
|
+
import { getGroupId } from './getGroupId'
|
|
12
|
+
import { getSearchResults } from './getSearchResults'
|
|
13
|
+
import { getSearchSuggestions } from './getSearchSuggestions'
|
|
14
|
+
import { isSuggestionsEnabled } from './getSearchSuggestionsInput'
|
|
15
|
+
import { getStoreConfig } from './getStoreConfig'
|
|
16
|
+
import { sortingOptions } from './sortOptions'
|
|
17
|
+
|
|
18
|
+
function isAlgoliaResponse<T extends object>(
|
|
19
|
+
root: T,
|
|
20
|
+
): root is T & { algoliaSearchResults: AlgoliasearchResponse } {
|
|
21
|
+
return 'algoliaSearchResults' in root
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function hasSearchRequest(info: GraphQLResolveInfo) {
|
|
25
|
+
const hasItems = hasSelectionSetPath(info.operation.selectionSet, 'products.items')
|
|
26
|
+
const hasAggregations = hasSelectionSetPath(info.operation.selectionSet, 'products.aggregations')
|
|
27
|
+
const hasPageInfo = hasSelectionSetPath(info.operation.selectionSet, 'products.page_info')
|
|
28
|
+
const hasTotalCount = hasSelectionSetPath(info.operation.selectionSet, 'products.total_count')
|
|
29
|
+
const hasSortFields = hasSelectionSetPath(info.operation.selectionSet, 'products.sort_fields')
|
|
30
|
+
|
|
31
|
+
return Boolean(hasItems || hasAggregations || hasPageInfo || hasTotalCount || hasSortFields)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function hasSuggestionsRequest(info: GraphQLResolveInfo) {
|
|
35
|
+
return hasSelectionSetPath(info.operation.selectionSet, 'products.suggestions')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isGraphQLError(err: unknown): err is GraphQLError {
|
|
39
|
+
return !!(err as GraphQLError)?.message
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const resolvers: Resolvers = {
|
|
43
|
+
Products: {
|
|
44
|
+
aggregations: async (root, _args, context) => {
|
|
45
|
+
if (!isAlgoliaResponse(root)) return root.aggregations ?? null
|
|
46
|
+
|
|
47
|
+
return algoliaFacetsToAggregations(
|
|
48
|
+
root.algoliaSearchResults?.facets,
|
|
49
|
+
await getAttributeList(context),
|
|
50
|
+
await getStoreConfig(context),
|
|
51
|
+
await getCategoryList(context),
|
|
52
|
+
getGroupId(context),
|
|
53
|
+
)
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
sort_fields: async (root, _args, context) => {
|
|
57
|
+
if (isAlgoliaResponse(root)) {
|
|
58
|
+
return {
|
|
59
|
+
default: 'relevance',
|
|
60
|
+
options: Object.values(
|
|
61
|
+
sortingOptions(
|
|
62
|
+
await getAlgoliaSettings(context),
|
|
63
|
+
await getAttributeList(context),
|
|
64
|
+
context,
|
|
65
|
+
),
|
|
66
|
+
),
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return root.sort_fields ?? null
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
total_count: (root) => {
|
|
73
|
+
if (!isAlgoliaResponse(root)) return root.total_count ?? null
|
|
74
|
+
return root.algoliaSearchResults?.nbHits
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
page_info: (root) => {
|
|
78
|
+
if (!isAlgoliaResponse(root)) return root.page_info ?? null
|
|
79
|
+
return {
|
|
80
|
+
current_page: root.algoliaSearchResults.page + 1,
|
|
81
|
+
page_size: root.algoliaSearchResults.hitsPerPage,
|
|
82
|
+
total_pages: root.algoliaSearchResults.nbPages,
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
items: async (root, args, context) => {
|
|
87
|
+
if (!isAlgoliaResponse(root)) return root.items ?? null
|
|
88
|
+
|
|
89
|
+
const items: (ProductsItemsItem | null)[] = []
|
|
90
|
+
|
|
91
|
+
const config = await getStoreConfig(context)
|
|
92
|
+
for (const hit of root.algoliaSearchResults.hits) {
|
|
93
|
+
if (hit?.objectID) {
|
|
94
|
+
const product = algoliaHitToMagentoProduct(hit, config, getGroupId(context))
|
|
95
|
+
items.push(product)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return items
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
Query: {
|
|
103
|
+
products: async (root, args, context, info) => {
|
|
104
|
+
const isAgolia = (args.filter?.engine?.in ?? [args.filter?.engine?.eq])[0] === 'algolia'
|
|
105
|
+
|
|
106
|
+
if (!isAgolia) return context.m2.Query.products({ root, args, context, info })
|
|
107
|
+
|
|
108
|
+
const searchSuggestsions =
|
|
109
|
+
isSuggestionsEnabled() &&
|
|
110
|
+
hasSuggestionsRequest(info) &&
|
|
111
|
+
args.search &&
|
|
112
|
+
getSearchSuggestions(args.search, context)
|
|
113
|
+
|
|
114
|
+
const searchResults = hasSearchRequest(info) ? getSearchResults(args, context, info) : null
|
|
115
|
+
|
|
116
|
+
if (isGraphQLError(await searchResults))
|
|
117
|
+
return context.m2.Query.products({ root, args, context, info })
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
algoliaSearchResults: await searchResults,
|
|
121
|
+
suggestions: (await searchSuggestsions) || null,
|
|
122
|
+
algolia_queryID: (await searchResults)?.queryID,
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AlgoliasettingsResponse,
|
|
3
|
+
MeshContext,
|
|
4
|
+
ProductAttributeSortInput,
|
|
5
|
+
SortEnum,
|
|
6
|
+
SortField,
|
|
7
|
+
} from '@graphcommerce/graphql-mesh'
|
|
8
|
+
import { nonNullable } from '@graphcommerce/magento-customer'
|
|
9
|
+
import { AttributeList } from './getAttributeList'
|
|
10
|
+
import { getIndexName } from './getIndexName'
|
|
11
|
+
|
|
12
|
+
export type SortingOptions = Record<string, SortField & { dirs: SortEnum[] }>
|
|
13
|
+
|
|
14
|
+
export async function sortingOptions(
|
|
15
|
+
settings: AlgoliasettingsResponse,
|
|
16
|
+
attributeList: AttributeList,
|
|
17
|
+
context: MeshContext,
|
|
18
|
+
): Promise<SortingOptions> {
|
|
19
|
+
const sortRecord: SortingOptions = {
|
|
20
|
+
relevance: { label: 'Relevance', value: 'relevance', dirs: ['DESC'] },
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
;(settings.replicas ?? [])
|
|
24
|
+
.map((replica) => {
|
|
25
|
+
if (!replica) return null
|
|
26
|
+
|
|
27
|
+
const regex = new RegExp(
|
|
28
|
+
`virtual\\(${getIndexName(context)}_(?<sortIndex>.*)_(?<dir>[^_]+)\\)`,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
const res = regex.exec(replica)
|
|
32
|
+
if (!res) return null
|
|
33
|
+
|
|
34
|
+
const { dir, sortIndex } = res.groups ?? {}
|
|
35
|
+
|
|
36
|
+
return dir.toUpperCase() === 'DESC'
|
|
37
|
+
? { dir: 'DESC' as const, sortIndex }
|
|
38
|
+
: { dir: 'ASC' as const, sortIndex }
|
|
39
|
+
})
|
|
40
|
+
.filter(nonNullable)
|
|
41
|
+
.forEach((curr) => {
|
|
42
|
+
let attributeCode = curr.sortIndex
|
|
43
|
+
if (curr.sortIndex.startsWith('price')) attributeCode = 'price'
|
|
44
|
+
|
|
45
|
+
if (!sortRecord[attributeCode]) {
|
|
46
|
+
sortRecord[attributeCode] = {
|
|
47
|
+
value: attributeCode,
|
|
48
|
+
label: attributeList.find((attr) => attr.code === attributeCode)?.label,
|
|
49
|
+
dirs: [curr.dir],
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
sortRecord[attributeCode].dirs.push(curr.dir)
|
|
53
|
+
}
|
|
54
|
+
}, {})
|
|
55
|
+
|
|
56
|
+
return sortRecord
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function getSortedIndex(
|
|
60
|
+
context: MeshContext,
|
|
61
|
+
settings: Promise<AlgoliasettingsResponse>,
|
|
62
|
+
sortInput: ProductAttributeSortInput | null = {},
|
|
63
|
+
): Promise<string> {
|
|
64
|
+
const baseIndex = getIndexName(context)
|
|
65
|
+
// const availableSorting = Object.values(sortOptions)
|
|
66
|
+
const [attr, dir] = Object.entries(sortInput ?? {}).filter(nonNullable)?.[0] ?? []
|
|
67
|
+
if (!attr || !dir) return baseIndex
|
|
68
|
+
|
|
69
|
+
const found = (await settings).replicas?.find(
|
|
70
|
+
(replica) =>
|
|
71
|
+
replica?.startsWith(`virtual(${baseIndex}_${attr}`) &&
|
|
72
|
+
replica?.endsWith(`${dir?.toLowerCase()})`),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
return found ? found?.slice(8, -1) : baseIndex
|
|
76
|
+
}
|
package/mesh/utils.ts
ADDED
package/next-env.d.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@graphcommerce/algolia-products",
|
|
3
|
+
"homepage": "https://www.graphcommerce.org/",
|
|
4
|
+
"repository": "github:graphcommerce-org/graphcommerce",
|
|
5
|
+
"version": "9.0.0-canary.100",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"prettier": "@graphcommerce/prettier-config-pwa",
|
|
8
|
+
"eslintConfig": {
|
|
9
|
+
"extends": "@graphcommerce/eslint-config-pwa",
|
|
10
|
+
"parserOptions": {
|
|
11
|
+
"project": "./tsconfig.json"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"generate": "tsx scripts/generate-spec.mts"
|
|
16
|
+
},
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"@graphcommerce/google-datalayer": "^9.0.0-canary.100",
|
|
19
|
+
"@graphcommerce/graphql": "^9.0.0-canary.100",
|
|
20
|
+
"@graphcommerce/graphql-mesh": "^9.0.0-canary.100",
|
|
21
|
+
"@graphcommerce/magento-customer": "^9.0.0-canary.100",
|
|
22
|
+
"@graphcommerce/magento-product": "^9.0.0-canary.100",
|
|
23
|
+
"@graphcommerce/magento-search": "^9.0.0-canary.100",
|
|
24
|
+
"@graphcommerce/next-config": "^9.0.0-canary.100",
|
|
25
|
+
"@graphcommerce/next-ui": "^9.0.0-canary.100",
|
|
26
|
+
"react": "^18.2.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"graphql": "^16.0.0",
|
|
30
|
+
"tsx": "^4.16.2"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { GraphQLProviderProps } from '@graphcommerce/graphql'
|
|
2
|
+
import type { PluginConfig, PluginProps } from '@graphcommerce/next-config'
|
|
3
|
+
import { customerGroupIdLink } from '../link/customerGroupIdLink'
|
|
4
|
+
|
|
5
|
+
export const config: PluginConfig = {
|
|
6
|
+
type: 'component',
|
|
7
|
+
module: '@graphcommerce/graphql',
|
|
8
|
+
ifConfig: 'algolia.customerGroupPricingEnabled',
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function GraphQLProvider(props: PluginProps<GraphQLProviderProps>) {
|
|
12
|
+
const { Prev, links = [], ...rest } = props
|
|
13
|
+
return <Prev {...rest} links={[...links, customerGroupIdLink]} />
|
|
14
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ProductItemsGridProps } from '@graphcommerce/magento-product'
|
|
2
|
+
import type { PluginConfig, PluginProps } from '@graphcommerce/next-config'
|
|
3
|
+
import { useMemo } from 'react'
|
|
4
|
+
import { AlgoliaQueryContext } from '../hooks/useAlgoliaQueryContext'
|
|
5
|
+
|
|
6
|
+
export const config: PluginConfig = {
|
|
7
|
+
module: '@graphcommerce/magento-product',
|
|
8
|
+
type: 'component',
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function ProductListItemsBase(props: PluginProps<ProductItemsGridProps>) {
|
|
12
|
+
const { Prev, ...rest } = props
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<AlgoliaQueryContext.Provider
|
|
16
|
+
value={useMemo(() => ({ queryID: rest.algolia_queryID }), [rest.algolia_queryID])}
|
|
17
|
+
>
|
|
18
|
+
<Prev {...rest} />
|
|
19
|
+
</AlgoliaQueryContext.Provider>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
useProductListApplyCategoryDefaults as useProductListApplyDefaults,
|
|
3
|
+
productListApplyCategoryDefaults as productListApplyDefaults,
|
|
4
|
+
categoryDefaultsToProductListFilters as defaultsToProductListFilters,
|
|
5
|
+
} from '@graphcommerce/magento-product'
|
|
6
|
+
import { FunctionPlugin, PluginConfig } from '@graphcommerce/next-config'
|
|
7
|
+
import { applyEngineVariables } from '../utils/applyEngineVariable'
|
|
8
|
+
|
|
9
|
+
export const config: PluginConfig = {
|
|
10
|
+
module: '@graphcommerce/magento-product',
|
|
11
|
+
type: 'function',
|
|
12
|
+
ifConfig: 'algolia.catalogEnabled',
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const useProductListApplyCategoryDefaults: FunctionPlugin<
|
|
16
|
+
typeof useProductListApplyDefaults
|
|
17
|
+
> = (prev, params, category) => applyEngineVariables(prev(params, category))
|
|
18
|
+
|
|
19
|
+
export const productListApplyCategoryDefaults: FunctionPlugin<
|
|
20
|
+
typeof productListApplyDefaults
|
|
21
|
+
> = async (prev, params, conf, category) => applyEngineVariables(await prev(params, conf, category))
|
|
22
|
+
|
|
23
|
+
export const categoryDefaultsToProductListFilters: FunctionPlugin<
|
|
24
|
+
typeof defaultsToProductListFilters
|
|
25
|
+
> = (prev, variables) => applyEngineVariables(prev(variables))
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
useProductListApplySearchDefaults as useProductListApplyDefaults,
|
|
3
|
+
productListApplySearchDefaults as productListApplyDefaults,
|
|
4
|
+
searchDefaultsToProductListFilters as defaultsToProductListFilters,
|
|
5
|
+
} from '@graphcommerce/magento-search'
|
|
6
|
+
import { FunctionPlugin, PluginConfig } from '@graphcommerce/next-config'
|
|
7
|
+
import { applyEngineVariables } from '../utils/applyEngineVariable'
|
|
8
|
+
|
|
9
|
+
export const config: PluginConfig = {
|
|
10
|
+
module: '@graphcommerce/magento-search',
|
|
11
|
+
type: 'function',
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const useProductListApplySearchDefaults: FunctionPlugin<
|
|
15
|
+
typeof useProductListApplyDefaults
|
|
16
|
+
> = (prev, params) => applyEngineVariables(prev(params))
|
|
17
|
+
|
|
18
|
+
export const productListApplySearchDefaults: FunctionPlugin<typeof productListApplyDefaults> = (
|
|
19
|
+
prev,
|
|
20
|
+
params,
|
|
21
|
+
conf,
|
|
22
|
+
) => applyEngineVariables(prev(params, conf))
|
|
23
|
+
|
|
24
|
+
export const searchDefaultsToProductListFilters: FunctionPlugin<
|
|
25
|
+
typeof defaultsToProductListFilters
|
|
26
|
+
> = (prev, variables) => applyEngineVariables(prev(variables))
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { meshConfig as meshConfigBase } from '@graphcommerce/graphql-mesh/meshConfig'
|
|
2
|
+
import type { FunctionPlugin, PluginConfig } from '@graphcommerce/next-config'
|
|
3
|
+
|
|
4
|
+
export const config: PluginConfig = {
|
|
5
|
+
module: '@graphcommerce/graphql-mesh/meshConfig',
|
|
6
|
+
type: 'function',
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const meshConfig: FunctionPlugin<typeof meshConfigBase> = (
|
|
10
|
+
prev,
|
|
11
|
+
baseConfig,
|
|
12
|
+
graphCommerceConfig,
|
|
13
|
+
) =>
|
|
14
|
+
prev(
|
|
15
|
+
{
|
|
16
|
+
...baseConfig,
|
|
17
|
+
sources: [
|
|
18
|
+
...baseConfig.sources,
|
|
19
|
+
{
|
|
20
|
+
name: 'algolia',
|
|
21
|
+
handler: {
|
|
22
|
+
openapi: {
|
|
23
|
+
endpoint: `https://${graphCommerceConfig.algolia.applicationId}.algolia.net/`,
|
|
24
|
+
source: '@graphcommerce/algolia-products/algolia-spec.yaml',
|
|
25
|
+
ignoreErrorResponses: true,
|
|
26
|
+
schemaHeaders: {
|
|
27
|
+
'X-Algolia-Application-Id': graphCommerceConfig.algolia.applicationId,
|
|
28
|
+
'X-Algolia-API-Key': graphCommerceConfig.algolia.searchOnlyApiKey,
|
|
29
|
+
},
|
|
30
|
+
operationHeaders: {
|
|
31
|
+
'X-Algolia-Application-Id': graphCommerceConfig.algolia.applicationId,
|
|
32
|
+
'X-Algolia-API-Key': graphCommerceConfig.algolia.searchOnlyApiKey,
|
|
33
|
+
},
|
|
34
|
+
selectQueryOrMutationField: [
|
|
35
|
+
{ type: 'Query', fieldName: 'searchSingleIndex' },
|
|
36
|
+
{ type: 'Query', fieldName: 'searchForFacetValues' },
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
transforms: [
|
|
41
|
+
{
|
|
42
|
+
prefix: {
|
|
43
|
+
value: 'algolia_',
|
|
44
|
+
includeRootOperations: true,
|
|
45
|
+
includeTypes: false,
|
|
46
|
+
mode: 'bare',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
prefix: {
|
|
51
|
+
value: 'Algolia',
|
|
52
|
+
includeRootOperations: false,
|
|
53
|
+
includeTypes: true,
|
|
54
|
+
mode: 'bare',
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
additionalResolvers: [
|
|
61
|
+
...(baseConfig.additionalResolvers ?? []),
|
|
62
|
+
'@graphcommerce/algolia-products/mesh/resolvers.ts',
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
graphCommerceConfig,
|
|
66
|
+
)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
type AlgoliaPriceValue {
|
|
2
|
+
default: Float
|
|
3
|
+
default_formatted: String
|
|
4
|
+
special_from_date: String
|
|
5
|
+
special_to_date: String
|
|
6
|
+
default_max: Float
|
|
7
|
+
default_original_formatted: String
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
type AlgoliaPrice {
|
|
11
|
+
SOME_CURRENCY: AlgoliaPriceValue
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
scalar AlgoliaSku
|
|
15
|
+
|
|
16
|
+
type AlgoliaProductHitAdditionalProperties {
|
|
17
|
+
name: String
|
|
18
|
+
sku: AlgoliaSku
|
|
19
|
+
price: AlgoliaPrice
|
|
20
|
+
|
|
21
|
+
# categories
|
|
22
|
+
# categories_without_path
|
|
23
|
+
# categoryIds
|
|
24
|
+
is_stock: Int
|
|
25
|
+
ordered_qty: Int
|
|
26
|
+
created_at: String
|
|
27
|
+
url: String
|
|
28
|
+
visibility_search: Int
|
|
29
|
+
visibility_catalog: Int
|
|
30
|
+
type_id: String
|
|
31
|
+
rating_summary: String
|
|
32
|
+
thumbnail_url: String
|
|
33
|
+
image_url: String
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type Algoliahit {
|
|
37
|
+
additionalProperties: AlgoliaProductHitAdditionalProperties
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
input ProductAttributeFilterInput {
|
|
41
|
+
"""
|
|
42
|
+
When set to true, the query is passed to Algolia.
|
|
43
|
+
|
|
44
|
+
This is faster, but is only recommended for search and category pages as the data could be out of date and many fields of a product aren't supported.
|
|
45
|
+
"""
|
|
46
|
+
engine: FilterEqualTypeInput
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
input CategoryFilterInput {
|
|
50
|
+
"""
|
|
51
|
+
When set to true, the categories query is passed to Algolia
|
|
52
|
+
|
|
53
|
+
This is faster, but is only recommended for search and category pages as the data could be out of date and many fields of a product aren't supported.
|
|
54
|
+
"""
|
|
55
|
+
engine: FilterEqualTypeInput
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
type Products {
|
|
59
|
+
algolia_queryID: String
|
|
60
|
+
}
|