@graphcommerce/algolia-recommend 9.0.0-canary.84

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.
@@ -0,0 +1,19 @@
1
+ import { algoliaHitToMagentoProduct } from '@graphcommerce/algolia-products'
2
+ import { getGroupId } from '@graphcommerce/algolia-products/mesh/getGroupId'
3
+ import { getStoreConfig } from '@graphcommerce/algolia-products/mesh/getStoreConfig'
4
+ import type {
5
+ MeshContext,
6
+ AlgoliarecommendationsHit,
7
+ Algoliahit,
8
+ } from '@graphcommerce/graphql-mesh'
9
+
10
+ export async function createProductMapper(context: MeshContext) {
11
+ const storeConfig = await getStoreConfig(context)
12
+ const groupId = getGroupId(context)
13
+
14
+ const isAlgoliaRecommendHit = (hit: AlgoliarecommendationsHit | Algoliahit): hit is Algoliahit =>
15
+ '__typename' in hit && hit.__typename === 'AlgoliarecommendHit'
16
+
17
+ return (hit: AlgoliarecommendationsHit) =>
18
+ isAlgoliaRecommendHit(hit) ? algoliaHitToMagentoProduct(hit, storeConfig, groupId) : null
19
+ }
@@ -0,0 +1,13 @@
1
+ import type {
2
+ AlgoliarecommendationsHit,
3
+ AlgoliatrendingFacetHit,
4
+ TrendingFacetValue,
5
+ } from '@graphcommerce/graphql-mesh'
6
+
7
+ export function createFacetValueMapper() {
8
+ const isAlgoliaRecommendHit = (hit: AlgoliarecommendationsHit): hit is AlgoliatrendingFacetHit =>
9
+ !!hit && '__typename' in hit && hit.__typename === 'AlgoliatrendingFacetHit'
10
+
11
+ return (hit: AlgoliarecommendationsHit): TrendingFacetValue | null =>
12
+ isAlgoliaRecommendHit(hit) ? hit : null
13
+ }
@@ -0,0 +1,49 @@
1
+ import { getSearchResultsInput } from '@graphcommerce/algolia-products'
2
+ import {
3
+ QueryproductsArgs,
4
+ MeshContext,
5
+ AlgoliasearchParamsObject_Input,
6
+ AlgoliaLookingSimilarInput,
7
+ } from '@graphcommerce/graphql-mesh'
8
+
9
+ export async function getRecommendationQueryInput(
10
+ args: QueryproductsArgs,
11
+ context: MeshContext,
12
+ ): Promise<AlgoliasearchParamsObject_Input> {
13
+ const queryParameters = await getSearchResultsInput(args, context)
14
+
15
+ if (queryParameters?.facets) delete queryParameters.facets
16
+ if (queryParameters?.hitsPerPage) delete queryParameters.hitsPerPage
17
+ if (queryParameters?.page || queryParameters?.page === 0) {
18
+ delete queryParameters.page
19
+ }
20
+
21
+ return queryParameters
22
+ }
23
+
24
+ export async function getRecommendationsArgs(
25
+ root: { uid?: string },
26
+ args: { input?: AlgoliaLookingSimilarInput | null },
27
+ context: MeshContext,
28
+ ) {
29
+ const { fallback, filter, maxRecommendations = 8, search, threshold = 75 } = args.input ?? {}
30
+
31
+ return {
32
+ objectID: atob(root.uid ?? ''),
33
+ threshold,
34
+ maxRecommendations,
35
+ queryParameters: await getRecommendationQueryInput(
36
+ { filter, pageSize: maxRecommendations, search },
37
+ context,
38
+ ),
39
+ fallbackParameters: await getRecommendationQueryInput(
40
+ {
41
+ filter: fallback?.filter,
42
+ pageSize: maxRecommendations,
43
+ search: fallback?.search,
44
+ sort: fallback?.sort,
45
+ },
46
+ context,
47
+ ),
48
+ }
49
+ }
@@ -0,0 +1,81 @@
1
+ import { getIndexName } from '@graphcommerce/algolia-products/mesh/getIndexName'
2
+ import type {
3
+ AlgoliarecommendationsRequest_Input,
4
+ MeshContext,
5
+ AlgoliarecommendationsHit,
6
+ } from '@graphcommerce/graphql-mesh'
7
+ import { nonNullable } from '@graphcommerce/next-ui'
8
+ import type { GraphQLResolveInfo } from 'graphql'
9
+ import type { Simplify } from 'type-fest'
10
+
11
+ const inputToModel = {
12
+ Trending_items_Input: 'trending_items' as const,
13
+ Trending_facet_values_Input: 'trending_facets' as const,
14
+ Frequently_bought_together_Input: 'bought_together' as const,
15
+ Looking_similar_Input: 'looking_similar' as const,
16
+ Related_products_Input: 'related_products' as const,
17
+ }
18
+ function isAlgoliaResponse<T extends object>(root: T): root is T & { uid: string } {
19
+ return 'uid' in root
20
+ }
21
+ function argsFromKeysInput(keys, args, context) {
22
+ const body = keys
23
+ .map(
24
+ (key) =>
25
+ ({
26
+ [key.keyInput]: {
27
+ model: inputToModel[key.keyInput as string],
28
+ indexName: getIndexName(context),
29
+
30
+ ...args,
31
+ objectID: key.objectId,
32
+ },
33
+ }) as unknown as AlgoliarecommendationsRequest_Input,
34
+ )
35
+ .filter(nonNullable)
36
+
37
+ const returnObject = { input: { requests: body } }
38
+
39
+ return returnObject
40
+ }
41
+ export async function getRecommendations<
42
+ K extends keyof AlgoliarecommendationsRequest_Input,
43
+ Input extends AlgoliarecommendationsRequest_Input[K],
44
+ R,
45
+ >(
46
+ root: object,
47
+ keyInput: K,
48
+ args: Simplify<Omit<NonNullable<Input>, 'indexName' | 'model'>>,
49
+ context: MeshContext,
50
+ info: GraphQLResolveInfo,
51
+ mapper: (hit: AlgoliarecommendationsHit) => R,
52
+ ) {
53
+ if (!isAlgoliaResponse(root)) {
54
+ return []
55
+ }
56
+ return (
57
+ (await context.algoliaRecommend.Query.algolia_getRecommendations({
58
+ key: { keyInput, objectId: atob(root.uid) },
59
+ argsFromKeys: (keys) => argsFromKeysInput(keys, args, context),
60
+ valuesFromResults: (res, keys) =>
61
+ keys
62
+ .map((_key, index) => res?.results[index])
63
+ .map((r) => r?.hits.map((hit) => hit && mapper(hit)).filter(nonNullable)) ?? null,
64
+ selectionSet: /* GraphQL */ `
65
+ {
66
+ results {
67
+ nbHits
68
+ hits {
69
+ ... on AlgoliarecommendHit {
70
+ objectID
71
+ additionalProperties
72
+ }
73
+ }
74
+ }
75
+ }
76
+ `,
77
+ context,
78
+ info,
79
+ })) ?? null
80
+ )
81
+ }
@@ -0,0 +1,239 @@
1
+ /* eslint-disable @typescript-eslint/require-await */
2
+ import fragments from '@graphcommerce/graphql/generated/fragments.json'
3
+ import type {
4
+ AlgoliaLookingSimilarInput,
5
+ AlgoliaRelatedProductsInput,
6
+ MeshContext,
7
+ ProductInterfaceResolvers,
8
+ ResolverFn,
9
+ Resolvers,
10
+ ResolversParentTypes,
11
+ ResolversTypes,
12
+ } from '@graphcommerce/graphql-mesh'
13
+ import {
14
+ GraphCommerceAlgoliaRecommendationLocation,
15
+ InputMaybe,
16
+ Maybe,
17
+ } from '@graphcommerce/next-config'
18
+ import { createProductMapper } from './createProductMapper'
19
+ import { createFacetValueMapper } from './createValueFacetMapper'
20
+ import { getRecommendationsArgs } from './getRecommendationArgs'
21
+ import { getRecommendations } from './getRecommendations'
22
+
23
+ type ProductTypes = NonNullable<Awaited<ReturnType<ProductInterfaceResolvers['__resolveType']>>>
24
+ const productInterfaceTypes = fragments.possibleTypes.ProductInterface as ProductTypes[]
25
+
26
+ const resolvers: Resolvers = {
27
+ Query: {
28
+ trendingProducts: async (root, args, context, info) => {
29
+ const { facetName, facetValue } = args.input
30
+ const { threshold, fallbackParameters, maxRecommendations, queryParameters } =
31
+ await getRecommendationsArgs(root, args, context)
32
+ return getRecommendations(
33
+ root,
34
+ 'Trending_items_Input',
35
+ {
36
+ threshold,
37
+ facetName,
38
+ facetValue,
39
+ fallbackParameters,
40
+ maxRecommendations,
41
+ queryParameters,
42
+ },
43
+ context,
44
+ info,
45
+ await createProductMapper(context),
46
+ )
47
+ },
48
+ trendingFacetValues: async (root, args, context, info) => {
49
+ const { threshold, fallbackParameters, maxRecommendations, queryParameters } =
50
+ await getRecommendationsArgs(root, args, context)
51
+ return getRecommendations(
52
+ root,
53
+ 'Trending_facet_values_Input',
54
+ {
55
+ facetName: args.input.facetName,
56
+ threshold,
57
+ fallbackParameters,
58
+ maxRecommendations,
59
+ queryParameters,
60
+ },
61
+ context,
62
+ info,
63
+ createFacetValueMapper(),
64
+ )
65
+ },
66
+ },
67
+ }
68
+
69
+ function isEnabled(location: InputMaybe<GraphCommerceAlgoliaRecommendationLocation> | undefined) {
70
+ return location && location !== 'DISABLED'
71
+ }
72
+
73
+ function enumToLocation(
74
+ location: InputMaybe<GraphCommerceAlgoliaRecommendationLocation> | undefined,
75
+ ) {
76
+ if (!isEnabled(location)) throw Error('Check for isEnabled before calling this function')
77
+ if (location === 'CROSSSELL_PRODUCTS') return 'crosssell_products' as const
78
+ if (location === 'UPSELL_PRODUCTS') return 'upsell_products' as const
79
+ return 'related_products' as const
80
+ }
81
+
82
+ type ProductResolver = ResolverFn<
83
+ Maybe<Array<Maybe<ResolversTypes['ProductInterface']>>>,
84
+ ResolversParentTypes['ProductInterface'],
85
+ MeshContext,
86
+ Record<string, never>
87
+ >
88
+
89
+ if (isEnabled(import.meta.graphCommerce.algolia.relatedProducts)) {
90
+ const resolve: ProductResolver = async (root, args, context, info) => {
91
+ const { objectID, threshold, fallbackParameters, maxRecommendations, queryParameters } =
92
+ await getRecommendationsArgs(root, args, context)
93
+
94
+ return getRecommendations(
95
+ root,
96
+ 'Related_products_Input',
97
+ { objectID, threshold, fallbackParameters, maxRecommendations, queryParameters },
98
+ context,
99
+ info,
100
+ await createProductMapper(context),
101
+ )
102
+ }
103
+
104
+ productInterfaceTypes.forEach((productType) => {
105
+ if (!resolvers[productType]) resolvers[productType] = {}
106
+ resolvers[productType][enumToLocation(import.meta.graphCommerce.algolia.relatedProducts)] = {
107
+ selectionSet: `{ uid }`,
108
+ resolve,
109
+ }
110
+ })
111
+ }
112
+
113
+ if (isEnabled(import.meta.graphCommerce.algolia.lookingSimilar)) {
114
+ const resolve: ProductResolver = async (root, args, context, info) => {
115
+ const { objectID, threshold, fallbackParameters, maxRecommendations, queryParameters } =
116
+ await getRecommendationsArgs(root, args, context)
117
+ return getRecommendations(
118
+ root,
119
+ 'Looking_similar_Input',
120
+ { objectID, threshold, fallbackParameters, maxRecommendations, queryParameters },
121
+ context,
122
+ info,
123
+ await createProductMapper(context),
124
+ )
125
+ }
126
+
127
+ productInterfaceTypes.forEach((productType) => {
128
+ if (!resolvers[productType]) resolvers[productType] = {}
129
+ resolvers[productType][enumToLocation(import.meta.graphCommerce.algolia.lookingSimilar)] = {
130
+ selectionSet: `{ uid }`,
131
+ resolve,
132
+ }
133
+ })
134
+ }
135
+
136
+ if (isEnabled(import.meta.graphCommerce.algolia.frequentlyBoughtTogether)) {
137
+ const resolver: ProductResolver = async (root, args, context, info) => {
138
+ const { objectID, threshold, maxRecommendations, queryParameters } =
139
+ await getRecommendationsArgs(root, args, context)
140
+
141
+ return getRecommendations(
142
+ root,
143
+ 'Frequently_bought_together_Input',
144
+ { objectID, threshold, maxRecommendations, queryParameters },
145
+ context,
146
+ info,
147
+ await createProductMapper(context),
148
+ )
149
+ }
150
+
151
+ productInterfaceTypes.forEach((productType) => {
152
+ if (!resolvers[productType]) resolvers[productType] = {}
153
+ resolvers[productType][
154
+ enumToLocation(import.meta.graphCommerce.algolia.frequentlyBoughtTogether)
155
+ ] = resolver
156
+ })
157
+ }
158
+
159
+ const similar: ResolverFn<
160
+ Maybe<Array<Maybe<ResolversTypes['ProductInterface']>>>,
161
+ ResolversParentTypes['ProductInterface'],
162
+ MeshContext,
163
+ { input?: AlgoliaLookingSimilarInput | null }
164
+ > = async (root, args, context, info) => {
165
+ const { objectID, threshold, fallbackParameters, maxRecommendations, queryParameters } =
166
+ await getRecommendationsArgs(root, args, context)
167
+
168
+ return getRecommendations(
169
+ root,
170
+ 'Looking_similar_Input',
171
+ { objectID, threshold, fallbackParameters, maxRecommendations, queryParameters },
172
+ context,
173
+ info,
174
+ await createProductMapper(context),
175
+ )
176
+ }
177
+
178
+ const related: ResolverFn<
179
+ Maybe<Array<Maybe<ResolversTypes['ProductInterface']>>>,
180
+ ResolversParentTypes['ProductInterface'],
181
+ MeshContext,
182
+ { input?: AlgoliaRelatedProductsInput | null }
183
+ > = async (root, args, context, info) => {
184
+ const { objectID, threshold, maxRecommendations, queryParameters } = await getRecommendationsArgs(
185
+ root,
186
+ args,
187
+ context,
188
+ )
189
+ return getRecommendations(
190
+ root,
191
+ 'Related_products_Input',
192
+ {
193
+ objectID,
194
+ threshold,
195
+ // fallbackParameters,
196
+ maxRecommendations,
197
+ queryParameters,
198
+ },
199
+ context,
200
+ info,
201
+ await createProductMapper(context),
202
+ )
203
+ }
204
+
205
+ const together: ResolverFn<
206
+ Maybe<Array<Maybe<ResolversTypes['ProductInterface']>>>,
207
+ ResolversParentTypes['ProductInterface'],
208
+ MeshContext,
209
+ { input?: AlgoliaRelatedProductsInput | null }
210
+ > = async (root, args, context, info) => {
211
+ const { objectID, threshold, maxRecommendations, queryParameters } = await getRecommendationsArgs(
212
+ root,
213
+ args,
214
+ context,
215
+ )
216
+ return getRecommendations(
217
+ root,
218
+ 'Frequently_bought_together_Input',
219
+ { objectID, threshold, maxRecommendations, queryParameters },
220
+ context,
221
+ info,
222
+ await createProductMapper(context),
223
+ )
224
+ }
225
+
226
+ productInterfaceTypes.forEach((productType) => {
227
+ if (!resolvers[productType]) resolvers[productType] = {}
228
+
229
+ resolvers[productType].algolia_looking_similar = { selectionSet: `{ uid }`, resolve: similar }
230
+
231
+ resolvers[productType].algolia_related_products = { selectionSet: `{ uid }`, resolve: related }
232
+
233
+ resolvers[productType].algolia_frequently_bought_together = {
234
+ selectionSet: `{ uid }`,
235
+ resolve: together,
236
+ }
237
+ })
238
+
239
+ export default resolvers
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@graphcommerce/algolia-recommend",
3
+ "homepage": "https://www.graphcommerce.org/",
4
+ "repository": "github:graphcommerce-org/graphcommerce",
5
+ "version": "9.0.0-canary.84",
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-recommend-spec.mts"
16
+ },
17
+ "peerDependencies": {
18
+ "@graphcommerce/algolia-products": "^9.0.0-canary.84",
19
+ "@graphcommerce/graphql": "^9.0.0-canary.84",
20
+ "@graphcommerce/graphql-mesh": "^9.0.0-canary.84",
21
+ "@graphcommerce/next-config": "^9.0.0-canary.84",
22
+ "@graphcommerce/next-ui": "^9.0.0-canary.84",
23
+ "react": "^18.2.0"
24
+ },
25
+ "devDependencies": {
26
+ "graphql": "^16.0.0",
27
+ "tsx": "^4.16.2"
28
+ }
29
+ }
@@ -0,0 +1,63 @@
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: 'algoliaRecommend',
21
+ handler: {
22
+ openapi: {
23
+ endpoint: `https://${graphCommerceConfig.algolia.applicationId}.algolia.net/`,
24
+ source: '@graphcommerce/algolia-recommend/algolia-recommend-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: [{ type: 'Query', fieldName: 'getRecommendations' }],
35
+ },
36
+ },
37
+ transforms: [
38
+ {
39
+ prefix: {
40
+ value: 'algolia_',
41
+ includeRootOperations: true,
42
+ includeTypes: false,
43
+ mode: 'bare',
44
+ },
45
+ },
46
+ {
47
+ prefix: {
48
+ value: 'Algolia',
49
+ includeRootOperations: false,
50
+ includeTypes: true,
51
+ mode: 'bare',
52
+ },
53
+ },
54
+ ],
55
+ },
56
+ ],
57
+ additionalResolvers: [
58
+ ...(baseConfig.additionalResolvers ?? []),
59
+ '@graphcommerce/algolia-recommend/mesh/resolvers.ts',
60
+ ],
61
+ },
62
+ graphCommerceConfig,
63
+ )
@@ -0,0 +1,137 @@
1
+ input AlgoliaFallbackParams {
2
+ """
3
+ One or more keywords to use in a full-text search.
4
+ """
5
+ search: String
6
+ """
7
+ The product attributes to search for and return.
8
+ """
9
+ filter: ProductAttributeFilterInput
10
+ """
11
+ Specifies which attributes to sort on, and whether to return the results in ascending or descending order.
12
+ """
13
+ sort: ProductAttributeSortInput
14
+ }
15
+
16
+ input AlgoliaLookingSimilarInput {
17
+ maxRecommendations: Int = 8
18
+
19
+ """
20
+ Minimum score a recommendation must have to be included in the response.
21
+ """
22
+ threshold: Float! = 75
23
+
24
+ """
25
+ One or more keywords to use in a full-text search.
26
+ """
27
+ search: String
28
+
29
+ """
30
+ The product attributes to search for and return.
31
+ """
32
+ filter: ProductAttributeFilterInput
33
+
34
+ """
35
+ When there are no related products, use this fallback query
36
+ """
37
+ fallback: AlgoliaFallbackParams
38
+ }
39
+
40
+ input AlgoliaFrequentlyBoughtTogetherInput {
41
+ maxRecommendations: Int = 8
42
+
43
+ """
44
+ Minimum score a recommendation must have to be included in the response.
45
+ """
46
+ threshold: Float! = 75
47
+
48
+ """
49
+ One or more keywords to use in a full-text search.
50
+ """
51
+ search: String
52
+
53
+ """
54
+ The product attributes to search for and return.
55
+ """
56
+ filter: ProductAttributeFilterInput
57
+ }
58
+
59
+ input AlgoliaRelatedProductsInput {
60
+ maxRecommendations: Int = 8
61
+
62
+ """
63
+ Minimum score a recommendation must have to be included in the response.
64
+ """
65
+ threshold: Float! = 75
66
+
67
+ """
68
+ One or more keywords to use in a full-text search.
69
+ """
70
+ search: String
71
+
72
+ """
73
+ The product attributes to search for and return.
74
+ """
75
+ filter: ProductAttributeFilterInput
76
+
77
+ """
78
+ When there are no related products, use this fallback query
79
+ """
80
+ fallback: AlgoliaFallbackParams
81
+ }
82
+
83
+ interface ProductInterface {
84
+ algolia_looking_similar(input: AlgoliaLookingSimilarInput): [ProductInterface]
85
+ algolia_frequently_bought_together(
86
+ input: AlgoliaFrequentlyBoughtTogetherInput
87
+ ): [ProductInterface]
88
+ algolia_related_products(input: AlgoliaRelatedProductsInput): [ProductInterface]
89
+ }
90
+
91
+ type SimpleProduct implements ProductInterface {
92
+ algolia_looking_similar(input: AlgoliaLookingSimilarInput): [ProductInterface]
93
+ algolia_frequently_bought_together(
94
+ input: AlgoliaFrequentlyBoughtTogetherInput
95
+ ): [ProductInterface]
96
+ algolia_related_products(input: AlgoliaRelatedProductsInput): [ProductInterface]
97
+ }
98
+
99
+ type VirtualProduct implements ProductInterface {
100
+ algolia_looking_similar(input: AlgoliaLookingSimilarInput): [ProductInterface]
101
+ algolia_frequently_bought_together(
102
+ input: AlgoliaFrequentlyBoughtTogetherInput
103
+ ): [ProductInterface]
104
+ algolia_related_products(input: AlgoliaRelatedProductsInput): [ProductInterface]
105
+ }
106
+
107
+ type ConfigurableProduct implements ProductInterface {
108
+ algolia_looking_similar(input: AlgoliaLookingSimilarInput): [ProductInterface]
109
+ algolia_frequently_bought_together(
110
+ input: AlgoliaFrequentlyBoughtTogetherInput
111
+ ): [ProductInterface]
112
+ algolia_related_products(input: AlgoliaRelatedProductsInput): [ProductInterface]
113
+ }
114
+
115
+ type BundleProduct implements ProductInterface {
116
+ algolia_looking_similar(input: AlgoliaLookingSimilarInput): [ProductInterface]
117
+ algolia_frequently_bought_together(
118
+ input: AlgoliaFrequentlyBoughtTogetherInput
119
+ ): [ProductInterface]
120
+ algolia_related_products(input: AlgoliaRelatedProductsInput): [ProductInterface]
121
+ }
122
+
123
+ type DownloadableProduct implements ProductInterface {
124
+ algolia_looking_similar(input: AlgoliaLookingSimilarInput): [ProductInterface]
125
+ algolia_frequently_bought_together(
126
+ input: AlgoliaFrequentlyBoughtTogetherInput
127
+ ): [ProductInterface]
128
+ algolia_related_products(input: AlgoliaRelatedProductsInput): [ProductInterface]
129
+ }
130
+
131
+ type GroupedProduct implements ProductInterface {
132
+ algolia_looking_similar(input: AlgoliaLookingSimilarInput): [ProductInterface]
133
+ algolia_frequently_bought_together(
134
+ input: AlgoliaFrequentlyBoughtTogetherInput
135
+ ): [ProductInterface]
136
+ algolia_related_products(input: AlgoliaRelatedProductsInput): [ProductInterface]
137
+ }