@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,261 @@
1
+ import type { Cache, Image, NotFoundError, Product, ProductAttribute, ProductAttributeIdentifier, ProductAttributeValueIdentifier, ProductOptionIdentifier, ProductOptionValueIdentifier, ProductQueryById, ProductQueryBySKU, ProductQueryBySlug, ProductVariant, ProductVariantOption, RequestContext, Result } from '@reactionary/core';
2
+ import {
3
+ CategoryIdentifierSchema,
4
+ ProductIdentifierSchema,
5
+ ProductProvider,
6
+ ProductQueryByIdSchema,
7
+ ProductQueryBySKUSchema,
8
+ ProductQueryBySlugSchema,
9
+ ProductSchema,
10
+ Reactionary,
11
+ success,
12
+ error,
13
+ } from '@reactionary/core';
14
+ import createDebug from 'debug';
15
+ import type { MedusaAPI } from '../core/client.js';
16
+ import type { MedusaConfiguration } from '../schema/configuration.schema.js';
17
+
18
+ import type { StoreProduct, StoreProductImage, StoreProductVariant } from '@medusajs/types';
19
+
20
+ const debug = createDebug('reactionary:medusa:product');
21
+
22
+ export class MedusaProductProvider extends ProductProvider {
23
+ protected config: MedusaConfiguration;
24
+
25
+ constructor(config: MedusaConfiguration, cache: Cache, context: RequestContext, public medusaApi: MedusaAPI) {
26
+ super(cache, context);
27
+ this.config = config;
28
+ }
29
+
30
+ @Reactionary({
31
+ inputSchema: ProductQueryByIdSchema,
32
+ outputSchema: ProductSchema,
33
+ cache: true,
34
+ cacheTimeToLiveInSeconds: 300,
35
+ currencyDependentCaching: false,
36
+ localeDependentCaching: true
37
+ })
38
+ public override async getById(payload: ProductQueryById): Promise<Result<Product>> {
39
+ const client = await this.medusaApi.getClient();
40
+ if (debug.enabled) {
41
+ debug(`Fetching product by ID: ${payload.identifier.key}`);
42
+ }
43
+ let response;
44
+ try {
45
+ response = await client.store.product.retrieve(payload.identifier.key, {
46
+ fields: '+metadata,+categories.metadata.*',
47
+ });
48
+
49
+ } catch(error) {
50
+ if (debug.enabled) {
51
+ debug(`Product with ID: ${payload.identifier.key} not found, returning empty product. Error %O `, error);
52
+ }
53
+ return success(this.createEmptyProduct(payload.identifier.key));
54
+ }
55
+ return success(this.parseSingle(response.product));
56
+ }
57
+
58
+ @Reactionary({
59
+ inputSchema: ProductQueryBySlugSchema,
60
+ outputSchema: ProductSchema,
61
+ cache: true,
62
+ cacheTimeToLiveInSeconds: 300,
63
+ currencyDependentCaching: false,
64
+ localeDependentCaching: true
65
+ })
66
+ public override async getBySlug(payload: ProductQueryBySlug): Promise<Result<Product, NotFoundError>> {
67
+ const client = await this.medusaApi.getClient();
68
+ if (debug.enabled) {
69
+ debug(`Fetching product by slug: ${payload.slug}`);
70
+ }
71
+
72
+ const response = await client.store.product.list({
73
+ handle: payload.slug,
74
+ limit: 1,
75
+ offset: 0,
76
+ fields: '+metadata.*',
77
+ });
78
+
79
+ if (debug.enabled) {
80
+ debug(`Found ${response.count} products for slug: ${payload.slug}`);
81
+ }
82
+
83
+ if (response.count === 0) {
84
+ return error<NotFoundError>({
85
+ type: 'NotFound',
86
+ identifier: payload
87
+ });
88
+ }
89
+ return success(this.parseSingle(response.products[0]));
90
+ }
91
+
92
+
93
+ @Reactionary({
94
+ inputSchema: ProductQueryBySKUSchema,
95
+ outputSchema: ProductSchema,
96
+ cache: true,
97
+ cacheTimeToLiveInSeconds: 300,
98
+ currencyDependentCaching: false,
99
+ localeDependentCaching: true
100
+ })
101
+ public override async getBySKU(payload: ProductQueryBySKU): Promise<Result<Product>> {
102
+ if (debug.enabled) {
103
+ debug(`Fetching product by SKU: ${Array.isArray(payload) ? payload.join(', ') : payload}`);
104
+ }
105
+ const sku = payload.variant.sku;
106
+ const product = await this.medusaApi.resolveProductForSKU(sku);
107
+
108
+ const variant = product.variants?.find((v) => v.sku === sku);
109
+ if (!variant) {
110
+ throw new Error(`Variant with SKU ${sku} not found`);
111
+ }
112
+ product.variants = [];
113
+ product.variants.push(variant);
114
+
115
+ // For simplicity, return the first matched product
116
+ return success(this.parseSingle(product));
117
+ }
118
+
119
+ protected parseSingle(_body: StoreProduct): Product {
120
+ const identifier = ProductIdentifierSchema.parse({ key: _body.id });
121
+ const name = _body.title;
122
+ const slug = _body.handle;
123
+ const description = _body.description || '' || _body.subtitle || '';
124
+ const parentCategories = [];
125
+ parentCategories.push(
126
+ ...
127
+ _body.categories?.map( (cat) => cat.metadata?.['external_id'] ).map( (id) => CategoryIdentifierSchema.parse({ key: id || '' }) ) || []
128
+ )
129
+ const sharedAttributes = this.parseAttributes(_body);
130
+
131
+ if (!_body.variants) {
132
+ debug('Product has no variants', _body);
133
+ throw new Error('Product has no variants ' + _body.id);
134
+ }
135
+ const mainVariant = this.parseVariant(_body.variants[0], _body);
136
+
137
+
138
+ const otherVariants = [];
139
+ if (_body.variants.length > 1) {
140
+ otherVariants.push(
141
+ ..._body.variants.slice(1).map( (variant) => this.parseVariant(variant, _body) )
142
+ );
143
+ }
144
+
145
+ const result = {
146
+ brand: '',
147
+ description,
148
+ identifier,
149
+ longDescription: '',
150
+ mainVariant,
151
+ manufacturer: '',
152
+ name,
153
+ options: [],
154
+ parentCategories,
155
+ published: true,
156
+ sharedAttributes,
157
+ slug,
158
+ variants: otherVariants,
159
+ } satisfies Product;
160
+
161
+ return result;
162
+ }
163
+
164
+ protected parseVariant(variant: StoreProductVariant, product: StoreProduct) {
165
+
166
+
167
+ const options = (variant.options ?? []).map( (option) => {
168
+
169
+ const optionId: ProductOptionIdentifier = { key: option.option_id || '' };
170
+ const title = option.option?.title || '?';
171
+ const valueIdentifier: ProductOptionValueIdentifier = { key: option.option_id || '', option: optionId };
172
+ const value = option.value || '';
173
+
174
+ const result: ProductVariantOption = {
175
+ identifier: optionId,
176
+ name: title,
177
+ value: {
178
+ identifier: valueIdentifier,
179
+ label: value,
180
+ },
181
+ }
182
+ return result;
183
+ });
184
+
185
+
186
+ const result: ProductVariant = {
187
+ identifier: {
188
+ sku: variant.sku || '',
189
+ },
190
+ name: variant.title || product.title,
191
+ upc: variant.upc || '',
192
+ ean: variant.ean || '',
193
+
194
+ images: (product.images || []).map((img: StoreProductImage) => {
195
+ return {
196
+ sourceUrl: img.url,
197
+ altText: variant.title || product.title || '',
198
+ } satisfies Image;
199
+ }),
200
+ options: options,
201
+ gtin: variant.ean || '',
202
+ barcode: variant.ean || ''
203
+ };
204
+
205
+ return result;
206
+ }
207
+
208
+ protected createSynthAttribute(key: string, name: string, value: string ): ProductAttribute {
209
+ const attributeIdentifier: ProductAttributeIdentifier = { key };
210
+ const valueIdentifier: ProductAttributeValueIdentifier = { key: `${key}-${value}`};
211
+
212
+ const attribute: ProductAttribute = {
213
+ identifier: { key },
214
+ name,
215
+ group: '',
216
+ values: [
217
+ {
218
+ identifier: valueIdentifier,
219
+ label: String(value),
220
+ value: value,
221
+ }
222
+ ]
223
+ };
224
+ return attribute;
225
+ }
226
+
227
+ protected parseAttributes(_body: StoreProduct): Array<ProductAttribute> {
228
+ const sharedAttributes = [];
229
+
230
+ if (_body.origin_country) {
231
+ sharedAttributes.push(this.createSynthAttribute('origin_country', 'Origin Country', _body.origin_country));
232
+ }
233
+
234
+ if (_body.height) {
235
+ sharedAttributes.push(this.createSynthAttribute('height', 'Height', String(_body.height)));
236
+ }
237
+
238
+ if (_body.weight) {
239
+ sharedAttributes.push(this.createSynthAttribute('weight', 'Weight', String(_body.weight)));
240
+ }
241
+
242
+ if (_body.length) {
243
+ sharedAttributes.push(this.createSynthAttribute('length', 'Length', String(_body.length)));
244
+ }
245
+
246
+ if (_body.width) {
247
+ sharedAttributes.push(this.createSynthAttribute('width', 'Width', String(_body.width)));
248
+ }
249
+
250
+ if (_body.material) {
251
+ sharedAttributes.push(this.createSynthAttribute('material', 'Material', _body.material));
252
+ }
253
+ if (_body.metadata) {
254
+ for (const [key, value] of Object.entries(_body.metadata)) {
255
+ sharedAttributes.push(this.createSynthAttribute(key, key, String(value)));
256
+ }
257
+ }
258
+ return sharedAttributes;
259
+ }
260
+ }
261
+
@@ -0,0 +1,392 @@
1
+ import type {
2
+ InvalidInputError} from '@reactionary/core';
3
+ import {
4
+ type Profile,
5
+ type ProfileMutationAddShippingAddress,
6
+ type ProfileMutationMakeShippingAddressDefault,
7
+ type ProfileMutationRemoveShippingAddress,
8
+ type ProfileMutationSetBillingAddress,
9
+ type ProfileMutationUpdate,
10
+ type ProfileMutationUpdateShippingAddress,
11
+ type ProfileQuerySelf as ProfileQueryById,
12
+ type RequestContext,
13
+ type Cache,
14
+ type Result,
15
+ type NotFoundError,
16
+ ProfileProvider,
17
+ Reactionary,
18
+ ProfileSchema,
19
+ ProfileMutationUpdateSchema,
20
+ ProfileMutationAddShippingAddressSchema,
21
+ ProfileMutationUpdateShippingAddressSchema,
22
+ ProfileMutationRemoveShippingAddressSchema,
23
+ ProfileMutationMakeShippingAddressDefaultSchema,
24
+ ProfileMutationSetBillingAddressSchema,
25
+ success,
26
+ type Address,
27
+ ProfileQueryByIdSchema,
28
+ error
29
+ } from '@reactionary/core';
30
+ import type { MedusaConfiguration } from '../schema/configuration.schema.js';
31
+ import type { MedusaAPI } from '../core/client.js';
32
+ import createDebug from 'debug';
33
+ import type { StoreCreateCustomerAddress, StoreCustomer, StoreCustomerAddress } from '@medusajs/types';
34
+
35
+ const debug = createDebug('reactionary:medusa:profile');
36
+ /**
37
+ * Medusa Profile Provider
38
+ *
39
+ * Implements profile management using Medusa's customer APIs.
40
+ *
41
+ * TODO:
42
+ * - handle email and phone verification status properly using metadata or other means.
43
+ * - handle guest user scenarios (if applicable).
44
+ * - improve error handling and edge cases.
45
+ */
46
+ export class MedusaProfileProvider extends ProfileProvider {
47
+ protected config: MedusaConfiguration;
48
+ protected includedFields = ['+metadata.*'];
49
+
50
+ constructor(
51
+ config: MedusaConfiguration,
52
+ cache: Cache,
53
+ context: RequestContext,
54
+ public medusaApi: MedusaAPI
55
+ ) {
56
+ super(cache, context);
57
+
58
+ this.config = config;
59
+ }
60
+
61
+ @Reactionary({
62
+ inputSchema: ProfileQueryByIdSchema,
63
+ outputSchema: ProfileSchema,
64
+ })
65
+ public async getById(payload: ProfileQueryById): Promise<Result<Profile, NotFoundError>> {
66
+ debug('getById', payload);
67
+
68
+ const client = await this.medusaApi.getClient();
69
+ const customerResponse = await client.store.customer.retrieve({ fields: this.includedFields.join(',') });
70
+
71
+ if (!customerResponse.customer) {
72
+ return error<NotFoundError>({
73
+ identifier: payload.identifier,
74
+ type: 'NotFound',
75
+ });
76
+ }
77
+
78
+ const model = this.parseSingle(customerResponse.customer);
79
+ return success(model);
80
+ }
81
+
82
+ @Reactionary({
83
+ inputSchema: ProfileMutationUpdateSchema,
84
+ outputSchema: ProfileSchema,
85
+ })
86
+ public async update(payload: ProfileMutationUpdate): Promise<Result<Profile, NotFoundError>> {
87
+ debug('update', payload);
88
+
89
+ const client = await this.medusaApi.getClient();
90
+
91
+ const customerResponse = await client.store.customer.retrieve({ fields: this.includedFields.join(',') });
92
+ if (!customerResponse.customer) {
93
+ return error<NotFoundError>({
94
+ type: 'NotFound',
95
+ identifier: payload.identifier,
96
+ });
97
+ }
98
+
99
+ const customer = customerResponse.customer;
100
+
101
+ const updatedResponse = await client.store.customer.update({
102
+ phone: payload.phone ?? customer.phone,
103
+ }, { fields: this.includedFields.join(',') });
104
+
105
+ const model = this.parseSingle(updatedResponse.customer);
106
+ return success(model);
107
+ }
108
+
109
+ @Reactionary({
110
+ inputSchema: ProfileMutationAddShippingAddressSchema,
111
+ outputSchema: ProfileSchema,
112
+ })
113
+ public async addShippingAddress(payload: ProfileMutationAddShippingAddress): Promise<Result<Profile, NotFoundError>> {
114
+ debug('addShippingAddress', payload);
115
+
116
+ const client = await this.medusaApi.getClient();
117
+
118
+ const medusaAddress = this.createMedusaAddress(payload.address);
119
+
120
+ // check if any address with the same nickName exists
121
+ const customer = await client.store.customer.retrieve({ fields: this.includedFields.join(',') });
122
+ if (!customer.customer) {
123
+ return error<NotFoundError>({
124
+ type: 'NotFound',
125
+ identifier: payload.identifier,
126
+ });
127
+ }
128
+ const existingAddress = customer.customer.addresses.find(addr => addr.address_name === payload.address.identifier.nickName);
129
+ if (existingAddress) {
130
+ return error<InvalidInputError>({
131
+ type: 'InvalidInput',
132
+ error: {
133
+ message: 'Address with the same nickname already exists',
134
+ }
135
+ });
136
+ }
137
+
138
+ const response = await client.store.customer.createAddress(medusaAddress, { fields: this.includedFields.join(',') });
139
+ if (!response.customer) {
140
+ return error<InvalidInputError>({
141
+ type: 'InvalidInput',
142
+ error: {
143
+ message: 'Failed to add shipping address',
144
+ }
145
+ });
146
+ }
147
+
148
+ const model = this.parseSingle(response.customer);
149
+ return success(model);
150
+ }
151
+
152
+ @Reactionary({
153
+ inputSchema: ProfileMutationUpdateShippingAddressSchema,
154
+ outputSchema: ProfileSchema,
155
+ })
156
+ public async updateShippingAddress(payload: ProfileMutationUpdateShippingAddress): Promise<Result<Profile, NotFoundError>> {
157
+ debug('updateShippingAddress', payload);
158
+
159
+ const client = await this.medusaApi.getClient();
160
+
161
+ const customer = await client.store.customer.retrieve({ fields: this.includedFields.join(',') });
162
+ if (!customer.customer) {
163
+ return error<NotFoundError>({
164
+ type: 'NotFound',
165
+ identifier: payload.identifier ,
166
+ });
167
+ }
168
+
169
+ const medusaAddress = this.createMedusaAddress(payload.address);
170
+
171
+ const existingAddress = customer.customer.addresses.find(addr => addr.address_name === payload.address.identifier.nickName);
172
+ if (!existingAddress) {
173
+ return error<NotFoundError>({
174
+ type: 'NotFound',
175
+ identifier: payload.address.identifier
176
+ });
177
+ }
178
+
179
+ const response = await client.store.customer.updateAddress(existingAddress.id, medusaAddress , { fields: this.includedFields.join(',') });
180
+ if (!response.customer) {
181
+ return error<InvalidInputError>({
182
+ type: 'InvalidInput',
183
+ error: {
184
+ message: 'Failed to add shipping address',
185
+ }
186
+ })
187
+ }
188
+
189
+ const model = this.parseSingle(response.customer);
190
+ return success(model);
191
+ }
192
+
193
+
194
+
195
+
196
+
197
+ @Reactionary({
198
+ inputSchema: ProfileMutationRemoveShippingAddressSchema,
199
+ outputSchema: ProfileSchema,
200
+ })
201
+ public async removeShippingAddress(payload: ProfileMutationRemoveShippingAddress): Promise<Result<Profile, NotFoundError>> {
202
+ debug('removeShippingAddress', payload);
203
+ const client = await this.medusaApi.getClient();
204
+
205
+ const customer = await client.store.customer.retrieve({ fields: this.includedFields.join(',') });
206
+ if (!customer.customer) {
207
+ return error<NotFoundError>({
208
+ type: 'NotFound',
209
+ identifier: payload.identifier,
210
+ });
211
+ }
212
+ const existingAddress = customer.customer.addresses.find(addr => addr.address_name === payload.addressIdentifier.nickName);
213
+ if (!existingAddress) {
214
+ return error<NotFoundError>({
215
+ type: 'NotFound',
216
+ identifier: payload.addressIdentifier,
217
+ });
218
+ }
219
+
220
+ const response = await client.store.customer.deleteAddress(existingAddress.id,{ fields: this.includedFields.join(',') });
221
+ if (!response.deleted) {
222
+ return error<InvalidInputError>({
223
+ type: 'InvalidInput',
224
+ error: {
225
+ message: 'Failed to delete shipping address',
226
+ }
227
+ });
228
+ }
229
+
230
+ const customerAfterDelete = await client.store.customer.retrieve({ fields: this.includedFields.join(',') });
231
+
232
+ const model = this.parseSingle(customerAfterDelete.customer!);
233
+ return success(model);
234
+ }
235
+
236
+
237
+ @Reactionary({
238
+ inputSchema: ProfileMutationMakeShippingAddressDefaultSchema,
239
+ outputSchema: ProfileSchema,
240
+ })
241
+ public async makeShippingAddressDefault(payload: ProfileMutationMakeShippingAddressDefault): Promise<Result<Profile, NotFoundError>> {
242
+ debug('makeShippingAddressDefault', payload);
243
+
244
+ const client = await this.medusaApi.getClient();
245
+
246
+ const customer = await client.store.customer.retrieve({ fields: this.includedFields.join(',') });
247
+ if (!customer.customer) {
248
+ return error<NotFoundError>({
249
+ type: 'NotFound',
250
+ identifier: payload.identifier,
251
+ });
252
+ }
253
+ const existingAddress = customer.customer.addresses.find(addr => addr.address_name === payload.addressIdentifier.nickName);
254
+
255
+ if (!existingAddress) {
256
+ return error<NotFoundError>({
257
+ type: 'NotFound',
258
+ identifier: payload.addressIdentifier ,
259
+ });
260
+ }
261
+
262
+ const response = await client.store.customer.updateAddress(existingAddress.id, {
263
+ is_default_shipping: true
264
+ }, { fields: this.includedFields.join(',') }
265
+ );
266
+
267
+ const model = this.parseSingle(response.customer!);
268
+ return success(model);
269
+ }
270
+
271
+ @Reactionary({
272
+ inputSchema: ProfileMutationSetBillingAddressSchema,
273
+ outputSchema: ProfileSchema,
274
+ })
275
+ public async setBillingAddress(payload: ProfileMutationSetBillingAddress): Promise<Result<Profile, NotFoundError>> {
276
+ debug('setBillingAddress', payload);
277
+
278
+
279
+ const client = await this.medusaApi.getClient();
280
+
281
+ const customerResponse = await client.store.customer.retrieve({ fields: this.includedFields.join(',') });
282
+ if (!customerResponse.customer) {
283
+ return error<NotFoundError>({
284
+ type: 'NotFound',
285
+ identifier: payload.identifier,
286
+ });
287
+ }
288
+ let customer = customerResponse.customer;
289
+
290
+ // check that this nickname is not used by another address
291
+ const existingAddressWithNickname = customer.addresses.find(addr => addr.address_name === payload.address.identifier.nickName);
292
+ if (existingAddressWithNickname && !existingAddressWithNickname.is_default_billing) {
293
+ return error<InvalidInputError>({
294
+ type: 'InvalidInput',
295
+ error: {
296
+ message: 'Another address with the same nickname already exists',
297
+ }
298
+ });
299
+ }
300
+
301
+
302
+ const newAddr = this.createMedusaAddress(payload.address);
303
+ newAddr.is_default_billing = true;
304
+
305
+ // two scenarios: Either we already have a billing addres, in which case we update it, or we dont and we need to create it.
306
+ const existingBillingAddress = customer.addresses.find(addr => addr.is_default_billing);
307
+ if (existingBillingAddress) {
308
+ const updateAddressResponse = await client.store.customer.updateAddress(existingBillingAddress.id, newAddr, { fields: this.includedFields.join(',') });
309
+ customer = updateAddressResponse.customer;
310
+ } else {
311
+ const createAddressResponse = await client.store.customer.createAddress(newAddr, { fields: this.includedFields.join(',') });
312
+ customer = createAddressResponse.customer;
313
+ }
314
+
315
+ const model = this.parseSingle(customer);
316
+ return success(model);
317
+ }
318
+
319
+ protected createMedusaAddress(address: Address): StoreCreateCustomerAddress {
320
+ return {
321
+ address_name: address.identifier.nickName,
322
+ first_name: address.firstName,
323
+ last_name: address.lastName,
324
+ address_1: address.streetAddress,
325
+ address_2: address.streetNumber,
326
+ city: address.city,
327
+ province: address.region,
328
+ postal_code: address.postalCode,
329
+ country_code: address.countryCode,
330
+ } satisfies StoreCreateCustomerAddress;
331
+ }
332
+
333
+ protected parseAddress(address: StoreCustomerAddress): Address {
334
+ return {
335
+ identifier: {
336
+ nickName: address.address_name || '',
337
+ },
338
+ firstName: address.first_name || '',
339
+ lastName: address.last_name || '',
340
+ streetAddress: address.address_1 || '',
341
+ streetNumber: address.address_2 || '',
342
+ city: address.city || '',
343
+ region: address.province || '',
344
+ postalCode: address.postal_code || '',
345
+ countryCode: address.country_code || '',
346
+ }
347
+ }
348
+
349
+ protected parseSingle(customer: StoreCustomer): Profile {
350
+ const email = customer.email;
351
+ const emailVerified = customer.metadata?.['email_verified'] === 'true';
352
+
353
+ const phone = customer.phone || '';
354
+ const phoneVerified = customer.metadata?.['phone_verified'] === 'true';
355
+
356
+ const addresses = customer.addresses || [];
357
+ let billingAddress: Address | undefined = undefined;
358
+ let shippingAddress: Address | undefined = undefined;
359
+
360
+ const existingBillingAddress = customer.addresses.find(addr => addr.is_default_billing);
361
+ if (existingBillingAddress) {
362
+ billingAddress = this.parseAddress(existingBillingAddress);
363
+ }
364
+
365
+ const existingShippingAddress = customer.addresses.find(addr => addr.is_default_shipping);
366
+ if (existingShippingAddress) {
367
+ shippingAddress = this.parseAddress(existingShippingAddress);
368
+ }
369
+
370
+ const alternateShippingAddresses: Address[] = [];
371
+
372
+ alternateShippingAddresses.push(...addresses.filter(x => ! (x.is_default_billing || x.is_default_shipping)).map( addr => this.parseAddress(addr)));
373
+
374
+ return {
375
+ identifier: {
376
+ userId: customer.id,
377
+ },
378
+ email,
379
+ emailVerified,
380
+ phone,
381
+ phoneVerified,
382
+ billingAddress: billingAddress,
383
+ shippingAddress: shippingAddress,
384
+ alternateShippingAddresses: alternateShippingAddresses,
385
+ createdAt: new Date(customer.created_at || '').toISOString(),
386
+ updatedAt: new Date(customer.updated_at || '').toISOString()
387
+ } satisfies Profile;
388
+ }
389
+
390
+
391
+
392
+ }
@@ -0,0 +1,18 @@
1
+ import { CapabilitiesSchema } from "@reactionary/core";
2
+ import type { z } from 'zod';
3
+
4
+ export const MedusaCapabilitiesSchema = CapabilitiesSchema.pick({
5
+ productSearch: true,
6
+ cart: true,
7
+ checkout: true,
8
+ category: true,
9
+ product: true,
10
+ price: true,
11
+ order: true,
12
+ orderSearch: true,
13
+ inventory: true,
14
+ identity: true,
15
+ profile: true
16
+ }).partial();
17
+
18
+ export type MedusaCapabilities = z.infer<typeof MedusaCapabilitiesSchema>;
@@ -0,0 +1,11 @@
1
+ import { z } from 'zod';
2
+
3
+ export const MedusaConfigurationSchema = z.looseObject({
4
+ publishable_key: z.string().describe('The publishable API key for the Medusa store. Used for all storefront operations.'),
5
+ adminApiKey: z.string().describe('The API key for Medusa admin operations. Needed for the few tasks that require admin access.'),
6
+ apiUrl: z.string().describe('The base URL for the Medusa API.'),
7
+ defaultCurrency: z.string().default(''),
8
+ allCurrencies: z.string().array().default([]),
9
+ });
10
+
11
+ export type MedusaConfiguration = z.infer<typeof MedusaConfigurationSchema>;