@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.
- package/.env-template +19 -0
- package/.github/workflows/pull-request.yml +3 -1
- package/.github/workflows/release.yml +9 -0
- package/.vscode/extensions.json +0 -2
- package/LICENSE +21 -0
- package/README.md +175 -23
- package/core/package.json +6 -3
- package/core/src/cache/cache.interface.ts +1 -0
- package/core/src/cache/index.ts +4 -0
- package/core/src/cache/memory-cache.ts +30 -2
- package/core/src/cache/noop-cache.ts +15 -1
- package/core/src/cache/redis-cache.ts +20 -0
- package/core/src/client/client-builder.ts +71 -54
- package/core/src/client/client.ts +9 -47
- package/core/src/client/index.ts +2 -0
- package/core/src/decorators/index.ts +1 -0
- package/core/src/decorators/reactionary.decorator.ts +203 -34
- package/core/src/index.ts +6 -19
- package/core/src/initialization.ts +1 -18
- package/core/src/metrics/metrics.ts +67 -0
- package/core/src/providers/analytics.provider.ts +1 -6
- package/core/src/providers/base.provider.ts +5 -69
- package/core/src/providers/cart.provider.ts +15 -55
- package/core/src/providers/category.provider.ts +7 -11
- package/core/src/providers/checkout.provider.ts +17 -15
- package/core/src/providers/identity.provider.ts +6 -8
- package/core/src/providers/index.ts +2 -1
- package/core/src/providers/inventory.provider.ts +15 -5
- package/core/src/providers/order-search.provider.ts +29 -0
- package/core/src/providers/order.provider.ts +47 -15
- package/core/src/providers/price.provider.ts +30 -36
- package/core/src/providers/product-search.provider.ts +61 -0
- package/core/src/providers/product.provider.ts +71 -12
- package/core/src/providers/profile.provider.ts +74 -14
- package/core/src/providers/store.provider.ts +3 -5
- package/core/src/schemas/capabilities.schema.ts +10 -3
- package/core/src/schemas/errors/generic.error.ts +9 -0
- package/core/src/schemas/errors/index.ts +4 -0
- package/core/src/schemas/errors/invalid-input.error.ts +9 -0
- package/core/src/schemas/errors/invalid-output.error.ts +9 -0
- package/core/src/schemas/errors/not-found.error.ts +9 -0
- package/core/src/schemas/index.ts +7 -0
- package/core/src/schemas/models/analytics.model.ts +2 -1
- package/core/src/schemas/models/base.model.ts +6 -24
- package/core/src/schemas/models/cart.model.ts +5 -8
- package/core/src/schemas/models/category.model.ts +4 -9
- package/core/src/schemas/models/checkout.model.ts +6 -7
- package/core/src/schemas/models/cost.model.ts +4 -3
- package/core/src/schemas/models/currency.model.ts +2 -1
- package/core/src/schemas/models/identifiers.model.ts +106 -62
- package/core/src/schemas/models/identity.model.ts +10 -19
- package/core/src/schemas/models/index.ts +2 -1
- package/core/src/schemas/models/inventory.model.ts +8 -5
- package/core/src/schemas/models/order-search.model.ts +28 -0
- package/core/src/schemas/models/order.model.ts +20 -26
- package/core/src/schemas/models/payment.model.ts +14 -17
- package/core/src/schemas/models/price.model.ts +11 -11
- package/core/src/schemas/models/product-search.model.ts +42 -0
- package/core/src/schemas/models/product.model.ts +64 -22
- package/core/src/schemas/models/profile.model.ts +19 -22
- package/core/src/schemas/models/shipping-method.model.ts +24 -29
- package/core/src/schemas/models/store.model.ts +9 -5
- package/core/src/schemas/mutations/analytics.mutation.ts +8 -7
- package/core/src/schemas/mutations/base.mutation.ts +2 -1
- package/core/src/schemas/mutations/cart.mutation.ts +33 -33
- package/core/src/schemas/mutations/checkout.mutation.ts +23 -30
- package/core/src/schemas/mutations/identity.mutation.ts +4 -3
- package/core/src/schemas/mutations/profile.mutation.ts +38 -3
- package/core/src/schemas/queries/base.query.ts +2 -1
- package/core/src/schemas/queries/cart.query.ts +3 -3
- package/core/src/schemas/queries/category.query.ts +18 -18
- package/core/src/schemas/queries/checkout.query.ts +7 -9
- package/core/src/schemas/queries/identity.query.ts +2 -1
- package/core/src/schemas/queries/index.ts +2 -1
- package/core/src/schemas/queries/inventory.query.ts +5 -5
- package/core/src/schemas/queries/order-search.query.ts +10 -0
- package/core/src/schemas/queries/order.query.ts +3 -2
- package/core/src/schemas/queries/price.query.ts +10 -4
- package/core/src/schemas/queries/product-search.query.ts +16 -0
- package/core/src/schemas/queries/product.query.ts +13 -6
- package/core/src/schemas/queries/profile.query.ts +5 -2
- package/core/src/schemas/queries/store.query.ts +6 -5
- package/core/src/schemas/result.ts +107 -0
- package/core/src/schemas/session.schema.ts +4 -4
- package/core/src/test/reactionary.decorator.spec.ts +249 -0
- package/core/src/zod-utils.ts +19 -0
- package/core/tsconfig.json +1 -1
- package/core/tsconfig.spec.json +2 -26
- package/core/vitest.config.ts +14 -0
- package/documentation/1-purpose.md +114 -0
- package/documentation/2-getting-started.md +229 -0
- package/documentation/3-querying-and-changing-data.md +74 -0
- package/documentation/4-product-data.md +107 -0
- package/documentation/5-cart-and-checkout.md +211 -0
- package/documentation/6-product-search.md +143 -0
- package/documentation/7-marketing.md +3 -0
- package/eslint.config.mjs +1 -0
- package/examples/node/eslint.config.mjs +1 -4
- package/examples/node/package.json +10 -3
- package/examples/node/project.json +4 -1
- package/examples/node/src/basic/basic-node-provider-model-extension.spec.ts +22 -23
- package/examples/node/src/basic/basic-node-provider-query-extension.spec.ts +15 -11
- package/examples/node/src/basic/basic-node-setup.spec.ts +44 -28
- package/examples/node/src/basic/client-creation.spec.ts +53 -0
- package/examples/node/src/capabilities/cart.spec.ts +255 -0
- package/examples/node/src/capabilities/category.spec.ts +193 -0
- package/examples/node/src/capabilities/checkout.spec.ts +341 -0
- package/examples/node/src/capabilities/identity.spec.ts +93 -0
- package/examples/node/src/capabilities/inventory.spec.ts +66 -0
- package/examples/node/src/capabilities/order-search.spec.ts +265 -0
- package/examples/node/src/capabilities/order.spec.ts +91 -0
- package/examples/node/src/capabilities/price.spec.ts +51 -0
- package/examples/node/src/capabilities/product-search.spec.ts +293 -0
- package/examples/node/src/capabilities/product.spec.ts +122 -0
- package/examples/node/src/capabilities/profile.spec.ts +316 -0
- package/examples/node/src/capabilities/store.spec.ts +26 -0
- package/examples/node/src/utils.ts +147 -0
- package/examples/node/tsconfig.json +9 -12
- package/examples/node/tsconfig.lib.json +1 -2
- package/examples/node/tsconfig.spec.json +2 -14
- package/examples/node/vitest.config.ts +14 -0
- package/migrations.json +22 -5
- package/nx.json +8 -47
- package/package.json +24 -96
- package/providers/algolia/README.md +39 -2
- package/providers/algolia/package.json +2 -1
- package/providers/algolia/src/core/initialize.ts +7 -14
- package/providers/algolia/src/index.ts +2 -4
- package/providers/algolia/src/providers/index.ts +1 -0
- package/providers/algolia/src/providers/product-search.provider.ts +241 -0
- package/providers/algolia/src/schema/capabilities.schema.ts +2 -3
- package/providers/algolia/src/schema/index.ts +3 -0
- package/providers/algolia/src/schema/search.schema.ts +8 -8
- package/providers/algolia/tsconfig.json +1 -1
- package/providers/algolia/tsconfig.lib.json +1 -1
- package/providers/algolia/tsconfig.spec.json +2 -14
- package/providers/algolia/vitest.config.ts +14 -0
- package/providers/commercetools/README.md +30 -3
- package/providers/commercetools/package.json +2 -1
- package/providers/commercetools/src/core/client.ts +178 -99
- package/providers/commercetools/src/core/initialize.ts +130 -74
- package/providers/commercetools/src/core/token-cache.ts +45 -0
- package/providers/commercetools/src/index.ts +3 -2
- package/providers/commercetools/src/providers/cart.provider.ts +281 -341
- package/providers/commercetools/src/providers/category.provider.ts +223 -138
- package/providers/commercetools/src/providers/checkout.provider.ts +631 -449
- package/providers/commercetools/src/providers/identity.provider.ts +50 -29
- package/providers/commercetools/src/providers/index.ts +2 -2
- package/providers/commercetools/src/providers/inventory.provider.ts +76 -74
- package/providers/commercetools/src/providers/order-search.provider.ts +220 -0
- package/providers/commercetools/src/providers/order.provider.ts +96 -61
- package/providers/commercetools/src/providers/price.provider.ts +147 -117
- package/providers/commercetools/src/providers/product-search.provider.ts +528 -0
- package/providers/commercetools/src/providers/product.provider.ts +249 -74
- package/providers/commercetools/src/providers/profile.provider.ts +445 -28
- package/providers/commercetools/src/providers/store.provider.ts +54 -40
- package/providers/commercetools/src/schema/capabilities.schema.ts +3 -1
- package/providers/commercetools/src/schema/commercetools.schema.ts +17 -3
- package/providers/commercetools/src/schema/configuration.schema.ts +1 -0
- package/providers/commercetools/src/schema/session.schema.ts +7 -0
- package/providers/commercetools/src/test/caching.spec.ts +82 -0
- package/providers/commercetools/src/test/identity.spec.ts +109 -0
- package/providers/commercetools/src/test/test-utils.ts +21 -19
- package/providers/commercetools/tsconfig.json +1 -1
- package/providers/commercetools/tsconfig.lib.json +1 -1
- package/providers/commercetools/tsconfig.spec.json +2 -14
- package/providers/commercetools/vitest.config.ts +15 -0
- package/providers/fake/README.md +20 -4
- package/providers/fake/package.json +2 -1
- package/providers/fake/src/core/initialize.ts +47 -49
- package/providers/fake/src/providers/analytics.provider.ts +5 -7
- package/providers/fake/src/providers/cart.provider.ts +163 -92
- package/providers/fake/src/providers/category.provider.ts +78 -50
- package/providers/fake/src/providers/checkout.provider.ts +254 -0
- package/providers/fake/src/providers/identity.provider.ts +57 -65
- package/providers/fake/src/providers/index.ts +6 -2
- package/providers/fake/src/providers/inventory.provider.ts +40 -36
- package/providers/fake/src/providers/order-search.provider.ts +78 -0
- package/providers/fake/src/providers/order.provider.ts +106 -0
- package/providers/fake/src/providers/price.provider.ts +93 -41
- package/providers/fake/src/providers/product-search.provider.ts +206 -0
- package/providers/fake/src/providers/product.provider.ts +56 -41
- package/providers/fake/src/providers/profile.provider.ts +147 -0
- package/providers/fake/src/providers/store.provider.ts +30 -20
- package/providers/fake/src/schema/capabilities.schema.ts +5 -1
- package/providers/fake/src/test/cart.provider.spec.ts +59 -80
- package/providers/fake/src/test/category.provider.spec.ts +145 -87
- package/providers/fake/src/test/checkout.provider.spec.ts +222 -0
- package/providers/fake/src/test/order-search.provider.spec.ts +50 -0
- package/providers/fake/src/test/order.provider.spec.ts +44 -0
- package/providers/fake/src/test/price.provider.spec.ts +50 -45
- package/providers/fake/src/test/product.provider.spec.ts +15 -7
- package/providers/fake/src/test/profile.provider.spec.ts +167 -0
- package/providers/fake/tsconfig.json +1 -1
- package/providers/fake/tsconfig.lib.json +1 -1
- package/providers/fake/tsconfig.spec.json +2 -12
- package/providers/fake/vitest.config.ts +14 -0
- package/providers/medusa/README.md +30 -0
- package/providers/medusa/TESTING.md +98 -0
- package/providers/medusa/eslint.config.mjs +19 -0
- package/providers/medusa/package.json +22 -0
- package/providers/medusa/project.json +34 -0
- package/providers/medusa/src/core/client.ts +370 -0
- package/providers/medusa/src/core/initialize.ts +78 -0
- package/providers/medusa/src/index.ts +13 -0
- package/providers/medusa/src/providers/cart.provider.ts +575 -0
- package/providers/medusa/src/providers/category.provider.ts +247 -0
- package/providers/medusa/src/providers/checkout.provider.ts +636 -0
- package/providers/medusa/src/providers/identity.provider.ts +137 -0
- package/providers/medusa/src/providers/inventory.provider.ts +173 -0
- package/providers/medusa/src/providers/order-search.provider.ts +202 -0
- package/providers/medusa/src/providers/order.provider.ts +226 -0
- package/providers/medusa/src/providers/price.provider.ts +140 -0
- package/providers/medusa/src/providers/product-search.provider.ts +243 -0
- package/providers/medusa/src/providers/product.provider.ts +261 -0
- package/providers/medusa/src/providers/profile.provider.ts +392 -0
- package/providers/medusa/src/schema/capabilities.schema.ts +18 -0
- package/providers/medusa/src/schema/configuration.schema.ts +11 -0
- package/providers/medusa/src/schema/medusa.schema.ts +31 -0
- package/providers/medusa/src/test/cart.provider.spec.ts +240 -0
- package/providers/medusa/src/test/category.provider.spec.ts +231 -0
- package/providers/medusa/src/test/checkout.spec.ts +349 -0
- package/providers/medusa/src/test/identity.provider.spec.ts +122 -0
- package/providers/medusa/src/test/inventory.provider.spec.ts +88 -0
- package/providers/medusa/src/test/large-cart.provider.spec.ts +103 -0
- package/providers/medusa/src/test/price.provider.spec.ts +104 -0
- package/providers/medusa/src/test/product.provider.spec.ts +146 -0
- package/providers/medusa/src/test/search.provider.spec.ts +203 -0
- package/providers/medusa/src/test/test-utils.ts +13 -0
- package/providers/medusa/src/utils/medusa-helpers.ts +89 -0
- package/providers/medusa/tsconfig.json +21 -0
- package/providers/medusa/tsconfig.lib.json +9 -0
- package/providers/medusa/tsconfig.spec.json +4 -0
- package/providers/medusa/vitest.config.ts +15 -0
- package/providers/meilisearch/README.md +48 -0
- package/providers/meilisearch/eslint.config.mjs +22 -0
- package/providers/meilisearch/package.json +13 -0
- package/providers/meilisearch/project.json +34 -0
- package/providers/meilisearch/src/core/initialize.ts +21 -0
- package/providers/meilisearch/src/index.ts +6 -0
- package/providers/meilisearch/src/providers/index.ts +1 -0
- package/providers/meilisearch/src/providers/order-search.provider.ts +222 -0
- package/providers/meilisearch/src/providers/product-search.provider.ts +251 -0
- package/providers/meilisearch/src/schema/capabilities.schema.ts +10 -0
- package/providers/meilisearch/src/schema/configuration.schema.ts +11 -0
- package/providers/meilisearch/src/schema/index.ts +3 -0
- package/providers/meilisearch/src/schema/search.schema.ts +14 -0
- package/providers/meilisearch/tsconfig.json +24 -0
- package/providers/meilisearch/tsconfig.lib.json +10 -0
- package/providers/meilisearch/tsconfig.spec.json +4 -0
- package/providers/meilisearch/vitest.config.ts +14 -0
- package/providers/posthog/package.json +2 -1
- package/providers/posthog/tsconfig.json +1 -1
- package/tsconfig.base.json +5 -0
- package/vitest.config.ts +10 -0
- package/core/src/providers/search.provider.ts +0 -18
- package/core/src/schemas/models/search.model.ts +0 -36
- package/core/src/schemas/queries/search.query.ts +0 -9
- package/examples/next/.swcrc +0 -30
- package/examples/next/eslint.config.mjs +0 -21
- package/examples/next/index.d.ts +0 -6
- package/examples/next/next-env.d.ts +0 -5
- package/examples/next/next.config.js +0 -31
- package/examples/next/project.json +0 -9
- package/examples/next/public/.gitkeep +0 -0
- package/examples/next/public/favicon.ico +0 -0
- package/examples/next/src/app/global.css +0 -0
- package/examples/next/src/app/layout.tsx +0 -18
- package/examples/next/src/app/page.module.scss +0 -2
- package/examples/next/src/app/page.tsx +0 -47
- package/examples/next/src/instrumentation.ts +0 -9
- package/examples/next/tsconfig.json +0 -44
- package/examples/node/jest.config.ts +0 -10
- package/jest.config.ts +0 -6
- package/jest.preset.js +0 -3
- package/providers/algolia/jest.config.ts +0 -10
- package/providers/algolia/src/providers/product.provider.ts +0 -66
- package/providers/algolia/src/providers/search.provider.ts +0 -106
- package/providers/algolia/src/test/search.provider.spec.ts +0 -91
- package/providers/commercetools/jest.config.cjs +0 -10
- package/providers/commercetools/src/providers/search.provider.ts +0 -96
- package/providers/commercetools/src/test/cart.provider.spec.ts +0 -199
- package/providers/commercetools/src/test/category.provider.spec.ts +0 -168
- package/providers/commercetools/src/test/checkout.provider.spec.ts +0 -312
- package/providers/commercetools/src/test/identity.provider.spec.ts +0 -88
- package/providers/commercetools/src/test/inventory.provider.spec.ts +0 -41
- package/providers/commercetools/src/test/price.provider.spec.ts +0 -81
- package/providers/commercetools/src/test/product.provider.spec.ts +0 -80
- package/providers/commercetools/src/test/profile.provider.spec.ts +0 -49
- package/providers/commercetools/src/test/search.provider.spec.ts +0 -61
- package/providers/commercetools/src/test/store.provider.spec.ts +0 -37
- package/providers/fake/jest.config.cjs +0 -10
- package/providers/fake/src/providers/search.provider.ts +0 -132
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Reactionary
|
|
2
|
+
|
|
3
|
+
Reactionary is an oppinionated abstraction layer between UX developer and Backend developer in a Composable Commerce environment.
|
|
4
|
+
|
|
5
|
+
In a composable commerce project, you will have a bag of services that together provide all the functionality you need on the site.
|
|
6
|
+
And over time, you will probably decide to move from one such service, to another, if you want to, say, have better search, or better recommendations.
|
|
7
|
+
|
|
8
|
+
Reactionary creates a vendor agnostic domain model, and set of providers towards those vendors, that will allow UX developers to focus on their own craft, rather than both having to learn NextJS/Nuxt/Svelte/Htmx, in addition to the 4-6 vendor apis that go into the site.
|
|
9
|
+
|
|
10
|
+
To coin a term, we talk about a UX developer needing access to certain capabiltiies.
|
|
11
|
+
|
|
12
|
+
These can be things like
|
|
13
|
+
1. Cart
|
|
14
|
+
1. Proile
|
|
15
|
+
1. Product Data
|
|
16
|
+
1. Product Search
|
|
17
|
+
1. Order History
|
|
18
|
+
|
|
19
|
+
Reactionary is intended to live on the server side of your frontend. It uses the publically available clients for known and services, and is not trying to optimize for backend bundle size. Instead, we try to optimize for developer speed.
|
|
20
|
+
|
|
21
|
+
It is the intention, that Reactionary will serve as the lowest layer of communication between the UX developer and the associated vendors.
|
|
22
|
+
This means, you are not really expected to use reactionary directly from your components or widgets or pages.
|
|
23
|
+
|
|
24
|
+
*Rather, Reactionary belongs behind your state-management system of choice.*
|
|
25
|
+
|
|
26
|
+
For Angular, this might be a dependency injected service, that uses reactionary instead of HttpClient, or take place in a `resource`. For React, you might have a set of lib-functions to manage site state, and from there call Reactionary when needed. That lib would also host your site business logic. And for NextJS, it might be something triggered by React Query, or a Context.
|
|
27
|
+
|
|
28
|
+
For this reason, we say that Reactionary is oppinionated on what your domain model will look like, and how it should interact, but it is unopinionated as to how to you manage your state and business logic.
|
|
29
|
+
|
|
30
|
+
In this document, we show various examples, using a mix of UI frameworks, or pseudo-ui frameworks, so here we might show Reactionary a bit closer to the widgets and components that it really belongs. This is only for brewity.
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
## Audience
|
|
34
|
+
This document is for UX developers who need to know abit about how reactionary works, and how the expected usage pattern should be.
|
|
35
|
+
It is also for Backend developers, who need to customize the providers and domain models, to match the specific domain of your site.
|
|
36
|
+
|
|
37
|
+
## Goal
|
|
38
|
+
|
|
39
|
+
The goal is
|
|
40
|
+
- To support any Node based frontend framework (NextJS, Rapi, Angular Universal, etc)
|
|
41
|
+
- To provide the capabilities needed to build both B2C and B2B sites
|
|
42
|
+
- To make it easy and natural to specialize/customize both the domain model, and provider
|
|
43
|
+
- To provide cross transactional caching and cache management
|
|
44
|
+
- To provide open telemetry data for proper monitoring and fault detection
|
|
45
|
+
|
|
46
|
+
## Design criteria
|
|
47
|
+
Reactionary will be a stateless framework. It will not keep or maintain any state between calls.
|
|
48
|
+
The UX framework used for the presentation layer is responsible for storing and retrieving any session data that reactionary needs to operate.
|
|
49
|
+
|
|
50
|
+
This also means, reactionary does not do any client-state or UI-state management at all. In the Service => Provider => Factory setting, reactionary is the Provider and Factory layer.
|
|
51
|
+
|
|
52
|
+
Each capability will have certain behavioral criteria that the UX designer can rely on. This means, occasioanlly there will be mandatory backend customizations required if a vendors product does not fully fit.
|
|
53
|
+
|
|
54
|
+
Where this is noted, a reference implementation will be made available.
|
|
55
|
+
|
|
56
|
+
Runtime is targeting modern ESM/ES6 systems.
|
|
57
|
+
|
|
58
|
+
We strive to avoid unbounded requests. This means, we don't generally allow for nested lists of things of indeterminate size. Instead a paged getter will be available to fetch a page of sub-items.
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
## FAQ
|
|
62
|
+
|
|
63
|
+
### What do you mean by capability?
|
|
64
|
+
Consider the product search of a regular/standard ecommerce site. It has a text field. When you type in it, it fetches a list of products, and a list of facets, each with a count. User can page the result list (directly, or virtually through endless scroll) , and maybe choose to sort.
|
|
65
|
+
|
|
66
|
+
This basic capabilitys UX journey can be tweaked and designed for mobile, and foldable, and ipads, and desktop, and once visually there, this is not likely to change.
|
|
67
|
+
However, there is significant difference in how this would be done using Commercetools' product search, Algolias search, Klevu (now Athos Commerce), Constructor.io or Doofinder.
|
|
68
|
+
|
|
69
|
+
By creating a unifying domain model you can have your UX work for any of the mentioned vendors without changing anything, aside from the configuration of which vendor to use.
|
|
70
|
+
|
|
71
|
+
### But couldn't i just to that myself? In react i could just have a useHook and hide all the things in there?
|
|
72
|
+
Sure, and on your next project you could do it again.... and on the next one, again, only here you had a different team, so its slightly different.. and so on. And the 4th project uses Svelte, so the hooks from earlier need to be rewritten.
|
|
73
|
+
|
|
74
|
+
Reactionary is targeting being that abstraction, so you can have consistent behavior between projects and between teams.
|
|
75
|
+
|
|
76
|
+
### Why would you use this, over, say, just using the graphql endpoint of your ecommerce
|
|
77
|
+
In a composable commerce project, the ecommerce system is no longer automatically the master system. You might have a DXE strategy, or you might have any number of other systems involved. By using a bridge framework like reactionary, you break the vendor lock, and can later pick a new ecommerce platform, where the performance is better, and the promotions more closely match your requirements, without having to redo the entire frontend project.
|
|
78
|
+
|
|
79
|
+
### Why dont i just create my own graphql server then, and aggregate all the vendors i use?
|
|
80
|
+
GraphQL servers are hard to get right, when you consider security, performance, caching et al.
|
|
81
|
+
GraphQL is great, if your site is a client-side rendered application, that needs to run on the smallest of bandwidth, and you have the time to artisinally craft the perfect payload for each call. But as SSR and Partial SSR is becoming the new norm, and the precense of a BFF is now part of the equation in most commercial projects, you do not gain anything significant from having your backend save 40 bytes from fetching product data on your own graphql server right next to it.
|
|
82
|
+
The BFF->Client protocol is typically page related, or feature related. Not a straight API forwarding.
|
|
83
|
+
|
|
84
|
+
### My DXE/CMS allows merging/exposing other endpoints via their GraphQL, isnt that the same then?
|
|
85
|
+
Even if you disregard that the DXE still has to fetch the data from your vendor, and this means somehow propagating some authentication information to the CMS, the exposed API is still the original API, meaning your frontend developers still need to learn Algolia vs Klevu vs Doofinder.
|
|
86
|
+
|
|
87
|
+
### What do you mean about specializing the domain model?
|
|
88
|
+
Consider a product with a descriptive set of attributes for "StorageRequirementCode" .
|
|
89
|
+
It is used visually on the PDP to show the value of that attribute, but there is a special requirement that if a product iand requires cooling, it cannot be added to the cart, unless you have a special permit, or maybe it means you can't choose pickup-in-store, and also it must be flagged on every page the product is shown, to alert the end customer that he needs to be aware of this.
|
|
90
|
+
|
|
91
|
+
As attributes are optional, and multivalued, this means you end up with something like this:
|
|
92
|
+
```
|
|
93
|
+
public requireRefrigeration = computed(() => {
|
|
94
|
+
const code = this.product()?.descriptiveAttributes?.find(
|
|
95
|
+
(attr) => attr.identifier === 'StorageRequirementCode'
|
|
96
|
+
);
|
|
97
|
+
return !!(
|
|
98
|
+
code &&
|
|
99
|
+
code.values &&
|
|
100
|
+
code.values.length > 0 &&
|
|
101
|
+
code.values[0].value === 'KØL'
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
if you are lucky, or inlined directly in the `tsx` or `.html` of the component in every place this is used.
|
|
106
|
+
|
|
107
|
+
This gets even worse, if you consider you might also have commandline utilities that pull out products for sitemap generation, or other things.
|
|
108
|
+
|
|
109
|
+
Instead of doing this distributed all over the place, where things will break down once it turns out that they need to add an additional StorageRequirementCode to the check, these kinds of manifestations of the products domain should be moved onto the product itself.
|
|
110
|
+
|
|
111
|
+
Ie, extend the schema with `requireRefrigeration` as a field on the product, with the logic applied centrally where the data is loaded, rather than where it is used. That way, all instances where this is accessed, looks like `product.requireRefrigiration` instead of the other thing, which makes the code infinetly more readable and maintainable, since your reactionary customizations are reused and shared between applications.
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# Getting started
|
|
2
|
+
|
|
3
|
+
You can use Reactionary on any node-based project. It can be a NextJS BFF, or a commandline utility meant to create product-feeds for Google or OpenAI, or technically, in a ReactNative, or NativeScript application on your iPhone/Android app.
|
|
4
|
+
|
|
5
|
+
In these examples i use `pnpm`, but you can use `yarn` or `npm` as you see fit.
|
|
6
|
+
|
|
7
|
+
We assume you have a project ready, already...
|
|
8
|
+
|
|
9
|
+
## Vendors
|
|
10
|
+
For this example we assume you are using Commercetools as the ecom capability, and Algolia as the search provider. Both have free trial options, that can get you started.
|
|
11
|
+
So, go ahead and set up new free trials for both of those, and generate API Clients and tokens for both. For Commercetools, you can use the "Storefront" preset.
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
## Installing packages
|
|
16
|
+
|
|
17
|
+
For our example we need to install providers for Commercetools and Algolia both.
|
|
18
|
+
|
|
19
|
+
`pnpm install @reactionary/core @reactionary/provider-algolia @reactionary/provider-commercetools`
|
|
20
|
+
|
|
21
|
+
We will also prepare for open telemetry reporting to NewRelic (also offers free trial, so go ahead and set that up)
|
|
22
|
+
|
|
23
|
+
`pnpm install @reactionary/otel`
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
## Errors as values
|
|
28
|
+
We have decided to adopt the Errors-as-values paradigm seen in other server-side APIs, to encourage that all state returned, is inspected for error state, rather than simply allowing an unexpected thrown exception to alter the flow of things.
|
|
29
|
+
|
|
30
|
+
This means generally, that calls follow this structure:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
const cartResponse = await this.client.cart.getById({...});
|
|
34
|
+
if (cartResponse.success) {
|
|
35
|
+
console.log('my cart is', cartResponse.value)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!cartResponse.success) {
|
|
39
|
+
console.log('Cart was not loadable due to ', cartResponse.error);
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
This might seem verbose, over simply assuming the value is set when it gets back, but it encourages handling minor problems immediately, rather than starting out developing only the happy-path, and then promising yourself to circle back and deal with errors later.
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
## Bootstrapping the client
|
|
47
|
+
|
|
48
|
+
You use the `ClientBuilder` from `@reactionary/core` to create a new client, and then specify which capabilties you need. Only the capabilties you mention will be prepared for you.
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
const client = new ClientBuilder()
|
|
52
|
+
.withCapability(
|
|
53
|
+
withCommercetoolsCapabilities(getCommercetoolsConfig(),
|
|
54
|
+
{
|
|
55
|
+
product: true,
|
|
56
|
+
cart: true,
|
|
57
|
+
order: true
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
.withCapability(
|
|
61
|
+
withAlgoliaCapabilities(getAlgoliaConfig(), {
|
|
62
|
+
productSearch: true
|
|
63
|
+
})
|
|
64
|
+
)
|
|
65
|
+
.withCache(new NoOpCache())
|
|
66
|
+
.build();
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
with `getCommercetoolsConfig` being a function like
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
export function getCommercetoolsTestConfiguration() {
|
|
76
|
+
return CommercetoolsConfigurationSchema.parse({
|
|
77
|
+
apiUrl: process.env['CTP_API_URL'] || '',
|
|
78
|
+
authUrl: process.env['CTP_AUTH_URL'] || '',
|
|
79
|
+
clientId: process.env['CTP_CLIENT_ID'] || '',
|
|
80
|
+
clientSecret: process.env['CTP_CLIENT_SECRET'] || '',
|
|
81
|
+
projectKey: process.env['CTP_PROJECT_KEY'] || '',
|
|
82
|
+
scopes: (process.env['CTP_SCOPES'] || '').split(',').map(x => x.trim()).filter(x => x && x.length > 0),
|
|
83
|
+
|
|
84
|
+
paymentMethods: [
|
|
85
|
+
PaymentMethodSchema.parse({
|
|
86
|
+
identifier: PaymentMethodIdentifierSchema.parse({
|
|
87
|
+
paymentProvider: 'stripe',
|
|
88
|
+
method: 'stripe',
|
|
89
|
+
name: 'Stripe',
|
|
90
|
+
}),
|
|
91
|
+
description: 'Stripe payment gateway'
|
|
92
|
+
})
|
|
93
|
+
]
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
and `getAlgoliaConfig` being something like
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
export function getCommercetoolsTestConfiguration() {
|
|
103
|
+
return AlgoliaConfigurationSchema.parse({
|
|
104
|
+
apiKey: process.env['ALGOLIA_API_KEY'] || '',
|
|
105
|
+
appId: process.env['ALGOLIA_APP_ID'] || '',
|
|
106
|
+
indexName: process.env['ALGOLIA_INDEX'] || '',
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
and, then ofc, you need to provide all those values, in, say, a `.env` file (that you will not be adding to git)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
### Establishing state
|
|
115
|
+
Before we can start calling vendors, we need to set up the context in which the requests will take place. This is information that is describing who the customer is, what session data he has, some information about the current request, and so forth. All calls processed during the processing of one request from the frontend, share the same request data, so usually this can be established in some middleware function of your api-server.
|
|
116
|
+
|
|
117
|
+
The expected lifecycle of a client created here, is for the duration of this single request.
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
// we have to create a request context with all the information the underlying layer might need.
|
|
121
|
+
// We are responsible for the session object, and persisting it between requests.
|
|
122
|
+
// This would normally be in a middleware
|
|
123
|
+
|
|
124
|
+
// getSession here is a utility function from the frontend framework (nextjs,react-router,whatever) that stores your
|
|
125
|
+
// users session data between calls.
|
|
126
|
+
const session = await getSession(
|
|
127
|
+
request.headers.get("Cookie")
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
// from this we create a requestContext
|
|
131
|
+
|
|
132
|
+
const reqCtx = createInitialRequestContext();
|
|
133
|
+
|
|
134
|
+
// optionally, set any locale settings from route or browser (or set it fixed,if there is only one language).
|
|
135
|
+
reqCtx.languageContext = LanguageContextSchema.parse({
|
|
136
|
+
locale: 'en-US',
|
|
137
|
+
currency: 'USD'
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
reqCtx.session = JSON.parse(session.get('reactionarySession') || '{}');
|
|
141
|
+
|
|
142
|
+
// the reactionarySession object contains all the session data that reactionary controls.
|
|
143
|
+
// if it isn't there (new session? ) we create it
|
|
144
|
+
if (!session.has('reactionarySession')) {
|
|
145
|
+
session.set('reactionarySession', JSON.stringify({}));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// we capture some information from the request, like some interesting headers and optional correlation ids sent from the frontend for
|
|
149
|
+
// traceability
|
|
150
|
+
reqCtx.clientIp = request.headers.get('X-Forwarded-For') || request.headers.get('Remote-Addr') || '';
|
|
151
|
+
reqCtx.userAgent = request.headers.get('User-Agent') || '';
|
|
152
|
+
reqCtx.isBot = /bot|crawler|spider|crawling/i.test(reqCtx.userAgent || '');
|
|
153
|
+
reqCtx.referrer = request.headers.get('Referer') || '';
|
|
154
|
+
reqCtx.correlationId = request.headers.get('X-Correlation-ID') || 'remix-' + Math.random().toString(36).substring(2, 15);
|
|
155
|
+
|
|
156
|
+
// we now have all we need to set up a client, so we pass the request context to the client builder.
|
|
157
|
+
// inlined here for clarity. Usually you would put this in a utility function called something like createClient
|
|
158
|
+
const client = new ClientBuilder(reqCtx)
|
|
159
|
+
.withCapability(
|
|
160
|
+
withCommercetoolsCapabilities(getCommercetoolsConfig(),
|
|
161
|
+
{
|
|
162
|
+
product: true,
|
|
163
|
+
cart: true,
|
|
164
|
+
order: true
|
|
165
|
+
}
|
|
166
|
+
)
|
|
167
|
+
.withCapability(
|
|
168
|
+
withAlgoliaCapabilities(getAlgoliaConfig(), {
|
|
169
|
+
productSearch: true
|
|
170
|
+
})
|
|
171
|
+
)
|
|
172
|
+
.withCache(new NoOpCache())
|
|
173
|
+
.build();
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Then in your page/component/context
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
const meResponse = await client.identity.getSelf({});
|
|
180
|
+
if (meResponse.success) {
|
|
181
|
+
if (meResponse.value.type === 'Registered') {
|
|
182
|
+
// do something only for registered customers...
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Making calls to get data
|
|
188
|
+
Let us assume the page we are rendering wants to include some minicart information. Given the client we created above, you can now call
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
// first, check if we have a cart id registered on the session
|
|
192
|
+
let cartId = mySession.activeCartId;
|
|
193
|
+
|
|
194
|
+
// if not, lets see if the system might have it for us
|
|
195
|
+
if (!cartId) {
|
|
196
|
+
cartIdResponse = await client.cart.getActiveCartId();
|
|
197
|
+
if (cartIdResponse.success) {
|
|
198
|
+
cartId = cartIdResponse.value;
|
|
199
|
+
} else {
|
|
200
|
+
// we dont really care why it couldn't load. We just reset to a safe value
|
|
201
|
+
cartId = '';
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// no? then lets just zero it out, and start over.
|
|
207
|
+
if (!cartId) {
|
|
208
|
+
cartId = '';
|
|
209
|
+
}
|
|
210
|
+
const cartResponse = await client.cart.getById(cartId);
|
|
211
|
+
|
|
212
|
+
let totalSum = 0;
|
|
213
|
+
if (cartResponse.success) {
|
|
214
|
+
// store it for future reference
|
|
215
|
+
mySession.activeCartId = cartResponse.value.identifier.key;
|
|
216
|
+
totalSum = cartResponse.value.price.grandTotal.value;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
## Design decisions
|
|
225
|
+
We want Reactionary to be as unintrusive to the frontend frameworks best practice for state management. So we do not try to offer too many convenience methods that might slow down the site unnecessarily.
|
|
226
|
+
|
|
227
|
+
This is why we don't offer a `cart.getActiveCart()`, because in some situations identifying the active cart id, might require an extra call for each operation.
|
|
228
|
+
|
|
229
|
+
|
|
@@ -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.
|