@reactionary/source 0.6.1 → 0.6.2

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 (191) hide show
  1. package/README.md +14 -11
  2. package/core/src/client/client-builder.ts +54 -6
  3. package/core/src/factories/cart.factory.ts +40 -0
  4. package/core/src/factories/category.factory.ts +40 -0
  5. package/core/src/factories/checkout.factory.ts +59 -0
  6. package/core/src/factories/identity.factory.ts +26 -0
  7. package/core/src/factories/index.ts +15 -0
  8. package/core/src/factories/inventory.factory.ts +26 -0
  9. package/core/src/factories/order-search.factory.ts +33 -0
  10. package/core/src/factories/order.factory.ts +21 -0
  11. package/core/src/factories/price.factory.ts +29 -0
  12. package/core/src/factories/product-associations.factory.ts +30 -0
  13. package/core/src/factories/product-list.factory.ts +82 -0
  14. package/core/src/factories/product-reviews.factory.ts +58 -0
  15. package/core/src/factories/product-search.factory.ts +31 -0
  16. package/core/src/factories/product.factory.ts +21 -0
  17. package/core/src/factories/profile.factory.ts +23 -0
  18. package/core/src/factories/store.factory.ts +21 -0
  19. package/core/src/index.ts +3 -1
  20. package/core/src/providers/cart.provider.ts +12 -10
  21. package/core/src/providers/category.provider.ts +9 -7
  22. package/core/src/providers/checkout.provider.ts +14 -10
  23. package/core/src/providers/identity.provider.ts +7 -7
  24. package/core/src/providers/inventory.provider.ts +4 -4
  25. package/core/src/providers/order-search.provider.ts +4 -2
  26. package/core/src/providers/order.provider.ts +4 -4
  27. package/core/src/providers/price.provider.ts +5 -5
  28. package/core/src/providers/product-associations.provider.ts +6 -4
  29. package/core/src/providers/product-list.provider.ts +13 -8
  30. package/core/src/providers/product-reviews.provider.ts +8 -4
  31. package/core/src/providers/product-search.provider.ts +15 -28
  32. package/core/src/providers/product.provider.ts +6 -6
  33. package/core/src/providers/profile.provider.ts +8 -8
  34. package/core/src/providers/store.provider.ts +2 -2
  35. package/core/src/test/client-builder.spec.ts +81 -0
  36. package/examples/node/package.json +7 -7
  37. package/examples/node/src/basic/basic-node-provider-model-extension.spec.ts +10 -4
  38. package/examples/node/src/basic/basic-node-provider-query-extension.spec.ts +8 -2
  39. package/examples/node/src/basic/basic-node-setup.spec.ts +5 -1
  40. package/examples/node/src/basic/client-creation.spec.ts +3 -3
  41. package/examples/node/src/utils.ts +41 -41
  42. package/examples/node/tsconfig.lib.json +2 -1
  43. package/package.json +1 -1
  44. package/providers/algolia/src/core/initialize.ts +76 -21
  45. package/providers/algolia/src/core/initialize.types.ts +107 -0
  46. package/providers/algolia/src/factories/index.ts +1 -0
  47. package/providers/algolia/src/factories/product-search/product-search.factory.ts +182 -0
  48. package/providers/algolia/src/index.ts +2 -2
  49. package/providers/algolia/src/providers/analytics.provider.ts +4 -4
  50. package/providers/algolia/src/providers/product-search.provider.ts +67 -184
  51. package/providers/algolia/src/schema/capabilities.schema.ts +81 -7
  52. package/providers/algolia/src/test/analytics.spec.ts +9 -1
  53. package/providers/algolia/src/test/client-builder-product-search-extension.example.ts +85 -0
  54. package/providers/commercetools/src/core/capability-descriptors.ts +324 -0
  55. package/providers/commercetools/src/core/initialize.ts +35 -151
  56. package/providers/commercetools/src/core/initialize.types.ts +174 -0
  57. package/providers/commercetools/src/factories/cart/cart.factory.ts +142 -0
  58. package/providers/commercetools/src/factories/category/category.factory.ts +77 -0
  59. package/providers/commercetools/src/factories/checkout/checkout-initializer-overrides.example.ts +94 -0
  60. package/providers/commercetools/src/factories/checkout/checkout.factory.ts +338 -0
  61. package/providers/commercetools/src/factories/identity/identity.factory.ts +26 -0
  62. package/providers/commercetools/src/factories/inventory/inventory.factory.ts +49 -0
  63. package/providers/commercetools/src/factories/order/order.factory.ts +149 -0
  64. package/providers/commercetools/src/factories/order-search/order-search.factory.ts +108 -0
  65. package/providers/commercetools/src/factories/price/price.factory.ts +76 -0
  66. package/providers/commercetools/src/factories/product/product-factory-baseline.example.ts +14 -0
  67. package/providers/commercetools/src/factories/product/product-factory-schema-and-parse-extension.example.ts +35 -0
  68. package/providers/commercetools/src/factories/product/product-factory-schema-extension.example.ts +23 -0
  69. package/providers/commercetools/src/factories/product/product-initializer-factory-extension.example.ts +41 -0
  70. package/providers/commercetools/src/factories/product/product-provider-custom-method-only.example.ts +47 -0
  71. package/providers/commercetools/src/factories/product/product-provider-schema-signature-extension.example.ts +61 -0
  72. package/providers/commercetools/src/factories/product/product.factory.ts +220 -0
  73. package/providers/commercetools/src/factories/product/utils.example.ts +9 -0
  74. package/providers/commercetools/src/factories/product-associations/product-associations.factory.ts +103 -0
  75. package/providers/commercetools/src/factories/product-list/product-list.factory.ts +122 -0
  76. package/providers/commercetools/src/factories/product-reviews/product-reviews.factory.ts +81 -0
  77. package/providers/commercetools/src/factories/product-search/product-search.factory.ts +182 -0
  78. package/providers/commercetools/src/factories/profile/profile.factory.ts +94 -0
  79. package/providers/commercetools/src/factories/store/store.factory.ts +49 -0
  80. package/providers/commercetools/src/index.ts +15 -0
  81. package/providers/commercetools/src/providers/cart.provider.ts +67 -193
  82. package/providers/commercetools/src/providers/category.provider.ts +24 -64
  83. package/providers/commercetools/src/providers/checkout.provider.ts +50 -322
  84. package/providers/commercetools/src/providers/identity.provider.ts +35 -15
  85. package/providers/commercetools/src/providers/inventory.provider.ts +13 -31
  86. package/providers/commercetools/src/providers/order-search.provider.ts +16 -110
  87. package/providers/commercetools/src/providers/order.provider.ts +13 -144
  88. package/providers/commercetools/src/providers/price.provider.ts +37 -51
  89. package/providers/commercetools/src/providers/product-associations.provider.ts +39 -104
  90. package/providers/commercetools/src/providers/product-list.provider.ts +38 -23
  91. package/providers/commercetools/src/providers/product-reviews.provider.ts +34 -14
  92. package/providers/commercetools/src/providers/product-search.provider.ts +17 -170
  93. package/providers/commercetools/src/providers/product.provider.ts +20 -199
  94. package/providers/commercetools/src/providers/profile.provider.ts +27 -73
  95. package/providers/commercetools/src/providers/store.provider.ts +13 -31
  96. package/providers/commercetools/src/schema/capabilities.schema.ts +258 -20
  97. package/providers/commercetools/src/test/caching.spec.ts +18 -2
  98. package/providers/commercetools/src/test/client-builder-merge-extensions.example.ts +125 -0
  99. package/providers/fake/src/core/initialize.ts +213 -44
  100. package/providers/fake/src/core/initialize.types.ts +164 -0
  101. package/providers/fake/src/factories/cart/cart.factory.ts +34 -0
  102. package/providers/fake/src/factories/category/category.factory.ts +40 -0
  103. package/providers/fake/src/factories/checkout/checkout.factory.ts +53 -0
  104. package/providers/fake/src/factories/identity/identity.factory.ts +25 -0
  105. package/providers/fake/src/factories/index.ts +14 -0
  106. package/providers/fake/src/factories/inventory/inventory.factory.ts +25 -0
  107. package/providers/fake/src/factories/order/order.factory.ts +22 -0
  108. package/providers/fake/src/factories/order-search/order-search.factory.ts +27 -0
  109. package/providers/fake/src/factories/price/price.factory.ts +26 -0
  110. package/providers/fake/src/factories/product/product.factory.ts +22 -0
  111. package/providers/fake/src/factories/product-associations/product-associations.factory.ts +25 -0
  112. package/providers/fake/src/factories/product-reviews/product-reviews.factory.ts +53 -0
  113. package/providers/fake/src/factories/product-search/product-search.factory.ts +27 -0
  114. package/providers/fake/src/factories/profile/profile.factory.ts +22 -0
  115. package/providers/fake/src/factories/store/store.factory.ts +22 -0
  116. package/providers/fake/src/index.ts +2 -0
  117. package/providers/fake/src/providers/cart.provider.ts +23 -14
  118. package/providers/fake/src/providers/category.provider.ts +120 -105
  119. package/providers/fake/src/providers/checkout.provider.ts +39 -20
  120. package/providers/fake/src/providers/identity.provider.ts +40 -34
  121. package/providers/fake/src/providers/inventory.provider.ts +26 -24
  122. package/providers/fake/src/providers/order-search.provider.ts +38 -30
  123. package/providers/fake/src/providers/order.provider.ts +21 -37
  124. package/providers/fake/src/providers/price.provider.ts +42 -34
  125. package/providers/fake/src/providers/product-associations.provider.ts +23 -10
  126. package/providers/fake/src/providers/product-reviews.provider.ts +71 -69
  127. package/providers/fake/src/providers/product-search.provider.ts +43 -70
  128. package/providers/fake/src/providers/product.provider.ts +34 -32
  129. package/providers/fake/src/providers/profile.provider.ts +62 -55
  130. package/providers/fake/src/providers/store.provider.ts +38 -22
  131. package/providers/fake/src/schema/capabilities.schema.ts +175 -18
  132. package/providers/fake/src/test/cart.provider.spec.ts +20 -3
  133. package/providers/fake/src/test/category.provider.spec.ts +4 -1
  134. package/providers/fake/src/test/checkout.provider.spec.ts +12 -2
  135. package/providers/fake/src/test/client-builder-product-extension.example.ts +75 -0
  136. package/providers/fake/src/test/order-search.provider.spec.ts +4 -7
  137. package/providers/fake/src/test/order.provider.spec.ts +4 -6
  138. package/providers/fake/src/test/price.provider.spec.ts +3 -1
  139. package/providers/fake/src/test/product.provider.spec.ts +8 -2
  140. package/providers/fake/src/test/profile.provider.spec.ts +4 -2
  141. package/providers/google-analytics/src/core/initialize.ts +37 -12
  142. package/providers/google-analytics/src/core/initialize.types.ts +47 -0
  143. package/providers/google-analytics/src/index.ts +1 -0
  144. package/providers/google-analytics/src/schema/capabilities.schema.ts +31 -5
  145. package/providers/medusa/src/core/initialize.ts +324 -81
  146. package/providers/medusa/src/core/initialize.types.ts +184 -0
  147. package/providers/medusa/src/factories/cart/cart.factory.ts +34 -0
  148. package/providers/medusa/src/factories/category/category.factory.ts +37 -0
  149. package/providers/medusa/src/factories/checkout/checkout.factory.ts +50 -0
  150. package/providers/medusa/src/factories/identity/identity.factory.ts +22 -0
  151. package/providers/medusa/src/factories/index.ts +12 -0
  152. package/providers/medusa/src/factories/inventory/inventory.factory.ts +25 -0
  153. package/providers/medusa/src/factories/order/order.factory.ts +22 -0
  154. package/providers/medusa/src/factories/order-search/order-search.factory.ts +27 -0
  155. package/providers/medusa/src/factories/price/price.factory.ts +26 -0
  156. package/providers/medusa/src/factories/product/product.factory.ts +22 -0
  157. package/providers/medusa/src/factories/product-associations/product-associations.factory.ts +25 -0
  158. package/providers/medusa/src/factories/product-search/product-search.factory.ts +27 -0
  159. package/providers/medusa/src/factories/profile/profile.factory.ts +22 -0
  160. package/providers/medusa/src/index.ts +2 -0
  161. package/providers/medusa/src/providers/cart.provider.ts +33 -20
  162. package/providers/medusa/src/providers/category.provider.ts +30 -12
  163. package/providers/medusa/src/providers/checkout.provider.ts +42 -17
  164. package/providers/medusa/src/providers/identity.provider.ts +1 -1
  165. package/providers/medusa/src/providers/inventory.provider.ts +21 -7
  166. package/providers/medusa/src/providers/order-search.provider.ts +16 -5
  167. package/providers/medusa/src/providers/order.provider.ts +17 -5
  168. package/providers/medusa/src/providers/price.provider.ts +26 -7
  169. package/providers/medusa/src/providers/product-associations.provider.ts +19 -8
  170. package/providers/medusa/src/providers/product-search.provider.ts +19 -31
  171. package/providers/medusa/src/providers/product.provider.ts +47 -11
  172. package/providers/medusa/src/providers/profile.provider.ts +35 -11
  173. package/providers/medusa/src/schema/capabilities.schema.ts +229 -18
  174. package/providers/medusa/src/test/cart.provider.spec.ts +18 -2
  175. package/providers/medusa/src/test/category.provider.spec.ts +4 -1
  176. package/providers/medusa/src/test/checkout.spec.ts +9 -9
  177. package/providers/medusa/src/test/inventory.provider.spec.ts +3 -1
  178. package/providers/medusa/src/test/large-cart.provider.spec.ts +8 -2
  179. package/providers/medusa/src/test/price.provider.spec.ts +8 -1
  180. package/providers/medusa/src/test/product.provider.spec.ts +3 -1
  181. package/providers/medusa/src/test/search.provider.spec.ts +16 -3
  182. package/providers/meilisearch/src/core/initialize.ts +88 -21
  183. package/providers/meilisearch/src/core/initialize.types.ts +119 -0
  184. package/providers/meilisearch/src/factories/index.ts +2 -0
  185. package/providers/meilisearch/src/factories/order-search/order-search.factory.ts +27 -0
  186. package/providers/meilisearch/src/factories/product-search/product-search.factory.ts +27 -0
  187. package/providers/meilisearch/src/index.ts +2 -0
  188. package/providers/meilisearch/src/providers/index.ts +1 -0
  189. package/providers/meilisearch/src/providers/order-search.provider.ts +21 -6
  190. package/providers/meilisearch/src/providers/product-search.provider.ts +24 -8
  191. package/providers/meilisearch/src/schema/capabilities.schema.ts +95 -8
@@ -0,0 +1,107 @@
1
+ import type {
2
+ ClientFromCapabilities,
3
+ ProductSearchFactory,
4
+ } from '@reactionary/core';
5
+ import type { AlgoliaCapabilities } from '../schema/capabilities.schema.js';
6
+ import type { AlgoliaProductSearchFactory } from '../factories/product-search/product-search.factory.js';
7
+ import type { AlgoliaAnalyticsProvider } from '../providers/analytics.provider.js';
8
+ import type { AlgoliaProductRecommendationsProvider } from '../providers/product-recommendations.provider.js';
9
+ import type { AlgoliaProductSearchProvider } from '../providers/product-search.provider.js';
10
+
11
+ type EnabledCapability<TCapability> =
12
+ TCapability extends { enabled: true } ? true : false;
13
+
14
+ type NormalizeConfiguredCapabilities<T extends AlgoliaCapabilities> =
15
+ Omit<T, 'productSearch' | 'analytics' | 'productRecommendations'> & {
16
+ productSearch?: EnabledCapability<T['productSearch']>;
17
+ analytics?: EnabledCapability<T['analytics']>;
18
+ productRecommendations?: EnabledCapability<T['productRecommendations']>;
19
+ };
20
+
21
+ type ExtractCapabilityFactory<TCapability, TContract, TDefaultFactory> =
22
+ TCapability extends { enabled: true; factory?: infer TFactory }
23
+ ? TFactory extends TContract
24
+ ? TFactory
25
+ : TDefaultFactory
26
+ : TDefaultFactory;
27
+
28
+ type ExtractCapabilityProvider<TCapability, TDefaultProvider> =
29
+ TCapability extends { enabled: true; provider?: infer TProviderFactory }
30
+ ? TProviderFactory extends (...args: unknown[]) => infer TProvider
31
+ ? TProvider
32
+ : TDefaultProvider
33
+ : TDefaultProvider;
34
+
35
+ type CapabilityOverride<
36
+ TCapability,
37
+ TKey extends string,
38
+ TProvider,
39
+ > = TCapability extends { enabled: true }
40
+ ? { [K in TKey]: TProvider }
41
+ : Record<never, never>;
42
+
43
+ type ProductSearchFactoryFor<T extends AlgoliaCapabilities> =
44
+ ExtractCapabilityFactory<
45
+ T['productSearch'],
46
+ ProductSearchFactory,
47
+ AlgoliaProductSearchFactory
48
+ >;
49
+
50
+ type ProductSearchProviderFor<T extends AlgoliaCapabilities> =
51
+ ExtractCapabilityProvider<
52
+ T['productSearch'],
53
+ AlgoliaProductSearchProvider<ProductSearchFactoryFor<T>>
54
+ >;
55
+
56
+ type AnalyticsProviderFor<T extends AlgoliaCapabilities> =
57
+ ExtractCapabilityProvider<T['analytics'], AlgoliaAnalyticsProvider>;
58
+
59
+ type ProductRecommendationsProviderFor<T extends AlgoliaCapabilities> =
60
+ ExtractCapabilityProvider<
61
+ T['productRecommendations'],
62
+ AlgoliaProductRecommendationsProvider
63
+ >;
64
+
65
+ export type AlgoliaClientFromCapabilities<T extends AlgoliaCapabilities> = Omit<
66
+ ClientFromCapabilities<NormalizeConfiguredCapabilities<T>>,
67
+ 'productSearch' | 'analytics' | 'productRecommendations'
68
+ > &
69
+ CapabilityOverride<T['productSearch'], 'productSearch', ProductSearchProviderFor<T>> &
70
+ CapabilityOverride<T['analytics'], 'analytics', AnalyticsProviderFor<T>> &
71
+ CapabilityOverride<
72
+ T['productRecommendations'],
73
+ 'productRecommendations',
74
+ ProductRecommendationsProviderFor<T>
75
+ >;
76
+
77
+ export function resolveCapabilityProvider<TFactory, TProvider, TProviderArgs>(
78
+ capability:
79
+ | {
80
+ factory?: TFactory;
81
+ provider?: (args: TProviderArgs) => TProvider;
82
+ }
83
+ | undefined,
84
+ defaults: {
85
+ factory: TFactory;
86
+ provider: (args: TProviderArgs) => TProvider;
87
+ },
88
+ buildProviderArgs: (factory: TFactory) => TProviderArgs,
89
+ ): TProvider {
90
+ const factory = capability?.factory ?? defaults.factory;
91
+ const provider = capability?.provider ?? defaults.provider;
92
+
93
+ return provider(buildProviderArgs(factory));
94
+ }
95
+
96
+ export function resolveProviderOnlyCapability<TProvider, TProviderArgs>(
97
+ capability:
98
+ | {
99
+ provider?: (args: TProviderArgs) => TProvider;
100
+ }
101
+ | undefined,
102
+ defaultProvider: (args: TProviderArgs) => TProvider,
103
+ args: TProviderArgs,
104
+ ): TProvider {
105
+ const provider = capability?.provider ?? defaultProvider;
106
+ return provider(args);
107
+ }
@@ -0,0 +1 @@
1
+ export * from './product-search/product-search.factory.js';
@@ -0,0 +1,182 @@
1
+ import type {
2
+ AnyProductSearchResultSchema,
3
+ FacetIdentifier,
4
+ FacetValueIdentifier,
5
+ ProductSearchFactory,
6
+ ProductSearchQueryByTerm,
7
+ ProductSearchResultFacet,
8
+ ProductSearchResultFacetValue,
9
+ ProductSearchResultItem,
10
+ ProductSearchResultItemVariant,
11
+ RequestContext,
12
+ } from '@reactionary/core';
13
+ import {
14
+ FacetIdentifierSchema,
15
+ FacetValueIdentifierSchema,
16
+ ImageSchema,
17
+ ProductSearchResultFacetSchema,
18
+ ProductSearchResultFacetValueSchema,
19
+ ProductSearchResultItemVariantSchema,
20
+ } from '@reactionary/core';
21
+ import type * as z from 'zod';
22
+ import type { SearchResponse } from 'algoliasearch';
23
+ import type {
24
+ AlgoliaNativeRecord,
25
+ AlgoliaNativeVariant,
26
+ AlgoliaProductSearchResult,
27
+ } from '../../schema/search.schema.js';
28
+ import type { AlgoliaProductSearchResultSchema } from '../../schema/search.schema.js';
29
+
30
+ export class AlgoliaProductSearchFactory<
31
+ TProductSearchResultSchema extends AnyProductSearchResultSchema = typeof AlgoliaProductSearchResultSchema,
32
+ > implements ProductSearchFactory<TProductSearchResultSchema>
33
+ {
34
+ public readonly productSearchResultSchema: TProductSearchResultSchema;
35
+
36
+ constructor(productSearchResultSchema: TProductSearchResultSchema) {
37
+ this.productSearchResultSchema = productSearchResultSchema;
38
+ }
39
+
40
+ public parseSearchResult(
41
+ _context: RequestContext,
42
+ data: unknown,
43
+ query: ProductSearchQueryByTerm,
44
+ ): z.output<TProductSearchResultSchema> {
45
+ const body = this.parseInput(data);
46
+ const items = body.hits.map((hit) => this.parseSingle(hit));
47
+
48
+ let facets: ProductSearchResultFacet[] = [];
49
+ for (const id in body.facets) {
50
+ const values = body.facets[id];
51
+ const facetId = FacetIdentifierSchema.parse({ key: id });
52
+ facets.push(this.parseFacet(facetId, values));
53
+ }
54
+
55
+ const selectedCategoryFacet =
56
+ query.search.facets.find((x) => x.facet.key === 'categories') ||
57
+ query.search.categoryFilter;
58
+
59
+ let subCategoryFacet;
60
+ if (selectedCategoryFacet) {
61
+ const valueDepth = selectedCategoryFacet.key.split(' > ').length;
62
+ subCategoryFacet = facets.find(
63
+ (f) => f.identifier.key === `hierarchy.lvl${valueDepth}`,
64
+ );
65
+ } else {
66
+ subCategoryFacet = facets.find((f) => f.identifier.key === 'hierarchy.lvl0');
67
+ }
68
+
69
+ if (subCategoryFacet) {
70
+ subCategoryFacet.identifier = FacetIdentifierSchema.parse({ key: 'categories' });
71
+ subCategoryFacet.name = 'Categories';
72
+ for (const value of subCategoryFacet.values) {
73
+ const pathParts = value.identifier.key.split(' > ');
74
+ value.identifier.facet = subCategoryFacet.identifier;
75
+ value.name = pathParts[pathParts.length - 1];
76
+ }
77
+ }
78
+
79
+ facets = facets.filter((f) => !f.identifier.key.startsWith('hierarchy.lvl'));
80
+
81
+ const result = {
82
+ identifier: {
83
+ term: query.search.term,
84
+ facets: query.search.facets,
85
+ filters: query.search.filters,
86
+ paginationOptions: query.search.paginationOptions,
87
+ index: body.index || '',
88
+ key: body.queryID || '',
89
+ },
90
+ pageNumber: (body.page || 0) + 1,
91
+ pageSize: body.hitsPerPage || 0,
92
+ totalCount: body.nbHits || 0,
93
+ totalPages: body.nbPages || 0,
94
+ items,
95
+ facets,
96
+ } satisfies AlgoliaProductSearchResult;
97
+
98
+ return this.productSearchResultSchema.parse(result);
99
+ }
100
+
101
+ protected parseSingle(body: AlgoliaNativeRecord): ProductSearchResultItem {
102
+ return {
103
+ identifier: { key: body.objectID },
104
+ name: body.name || body.objectID,
105
+ slug: body.slug || body.objectID,
106
+ variants: [...(body.variants || [])].map((variant) =>
107
+ this.parseVariant(variant, body),
108
+ ),
109
+ } satisfies ProductSearchResultItem;
110
+ }
111
+
112
+ protected parseVariant(
113
+ variant: AlgoliaNativeVariant,
114
+ product: AlgoliaNativeRecord,
115
+ ): ProductSearchResultItemVariant {
116
+ return ProductSearchResultItemVariantSchema.parse({
117
+ variant: {
118
+ sku: variant.sku,
119
+ },
120
+ image: ImageSchema.parse({
121
+ sourceUrl: variant.image,
122
+ altText: product.name || '',
123
+ }),
124
+ } satisfies Partial<ProductSearchResultItemVariant>);
125
+ }
126
+
127
+ protected parseFacet(
128
+ facetIdentifier: FacetIdentifier,
129
+ facetValues: Record<string, number>,
130
+ ): ProductSearchResultFacet {
131
+ const result = ProductSearchResultFacetSchema.parse({
132
+ identifier: facetIdentifier,
133
+ name: facetIdentifier.key,
134
+ values: [],
135
+ } satisfies Partial<ProductSearchResultFacet>);
136
+
137
+ for (const valueId in facetValues) {
138
+ const count = facetValues[valueId];
139
+ const facetValueIdentifier = FacetValueIdentifierSchema.parse({
140
+ facet: facetIdentifier,
141
+ key: valueId,
142
+ } satisfies Partial<FacetValueIdentifier>);
143
+
144
+ result.values.push(this.parseFacetValue(facetValueIdentifier, valueId, count));
145
+ }
146
+
147
+ return result;
148
+ }
149
+
150
+ protected parseFacetValue(
151
+ facetValueIdentifier: FacetValueIdentifier,
152
+ label: string,
153
+ count: number,
154
+ ): ProductSearchResultFacetValue {
155
+ return ProductSearchResultFacetValueSchema.parse({
156
+ identifier: facetValueIdentifier,
157
+ name: label,
158
+ count,
159
+ active: false,
160
+ } satisfies Partial<ProductSearchResultFacetValue>);
161
+ }
162
+
163
+ protected parseInput(data: unknown): SearchResponse<AlgoliaNativeRecord> {
164
+ if (!this.isSearchResponse(data)) {
165
+ throw new Error('Invalid Algolia search response');
166
+ }
167
+
168
+ return data;
169
+ }
170
+
171
+ protected isSearchResponse(data: unknown): data is SearchResponse<AlgoliaNativeRecord> {
172
+ if (!data || typeof data !== 'object') {
173
+ return false;
174
+ }
175
+
176
+ if (!('hits' in data) || !Array.isArray(data.hits)) {
177
+ return false;
178
+ }
179
+
180
+ return true;
181
+ }
182
+ }
@@ -1,6 +1,6 @@
1
1
  export * from './core/initialize.js';
2
- export * from './providers/product-search.provider.js';
3
- export * from './providers/product-recommendations.provider.js';
2
+ export * from './providers/index.js';
3
+ export * from './factories/index.js';
4
4
 
5
5
  export * from './schema/configuration.schema.js';
6
6
  export * from './schema/capabilities.schema.js';
@@ -48,7 +48,7 @@ export class AlgoliaAnalyticsProvider extends AnalyticsProvider {
48
48
  .key,
49
49
  } satisfies AddedToCartObjectIDsAfterSearch;
50
50
 
51
- const response = await this.client.pushEvents({
51
+ await this.client.pushEvents({
52
52
  events: [algoliaEvent],
53
53
  });
54
54
  }
@@ -69,7 +69,7 @@ export class AlgoliaAnalyticsProvider extends AnalyticsProvider {
69
69
  .key,
70
70
  } satisfies ClickedObjectIDsAfterSearch;
71
71
 
72
- const response = await this.client.pushEvents({
72
+ await this.client.pushEvents({
73
73
  events: [algoliaEvent],
74
74
  });
75
75
  }
@@ -87,7 +87,7 @@ export class AlgoliaAnalyticsProvider extends AnalyticsProvider {
87
87
  userToken: this.context.session.identityContext.personalizationKey,
88
88
  } satisfies ViewedObjectIDs;
89
89
 
90
- const response = await this.client.pushEvents({
90
+ await this.client.pushEvents({
91
91
  events: [algoliaEvent],
92
92
  });
93
93
  }
@@ -107,7 +107,7 @@ export class AlgoliaAnalyticsProvider extends AnalyticsProvider {
107
107
  userToken: this.context.session.identityContext.personalizationKey,
108
108
  } satisfies PurchasedObjectIDs;
109
109
 
110
- const response = await this.client.pushEvents({
110
+ await this.client.pushEvents({
111
111
  events: [algoliaEvent],
112
112
  });
113
113
  }
@@ -4,233 +4,116 @@ import {
4
4
  FacetIdentifierSchema,
5
5
  type FacetValueIdentifier,
6
6
  FacetValueIdentifierSchema,
7
- ImageSchema,
8
7
  ProductSearchProvider,
8
+ type ProductSearchFactory,
9
+ type ProductSearchFactoryOutput,
10
+ type ProductSearchFactoryWithOutput,
9
11
  type ProductSearchQueryByTerm,
10
12
  ProductSearchQueryByTermSchema,
11
13
  type ProductSearchQueryCreateNavigationFilter,
12
- type ProductSearchResult,
13
- type ProductSearchResultFacet,
14
- ProductSearchResultFacetSchema,
15
- type ProductSearchResultFacetValue,
16
- ProductSearchResultFacetValueSchema,
17
- type ProductSearchResultItem,
18
- type ProductSearchResultItemVariant,
19
- ProductSearchResultItemVariantSchema,
20
- ProductSearchResultSchema,
21
- Reactionary,
22
14
  type RequestContext,
23
15
  type Result,
24
- success
16
+ ProductSearchResultSchema,
17
+ Reactionary,
18
+ success,
25
19
  } from '@reactionary/core';
26
- import { algoliasearch, type SearchResponse } from 'algoliasearch';
20
+ import { algoliasearch } from 'algoliasearch';
27
21
  import type { AlgoliaConfiguration } from '../schema/configuration.schema.js';
28
- import type { AlgoliaNativeRecord, AlgoliaNativeVariant, AlgoliaProductSearchResult } from '../schema/search.schema.js';
22
+ import type { AlgoliaNativeRecord } from '../schema/search.schema.js';
23
+ import type { AlgoliaProductSearchFactory } from '../factories/product-search/product-search.factory.js';
29
24
 
30
-
31
-
32
- export class AlgoliaProductSearchProvider extends ProductSearchProvider {
25
+ export class AlgoliaProductSearchProvider<
26
+ TFactory extends ProductSearchFactory = AlgoliaProductSearchFactory,
27
+ > extends ProductSearchProvider<ProductSearchFactoryOutput<TFactory>> {
33
28
  protected config: AlgoliaConfiguration;
34
-
35
- constructor(cache: Cache, context: RequestContext, config: AlgoliaConfiguration) {
29
+ protected factory: ProductSearchFactoryWithOutput<TFactory>;
30
+
31
+ constructor(
32
+ cache: Cache,
33
+ context: RequestContext,
34
+ config: AlgoliaConfiguration,
35
+ factory: ProductSearchFactoryWithOutput<TFactory>,
36
+ ) {
36
37
  super(cache, context);
37
38
  this.config = config;
39
+ this.factory = factory;
38
40
  }
39
41
 
40
42
  protected queryByTermPayload(payload: ProductSearchQueryByTerm) {
43
+ const facetsThatAreNotCategory = payload.search.facets.filter(
44
+ (x) => x.facet.key !== 'categories',
45
+ );
46
+ const categoryFacet =
47
+ payload.search.facets.find((x) => x.facet.key === 'categories') ||
48
+ payload.search.categoryFilter;
41
49
 
42
- const facetsThatAreNotCategory = payload.search.facets.filter(x => x.facet.key !== 'categories');
43
- const categoryFacet = payload.search.facets.find(x => x.facet.key === 'categories') || payload.search.categoryFilter;
44
-
45
- const finalFilters = [...payload.search.filters || []];
46
-
50
+ const finalFilters = [...(payload.search.filters || [])];
47
51
 
48
52
  const finalFacetFilters = [
49
- ...facetsThatAreNotCategory.map(
50
- (x) => `${x.facet.key}:${x.key}`
51
- ),
52
- ]
53
+ ...facetsThatAreNotCategory.map((x) => `${x.facet.key}:${x.key}`),
54
+ ];
55
+
53
56
  if (categoryFacet) {
54
57
  finalFilters.push(`categories:"${categoryFacet.key}"`);
55
58
  }
59
+
56
60
  return {
57
- indexName: this.config.indexName,
58
- query: payload.search.term,
59
- page: payload.search.paginationOptions.pageNumber - 1,
60
- hitsPerPage: payload.search.paginationOptions.pageSize,
61
- facets: ['*'],
62
- analytics: true,
63
- clickAnalytics: true,
64
- facetFilters: finalFacetFilters,
65
- filters: (finalFilters || [])
66
- .join(' AND '),
67
- };
61
+ indexName: this.config.indexName,
62
+ query: payload.search.term,
63
+ page: payload.search.paginationOptions.pageNumber - 1,
64
+ hitsPerPage: payload.search.paginationOptions.pageSize,
65
+ facets: ['*'],
66
+ analytics: true,
67
+ clickAnalytics: true,
68
+ facetFilters: finalFacetFilters,
69
+ filters: finalFilters.join(' AND '),
70
+ };
68
71
  }
69
72
 
70
73
  @Reactionary({
71
74
  inputSchema: ProductSearchQueryByTermSchema,
72
- outputSchema: ProductSearchResultSchema
75
+ outputSchema: ProductSearchResultSchema,
73
76
  })
74
77
  public override async queryByTerm(
75
- payload: ProductSearchQueryByTerm
76
- ): Promise<Result<ProductSearchResult>> {
78
+ payload: ProductSearchQueryByTerm,
79
+ ): Promise<Result<ProductSearchFactoryOutput<TFactory>>> {
77
80
  const client = algoliasearch(this.config.appId, this.config.apiKey);
78
81
 
79
-
80
-
81
82
  const remote = await client.search<AlgoliaNativeRecord>({
82
- requests: [
83
- this.queryByTermPayload(payload)
84
- ],
83
+ requests: [this.queryByTermPayload(payload)],
85
84
  });
86
85
 
87
- const input = remote.results[0] as SearchResponse<AlgoliaNativeRecord>;
88
- const result = this.parsePaginatedResult(input, payload) as AlgoliaProductSearchResult;
86
+ const input = remote.results[0];
87
+ const result = this.factory.parseSearchResult(this.context, input, payload);
89
88
 
90
- // mark selected facets as active
91
- for(const selectedFacet of payload.search.facets) {
92
- const facet = result.facets.find((f) => f.identifier.key === selectedFacet.facet.key);
93
- if(facet) {
94
- const value = facet.values.find((v) => v.identifier.key === selectedFacet.key);
95
- if(value) {
96
- value.active = true;
97
- }
89
+ for (const selectedFacet of payload.search.facets) {
90
+ const facet = result.facets.find(
91
+ (f) => f.identifier.key === selectedFacet.facet.key,
92
+ );
93
+ if (!facet) {
94
+ continue;
95
+ }
96
+ const value = facet.values.find((v) => v.identifier.key === selectedFacet.key);
97
+ if (value) {
98
+ value.active = true;
98
99
  }
99
100
  }
100
101
 
101
102
  return success(result);
102
103
  }
103
104
 
104
- public override async createCategoryNavigationFilter(payload: ProductSearchQueryCreateNavigationFilter): Promise<Result<FacetValueIdentifier>> {
105
-
105
+ public override async createCategoryNavigationFilter(
106
+ payload: ProductSearchQueryCreateNavigationFilter,
107
+ ): Promise<Result<FacetValueIdentifier>> {
106
108
  const facetIdentifier = FacetIdentifierSchema.parse({
107
- key: 'categories'
108
- });
109
+ key: 'categories',
110
+ } satisfies Partial<FacetIdentifier>);
111
+
109
112
  const facetValueIdentifier = FacetValueIdentifierSchema.parse({
110
113
  facet: facetIdentifier,
111
- key: payload.categoryPath.map(c => c.name).join(' > ')
112
- });
113
- return success(facetValueIdentifier);
114
- }
114
+ key: payload.categoryPath.map((c) => c.name).join(' > '),
115
+ } satisfies Partial<FacetValueIdentifier>);
115
116
 
116
-
117
- protected parseSingle(body: AlgoliaNativeRecord) {
118
- const product = {
119
- identifier: { key: body.objectID },
120
- name: body.name || body.objectID,
121
- slug: body.slug || body.objectID,
122
- variants: [ ... (body.variants || []) ].map(variant => this.parseVariant(variant, body)),
123
- } satisfies ProductSearchResultItem;
124
-
125
- return product;
126
- }
127
-
128
- protected override parseVariant(variant: AlgoliaNativeVariant, product: AlgoliaNativeRecord): ProductSearchResultItemVariant {
129
- const result = ProductSearchResultItemVariantSchema.parse({
130
- variant: {
131
- sku: variant.sku
132
- },
133
- image: ImageSchema.parse({
134
- sourceUrl: variant.image,
135
- altText: product.name || '',
136
- })
137
- } satisfies Partial<ProductSearchResultItemVariant>);
138
-
139
- return result;
140
- }
141
-
142
- protected parsePaginatedResult(body: SearchResponse<AlgoliaNativeRecord>, query: ProductSearchQueryByTerm) {
143
- const items = body.hits.map((hit) => this.parseSingle(hit));
144
- let facets: ProductSearchResultFacet[] = [];
145
- for (const id in body.facets) {
146
- const f = body.facets[id];
147
- const facetId = FacetIdentifierSchema.parse({
148
- key: id
149
- })
150
- const facet = this.parseFacet(facetId, f);
151
- facets.push(facet);
152
- }
153
-
154
-
155
- // we do something to convert the hierachy.lvl.n facet values into something more usable
156
- const selectedCategoryFacet = query.search.facets.find(x => x.facet.key === 'categories') || query.search.categoryFilter;
157
- let subCategoryFacet;
158
- if(selectedCategoryFacet) {
159
- const valueDepth = selectedCategoryFacet.key.split(' > ').length;
160
- // ok, so input defined a facet value from level X, we return hierarchy.lvl.(X+1) as subcategories.
161
- // hierarchy counts from 0, so length is already pointing to 'lvl.(X+1)'
162
- subCategoryFacet = facets.find(f => f.identifier.key === `hierarchy.lvl${valueDepth}`);
163
- } else {
164
- // and remap lvl0 to 'categories'
165
- subCategoryFacet = facets.find(f => f.identifier.key === 'hierarchy.lvl0');
166
- }
167
-
168
- if(subCategoryFacet) {
169
- // remap to 'categories' facet
170
- subCategoryFacet.identifier = FacetIdentifierSchema.parse({
171
- key: 'categories'
172
- });
173
- subCategoryFacet.name = 'Categories';
174
- for(const v of subCategoryFacet.values) {
175
- const pathParts = v.identifier.key.split(' > ');
176
- v.identifier.facet = subCategoryFacet.identifier;
177
- v.name = pathParts[pathParts.length -1];
178
- }
179
- }
180
-
181
- // remove other hierarchy facets
182
- facets = facets.filter(f => !f.identifier.key.startsWith('hierarchy.lvl'));
183
-
184
-
185
-
186
- const result = {
187
- identifier: {
188
- term: query.search.term,
189
- facets: query.search.facets,
190
- filters: query.search.filters,
191
- paginationOptions: query.search.paginationOptions,
192
- index: body.index || '',
193
- key: body.queryID || '',
194
- },
195
- pageNumber: (body.page || 0) + 1,
196
- pageSize: body.hitsPerPage || 0,
197
- totalCount: body.nbHits || 0,
198
- totalPages: body.nbPages || 0,
199
- items: items,
200
- facets,
201
- } satisfies AlgoliaProductSearchResult;
202
-
203
- return result;
204
- }
205
-
206
- protected parseFacet(facetIdentifier: FacetIdentifier, facetValues: Record<string, number>) : ProductSearchResultFacet {
207
- const result: ProductSearchResultFacet = ProductSearchResultFacetSchema.parse({
208
- identifier: facetIdentifier,
209
- name: facetIdentifier.key,
210
- values: []
211
- });
212
-
213
- for (const vid in facetValues) {
214
- const fv = facetValues[vid];
215
-
216
- const facetValueIdentifier = FacetValueIdentifierSchema.parse({
217
- facet: facetIdentifier,
218
- key: vid
219
- } satisfies Partial<FacetValueIdentifier>);
220
-
221
- result.values.push(this.parseFacetValue(facetValueIdentifier, vid, fv));
222
- }
223
- return result;
224
- }
225
-
226
- protected parseFacetValue(facetValueIdentifier: FacetValueIdentifier, label: string, count: number) : ProductSearchResultFacetValue {
227
- return ProductSearchResultFacetValueSchema.parse({
228
- identifier: facetValueIdentifier,
229
- name: label,
230
- count: count,
231
- active: false,
232
- } satisfies Partial<ProductSearchResultFacetValue>);
117
+ return success(facetValueIdentifier);
233
118
  }
234
-
235
-
236
119
  }