@reactionary/source 0.0.51 → 0.2.15

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 (325) 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 -2
  8. package/core/src/cache/cache.interface.ts +2 -1
  9. package/core/src/cache/index.ts +4 -0
  10. package/core/src/cache/memory-cache.ts +32 -4
  11. package/core/src/cache/noop-cache.ts +16 -2
  12. package/core/src/cache/redis-cache.ts +21 -1
  13. package/core/src/client/client-builder.ts +71 -54
  14. package/core/src/client/client.ts +17 -55
  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 +210 -21
  18. package/core/src/index.ts +6 -19
  19. package/core/src/initialization.ts +2 -19
  20. package/core/src/metrics/metrics.ts +67 -0
  21. package/core/src/providers/analytics.provider.ts +2 -7
  22. package/core/src/providers/base.provider.ts +6 -70
  23. package/core/src/providers/cart.provider.ts +17 -57
  24. package/core/src/providers/category.provider.ts +9 -18
  25. package/core/src/providers/checkout.provider.ts +21 -19
  26. package/core/src/providers/identity.provider.ts +10 -12
  27. package/core/src/providers/index.ts +14 -13
  28. package/core/src/providers/inventory.provider.ts +18 -8
  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 +32 -38
  32. package/core/src/providers/product-search.provider.ts +61 -0
  33. package/core/src/providers/product.provider.ts +75 -16
  34. package/core/src/providers/profile.provider.ts +77 -17
  35. package/core/src/providers/store.provider.ts +6 -8
  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 +3 -2
  44. package/core/src/schemas/models/base.model.ts +6 -24
  45. package/core/src/schemas/models/cart.model.ts +7 -16
  46. package/core/src/schemas/models/category.model.ts +6 -11
  47. package/core/src/schemas/models/checkout.model.ts +11 -14
  48. package/core/src/schemas/models/cost.model.ts +5 -4
  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 +12 -21
  52. package/core/src/schemas/models/index.ts +20 -19
  53. package/core/src/schemas/models/inventory.model.ts +10 -7
  54. package/core/src/schemas/models/order-search.model.ts +28 -0
  55. package/core/src/schemas/models/order.model.ts +25 -32
  56. package/core/src/schemas/models/payment.model.ts +17 -20
  57. package/core/src/schemas/models/price.model.ts +14 -14
  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 +21 -24
  61. package/core/src/schemas/models/shipping-method.model.ts +28 -33
  62. package/core/src/schemas/models/store.model.ts +10 -6
  63. package/core/src/schemas/mutations/analytics.mutation.ts +9 -8
  64. package/core/src/schemas/mutations/base.mutation.ts +2 -1
  65. package/core/src/schemas/mutations/cart.mutation.ts +37 -37
  66. package/core/src/schemas/mutations/checkout.mutation.ts +24 -31
  67. package/core/src/schemas/mutations/identity.mutation.ts +5 -4
  68. package/core/src/schemas/mutations/index.ts +10 -10
  69. package/core/src/schemas/mutations/profile.mutation.ts +39 -4
  70. package/core/src/schemas/queries/base.query.ts +2 -1
  71. package/core/src/schemas/queries/cart.query.ts +5 -5
  72. package/core/src/schemas/queries/category.query.ts +21 -21
  73. package/core/src/schemas/queries/checkout.query.ts +9 -11
  74. package/core/src/schemas/queries/identity.query.ts +3 -2
  75. package/core/src/schemas/queries/index.ts +14 -13
  76. package/core/src/schemas/queries/inventory.query.ts +6 -6
  77. package/core/src/schemas/queries/order-search.query.ts +10 -0
  78. package/core/src/schemas/queries/order.query.ts +5 -4
  79. package/core/src/schemas/queries/price.query.ts +11 -5
  80. package/core/src/schemas/queries/product-search.query.ts +16 -0
  81. package/core/src/schemas/queries/product.query.ts +14 -7
  82. package/core/src/schemas/queries/profile.query.ts +6 -3
  83. package/core/src/schemas/queries/store.query.ts +7 -6
  84. package/core/src/schemas/result.ts +107 -0
  85. package/core/src/schemas/session.schema.ts +6 -6
  86. package/core/src/test/reactionary.decorator.spec.ts +249 -0
  87. package/core/src/zod-utils.ts +19 -0
  88. package/core/tsconfig.json +3 -2
  89. package/core/tsconfig.spec.json +2 -26
  90. package/core/vitest.config.ts +14 -0
  91. package/documentation/1-purpose.md +114 -0
  92. package/documentation/2-getting-started.md +229 -0
  93. package/documentation/3-querying-and-changing-data.md +74 -0
  94. package/documentation/4-product-data.md +107 -0
  95. package/documentation/5-cart-and-checkout.md +211 -0
  96. package/documentation/6-product-search.md +143 -0
  97. package/documentation/7-marketing.md +3 -0
  98. package/eslint.config.mjs +1 -0
  99. package/examples/node/eslint.config.mjs +1 -4
  100. package/examples/node/package.json +11 -3
  101. package/examples/node/project.json +4 -1
  102. package/examples/node/src/basic/basic-node-provider-model-extension.spec.ts +22 -23
  103. package/examples/node/src/basic/basic-node-provider-query-extension.spec.ts +15 -11
  104. package/examples/node/src/basic/basic-node-setup.spec.ts +44 -28
  105. package/examples/node/src/basic/client-creation.spec.ts +53 -0
  106. package/examples/node/src/capabilities/cart.spec.ts +255 -0
  107. package/examples/node/src/capabilities/category.spec.ts +193 -0
  108. package/examples/node/src/capabilities/checkout.spec.ts +341 -0
  109. package/examples/node/src/capabilities/identity.spec.ts +93 -0
  110. package/examples/node/src/capabilities/inventory.spec.ts +66 -0
  111. package/examples/node/src/capabilities/order-search.spec.ts +159 -0
  112. package/examples/node/src/capabilities/order.spec.ts +91 -0
  113. package/examples/node/src/capabilities/price.spec.ts +51 -0
  114. package/examples/node/src/capabilities/product-search.spec.ts +293 -0
  115. package/examples/node/src/capabilities/product.spec.ts +122 -0
  116. package/examples/node/src/capabilities/profile.spec.ts +316 -0
  117. package/examples/node/src/capabilities/store.spec.ts +26 -0
  118. package/examples/node/src/utils.ts +137 -0
  119. package/examples/node/tsconfig.json +9 -11
  120. package/examples/node/tsconfig.lib.json +1 -2
  121. package/examples/node/tsconfig.spec.json +2 -13
  122. package/examples/node/vitest.config.ts +14 -0
  123. package/migrations.json +22 -5
  124. package/nx.json +8 -47
  125. package/package.json +24 -96
  126. package/providers/algolia/README.md +39 -2
  127. package/providers/algolia/package.json +3 -1
  128. package/providers/algolia/src/core/initialize.ts +9 -16
  129. package/providers/algolia/src/index.ts +4 -6
  130. package/providers/algolia/src/providers/index.ts +1 -0
  131. package/providers/algolia/src/providers/product-search.provider.ts +241 -0
  132. package/providers/algolia/src/schema/capabilities.schema.ts +2 -3
  133. package/providers/algolia/src/schema/index.ts +3 -0
  134. package/providers/algolia/src/schema/search.schema.ts +8 -8
  135. package/providers/algolia/tsconfig.json +2 -1
  136. package/providers/algolia/tsconfig.lib.json +1 -1
  137. package/providers/algolia/tsconfig.spec.json +2 -13
  138. package/providers/algolia/vitest.config.ts +14 -0
  139. package/providers/commercetools/README.md +30 -3
  140. package/providers/commercetools/package.json +3 -2
  141. package/providers/commercetools/src/core/client.ts +179 -100
  142. package/providers/commercetools/src/core/initialize.ts +131 -75
  143. package/providers/commercetools/src/core/token-cache.ts +45 -0
  144. package/providers/commercetools/src/index.ts +11 -10
  145. package/providers/commercetools/src/providers/cart.provider.ts +282 -356
  146. package/providers/commercetools/src/providers/category.provider.ts +223 -147
  147. package/providers/commercetools/src/providers/checkout.provider.ts +631 -449
  148. package/providers/commercetools/src/providers/identity.provider.ts +51 -30
  149. package/providers/commercetools/src/providers/index.ts +12 -12
  150. package/providers/commercetools/src/providers/inventory.provider.ts +77 -77
  151. package/providers/commercetools/src/providers/order-search.provider.ts +220 -0
  152. package/providers/commercetools/src/providers/order.provider.ts +97 -64
  153. package/providers/commercetools/src/providers/price.provider.ts +148 -118
  154. package/providers/commercetools/src/providers/product-search.provider.ts +528 -0
  155. package/providers/commercetools/src/providers/product.provider.ts +251 -81
  156. package/providers/commercetools/src/providers/profile.provider.ts +446 -29
  157. package/providers/commercetools/src/providers/store.provider.ts +56 -42
  158. package/providers/commercetools/src/schema/capabilities.schema.ts +3 -1
  159. package/providers/commercetools/src/schema/commercetools.schema.ts +17 -3
  160. package/providers/commercetools/src/schema/configuration.schema.ts +1 -0
  161. package/providers/commercetools/src/schema/session.schema.ts +7 -0
  162. package/providers/commercetools/src/test/caching.spec.ts +82 -0
  163. package/providers/commercetools/src/test/identity.spec.ts +109 -0
  164. package/providers/commercetools/src/test/test-utils.ts +21 -19
  165. package/providers/commercetools/tsconfig.json +2 -1
  166. package/providers/commercetools/tsconfig.lib.json +1 -1
  167. package/providers/commercetools/tsconfig.spec.json +2 -13
  168. package/providers/commercetools/vitest.config.ts +15 -0
  169. package/providers/fake/README.md +20 -4
  170. package/providers/fake/package.json +3 -2
  171. package/providers/fake/src/core/initialize.ts +52 -54
  172. package/providers/fake/src/index.ts +4 -4
  173. package/providers/fake/src/providers/analytics.provider.ts +6 -8
  174. package/providers/fake/src/providers/cart.provider.ts +165 -94
  175. package/providers/fake/src/providers/category.provider.ts +79 -51
  176. package/providers/fake/src/providers/checkout.provider.ts +254 -0
  177. package/providers/fake/src/providers/identity.provider.ts +58 -66
  178. package/providers/fake/src/providers/index.ts +13 -9
  179. package/providers/fake/src/providers/inventory.provider.ts +41 -37
  180. package/providers/fake/src/providers/order-search.provider.ts +78 -0
  181. package/providers/fake/src/providers/order.provider.ts +106 -0
  182. package/providers/fake/src/providers/price.provider.ts +94 -42
  183. package/providers/fake/src/providers/product-search.provider.ts +206 -0
  184. package/providers/fake/src/providers/product.provider.ts +57 -42
  185. package/providers/fake/src/providers/profile.provider.ts +147 -0
  186. package/providers/fake/src/providers/store.provider.ts +31 -22
  187. package/providers/fake/src/schema/capabilities.schema.ts +5 -1
  188. package/providers/fake/src/test/cart.provider.spec.ts +62 -83
  189. package/providers/fake/src/test/category.provider.spec.ts +147 -89
  190. package/providers/fake/src/test/checkout.provider.spec.ts +222 -0
  191. package/providers/fake/src/test/order-search.provider.spec.ts +50 -0
  192. package/providers/fake/src/test/order.provider.spec.ts +44 -0
  193. package/providers/fake/src/test/price.provider.spec.ts +52 -47
  194. package/providers/fake/src/test/product.provider.spec.ts +18 -10
  195. package/providers/fake/src/test/profile.provider.spec.ts +167 -0
  196. package/providers/fake/src/test/test-utils.ts +1 -1
  197. package/providers/fake/tsconfig.json +2 -1
  198. package/providers/fake/tsconfig.lib.json +1 -1
  199. package/providers/fake/tsconfig.spec.json +2 -14
  200. package/providers/fake/vitest.config.ts +14 -0
  201. package/providers/medusa/README.md +30 -0
  202. package/providers/medusa/TESTING.md +98 -0
  203. package/{trpc → providers/medusa}/eslint.config.mjs +1 -1
  204. package/providers/medusa/package.json +22 -0
  205. package/providers/medusa/project.json +34 -0
  206. package/providers/medusa/src/core/client.ts +370 -0
  207. package/providers/medusa/src/core/initialize.ts +78 -0
  208. package/providers/medusa/src/index.ts +13 -0
  209. package/providers/medusa/src/providers/cart.provider.ts +575 -0
  210. package/providers/medusa/src/providers/category.provider.ts +247 -0
  211. package/providers/medusa/src/providers/checkout.provider.ts +636 -0
  212. package/providers/medusa/src/providers/identity.provider.ts +137 -0
  213. package/providers/medusa/src/providers/inventory.provider.ts +173 -0
  214. package/providers/medusa/src/providers/order-search.provider.ts +201 -0
  215. package/providers/medusa/src/providers/order.provider.ts +226 -0
  216. package/providers/medusa/src/providers/price.provider.ts +140 -0
  217. package/providers/medusa/src/providers/product-search.provider.ts +243 -0
  218. package/providers/medusa/src/providers/product.provider.ts +261 -0
  219. package/providers/medusa/src/providers/profile.provider.ts +392 -0
  220. package/providers/medusa/src/schema/capabilities.schema.ts +18 -0
  221. package/providers/medusa/src/schema/configuration.schema.ts +11 -0
  222. package/providers/medusa/src/schema/medusa.schema.ts +31 -0
  223. package/providers/medusa/src/test/cart.provider.spec.ts +240 -0
  224. package/providers/medusa/src/test/category.provider.spec.ts +231 -0
  225. package/providers/medusa/src/test/checkout.spec.ts +349 -0
  226. package/providers/medusa/src/test/identity.provider.spec.ts +122 -0
  227. package/providers/medusa/src/test/inventory.provider.spec.ts +88 -0
  228. package/providers/medusa/src/test/large-cart.provider.spec.ts +103 -0
  229. package/providers/medusa/src/test/price.provider.spec.ts +104 -0
  230. package/providers/medusa/src/test/product.provider.spec.ts +146 -0
  231. package/providers/medusa/src/test/search.provider.spec.ts +203 -0
  232. package/providers/medusa/src/test/test-utils.ts +13 -0
  233. package/providers/medusa/src/utils/medusa-helpers.ts +89 -0
  234. package/providers/medusa/tsconfig.json +21 -0
  235. package/providers/medusa/tsconfig.lib.json +9 -0
  236. package/providers/medusa/tsconfig.spec.json +4 -0
  237. package/providers/medusa/vitest.config.ts +15 -0
  238. package/providers/meilisearch/README.md +48 -0
  239. package/{otel → providers/meilisearch}/eslint.config.mjs +1 -2
  240. package/providers/meilisearch/package.json +13 -0
  241. package/providers/meilisearch/project.json +34 -0
  242. package/providers/meilisearch/src/core/initialize.ts +16 -0
  243. package/providers/meilisearch/src/index.ts +5 -0
  244. package/providers/meilisearch/src/providers/index.ts +1 -0
  245. package/providers/meilisearch/src/providers/product-search.provider.ts +251 -0
  246. package/providers/meilisearch/src/schema/capabilities.schema.ts +9 -0
  247. package/providers/meilisearch/src/schema/configuration.schema.ts +10 -0
  248. package/providers/meilisearch/src/schema/index.ts +3 -0
  249. package/providers/meilisearch/src/schema/search.schema.ts +14 -0
  250. package/{otel → providers/meilisearch}/tsconfig.json +3 -2
  251. package/{trpc → providers/meilisearch}/tsconfig.lib.json +2 -2
  252. package/providers/meilisearch/tsconfig.spec.json +4 -0
  253. package/providers/meilisearch/vitest.config.ts +14 -0
  254. package/providers/posthog/package.json +5 -4
  255. package/providers/posthog/project.json +2 -2
  256. package/providers/posthog/src/core/initialize.ts +2 -2
  257. package/providers/posthog/src/index.ts +3 -3
  258. package/providers/posthog/tsconfig.json +2 -1
  259. package/tsconfig.base.json +7 -3
  260. package/vitest.config.ts +10 -0
  261. package/core/src/providers/search.provider.ts +0 -18
  262. package/core/src/schemas/models/search.model.ts +0 -37
  263. package/core/src/schemas/queries/search.query.ts +0 -9
  264. package/examples/next/.swcrc +0 -30
  265. package/examples/next/eslint.config.mjs +0 -21
  266. package/examples/next/index.d.ts +0 -6
  267. package/examples/next/next-env.d.ts +0 -5
  268. package/examples/next/next.config.js +0 -20
  269. package/examples/next/project.json +0 -9
  270. package/examples/next/public/.gitkeep +0 -0
  271. package/examples/next/public/favicon.ico +0 -0
  272. package/examples/next/src/app/global.css +0 -0
  273. package/examples/next/src/app/layout.tsx +0 -18
  274. package/examples/next/src/app/page.module.scss +0 -2
  275. package/examples/next/src/app/page.tsx +0 -48
  276. package/examples/next/src/instrumentation.ts +0 -9
  277. package/examples/next/tsconfig.json +0 -44
  278. package/examples/node/jest.config.ts +0 -10
  279. package/jest.config.ts +0 -6
  280. package/jest.preset.js +0 -3
  281. package/otel/README.md +0 -227
  282. package/otel/package.json +0 -11
  283. package/otel/pnpm-lock.yaml +0 -805
  284. package/otel/project.json +0 -33
  285. package/otel/src/index.ts +0 -22
  286. package/otel/src/metrics.ts +0 -76
  287. package/otel/src/provider-instrumentation.ts +0 -108
  288. package/otel/src/test/otel.spec.ts +0 -8
  289. package/otel/src/trace-decorator.ts +0 -226
  290. package/otel/src/tracer.ts +0 -83
  291. package/otel/src/trpc-middleware.ts +0 -128
  292. package/otel/tsconfig.lib.json +0 -23
  293. package/otel/tsconfig.spec.json +0 -28
  294. package/otel/vite.config.ts +0 -24
  295. package/providers/algolia/jest.config.ts +0 -10
  296. package/providers/algolia/src/providers/product.provider.ts +0 -66
  297. package/providers/algolia/src/providers/search.provider.ts +0 -106
  298. package/providers/algolia/src/test/search.provider.spec.ts +0 -91
  299. package/providers/commercetools/jest.config.ts +0 -10
  300. package/providers/commercetools/src/providers/search.provider.ts +0 -98
  301. package/providers/commercetools/src/test/cart.provider.spec.ts +0 -199
  302. package/providers/commercetools/src/test/category.provider.spec.ts +0 -168
  303. package/providers/commercetools/src/test/checkout.provider.spec.ts +0 -312
  304. package/providers/commercetools/src/test/identity.provider.spec.ts +0 -88
  305. package/providers/commercetools/src/test/inventory.provider.spec.ts +0 -41
  306. package/providers/commercetools/src/test/price.provider.spec.ts +0 -81
  307. package/providers/commercetools/src/test/product.provider.spec.ts +0 -80
  308. package/providers/commercetools/src/test/profile.provider.spec.ts +0 -49
  309. package/providers/commercetools/src/test/search.provider.spec.ts +0 -61
  310. package/providers/commercetools/src/test/store.provider.spec.ts +0 -37
  311. package/providers/fake/jest.config.ts +0 -10
  312. package/providers/fake/src/providers/search.provider.ts +0 -135
  313. package/trpc/README.md +0 -7
  314. package/trpc/__mocks__/superjson.js +0 -25
  315. package/trpc/jest.config.ts +0 -14
  316. package/trpc/package.json +0 -14
  317. package/trpc/project.json +0 -31
  318. package/trpc/src/client.ts +0 -175
  319. package/trpc/src/index.ts +0 -44
  320. package/trpc/src/integration.spec.ts +0 -223
  321. package/trpc/src/server.ts +0 -125
  322. package/trpc/src/transparent-client.spec.ts +0 -161
  323. package/trpc/src/types.ts +0 -144
  324. package/trpc/tsconfig.json +0 -16
  325. package/trpc/tsconfig.spec.json +0 -15
@@ -0,0 +1,74 @@
1
+ # Querying and Changing data
2
+
3
+ All reactionary calls take an object-based parameter set, and can access the `RequestContext` from the client in which the call is made.
4
+
5
+ For getters, this parameter object is called a Query, and for functions that change state, they are called Mutations. This is the chosen termnology.
6
+
7
+ The naming convention states, that your query must be called
8
+ `<Noun>Query<NameOfQuery>Schema`
9
+
10
+ We strive to have all queries be named for what they constrain the dataset by, making the most frequent pattern `<Noun>QueryBy<Fields>Schema`
11
+
12
+ Examples of this are (Query)
13
+ ```ts
14
+ export const CategoryQueryBySlugSchema = BaseQuerySchema.extend({
15
+ slug: z.string().default(''),
16
+ });
17
+ ```
18
+
19
+
20
+ Other variations exist where the Query has mulitple parameters
21
+ ```ts
22
+ export const CategoryQueryForChildCategoriesSchema = BaseQuerySchema.extend({
23
+ parentId: CategoryIdentifierSchema.default(() => CategoryIdentifierSchema.parse({})),
24
+ paginationOptions: PaginationOptionsSchema.default(() => PaginationOptionsSchema.parse({})),
25
+ });
26
+ ```
27
+
28
+ Likewise for changing data, you will see that the mutator object has a naming convention of `<Noun>Mutation<Operation>Schema`, example
29
+
30
+ ```ts
31
+ export const CartMutationItemAddSchema = BaseMutationSchema.extend({
32
+ cart: CartIdentifierSchema.nonoptional(),
33
+ variant: ProductVariantIdentifierSchema.nonoptional(),
34
+ quantity: z.number()
35
+ });
36
+ ```
37
+
38
+
39
+
40
+ ## Paging
41
+ All getters that return lists, are by design paginated, with a maximum of 50 items returned pr page.
42
+
43
+ All pages are indexed with 1 being the first page. This makes it easier to render, and since Reactionary provides all the relevant numbers, you are not really expected to do math on the data yourself.
44
+
45
+ Pagination options are always set as a nested object on the Query, called `paginationOptions`, and the result object, always reflects both `pageNumber`, `pageSize`, `totalCount` and `totalPages` back out.
46
+
47
+
48
+
49
+
50
+ ## Design Decisions
51
+ We want all aspects of Reactionary to be custommizable and extendable. It is for this reason, we are using the object-payloads for parameters, as this allows you to specialize and extend the query for your own purpose, without violating any of the underlying mechanisms.
52
+
53
+ Ie, if your site operated on mulitple catalogs, and you needed to load some catalog specific information in the `getBySlug` call, you can add the extra query parameters by extending the `CategoryQueryBySlugSchema`, and likewise for mutations, if you need to send more data to `add-to-cart`, you can extend the `CartMutationItemAddSchema`.
54
+
55
+
56
+ All data and parameters is validated by `Zod` on the recieving end. This means, we adhere to the schema definitions very strictly, and you will get runtime errors if you provide bad or faulty data. This is intentional, as it requires you to fix the data, or fix your logic when certain invariants are not met. While this might seem annoying at first, it helps make things alot more maintainable over time, and has the added benefit of incentivising data-integrity checks at more levels.
57
+
58
+
59
+ To ensure you also get compile time errors you can use the `satisfies` construct.
60
+
61
+ ie
62
+ ```ts
63
+ const clickedCategory = <the id of the category the user just clicked>
64
+ const childCategoriesResponse = await client.category.findChildCategories({
65
+ parId: clickedCategory,
66
+ paginationOptions: {
67
+ pageNumber: 1,
68
+ pageSize: 40
69
+ }
70
+ } satisfies CategoryQueryForChildCategories)
71
+ ```
72
+
73
+ this will give a compile time error that the `parentId` is missing.
74
+
@@ -0,0 +1,107 @@
1
+ # Product data
2
+
3
+ Reactionary uses a Product/Variant model for product data.
4
+
5
+ The product is the carrier of organizational data (`seoslug`, `parentCategory`, etc), and shared marketing data `description`, `sharedAttributes`, etc.
6
+
7
+ All products will have at least one variant. Variants represent the buyable version of the product, or more commonly called the `SKU`.
8
+
9
+ It is mainly when navigating to the PDP you will need to load the product directly.
10
+
11
+
12
+
13
+ ## Navigating to the PDP
14
+ The PDP will be accessible by seo slug. From talking to our internal PIM people, it is apparent that virtually noone expects to be able to have a product-description seperate from all its variants, so for that reason, the assumption is that when you look at a PDP, you are looking at the Product + one of its variants.
15
+
16
+ The `Product` model has a `.mainVariant` sub-field that contains the variant specific information (`sku`, `ean`, `name`, `images` etc)
17
+
18
+ There is no guarantees which variant (if you have multiple) that will be returned in the call to `ProductProvider#getBySlug`.
19
+
20
+ If you want to allow navigating to a specific variant, you have to add your own url-scheme that includes the `sku`. This would typically be, if you want to persist your attribute selection to the url, so customer can copy/paste url and send to someone.
21
+
22
+ ```ts
23
+ // assume route contains the current url..
24
+ const productResponse = await client.product.getBySlug({ slug: route.url }, reqCtx);
25
+
26
+ if (productResponse.success) {
27
+ const product = productResponse.value;
28
+ const desc = product.description
29
+ const mainImage = product.mainVariant.images[0].sourceUrl
30
+ }
31
+
32
+ ```
33
+
34
+ ## Get SKU
35
+ If you need to show some data for your cart items, or checkout items, you can use the `ProductProvider#getBySKU` call. It will return a `Product`, but with the `mainVariant` set to the variant with the `sku` you passed as argument.
36
+
37
+ ```ts
38
+ for(const item of cart.items) {
39
+ const productResponse = await client.product.getBySKU({ variant: item.variant });
40
+ if (productResponse.success) {
41
+ const cartVariant = productResponse.value.mainVariant;
42
+ const cartVariantImage = productResponse.value.mainVariant.images[0].sourceUrl;
43
+ }
44
+ }
45
+ ```
46
+
47
+ ## Getting category information for breadcrumbs or megamenus
48
+ You use the `CategoryProvider#getBreadcrumbPathToCategory` to get the full navigational path from a products parent category to the root of the site.
49
+
50
+ ```ts
51
+ const breadcrumbResponse = await client.category.getBreadcrumbPathToCategory({ id: product.parentCategories[0] });
52
+ if (breadcrumbResponse.success) {
53
+ const breadcrumb = breadcrumbResponse.value;
54
+ }
55
+ ```
56
+
57
+ To render site menus, you can use this call
58
+ ```ts
59
+ const topCategoriesResponse = await client.category.findTopCategories({ paginationOptions: { pageNumber: 1, pageSize: 15 }});
60
+ if (topCategoriesResponse.success) {
61
+ const topCategories = topCategoriesResponse.value;
62
+ }
63
+ ```
64
+
65
+ Note, both for topCategories and childCategories you have to use pagination options. By design, we do not allow for unbounded calls. The maximum pageSize is 50, but the recommended pageSize is 20.
66
+ On some sites you might WANT to load everything in one go, but it should be considered a code-smell. Why would the customer want to wait to load 1000 categories. He can't reasonably do anything with it anyway.
67
+
68
+
69
+
70
+
71
+ ## Specializing the product
72
+ When you get to know your project and customers domain, you will want to add logic to make the domain model more specialized to your field.
73
+ This helps make the code easier to read later on.
74
+
75
+ See `basic-node-provider-model-extension.spec.ts` for an example of how to do this.
76
+
77
+ It is *highly* recommended that you take the time to do this.
78
+
79
+ ```ts
80
+ const PharmacyProductSchema = ProductSchema.extend({
81
+ // if true, the product is an RX product, and cant be buyable unless you have a prescription
82
+ isPrescriptionProduct: z.boolean().default(false)
83
+ });
84
+ type PharmacyProduct = z.infer<typeof PharmacyProductSchema>;
85
+
86
+ class PharmacyProductProvider extends MedusaProductProvider<PharmacyProduct> {
87
+
88
+ protected override parseSingle(_body: StoreProduct, reqCtx: RequestContext): T {
89
+ const model = super.parseSingle(body);
90
+
91
+ if (_body.metaData['rx-product'] === 'true') {
92
+ model.isPrescriptionProduct = true;
93
+ }
94
+ }
95
+ }
96
+ ```
97
+
98
+
99
+ ## Design decisions
100
+ Since , in some frameworks, the entirety of the object can be serialized between BFF and Client, we have decided not to include a list of all variants directly on the `Product` model.
101
+
102
+ The products identifier is meaningless, and no semantic meaning is assigned to it, except it should be something that is very unlikely to change in the upstream PIM
103
+
104
+ ## FAQ about products
105
+
106
+ ### What should I do, if i dont want to load all variants at once
107
+ Consider adding a specialized `getSKUList` function, that returns a paged result set of variants. This can be helpful if the number of skus are unbounded.
@@ -0,0 +1,211 @@
1
+ # Dealing with carts and checkouts
2
+
3
+
4
+ # Design decision
5
+ It was decided to adopt a pattern where the cart is not the thing that you check out. Rather the cart is the data entity responsible for recording your product selections, and calculating your price.
6
+
7
+ When you want to start the checkout process, you create a new checkout session, based on your cart, at which point the cart is considered read-only.
8
+
9
+ While we have seen examples of cart pages where you can change quantity, remove items, or add upsells up until the second you press "pay", the new style of checkout focuses on getting you to pay, as fast as possible.
10
+
11
+ This means, you can have all the upsell stuff on the "review cart page", but once you decide to "go to checkout", the focus is "Where, and how are you paying".
12
+
13
+
14
+ # Initiating checkout
15
+ The checkout takes as input a cart, any billing address you might have from being logged in, and returns a different object.
16
+
17
+ ```ts
18
+ const checkoutResponse = await client.checkout.initiateCheckoutForCart({
19
+ cart: cart.value,
20
+ billingAddress: address,
21
+ notificationEmail: email,
22
+ notificationPhone: phone,
23
+ });
24
+ if (!checkoutResponse.success) {
25
+ throw new Response("Error initiating checkout " + checkout.error, { status: 500 });
26
+ }
27
+
28
+ const routeId = checkoutResponse.value.identifier.key;
29
+
30
+ ```
31
+ UI wise you can consider these values tied to this checkout, so its perfectly fine to have a flow where the address is edited later on. This is just the seed address/the billing address that allows us to make informed decisions about the kind of taxes and shipping you are going to be presented with.
32
+
33
+ The checkout's id can then be used on your frontend url scheme to refer to this checkout session.
34
+
35
+
36
+ ## Setting a shipping address
37
+ Depending on your flow, you might ask for a seperate shipping address (if not set, the billing address is assumed as the shipping address as well).
38
+ The shipping address might affect the ways in which you can get things shipped, so maybe set this before picking the shipping provider.
39
+
40
+ ```ts
41
+ const shippingAddress = {...} satisifes Address;
42
+ const updatedCheckoutResponse = await client.checkout.setShippingAddress({ checkout: { key: routeParams.checkoutId }, shippingAddress: shippingAddress });
43
+
44
+ if (!updatedCheckoutResponse.success) {
45
+ throw new Response("Error setting shipping address on checkout " + checkout.error, { status: 500 });
46
+ }
47
+ ```
48
+
49
+
50
+ ## Setting a shipping instruction
51
+ A shipping instruction is to shipping like a payment instruction is to payment. It is the combined set of choices that define how this order is requested delivered. Ie, the method, the pickup point, and any instructions you might have for the carrier.
52
+
53
+ To get the list of available shipping methods use
54
+
55
+ ```ts
56
+ // lets double check that the checkout still exists...
57
+ const checkout = await client.checkout.getById({ identifier: { key: checkoutId || "" } });
58
+ if (!checkout.success) {
59
+ throw new Response("Checkout Not Found", { status: 404 });
60
+ }
61
+
62
+ const availableShippingMethodsResponse = await client.checkout.getAvailableShippingMethods({ checkout: checkout.value.identifier})
63
+ if (availableShippingMethodsResponse.success) {
64
+ const availableShippingMethods = availableShippingMethodsResponse.value;
65
+ console.log(availableShippingMethods[0])
66
+ }
67
+
68
+ ```
69
+ Once picked, you can then set the full shipping instruction
70
+
71
+ ```ts
72
+ const formData = await request.formData();
73
+
74
+ const selectedMethod = formData.get("shippingMethod") as string;
75
+ const shippingInstructions = formData.get("shippingInstructions") as string;
76
+ const allowUnattendedDelivery = formData.get("allowUnattendedDelivery") === "on";
77
+
78
+
79
+ // verify the checkout wasn't deleted in the mean time and that you can access it
80
+ const checkout = await client.checkout.getById({ identifier: { key: checkoutId || "" } });
81
+ if (!checkout.success) {
82
+ throw new Response("Checkout Not Found", { status: 404 });
83
+ }
84
+
85
+ const shippingInstruction: ShippingInstruction = {
86
+ shippingMethod: { key: selectedMethod },
87
+ instructions: shippingInstructions,
88
+ pickupPoint: "",
89
+ consentForUnattendedDelivery: allowUnattendedDelivery,
90
+ }
91
+ console.log('Selected shipping method in action:', shippingInstruction);
92
+ const updatedCheckout = await client.checkout.setShippingInstruction({
93
+ checkout: checkout.value.identifier,
94
+ shippingInstruction: shippingInstruction,
95
+ });
96
+ if (!updatedCheckout.success) {
97
+ throw new Response("Error setting shipping instruction", { status: 500 });
98
+ }
99
+ ```
100
+
101
+ ## Setting payment instruction
102
+ A payment instruction consist of a choice of payment provider, and the amount of money to ask to authorize.
103
+ A checkout can have multiple payment instructions. Sometimes for complicated purchases (use multiple credit cards, or some from mobilepay and rest on creditcard), or for more mundane purposes, like applying store-credit, or using an electronic gift-card.
104
+
105
+ To get a list of available payment methods do something like
106
+ ```ts
107
+ const checkoutId = params.checkoutId;
108
+ // verify the checkout wasn't deleted in the mean time and that you can access it
109
+ const checkout = await client.checkout.getById({ identifier: { key: checkoutId || "" } });
110
+ if (!checkout.success) {
111
+ throw new Response("Checkout Not Found", { status: 404 });
112
+ }
113
+ const paymentMethodResponse = await client.checkout.getAvailablePaymentMethods({
114
+ checkout: { key: checkoutId || "" },
115
+ });
116
+ if (!paymentMethodResponse.success) {
117
+ throw new Response("Error fetching payment methods", { status: 500 });
118
+ }
119
+ const paymentMethods = paymentMethodsResponse.value;
120
+
121
+ ```
122
+ Add UI to display, and once picked, apply to checkout.
123
+
124
+ ```ts
125
+ const checkoutId = params.checkoutId;
126
+ const formData = await request.formData();
127
+
128
+ const selectedMethod = formData.get("paymentMethod") as string;
129
+
130
+ const session = await getSession(request.headers.get("Cookie"));
131
+ const reqCtx = await createReqContext(request, session);
132
+ const client = await createClient(reqCtx);
133
+
134
+ const checkout = await client.checkout.getById({ identifier: { key: checkoutId || "" } });
135
+ if (!checkout.success) {
136
+ throw new Response("Checkout Not Found", { status: 404 });
137
+ }
138
+ const paymentMethods = await client.checkout.getAvailablePaymentMethods({
139
+ checkout: checkout.value.identifier,
140
+ });
141
+ if (!paymentMethods.success) {
142
+ throw new Response("Error fetching payment methods", { status: 500 });
143
+ }
144
+
145
+ const paymentMethod = paymentMethods.value.find(
146
+ (method) => String(method.identifier.method) === selectedMethod
147
+ );
148
+ if (!paymentMethod) {
149
+ throw new Response("Invalid Payment Method", { status: 400 });
150
+ }
151
+
152
+
153
+ const updatedCheckout = await client.checkout.addPaymentInstruction({
154
+ checkout: checkout.value.identifier,
155
+ paymentInstruction: {
156
+ paymentMethod: paymentMethod.identifier,
157
+ amount: checkout.value.price.grandTotal,
158
+ protocolData: [],
159
+ },
160
+ });
161
+ if (!updatedCheckout.success) {
162
+ throw new Response("Error adding payment instruction", { status: 500 });
163
+ }
164
+ ```
165
+
166
+ At this point, your backend will have set up a payment intent with the PSP, and you can read out the data needed to either load the providers UI library and bootstrap it (stripe), or you will get a redirect url you can use to send the customer to the payment processor directly.
167
+
168
+ In this example, we detect that we
169
+ ```ts
170
+ const pendingPayment = updatedCheckout.value.paymentInstructions.find(x => x.status === 'pending');
171
+ if (pendingPayment?.paymentMethod.paymentProcessor === 'stripe') {
172
+ return redirect(`/checkout/${checkoutId}/payment/stripe`);
173
+ }
174
+ ```
175
+
176
+ And on that subpage specifc to stripe, you might extract hte client secret required to feed their component.
177
+
178
+ ```ts
179
+ const clientSecretData = pendingPayment.protocolData.find(
180
+ (data) =>
181
+ data.key === "stripe_clientSecret" || data.key === "client_secret",
182
+ );
183
+ ```
184
+
185
+
186
+ Finally, when the payment provider either returns (because you punched out, and provided a return url with the `checkoutId` in it), OR the inline component tells you everything is fine, it is time to close the deal
187
+
188
+ ```ts
189
+ const checkoutId = params.checkoutId;
190
+ const checkout = await client.checkout.getById({
191
+ identifier: { key: checkoutId || "" },
192
+ });
193
+
194
+ if (!checkout.success) {
195
+ throw new Response("Checkout Not Found", { status: 404 });
196
+ }
197
+ const finalizedCheckoutResponse = await client.checkout.finalizeCheckout({ checkout: checkout.value.identifier })
198
+
199
+ ```
200
+ The checkout returned, contains the state of things as they were at checkout. It also has a reference to the created order. Wheter you want your order confirmation to be based on either is project dependent. In some setups the order might come from a secondary OMS system, and it might take a while for the order to exist there.
201
+
202
+ ## FAQ
203
+
204
+ ### When you say the cart is readonly, what does that mean for the UX flow?
205
+ Whether or not the cart is ACTUALLY read only doesn't matter. The point is, that changes to cart during checkout session is not automatically reflected into the checkout. This ensures you don't need to deal with situations where customer has opened two browser windows and try to add more stuff to the cart during checkout.
206
+
207
+ The Checkout session contains all you need to render the UX flow. It has a snapshot of the carts contents at the time you initiated the login.
208
+
209
+
210
+
211
+
@@ -0,0 +1,143 @@
1
+ # Adding product search
2
+
3
+ A common pattern on most ecommerce sites, is to allow the user to search / navigate your catalog. For this purpose Reactionary provides the `ProductSearchProvider`.
4
+
5
+
6
+ ## Keyword search
7
+ You can react to the user submitting a keyword search, or maybe routing to a special landing page where the page-part is a search term, by doing something like this
8
+
9
+ ```ts
10
+ const searchresultResponse = await client.productSearch.queryByTerm(
11
+ {
12
+ search: {
13
+ facets: [],
14
+ paginationOptions: {
15
+ pageNumber: 1,
16
+ pageSize: 12,
17
+ },
18
+ term: 'glass',
19
+ filters: []
20
+ },
21
+ }
22
+ );
23
+
24
+ if (!searchresultResponse.success) {
25
+ throw new Response("Error fetching products", { status: 500 });
26
+ }
27
+ ```
28
+
29
+ What you get back is a miniturazed/specialized product model, optimized for PLP purposes.
30
+
31
+ In the Product/Variant mindset, the search returns Products, with enough of each variant to make it identifiable. Ie Image and SKU.
32
+ In addition, the variant part might also contain an indexed `Option`, which can be used to create swatches on the PLP if needed.
33
+
34
+ The result of the search is a paged result set, but with some added fields for `facets`
35
+
36
+ Since price and inventory are expected to be sourced from other areas, you have to look them up
37
+
38
+ ```ts
39
+ {
40
+ const pricePromises = productPageResponse.value.items.map(async (product) => {
41
+ if (product.variants.length === 0) {
42
+ //skip
43
+ return null;
44
+ }
45
+ return client.price.getCustomerPrice({
46
+ variant: product.variants[0].variant,
47
+ }).then(priceResponse => {
48
+ if (priceResponse.success) {
49
+ return priceResponse.value;
50
+ }
51
+ return null;
52
+ }
53
+ );
54
+ });
55
+ const prices = (await Promise.all(pricePromises)).filter((price): price is Price => price !== null);
56
+
57
+ ```
58
+
59
+ and then accessed further down like
60
+
61
+ ```ts
62
+ searchResult.items.map((product) => {
63
+ const imgUrl = product.variants[0].image[0].sourceUrl || 'assets/no-image.png'
64
+ const imgAlt = product.variants[0].iamge[0].altText || product.name;
65
+ const price = prices.find(x => x.variant.sku === product.variants[0].variant.sku )
66
+ return (
67
+ <div className={classnames("card")} key={product.identifier.key} sku={product.variants[0].variant.sku}>
68
+ <img
69
+ className={classnames("card__image")}
70
+ src={imgUrl}
71
+ alt="{imgAlt}"
72
+ />
73
+ <h5 className={classnames("card__title")}>{product.name}</h5>
74
+ <p>{price.value} {price.currency}</p>
75
+ <p className={classnames("card__desc")}>{product.description}</p>
76
+ if (product.directAddToCart) {
77
+ <button className={classnames("card__button")}>Add to Cart</button>
78
+ } else {
79
+ <button className={classnames("card__button")}>View variants</button>
80
+ }
81
+ </div>
82
+ );
83
+ })}
84
+ }
85
+ ```
86
+
87
+
88
+ Facets can be rendered in the same way. When a customer clicks a facet, you can redo the existing search, with that facet selected, because the original searchresult has the query information it was made for, as part of it.
89
+
90
+ ```ts
91
+ // customer has clicked some facet-valie, and we are passed its identifier. The identifier should be considered opaque at this point...
92
+
93
+ const existingSearchState = mySession.lastKeywordSearch;
94
+
95
+ const updatedFacets = [...existingSearchState.facets, newlyClickedFacetValue];
96
+ const paginationOptions = { ...existingPaginationOptions, pageNumber: 1 };
97
+
98
+ // keep everything in our search the same, except add the new facet, and reset to page 1.
99
+ const newSearchQuery = { search: { ...existingSearchState, facets: updatedFacets, paginationOptions };
100
+ const newSearchState = client.productSearch.queryByTerm(newSearchQuery, requestContext);
101
+
102
+ mySession.lastKeywordSearch = newSearchState.identifier;
103
+ ```
104
+
105
+ The above example shows how the responsibility for state management is split between the UX framework and Reactionary. Reactionary has no insight into the routing patterns or page transitions that may have occured, and have no way of knowing if we are in a continuous interaction on the same search or not.
106
+
107
+ ### Swatches / Variants on PLP
108
+
109
+ Generally, for sites that are Mobile first, or where the vast majority of interaction is expected to come from mobile devices, anything that becomes fiddly, should be avoided. It is hard for users to click one of 12 color swathes on your 400x200 product tile, without accidentially clicking the product itself.
110
+
111
+ But in some cases, where desktop is still the expected primary channel, you might want to add some indication that a product exists in various variations.
112
+
113
+ To do this, the data model provides one `ProductVariantOption` pr variant from the search index.
114
+
115
+ Note, that the search index Variants is expected to be a subset of the `Product` model variants. And only for the visually distinct variations.
116
+ So, if your `Product` has 6 sizes (XXL through XS), and 5 colors (red, green, blue, yellow, black), your Product Model might have 30 variants. But your search index should only have 5, one for each color, as this is the visually distinctive set for this product.
117
+
118
+ If your product has 20 variants, differing on some attribute `Length` or `Diameter`, those are not really good candidates for search index variations as they are not visually distinct. In this case the search index would contain only one variation, but, it would be marked as not directly addable to cart (`ProductSearchResultItemSchema#directAddToCart`).
119
+
120
+
121
+ ## Typeahead
122
+ TBD
123
+
124
+
125
+
126
+ ## Design
127
+ We consider category navigation just a special case of facetted filtering. Therefore, no dedicated function is available to do keyword search vs category search.
128
+
129
+ We may later decide to add a category version, if only to make it easier to seperate the analytics of them both.
130
+
131
+ The Variant of the SearchResultItem is set to only have one option, as that is what we see most frequently on sites.
132
+ These design patterns are well served under the current model:
133
+
134
+ 1. Products have 1 SKU, you can click Add to cart directly from PLP
135
+ 1. Products have multiple SKUs, you click tile, and get sent to PDP
136
+ 1. Products have multiple SKUs with one visually discerning attribute, you can click Add to cart directly from PLP
137
+ 1. Products have multiple SKUs,with one visually discerning attribute, and one or more non-visual. You click tile and gets sent to PDP with attribute selected
138
+ 1. Products have multiple SKUs, with no discerning attributes. You click tile and get sent to PDP
139
+
140
+ The only pattern that isn't well supported is
141
+ 1. Products have multiple SKUs with multiple visually discerning attribute, you can click Add to cart directly from PLP
142
+
143
+ It is felt that this is rare enough, that we can make that a specialization task if it comes up.
@@ -0,0 +1,3 @@
1
+ # Adding the personal touch
2
+
3
+
package/eslint.config.mjs CHANGED
@@ -9,6 +9,7 @@ export default [
9
9
  '**/dist',
10
10
  '**/vite.config.*.timestamp*',
11
11
  '**/vitest.config.*.timestamp*',
12
+ '**/vitest.config.ts'
12
13
  ],
13
14
  },
14
15
  {
@@ -8,10 +8,7 @@ export default [
8
8
  '@nx/dependency-checks': [
9
9
  'error',
10
10
  {
11
- ignoredFiles: [
12
- '{projectRoot}/eslint.config.{js,cjs,mjs}',
13
- '{projectRoot}/esbuild.config.{js,ts,mjs,mts}',
14
- ],
11
+ ignoredFiles: ['{projectRoot}/eslint.config.{js,cjs,mjs}'],
15
12
  },
16
13
  ],
17
14
  },
@@ -1,6 +1,14 @@
1
1
  {
2
2
  "name": "@reactionary/examples-node",
3
- "version": "0.0.1",
4
- "private": true,
5
- "dependencies": {}
3
+ "version": "0.2.15",
4
+ "main": "index.js",
5
+ "types": "src/index.d.ts",
6
+ "dependencies": {
7
+ "@reactionary/core": "0.2.15",
8
+ "@reactionary/provider-commercetools": "0.2.15",
9
+ "@reactionary/provider-algolia": "0.2.15",
10
+ "@reactionary/provider-medusa": "0.2.15",
11
+ "@reactionary/provider-meilisearch": "0.2.15"
12
+ },
13
+ "type": "module"
6
14
  }
@@ -5,6 +5,7 @@
5
5
  "projectType": "library",
6
6
  "tags": [],
7
7
  "targets": {
8
+
8
9
  "build": {
9
10
  "executor": "@nx/esbuild:esbuild",
10
11
  "outputs": ["{options.outputPath}"],
@@ -13,8 +14,10 @@
13
14
  "main": "examples/node/src/index.ts",
14
15
  "tsConfig": "examples/node/tsconfig.lib.json",
15
16
  "assets": ["examples/node/*.md"],
16
- "format": ["esm"]
17
+ "format": ["esm"],
18
+ "bundle": false
17
19
  }
18
20
  }
21
+
19
22
  }
20
23
  }