@reactionary/source 0.0.52 → 0.2.16

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 (293) hide show
  1. package/.env-template +19 -0
  2. package/.github/workflows/pull-request.yml +3 -1
  3. package/.github/workflows/release.yml +9 -0
  4. package/.vscode/extensions.json +0 -2
  5. package/LICENSE +21 -0
  6. package/README.md +175 -23
  7. package/core/package.json +6 -3
  8. package/core/src/cache/cache.interface.ts +1 -0
  9. package/core/src/cache/index.ts +4 -0
  10. package/core/src/cache/memory-cache.ts +30 -2
  11. package/core/src/cache/noop-cache.ts +15 -1
  12. package/core/src/cache/redis-cache.ts +20 -0
  13. package/core/src/client/client-builder.ts +71 -54
  14. package/core/src/client/client.ts +9 -47
  15. package/core/src/client/index.ts +2 -0
  16. package/core/src/decorators/index.ts +1 -0
  17. package/core/src/decorators/reactionary.decorator.ts +203 -34
  18. package/core/src/index.ts +6 -19
  19. package/core/src/initialization.ts +1 -18
  20. package/core/src/metrics/metrics.ts +67 -0
  21. package/core/src/providers/analytics.provider.ts +1 -6
  22. package/core/src/providers/base.provider.ts +5 -69
  23. package/core/src/providers/cart.provider.ts +15 -55
  24. package/core/src/providers/category.provider.ts +7 -11
  25. package/core/src/providers/checkout.provider.ts +17 -15
  26. package/core/src/providers/identity.provider.ts +6 -8
  27. package/core/src/providers/index.ts +2 -1
  28. package/core/src/providers/inventory.provider.ts +15 -5
  29. package/core/src/providers/order-search.provider.ts +29 -0
  30. package/core/src/providers/order.provider.ts +47 -15
  31. package/core/src/providers/price.provider.ts +30 -36
  32. package/core/src/providers/product-search.provider.ts +61 -0
  33. package/core/src/providers/product.provider.ts +71 -12
  34. package/core/src/providers/profile.provider.ts +74 -14
  35. package/core/src/providers/store.provider.ts +3 -5
  36. package/core/src/schemas/capabilities.schema.ts +10 -3
  37. package/core/src/schemas/errors/generic.error.ts +9 -0
  38. package/core/src/schemas/errors/index.ts +4 -0
  39. package/core/src/schemas/errors/invalid-input.error.ts +9 -0
  40. package/core/src/schemas/errors/invalid-output.error.ts +9 -0
  41. package/core/src/schemas/errors/not-found.error.ts +9 -0
  42. package/core/src/schemas/index.ts +7 -0
  43. package/core/src/schemas/models/analytics.model.ts +2 -1
  44. package/core/src/schemas/models/base.model.ts +6 -24
  45. package/core/src/schemas/models/cart.model.ts +5 -8
  46. package/core/src/schemas/models/category.model.ts +4 -9
  47. package/core/src/schemas/models/checkout.model.ts +6 -7
  48. package/core/src/schemas/models/cost.model.ts +4 -3
  49. package/core/src/schemas/models/currency.model.ts +2 -1
  50. package/core/src/schemas/models/identifiers.model.ts +106 -62
  51. package/core/src/schemas/models/identity.model.ts +10 -19
  52. package/core/src/schemas/models/index.ts +2 -1
  53. package/core/src/schemas/models/inventory.model.ts +8 -5
  54. package/core/src/schemas/models/order-search.model.ts +28 -0
  55. package/core/src/schemas/models/order.model.ts +20 -26
  56. package/core/src/schemas/models/payment.model.ts +14 -17
  57. package/core/src/schemas/models/price.model.ts +11 -11
  58. package/core/src/schemas/models/product-search.model.ts +42 -0
  59. package/core/src/schemas/models/product.model.ts +64 -22
  60. package/core/src/schemas/models/profile.model.ts +19 -22
  61. package/core/src/schemas/models/shipping-method.model.ts +24 -29
  62. package/core/src/schemas/models/store.model.ts +9 -5
  63. package/core/src/schemas/mutations/analytics.mutation.ts +8 -7
  64. package/core/src/schemas/mutations/base.mutation.ts +2 -1
  65. package/core/src/schemas/mutations/cart.mutation.ts +33 -33
  66. package/core/src/schemas/mutations/checkout.mutation.ts +23 -30
  67. package/core/src/schemas/mutations/identity.mutation.ts +4 -3
  68. package/core/src/schemas/mutations/profile.mutation.ts +38 -3
  69. package/core/src/schemas/queries/base.query.ts +2 -1
  70. package/core/src/schemas/queries/cart.query.ts +3 -3
  71. package/core/src/schemas/queries/category.query.ts +18 -18
  72. package/core/src/schemas/queries/checkout.query.ts +7 -9
  73. package/core/src/schemas/queries/identity.query.ts +2 -1
  74. package/core/src/schemas/queries/index.ts +2 -1
  75. package/core/src/schemas/queries/inventory.query.ts +5 -5
  76. package/core/src/schemas/queries/order-search.query.ts +10 -0
  77. package/core/src/schemas/queries/order.query.ts +3 -2
  78. package/core/src/schemas/queries/price.query.ts +10 -4
  79. package/core/src/schemas/queries/product-search.query.ts +16 -0
  80. package/core/src/schemas/queries/product.query.ts +13 -6
  81. package/core/src/schemas/queries/profile.query.ts +5 -2
  82. package/core/src/schemas/queries/store.query.ts +6 -5
  83. package/core/src/schemas/result.ts +107 -0
  84. package/core/src/schemas/session.schema.ts +4 -4
  85. package/core/src/test/reactionary.decorator.spec.ts +249 -0
  86. package/core/src/zod-utils.ts +19 -0
  87. package/core/tsconfig.json +1 -1
  88. package/core/tsconfig.spec.json +2 -26
  89. package/core/vitest.config.ts +14 -0
  90. package/documentation/1-purpose.md +114 -0
  91. package/documentation/2-getting-started.md +229 -0
  92. package/documentation/3-querying-and-changing-data.md +74 -0
  93. package/documentation/4-product-data.md +107 -0
  94. package/documentation/5-cart-and-checkout.md +211 -0
  95. package/documentation/6-product-search.md +143 -0
  96. package/documentation/7-marketing.md +3 -0
  97. package/eslint.config.mjs +1 -0
  98. package/examples/node/eslint.config.mjs +1 -4
  99. package/examples/node/package.json +10 -3
  100. package/examples/node/project.json +4 -1
  101. package/examples/node/src/basic/basic-node-provider-model-extension.spec.ts +22 -23
  102. package/examples/node/src/basic/basic-node-provider-query-extension.spec.ts +15 -11
  103. package/examples/node/src/basic/basic-node-setup.spec.ts +44 -28
  104. package/examples/node/src/basic/client-creation.spec.ts +53 -0
  105. package/examples/node/src/capabilities/cart.spec.ts +255 -0
  106. package/examples/node/src/capabilities/category.spec.ts +193 -0
  107. package/examples/node/src/capabilities/checkout.spec.ts +341 -0
  108. package/examples/node/src/capabilities/identity.spec.ts +93 -0
  109. package/examples/node/src/capabilities/inventory.spec.ts +66 -0
  110. package/examples/node/src/capabilities/order-search.spec.ts +265 -0
  111. package/examples/node/src/capabilities/order.spec.ts +91 -0
  112. package/examples/node/src/capabilities/price.spec.ts +51 -0
  113. package/examples/node/src/capabilities/product-search.spec.ts +293 -0
  114. package/examples/node/src/capabilities/product.spec.ts +122 -0
  115. package/examples/node/src/capabilities/profile.spec.ts +316 -0
  116. package/examples/node/src/capabilities/store.spec.ts +26 -0
  117. package/examples/node/src/utils.ts +147 -0
  118. package/examples/node/tsconfig.json +9 -12
  119. package/examples/node/tsconfig.lib.json +1 -2
  120. package/examples/node/tsconfig.spec.json +2 -14
  121. package/examples/node/vitest.config.ts +14 -0
  122. package/migrations.json +22 -5
  123. package/nx.json +8 -47
  124. package/package.json +24 -96
  125. package/providers/algolia/README.md +39 -2
  126. package/providers/algolia/package.json +2 -1
  127. package/providers/algolia/src/core/initialize.ts +7 -14
  128. package/providers/algolia/src/index.ts +2 -4
  129. package/providers/algolia/src/providers/index.ts +1 -0
  130. package/providers/algolia/src/providers/product-search.provider.ts +241 -0
  131. package/providers/algolia/src/schema/capabilities.schema.ts +2 -3
  132. package/providers/algolia/src/schema/index.ts +3 -0
  133. package/providers/algolia/src/schema/search.schema.ts +8 -8
  134. package/providers/algolia/tsconfig.json +1 -1
  135. package/providers/algolia/tsconfig.lib.json +1 -1
  136. package/providers/algolia/tsconfig.spec.json +2 -14
  137. package/providers/algolia/vitest.config.ts +14 -0
  138. package/providers/commercetools/README.md +30 -3
  139. package/providers/commercetools/package.json +2 -1
  140. package/providers/commercetools/src/core/client.ts +178 -99
  141. package/providers/commercetools/src/core/initialize.ts +130 -74
  142. package/providers/commercetools/src/core/token-cache.ts +45 -0
  143. package/providers/commercetools/src/index.ts +3 -2
  144. package/providers/commercetools/src/providers/cart.provider.ts +281 -341
  145. package/providers/commercetools/src/providers/category.provider.ts +223 -138
  146. package/providers/commercetools/src/providers/checkout.provider.ts +631 -449
  147. package/providers/commercetools/src/providers/identity.provider.ts +50 -29
  148. package/providers/commercetools/src/providers/index.ts +2 -2
  149. package/providers/commercetools/src/providers/inventory.provider.ts +76 -74
  150. package/providers/commercetools/src/providers/order-search.provider.ts +220 -0
  151. package/providers/commercetools/src/providers/order.provider.ts +96 -61
  152. package/providers/commercetools/src/providers/price.provider.ts +147 -117
  153. package/providers/commercetools/src/providers/product-search.provider.ts +528 -0
  154. package/providers/commercetools/src/providers/product.provider.ts +249 -74
  155. package/providers/commercetools/src/providers/profile.provider.ts +445 -28
  156. package/providers/commercetools/src/providers/store.provider.ts +54 -40
  157. package/providers/commercetools/src/schema/capabilities.schema.ts +3 -1
  158. package/providers/commercetools/src/schema/commercetools.schema.ts +17 -3
  159. package/providers/commercetools/src/schema/configuration.schema.ts +1 -0
  160. package/providers/commercetools/src/schema/session.schema.ts +7 -0
  161. package/providers/commercetools/src/test/caching.spec.ts +82 -0
  162. package/providers/commercetools/src/test/identity.spec.ts +109 -0
  163. package/providers/commercetools/src/test/test-utils.ts +21 -19
  164. package/providers/commercetools/tsconfig.json +1 -1
  165. package/providers/commercetools/tsconfig.lib.json +1 -1
  166. package/providers/commercetools/tsconfig.spec.json +2 -14
  167. package/providers/commercetools/vitest.config.ts +15 -0
  168. package/providers/fake/README.md +20 -4
  169. package/providers/fake/package.json +2 -1
  170. package/providers/fake/src/core/initialize.ts +47 -49
  171. package/providers/fake/src/providers/analytics.provider.ts +5 -7
  172. package/providers/fake/src/providers/cart.provider.ts +163 -92
  173. package/providers/fake/src/providers/category.provider.ts +78 -50
  174. package/providers/fake/src/providers/checkout.provider.ts +254 -0
  175. package/providers/fake/src/providers/identity.provider.ts +57 -65
  176. package/providers/fake/src/providers/index.ts +6 -2
  177. package/providers/fake/src/providers/inventory.provider.ts +40 -36
  178. package/providers/fake/src/providers/order-search.provider.ts +78 -0
  179. package/providers/fake/src/providers/order.provider.ts +106 -0
  180. package/providers/fake/src/providers/price.provider.ts +93 -41
  181. package/providers/fake/src/providers/product-search.provider.ts +206 -0
  182. package/providers/fake/src/providers/product.provider.ts +56 -41
  183. package/providers/fake/src/providers/profile.provider.ts +147 -0
  184. package/providers/fake/src/providers/store.provider.ts +30 -20
  185. package/providers/fake/src/schema/capabilities.schema.ts +5 -1
  186. package/providers/fake/src/test/cart.provider.spec.ts +59 -80
  187. package/providers/fake/src/test/category.provider.spec.ts +145 -87
  188. package/providers/fake/src/test/checkout.provider.spec.ts +222 -0
  189. package/providers/fake/src/test/order-search.provider.spec.ts +50 -0
  190. package/providers/fake/src/test/order.provider.spec.ts +44 -0
  191. package/providers/fake/src/test/price.provider.spec.ts +50 -45
  192. package/providers/fake/src/test/product.provider.spec.ts +15 -7
  193. package/providers/fake/src/test/profile.provider.spec.ts +167 -0
  194. package/providers/fake/tsconfig.json +1 -1
  195. package/providers/fake/tsconfig.lib.json +1 -1
  196. package/providers/fake/tsconfig.spec.json +2 -12
  197. package/providers/fake/vitest.config.ts +14 -0
  198. package/providers/medusa/README.md +30 -0
  199. package/providers/medusa/TESTING.md +98 -0
  200. package/providers/medusa/eslint.config.mjs +19 -0
  201. package/providers/medusa/package.json +22 -0
  202. package/providers/medusa/project.json +34 -0
  203. package/providers/medusa/src/core/client.ts +370 -0
  204. package/providers/medusa/src/core/initialize.ts +78 -0
  205. package/providers/medusa/src/index.ts +13 -0
  206. package/providers/medusa/src/providers/cart.provider.ts +575 -0
  207. package/providers/medusa/src/providers/category.provider.ts +247 -0
  208. package/providers/medusa/src/providers/checkout.provider.ts +636 -0
  209. package/providers/medusa/src/providers/identity.provider.ts +137 -0
  210. package/providers/medusa/src/providers/inventory.provider.ts +173 -0
  211. package/providers/medusa/src/providers/order-search.provider.ts +202 -0
  212. package/providers/medusa/src/providers/order.provider.ts +226 -0
  213. package/providers/medusa/src/providers/price.provider.ts +140 -0
  214. package/providers/medusa/src/providers/product-search.provider.ts +243 -0
  215. package/providers/medusa/src/providers/product.provider.ts +261 -0
  216. package/providers/medusa/src/providers/profile.provider.ts +392 -0
  217. package/providers/medusa/src/schema/capabilities.schema.ts +18 -0
  218. package/providers/medusa/src/schema/configuration.schema.ts +11 -0
  219. package/providers/medusa/src/schema/medusa.schema.ts +31 -0
  220. package/providers/medusa/src/test/cart.provider.spec.ts +240 -0
  221. package/providers/medusa/src/test/category.provider.spec.ts +231 -0
  222. package/providers/medusa/src/test/checkout.spec.ts +349 -0
  223. package/providers/medusa/src/test/identity.provider.spec.ts +122 -0
  224. package/providers/medusa/src/test/inventory.provider.spec.ts +88 -0
  225. package/providers/medusa/src/test/large-cart.provider.spec.ts +103 -0
  226. package/providers/medusa/src/test/price.provider.spec.ts +104 -0
  227. package/providers/medusa/src/test/product.provider.spec.ts +146 -0
  228. package/providers/medusa/src/test/search.provider.spec.ts +203 -0
  229. package/providers/medusa/src/test/test-utils.ts +13 -0
  230. package/providers/medusa/src/utils/medusa-helpers.ts +89 -0
  231. package/providers/medusa/tsconfig.json +21 -0
  232. package/providers/medusa/tsconfig.lib.json +9 -0
  233. package/providers/medusa/tsconfig.spec.json +4 -0
  234. package/providers/medusa/vitest.config.ts +15 -0
  235. package/providers/meilisearch/README.md +48 -0
  236. package/providers/meilisearch/eslint.config.mjs +22 -0
  237. package/providers/meilisearch/package.json +13 -0
  238. package/providers/meilisearch/project.json +34 -0
  239. package/providers/meilisearch/src/core/initialize.ts +21 -0
  240. package/providers/meilisearch/src/index.ts +6 -0
  241. package/providers/meilisearch/src/providers/index.ts +1 -0
  242. package/providers/meilisearch/src/providers/order-search.provider.ts +222 -0
  243. package/providers/meilisearch/src/providers/product-search.provider.ts +251 -0
  244. package/providers/meilisearch/src/schema/capabilities.schema.ts +10 -0
  245. package/providers/meilisearch/src/schema/configuration.schema.ts +11 -0
  246. package/providers/meilisearch/src/schema/index.ts +3 -0
  247. package/providers/meilisearch/src/schema/search.schema.ts +14 -0
  248. package/providers/meilisearch/tsconfig.json +24 -0
  249. package/providers/meilisearch/tsconfig.lib.json +10 -0
  250. package/providers/meilisearch/tsconfig.spec.json +4 -0
  251. package/providers/meilisearch/vitest.config.ts +14 -0
  252. package/providers/posthog/package.json +2 -1
  253. package/providers/posthog/tsconfig.json +1 -1
  254. package/tsconfig.base.json +5 -0
  255. package/vitest.config.ts +10 -0
  256. package/core/src/providers/search.provider.ts +0 -18
  257. package/core/src/schemas/models/search.model.ts +0 -36
  258. package/core/src/schemas/queries/search.query.ts +0 -9
  259. package/examples/next/.swcrc +0 -30
  260. package/examples/next/eslint.config.mjs +0 -21
  261. package/examples/next/index.d.ts +0 -6
  262. package/examples/next/next-env.d.ts +0 -5
  263. package/examples/next/next.config.js +0 -31
  264. package/examples/next/project.json +0 -9
  265. package/examples/next/public/.gitkeep +0 -0
  266. package/examples/next/public/favicon.ico +0 -0
  267. package/examples/next/src/app/global.css +0 -0
  268. package/examples/next/src/app/layout.tsx +0 -18
  269. package/examples/next/src/app/page.module.scss +0 -2
  270. package/examples/next/src/app/page.tsx +0 -47
  271. package/examples/next/src/instrumentation.ts +0 -9
  272. package/examples/next/tsconfig.json +0 -44
  273. package/examples/node/jest.config.ts +0 -10
  274. package/jest.config.ts +0 -6
  275. package/jest.preset.js +0 -3
  276. package/providers/algolia/jest.config.ts +0 -10
  277. package/providers/algolia/src/providers/product.provider.ts +0 -66
  278. package/providers/algolia/src/providers/search.provider.ts +0 -106
  279. package/providers/algolia/src/test/search.provider.spec.ts +0 -91
  280. package/providers/commercetools/jest.config.cjs +0 -10
  281. package/providers/commercetools/src/providers/search.provider.ts +0 -96
  282. package/providers/commercetools/src/test/cart.provider.spec.ts +0 -199
  283. package/providers/commercetools/src/test/category.provider.spec.ts +0 -168
  284. package/providers/commercetools/src/test/checkout.provider.spec.ts +0 -312
  285. package/providers/commercetools/src/test/identity.provider.spec.ts +0 -88
  286. package/providers/commercetools/src/test/inventory.provider.spec.ts +0 -41
  287. package/providers/commercetools/src/test/price.provider.spec.ts +0 -81
  288. package/providers/commercetools/src/test/product.provider.spec.ts +0 -80
  289. package/providers/commercetools/src/test/profile.provider.spec.ts +0 -49
  290. package/providers/commercetools/src/test/search.provider.spec.ts +0 -61
  291. package/providers/commercetools/src/test/store.provider.spec.ts +0 -37
  292. package/providers/fake/jest.config.cjs +0 -10
  293. package/providers/fake/src/providers/search.provider.ts +0 -132
@@ -0,0 +1,528 @@
1
+ import type {
2
+ ProductSearchFacetResult as CTProductSearchFacetResult,
3
+ ProductSearchFacetResultBucket as CTProductSearchFacetResultBucket,
4
+ ProductVariant as CTProductVariant,
5
+ ProductPagedSearchResponse,
6
+ ProductProjection,
7
+ ProductSearchFacetExpression,
8
+ } from '@commercetools/platform-sdk';
9
+ import type {
10
+ Cache,
11
+ FacetIdentifier,
12
+ FacetValueIdentifier,
13
+ ProductOptionIdentifier,
14
+ ProductSearchQueryByTerm,
15
+ ProductSearchQueryCreateNavigationFilter,
16
+ ProductSearchResult,
17
+ ProductSearchResultFacet,
18
+ ProductSearchResultFacetValue,
19
+ ProductSearchResultItem,
20
+ ProductSearchResultItemVariant,
21
+ ProductVariantIdentifier,
22
+ ProductVariantOption,
23
+ RequestContext,
24
+ Result,
25
+ SearchIdentifier,
26
+ } from '@reactionary/core';
27
+ import {
28
+ FacetIdentifierSchema,
29
+ FacetValueIdentifierSchema,
30
+ ImageSchema,
31
+ ProductOptionIdentifierSchema,
32
+ ProductSearchProvider,
33
+ ProductSearchQueryByTermSchema,
34
+ ProductSearchQueryCreateNavigationFilterSchema,
35
+ ProductSearchResultFacetSchema,
36
+ ProductSearchResultFacetValueSchema,
37
+ ProductSearchResultItemVariantSchema,
38
+ ProductSearchResultSchema,
39
+ ProductVariantIdentifierSchema,
40
+ ProductVariantOptionSchema,
41
+ Reactionary,
42
+ success,
43
+ } from '@reactionary/core';
44
+ import type { CommercetoolsConfiguration } from '../schema/configuration.schema.js';
45
+
46
+ import createDebug from 'debug';
47
+ import type { CommercetoolsAPI } from '../core/client.js';
48
+ import { CommercetoolsCategoryLookupSchema, CommercetoolsResolveCategoryQueryByKeySchema, type CommercetoolsCategoryLookup, type CommercetoolsResolveCategoryQueryById, type CommercetoolsResolveCategoryQueryByKey } from '../schema/commercetools.schema.js';
49
+
50
+ const debug = createDebug('reactionary:commercetools:search');
51
+
52
+ export class CommercetoolsSearchProvider extends ProductSearchProvider {
53
+ protected config: CommercetoolsConfiguration;
54
+ protected commercetools: CommercetoolsAPI;
55
+
56
+ constructor(
57
+ config: CommercetoolsConfiguration,
58
+ cache: Cache,
59
+ context: RequestContext,
60
+ commercetools: CommercetoolsAPI
61
+ ) {
62
+ super(cache, context);
63
+
64
+ this.config = config;
65
+ this.commercetools = commercetools;
66
+ }
67
+
68
+ protected async getClient() {
69
+ const client = await this.commercetools.getClient();
70
+ return client
71
+ .withProjectKey({ projectKey: this.config.projectKey })
72
+ .products();
73
+ }
74
+
75
+ protected async getFacetQuery(
76
+ payload: ProductSearchQueryByTerm,
77
+ selectedFacetValue: FacetValueIdentifier
78
+ ) {
79
+
80
+ if (selectedFacetValue.facet.key === 'categories') {
81
+ // const category = await this.resolveCategoryFromKey({ key: selectedFacetValue.key });
82
+ return {
83
+ exact: {
84
+ field: 'categoriesSubTree',
85
+ values: [selectedFacetValue.key],
86
+ fieldType: 'text',
87
+ },
88
+ };
89
+ }
90
+
91
+ return {
92
+ exact: {
93
+ field: selectedFacetValue.facet.key,
94
+ fieldType: 'text',
95
+ value: selectedFacetValue.key,
96
+ },
97
+ };
98
+ }
99
+
100
+ protected async resolveCategoryFromId(payload: CommercetoolsResolveCategoryQueryById): Promise<CommercetoolsCategoryLookup> {
101
+ const client = (await this.commercetools.getClient()).withProjectKey({ projectKey: this.config.projectKey });
102
+
103
+ const response = await client.categories().withId({ ID: payload.id }).get().execute();
104
+ if (!response.body || !response.body.name) {
105
+ throw new Error(`Category with ID ${payload.id} not found`);
106
+ }
107
+
108
+ const result: CommercetoolsCategoryLookup = {
109
+ id: response.body.id,
110
+ key: response.body.key,
111
+ name: response.body.name,
112
+ meta: {
113
+ cache: {
114
+ hit: false,
115
+ key: 'commercetools-internal-category-from-id-' + payload.id,
116
+ },
117
+ placeholder: false,
118
+ }
119
+ };
120
+ return result;
121
+ }
122
+
123
+ @Reactionary({
124
+ inputSchema: CommercetoolsResolveCategoryQueryByKeySchema,
125
+ outputSchema: CommercetoolsCategoryLookupSchema,
126
+ })
127
+ protected async resolveCategoryFromKey(payload: CommercetoolsResolveCategoryQueryByKey): Promise<CommercetoolsCategoryLookup> {
128
+ const client = (await this.commercetools.getClient()).withProjectKey({ projectKey: this.config.projectKey });
129
+
130
+ const response = await client.categories().withKey({ key: payload.key }).get().execute();
131
+ if (!response.body || !response.body.name) {
132
+ throw new Error(`Category with key ${payload.key} not found`);
133
+ }
134
+ const result = {
135
+ id: response.body.id,
136
+ key: response.body.key,
137
+ name: response.body.name,
138
+ meta: {
139
+ cache: {
140
+ hit: false,
141
+ key: 'commercetools-internal-category-from-key-' + payload.key,
142
+ },
143
+ placeholder: false,
144
+ }
145
+ };
146
+ return result;
147
+ }
148
+
149
+
150
+
151
+ protected async getCategoryFilterExpression(
152
+ payload: ProductSearchQueryByTerm
153
+ ) {
154
+ if (!payload.search.categoryFilter) {
155
+ return undefined;
156
+ }
157
+ // const category = await this.resolveCategoryFromKey({ key: payload.search.categoryFilter.key });
158
+ if (payload.search.categoryFilter.key) {
159
+ return {
160
+ exact: {
161
+ field: 'categoriesSubTree',
162
+ values: [payload.search.categoryFilter.key],
163
+ fieldType: 'text',
164
+ },
165
+ };
166
+ }
167
+ return undefined;
168
+ }
169
+
170
+ protected async getSearchTermExpression(payload: ProductSearchQueryByTerm) {
171
+ if (payload.search.term.trim().length === 0 || payload.search.term === '*') {
172
+ return undefined;
173
+ }
174
+
175
+ return {
176
+ or: [
177
+ {
178
+ fullText: {
179
+ field: 'name',
180
+ language: `${this.context.languageContext.locale}`,
181
+ value: payload.search.term,
182
+ },
183
+ },
184
+ {
185
+ fullText: {
186
+ field: 'description',
187
+ language: `${this.context.languageContext.locale}`,
188
+ value: payload.search.term,
189
+ },
190
+ },
191
+ {
192
+ fullText: {
193
+ field: 'searchKeywords',
194
+ language: `${this.context.languageContext.locale}`,
195
+ value: payload.search.term,
196
+ },
197
+ },
198
+ ],
199
+ };
200
+ }
201
+
202
+ protected async getFacetsQuery(payload: ProductSearchQueryByTerm) {
203
+ if (payload.search.facets.length === 0) {
204
+ return undefined;
205
+ }
206
+
207
+ const facetsToApply = await Promise.all(
208
+ payload.search.facets.map((facet) => this.getFacetQuery(payload, facet))
209
+ );
210
+
211
+ if (facetsToApply.length === 0) {
212
+ return undefined;
213
+ }
214
+ if (facetsToApply.length === 1) {
215
+ return facetsToApply[0];
216
+ }
217
+ return {
218
+ and: facetsToApply,
219
+ };
220
+ }
221
+
222
+ protected async getFacetsToReturn(
223
+ payload: ProductSearchQueryByTerm
224
+ ): Promise<ProductSearchFacetExpression[]> {
225
+ const facetsToReturn: ProductSearchFacetExpression[] = [];
226
+
227
+ const configFacets = ['categories', ...this.config.facetFieldsForSearch];
228
+
229
+ // the default behavior is to get a static list of facets from the config. In more advanced implementations, this could be dynamic based on the payload, ie based on category maybe
230
+ for (const facet of configFacets) {
231
+ facetsToReturn.push({
232
+ distinct: {
233
+ name: facet,
234
+ field: facet,
235
+ fieldType: 'text',
236
+ limit: 50,
237
+ },
238
+ });
239
+ }
240
+
241
+ return facetsToReturn;
242
+ }
243
+
244
+ @Reactionary({
245
+ inputSchema: ProductSearchQueryCreateNavigationFilterSchema,
246
+ outputSchema: FacetValueIdentifierSchema,
247
+ })
248
+ public override async createCategoryNavigationFilter(payload: ProductSearchQueryCreateNavigationFilter): Promise<Result<FacetValueIdentifier>> {
249
+ // In Commercetools, we can use the category ID to filter products by category
250
+
251
+ const categoryPath = payload.categoryPath;
252
+ const deepestCategory = categoryPath[categoryPath.length - 1];
253
+ const resolvedCategory = await this.resolveCategoryFromKey({ key: deepestCategory.identifier.key });
254
+ const resolvedId = resolvedCategory?.id;
255
+ const facetIdentifier: FacetIdentifier = {
256
+ key: 'categories',
257
+ }
258
+ const facetValueIdentifier: FacetValueIdentifier = {
259
+ facet: facetIdentifier,
260
+ key: resolvedId || 'unknown',
261
+ };
262
+
263
+ return success(facetValueIdentifier);
264
+ }
265
+
266
+
267
+
268
+ @Reactionary({
269
+ inputSchema: ProductSearchQueryByTermSchema,
270
+ outputSchema: ProductSearchResultSchema,
271
+ cache: true,
272
+ cacheTimeToLiveInSeconds: 300,
273
+ currencyDependentCaching: false,
274
+ localeDependentCaching: true
275
+ })
276
+ public override async queryByTerm(
277
+ payload: ProductSearchQueryByTerm
278
+ ): Promise<Result<ProductSearchResult>> {
279
+ const client = await this.getClient();
280
+
281
+ const facetsToReturn = await this.getFacetsToReturn(payload);
282
+ const facetsToApply = await this.getFacetsQuery(payload);
283
+ const searchTermExpression = await this.getSearchTermExpression(payload);
284
+ const categoryFilterExpression = await this.getCategoryFilterExpression(payload);
285
+
286
+ let finalFilterExpression: any = undefined;
287
+ if (searchTermExpression) {
288
+ finalFilterExpression = searchTermExpression;
289
+ }
290
+
291
+ if (facetsToApply) {
292
+ if (finalFilterExpression) {
293
+ finalFilterExpression = {
294
+ and: [finalFilterExpression, facetsToApply],
295
+ };
296
+ } else {
297
+ finalFilterExpression = facetsToApply;
298
+ }
299
+ }
300
+
301
+ if (categoryFilterExpression) {
302
+ if (finalFilterExpression) {
303
+ finalFilterExpression = {
304
+ and: [finalFilterExpression, categoryFilterExpression],
305
+ };
306
+ } else {
307
+ finalFilterExpression = categoryFilterExpression;
308
+ }
309
+ }
310
+
311
+ const response = await client
312
+ .search()
313
+ .post({
314
+ body: {
315
+ query: finalFilterExpression,
316
+ productProjectionParameters: {
317
+ storeProjection: this.context.storeIdentifier.key,
318
+ },
319
+ limit: payload.search.paginationOptions.pageSize,
320
+ offset:
321
+ (payload.search.paginationOptions.pageNumber - 1) *
322
+ payload.search.paginationOptions.pageSize,
323
+
324
+ facets: [...facetsToReturn],
325
+ },
326
+ })
327
+ .execute();
328
+
329
+ const responseBody = response.body;
330
+ const result = this.parsePaginatedResult(
331
+ responseBody,
332
+ payload
333
+ ) as ProductSearchResult;
334
+
335
+
336
+ // ok, we have to patch up the categories facet to have the category keys instead of ids
337
+ await this.patchCategoryFacetValues(result);
338
+
339
+
340
+ // mark selected facets as active
341
+ for(const selectedFacet of payload.search.facets) {
342
+ const facet = result.facets.find((f) => f.identifier.key === selectedFacet.facet.key);
343
+ if(facet) {
344
+ const value = facet.values.find((v) => v.identifier.key === selectedFacet.key);
345
+ if(value) {
346
+ value.active = true;
347
+ }
348
+ }
349
+ }
350
+
351
+
352
+ if (debug.enabled) {
353
+ debug(
354
+ `Search for term "${payload.search.term}" returned ${responseBody.results.length} products (page ${payload.search.paginationOptions.pageNumber} of ${result.totalPages})`
355
+ );
356
+ }
357
+
358
+ return success(result);
359
+ }
360
+
361
+ protected async patchCategoryFacetValues(result: ProductSearchResult) {
362
+ const categoryFacet = result.facets.find(
363
+ (f) => f.identifier.key === 'categories'
364
+ );
365
+ if (!categoryFacet) {
366
+ return;
367
+ }
368
+
369
+ const resolvedCategories = categoryFacet.values.map((facetValue) => this.resolveCategoryFromId({ id: facetValue.identifier.key }));
370
+ const categories = await Promise.all(resolvedCategories);
371
+ for (const facetValue of categoryFacet.values) {
372
+ try {
373
+ const category = categories.find((c) => c.id === facetValue.identifier.key);
374
+ if (!category) {
375
+ continue;
376
+ }
377
+ facetValue.name = category.name[this.context.languageContext.locale] || category.id;
378
+ } catch (error) {
379
+ if (debug.enabled) {
380
+ debug(`Error resolving category key for id ${facetValue.identifier.key}:`, error);
381
+ }
382
+ }
383
+ }
384
+ }
385
+
386
+
387
+ protected parseSingle(body: ProductProjection) {
388
+ const identifier = { key: body.id };
389
+ const name = body.name[this.context.languageContext.locale] || body.id;
390
+ const slug = body.slug?.[this.context.languageContext.locale] || body.id;
391
+ const variants = [body.masterVariant, ...body.variants].map((variant) =>
392
+ this.parseVariant(variant, body)
393
+ );
394
+
395
+ const product = {
396
+ identifier,
397
+ name,
398
+ slug,
399
+ variants,
400
+ } satisfies ProductSearchResultItem;
401
+
402
+ return product;
403
+ }
404
+
405
+ protected parsePaginatedResult(
406
+ body: ProductPagedSearchResponse,
407
+ query: ProductSearchQueryByTerm
408
+ ) {
409
+ const identifier = {
410
+ ...query.search,
411
+ } satisfies SearchIdentifier;
412
+
413
+ const products: ProductSearchResultItem[] = body.results.map((p) =>
414
+ this.parseSingle(p.productProjection!)
415
+ );
416
+ const facets: ProductSearchResultFacet[] = [];
417
+
418
+ for (const facet of body.facets) {
419
+ const facetIdentifier = FacetIdentifierSchema.parse({
420
+ key: facet.name,
421
+ } satisfies Partial<FacetIdentifier>);
422
+
423
+ const candidateFacet = this.parseFacet(facetIdentifier, facet);
424
+ if (candidateFacet.values.length > 0) {
425
+ facets.push(candidateFacet);
426
+ }
427
+ }
428
+
429
+ const result = {
430
+ identifier,
431
+ pageNumber: (Math.ceil(body.offset / body.limit) || 0) + 1,
432
+ pageSize: body.limit,
433
+ totalCount: body.total || 0,
434
+ totalPages: Math.ceil((body.total || 0) / body.limit || 0) + 1,
435
+ items: products,
436
+ facets,
437
+ } satisfies ProductSearchResult;
438
+
439
+ return result;
440
+ }
441
+
442
+ /**
443
+ * See version 0.0.81 for ProductProjection based facet parsing
444
+ * @param facetIdentifier
445
+ * @param facetValue
446
+ * @returns
447
+ */
448
+ protected parseFacet(
449
+ facetIdentifier: FacetIdentifier,
450
+ facet: CTProductSearchFacetResult
451
+ ): ProductSearchResultFacet {
452
+ const result: ProductSearchResultFacet =
453
+ ProductSearchResultFacetSchema.parse({
454
+ identifier: facetIdentifier,
455
+ name: facet.name,
456
+ values: [],
457
+ });
458
+
459
+ const distinctFacet = facet as CTProductSearchFacetResultBucket;
460
+ if (distinctFacet) {
461
+ distinctFacet.buckets.forEach((bucket) => {
462
+ const facetValueIdentifier = FacetValueIdentifierSchema.parse({
463
+ facet: facetIdentifier,
464
+ key: bucket.key,
465
+ } satisfies Partial<FacetValueIdentifier>);
466
+
467
+ result.values.push(
468
+ this.parseFacetValue(facetValueIdentifier, bucket.key, bucket.count)
469
+ );
470
+ });
471
+ }
472
+
473
+ return result;
474
+ }
475
+
476
+ protected parseFacetValue(
477
+ facetValueIdentifier: FacetValueIdentifier,
478
+ label: string,
479
+ count: number
480
+ ): ProductSearchResultFacetValue {
481
+
482
+ return ProductSearchResultFacetValueSchema.parse({
483
+ identifier: facetValueIdentifier,
484
+ name: label,
485
+ count: count,
486
+ active: false,
487
+ } satisfies Partial<ProductSearchResultFacetValue>);
488
+ }
489
+
490
+ protected parseVariant(
491
+ variant: CTProductVariant,
492
+ product: ProductProjection
493
+ ): ProductSearchResultItemVariant {
494
+ const sourceImage = variant.images?.[0];
495
+
496
+ const img = ImageSchema.parse({
497
+ sourceUrl: sourceImage?.url || '',
498
+ height: sourceImage?.dimensions.h || undefined,
499
+ width: sourceImage?.dimensions.w || undefined,
500
+ altText:
501
+ sourceImage?.label ||
502
+ product.name[this.context.languageContext.locale] ||
503
+ undefined,
504
+ });
505
+
506
+ const mappedOptions =
507
+ variant.attributes
508
+ ?.filter((x) => x.name === 'Color')
509
+ .map((opt) =>
510
+ ProductVariantOptionSchema.parse({
511
+ identifier: ProductOptionIdentifierSchema.parse({
512
+ key: opt.name,
513
+ } satisfies Partial<ProductOptionIdentifier>),
514
+ name: opt.value || '',
515
+ } satisfies Partial<ProductVariantOption>)
516
+ ) || [];
517
+
518
+ const mappedOption = mappedOptions?.[0];
519
+
520
+ return ProductSearchResultItemVariantSchema.parse({
521
+ variant: ProductVariantIdentifierSchema.parse({
522
+ sku: variant.sku || '',
523
+ } satisfies ProductVariantIdentifier),
524
+ image: img,
525
+ options: mappedOption,
526
+ } satisfies Partial<ProductSearchResultItemVariant>);
527
+ }
528
+ }