@mercurjs/docs 2.3.8-canary.2 → 2.3.8-canary.4

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 (114) hide show
  1. package/content/learn/architecture.mdx +166 -263
  2. package/content/platform/attribute/concepts/attribute-types.mdx +73 -46
  3. package/content/platform/attribute/concepts/global-vs-inline.mdx +54 -32
  4. package/content/platform/attribute/concepts/variant-axes.mdx +54 -32
  5. package/content/platform/attribute/guides/attach-attributes-to-a-product.mdx +96 -53
  6. package/content/platform/attribute/guides/create-a-variant-axis.mdx +106 -57
  7. package/content/platform/attribute/guides/create-an-attribute.mdx +105 -56
  8. package/content/platform/attribute/overview.mdx +37 -33
  9. package/content/platform/attribute/reference/data-models.mdx +53 -38
  10. package/content/platform/attribute/reference/events.mdx +47 -24
  11. package/content/platform/attribute/reference/links.mdx +98 -27
  12. package/content/platform/attribute/reference/service.mdx +71 -32
  13. package/content/platform/attribute/reference/workflows.mdx +158 -28
  14. package/content/platform/catalog/concepts/master-products.mdx +39 -37
  15. package/content/platform/catalog/concepts/product-seller-allowlist.mdx +56 -35
  16. package/content/platform/catalog/concepts/status-lifecycle.mdx +55 -49
  17. package/content/platform/catalog/concepts/variants-categories-collections.mdx +55 -35
  18. package/content/platform/catalog/guides/allowlist-stores.mdx +100 -37
  19. package/content/platform/catalog/guides/create-a-master-product.mdx +124 -48
  20. package/content/platform/catalog/guides/publish-or-reject-a-product.mdx +120 -55
  21. package/content/platform/catalog/overview.mdx +39 -35
  22. package/content/platform/catalog/reference/data-models.mdx +154 -43
  23. package/content/platform/catalog/reference/events.mdx +41 -25
  24. package/content/platform/catalog/reference/links.mdx +196 -28
  25. package/content/platform/catalog/reference/service.mdx +57 -35
  26. package/content/platform/catalog/reference/workflows.mdx +124 -30
  27. package/content/platform/commission/concepts/order-commission-lines.mdx +54 -52
  28. package/content/platform/commission/concepts/rule-matching.mdx +53 -63
  29. package/content/platform/commission/concepts/rules-and-rates.mdx +61 -59
  30. package/content/platform/commission/guides/batch-update-rules.mdx +69 -41
  31. package/content/platform/commission/guides/create-a-rate.mdx +101 -53
  32. package/content/platform/commission/guides/refresh-order-commission-lines.mdx +104 -35
  33. package/content/platform/commission/overview.mdx +43 -35
  34. package/content/platform/commission/reference/data-models.mdx +59 -45
  35. package/content/platform/commission/reference/events.mdx +28 -34
  36. package/content/platform/commission/reference/links.mdx +42 -33
  37. package/content/platform/commission/reference/service.mdx +103 -28
  38. package/content/platform/commission/reference/workflows.mdx +101 -27
  39. package/content/platform/offer/concepts/pricing-and-inventory.mdx +63 -49
  40. package/content/platform/offer/concepts/shipping.mdx +28 -29
  41. package/content/platform/offer/concepts/what-is-an-offer.mdx +48 -45
  42. package/content/platform/offer/guides/bulk-create-offers.mdx +97 -61
  43. package/content/platform/offer/guides/create-an-offer.mdx +112 -60
  44. package/content/platform/offer/guides/manage-offer-inventory.mdx +78 -38
  45. package/content/platform/offer/overview.mdx +39 -32
  46. package/content/platform/offer/reference/data-models.mdx +77 -47
  47. package/content/platform/offer/reference/events.mdx +44 -18
  48. package/content/platform/offer/reference/links.mdx +92 -40
  49. package/content/platform/offer/reference/service.mdx +66 -31
  50. package/content/platform/offer/reference/workflows.mdx +146 -37
  51. package/content/platform/order-group/concepts/computed-totals.mdx +40 -37
  52. package/content/platform/order-group/concepts/order-splitting.mdx +41 -39
  53. package/content/platform/order-group/concepts/the-order-group.mdx +39 -32
  54. package/content/platform/order-group/guides/list-order-groups.mdx +96 -56
  55. package/content/platform/order-group/guides/retrieve-an-order-group.mdx +93 -42
  56. package/content/platform/order-group/guides/split-a-cart.mdx +115 -40
  57. package/content/platform/order-group/overview.mdx +42 -32
  58. package/content/platform/order-group/reference/data-models.mdx +48 -31
  59. package/content/platform/order-group/reference/events.mdx +29 -17
  60. package/content/platform/order-group/reference/links.mdx +90 -29
  61. package/content/platform/order-group/reference/service.mdx +55 -31
  62. package/content/platform/order-group/reference/workflows.mdx +83 -24
  63. package/content/platform/payout/concepts/account-lifecycle.mdx +64 -67
  64. package/content/platform/payout/concepts/accounts-and-onboarding.mdx +50 -40
  65. package/content/platform/payout/concepts/payout-pipeline.mdx +70 -73
  66. package/content/platform/payout/guides/create-a-payout-account.mdx +65 -41
  67. package/content/platform/payout/guides/process-a-provider-webhook.mdx +84 -38
  68. package/content/platform/payout/guides/start-provider-onboarding.mdx +90 -46
  69. package/content/platform/payout/overview.mdx +43 -34
  70. package/content/platform/payout/reference/data-models.mdx +63 -36
  71. package/content/platform/payout/reference/events.mdx +56 -30
  72. package/content/platform/payout/reference/links.mdx +52 -22
  73. package/content/platform/payout/reference/service.mdx +72 -33
  74. package/content/platform/payout/reference/workflows.mdx +80 -19
  75. package/content/platform/product-edit/concepts/change-actions.mdx +52 -45
  76. package/content/platform/product-edit/concepts/change-pipeline.mdx +60 -52
  77. package/content/platform/product-edit/concepts/status-and-auto-confirm.mdx +43 -46
  78. package/content/platform/product-edit/guides/confirm-or-decline-a-change.mdx +104 -56
  79. package/content/platform/product-edit/guides/edit-a-product.mdx +126 -46
  80. package/content/platform/product-edit/guides/request-a-revision.mdx +99 -39
  81. package/content/platform/product-edit/overview.mdx +41 -34
  82. package/content/platform/product-edit/reference/data-models.mdx +73 -47
  83. package/content/platform/product-edit/reference/events.mdx +39 -29
  84. package/content/platform/product-edit/reference/links.mdx +32 -24
  85. package/content/platform/product-edit/reference/service.mdx +45 -26
  86. package/content/platform/product-edit/reference/workflows.mdx +173 -31
  87. package/content/platform/review/concepts/product-vs-seller-reviews.mdx +42 -41
  88. package/content/platform/review/concepts/ratings-and-moderation.mdx +45 -44
  89. package/content/platform/review/concepts/the-review-model.mdx +52 -45
  90. package/content/platform/review/guides/compute-aggregate-ratings.mdx +62 -37
  91. package/content/platform/review/guides/create-a-review.mdx +109 -39
  92. package/content/platform/review/guides/moderate-a-review.mdx +83 -37
  93. package/content/platform/review/guides/respond-to-a-review.mdx +85 -40
  94. package/content/platform/review/overview.mdx +43 -33
  95. package/content/platform/review/reference/data-models.mdx +41 -27
  96. package/content/platform/review/reference/events.mdx +55 -37
  97. package/content/platform/review/reference/links.mdx +80 -22
  98. package/content/platform/review/reference/service.mdx +61 -33
  99. package/content/platform/review/reference/workflows.mdx +80 -24
  100. package/content/platform/store/concepts/lifecycle.mdx +68 -44
  101. package/content/platform/store/concepts/store-entity.mdx +45 -27
  102. package/content/platform/store/concepts/team.mdx +56 -35
  103. package/content/platform/store/guides/create-a-store.mdx +102 -35
  104. package/content/platform/store/guides/manage-the-team.mdx +122 -32
  105. package/content/platform/store/guides/moderate-a-store.mdx +107 -38
  106. package/content/platform/store/overview.mdx +41 -34
  107. package/content/platform/store/reference/data-models.mdx +132 -57
  108. package/content/platform/store/reference/events.mdx +42 -17
  109. package/content/platform/store/reference/links.mdx +67 -49
  110. package/content/platform/store/reference/service.mdx +105 -33
  111. package/content/platform/store/reference/workflows.mdx +304 -24
  112. package/content/references/panel-extensions/layouts.mdx +169 -0
  113. package/llms.txt +112 -111
  114. package/package.json +1 -1
@@ -1,59 +1,86 @@
1
1
  ---
2
- title: "Attribute types"
2
+ title: "Attribute types and values"
3
3
  sidebarTitle: "Attribute types"
4
- description: "The five attribute types and the values they hold."
4
+ description: "The five attribute types, how each one stores its values, and the flags every attribute carries."
5
5
  ---
6
6
 
7
- In this document, you'll learn about the attribute record, its five types, and
8
- the values attached to it.
9
-
10
- ## Product attribute
11
-
12
- An attribute is a typed field in the shared catalog, represented by the
13
- `ProductAttribute` data model (table `product_attribute`, id prefix `pattr`).
14
- Each attribute carries a `name`, an optional `handle`, a `rank` for ordering, and
15
- a `type` that decides how its values are validated and rendered.
16
-
17
- ```ts
18
- const { result } = await createProductAttributesWorkflow(container).run({
19
- input: {
20
- attributes: [
21
- {
22
- name: "Material",
23
- type: "single_select",
24
- values: [{ name: "Cotton" }, { name: "Wool" }],
25
- },
26
- ],
27
- },
28
- })
7
+ An attribute is a typed field in the shared catalog, and its type decides what a product can hold for it: a choice from a list, free text, a measurement, or a yes/no flag. Picking the right type matters because it fixes how values are stored and linked, and you can’t change it once the attribute exists.
8
+
9
+ ## The attribute record
10
+
11
+ Every attribute is a `ProductAttribute` record (id prefix `pattr`) with a `name`, a `type`, and a few flags. Its selectable choices are separate `ProductAttributeValue` records (id prefix `pattrval`) that belong to it and are deleted with it.
12
+
13
+ ```ts title="src/scripts/seed-attributes.ts"
14
+ import type { ExecArgs } from "@medusajs/framework/types"
15
+ import { AttributeType } from "@mercurjs/types"
16
+ import { createProductAttributesWorkflow } from "@mercurjs/core/workflows"
17
+
18
+ export default async function seedAttributes({ container }: ExecArgs) {
19
+ await createProductAttributesWorkflow(container).run({
20
+ input: {
21
+ attributes: [
22
+ {
23
+ name: "Material",
24
+ type: AttributeType.SINGLE_SELECT,
25
+ values: [{ name: "Cotton" }, { name: "Linen" }],
26
+ },
27
+ ],
28
+ },
29
+ })
30
+ }
29
31
  ```
30
32
 
31
- The `type` field is one of the `AttributeType` enum values:
33
+ The script creates one global `single_select` attribute with two values. Because no `handle` is passed, the module derives `material` from the name.
32
34
 
33
- | Type | Value | Holds |
34
- | --- | --- | --- |
35
- | Single select | `single_select` | One choice from a fixed list of values |
36
- | Multi select | `multi_select` | Several choices from a fixed list of values |
37
- | Text | `text` | A free-form string |
38
- | Unit | `unit` | A numeric measurement (e.g. weight, capacity) |
39
- | Toggle | `toggle` | A boolean, backed by seeded `true` / `false` values |
35
+ ## The five types
36
+
37
+ The `type` field takes one of the `AttributeType` enum values. Each type stores what a product holds in a different way:
38
+
39
+ | Type | Enum | A product holds | Where the value comes from |
40
+ | --- | --- | --- | --- |
41
+ | Single select | `single_select` | One choice from a fixed list | Values you create on the attribute |
42
+ | Multi select | `multi_select` | Several choices from a fixed list | Values you create on the attribute |
43
+ | Text | `text` | A free-form string | A new value created when you attach it |
44
+ | Unit | `unit` | A measurement, such as `400` | A new value created when you attach it |
45
+ | Toggle | `toggle` | `true` or `false` | The two values seeded when the attribute is created |
46
+
47
+ Only a `multi_select` attribute can be a [variant axis](/platform/attribute/concepts/variant-axes). The other four types describe a product but never shape its variants.
48
+
49
+ ## How values link to products
40
50
 
41
- ## Product attribute value
51
+ A product doesn’t store attribute data on itself. Instead, each value it holds is linked to it through the product to value pivot link, and the shape of that link depends on the type:
42
52
 
43
- The choices for select-style attributes are `ProductAttributeValue` records
44
- (table `product_attribute_value`, id prefix `pattrval`). Each value belongs to
45
- one attribute (`attribute_id`), has its own `name`, `handle`, and `rank`, and is
46
- deleted along with its parent attribute.
53
+ - **Select types**: the product links to existing values you pick by id
54
+ - **Text and unit**: attaching creates a new `ProductAttributeValue` named after the entered content, for example `"400"`, and links it
55
+ - **Toggle**: the product links to the seeded `"true"` or `"false"` value, and no new value is ever created
47
56
 
48
57
  <Note>
49
- `single_select` and `multi_select` attributes hold a list of predefined
50
- `ProductAttributeValue` records. `text` and `unit` create a value on the fly
51
- from the entered content when attached to a product. `toggle` is seeded with
52
- its `true` / `false` values and never creates new ones.
58
+ `unit` stores the number as the value’s `name` string. The attribute has no separate unit-of-measure field, so put the unit in the attribute name, for example “Weight (kg)”.
53
59
  </Note>
54
60
 
55
- <Tip>
56
- Use `is_required` to enforce that a product must carry a value for the
57
- attribute, and `is_active` to retire an attribute from new use without deleting
58
- its history.
59
- </Tip>
61
+ ## Rules the module enforces
62
+
63
+ The Product Attribute service adds a few rules on top of the generated CRUD methods:
64
+
65
+ - **Type is fixed**: `updateProductAttributes` throws when you try to change the `type` of an existing attribute
66
+ - **Handles are derived**: a global attribute without a `handle` gets one from its name, and global select values get one from theirs
67
+ - **Handles are unique**: attribute handles are unique across the catalog, and value handles are unique per attribute
68
+ - **Toggle values are seeded**: creating a `toggle` attribute adds its `true` (rank 0) and `false` (rank 1) values
69
+
70
+ ## Flags on every attribute
71
+
72
+ Each attribute carries flags that control where it shows up and how it’s treated:
73
+
74
+ | Field | Default | Effect |
75
+ | --- | --- | --- |
76
+ | `is_required` | `false` | The attachment workflows refuse to remove the attribute from a product |
77
+ | `is_filterable` | `false` | Marks the attribute as a storefront facet |
78
+ | `is_variant_axis` | `false` | Makes a `multi_select` attribute generate a `ProductOption` |
79
+ | `is_active` | `true` | A status flag you can filter attribute lists by |
80
+ | `rank` | `0` | Sort order for attributes and for values within an attribute |
81
+
82
+ ## Related
83
+
84
+ - [Variant axes and Medusa product options](/platform/attribute/concepts/variant-axes)
85
+ - [Global and inline attributes](/platform/attribute/concepts/global-vs-inline)
86
+ - [Every field on the data models](/platform/attribute/reference/data-models)
@@ -1,50 +1,72 @@
1
1
  ---
2
- title: "Global vs inline & filtering"
2
+ title: "Global and inline attributes"
3
3
  sidebarTitle: "Global vs inline"
4
- description: "Catalog-wide attributes, product-scoped attributes, and storefront filtering."
4
+ description: "Catalog-wide attributes, attributes scoped to one product, category scoping, and storefront filters."
5
5
  ---
6
6
 
7
- In this document, you'll learn the difference between global and inline
8
- attributes and how an attribute becomes a storefront filter.
7
+ Every attribute is either global, a catalog entry any product can reuse, or inline, a one-off field created on a single product. Global attributes keep the catalog consistent across stores. Inline attributes cover the odd field one product needs without adding noise to the shared vocabulary.
9
8
 
10
- ## Scope
9
+ ## Scope is decided by product_id
11
10
 
12
- Every `ProductAttribute` is either **global** or **inline**, decided by its
13
- `product_id` field:
11
+ The `product_id` field on `ProductAttribute` decides the scope. It’s `null` for a global attribute and holds the product’s id for an inline one:
14
12
 
15
- | Scope | `product_id` | Meaning |
16
- | --- | --- | --- |
17
- | Global | `null` | A catalog entry, reusable across any product, listed in the operator's attribute catalog |
18
- | Inline | set | A one-off attribute scoped to a single product, created from that product's form and hidden from the global catalog |
13
+ | Scope | `product_id` | Created by | Listed in the catalog |
14
+ | --- | --- | --- | --- |
15
+ | Global | `null` | `createProductAttributesWorkflow` | Yes |
16
+ | Inline | `prod_…` | An inline entry in a product’s `add` list | No |
17
+
18
+ The admin, vendor, and store attribute list routes all filter on `product_id: null`, so inline attributes never appear in the catalog. You read them from the product through the `scoped_attributes` link instead.
19
19
 
20
20
  ```ts
21
- // Global: reusable across the catalog
22
- { name: "Material", type: "single_select", values: [{ name: "Cotton" }] }
21
+ const { data: [product] } = await query.graph({
22
+ entity: "product",
23
+ fields: ["id", "scoped_attributes.name", "scoped_attributes.values.name"],
24
+ filters: { id: "prod_01JD8Q1234567" },
25
+ })
26
+ ```
27
+
28
+ ## Create an inline attribute
29
+
30
+ You create an inline attribute while you attach it. An `add` entry with a `title` instead of an `id` creates a product-scoped attribute, its values, and the links in one step:
23
31
 
24
- // Inline: scoped to one product, created as it's attached
25
- { title: "Gift wrap", type: "toggle", product_id: "prod_123" }
32
+ ```ts
33
+ await createAndLinkProductAttributesToProductWorkflow(container).run({
34
+ input: {
35
+ product_id: "prod_01JD8Q1234567",
36
+ add: [{ title: "Gift wrap", value: true }],
37
+ },
38
+ })
26
39
  ```
27
40
 
28
- A global attribute is defined once and attached to many products. An inline
29
- attribute is created in the same step it is attached and only ever describes that
30
- one product. It is useful for the occasional one-off field that doesn't belong in
31
- the shared vocabulary.
41
+ When you omit `type`, the workflow infers it: `is_variant_axis: true` makes a `multi_select` axis, a boolean `value` makes a `toggle`, and anything else makes a `text` attribute. Inline attributes get no derived handle, so their names never clash with the catalog.
42
+
43
+ The two scopes behave differently over their life:
44
+
45
+ - **Rename**: an `update` entry with a `title` renames an inline attribute and, for an axis, its exclusive option, but it’s ignored for a global attribute
46
+ - **Remove**: removing an inline attribute from its product deletes the attribute and its values, while removing a global one drops the product’s value links and, for an axis, detaches the option
47
+ - **Delete**: `deleteProductAttributesWorkflow` refuses to delete a global attribute while any of its values are linked to a product
32
48
 
33
- <Note>
34
- Attributes describe products in the **shared master catalog**. They are never
35
- owned by a store. A seller sells against a master product through an offer, and
36
- the product's attributes come from the catalog, not from the seller.
37
- </Note>
49
+ ## Scope global attributes to categories
38
50
 
39
- ## Filtering
51
+ A global attribute can link to product categories through the `categories` link. The admin and vendor attribute lists accept a `product_category_id` filter and return two groups: attributes linked to that category, and attributes linked to no category at all. Category links change where an attribute is offered:
40
52
 
41
- An attribute with `is_filterable` set to `true` is exposed as a storefront
42
- filter, letting shoppers narrow the catalog by its values. Leave it `false` for
43
- descriptive-only attributes that shouldn't appear as facets.
53
+ - **No categories**: the attribute is offered for every product
54
+ - **One or more categories**: the attribute is offered only for products in those categories
55
+
56
+ Pass `category_ids` to `createProductAttributesWorkflow` to create the links with the attribute.
57
+
58
+ ## Storefront filters
59
+
60
+ Shoppers filter `GET /store/products` with `attributes[<handle>]=value` query parameters. The store middleware turns each one into a filter on `variants.options.value`, so filtering works on variant axis values. Several values for one attribute are OR’d, and separate attributes are AND’d.
61
+
62
+ `is_filterable` doesn’t change that query. It’s a flag your storefront reads from the store attribute list to decide which attributes to render as facets.
44
63
 
45
64
  <Tip>
46
- Filtering pairs naturally with variant axes: a filterable `multi_select` axis
47
- like Color both generates variants and lets shoppers filter by them. A
48
- descriptive `text` attribute like Care instructions is usually left
49
- non-filterable.
65
+ Mark axes such as Size and Color as filterable. Descriptive `text` attributes such as care instructions aren’t variant options, so the product filter can’t match them.
50
66
  </Tip>
67
+
68
+ ## Related
69
+
70
+ - [Attach attributes to a product](/platform/attribute/guides/attach-attributes-to-a-product)
71
+ - [Variant axes and Medusa product options](/platform/attribute/concepts/variant-axes)
72
+ - [The scoped_attributes and categories links](/platform/attribute/reference/links)
@@ -1,20 +1,24 @@
1
1
  ---
2
- title: "Variant axes"
2
+ title: "Variant axes and product options"
3
3
  sidebarTitle: "Variant axes"
4
- description: "How is_variant_axis mirrors a native ProductOption and generates variants."
4
+ description: "How a multi_select axis attribute mirrors a native Medusa ProductOption, and how products pick a subset of its values."
5
5
  ---
6
6
 
7
- In this document, you'll learn how an attribute becomes the axis a product's
8
- variants are generated from, and the mirror links that keep the two in sync.
7
+ A variant axis is a `multi_select` attribute that a product varies along, such as Size or Color. Mercur doesn’t invent its own variant system: each axis is backed by a native Medusa `ProductOption`, so variants, carts, and orders keep working the way Medusa expects while operators manage sizes and colors as catalog attributes.
9
8
 
10
- ## Variant-axis attribute
9
+ <img className="block dark:hidden" src="/images/figures/attribute-variant-axis-light.svg" alt="The Size axis attribute maps each value to a Medusa option value, S to optval_01JD8Q1 and so on, and the Linen shirt selects only S and M, so its variants behind it are S and M." />
10
+ <img className="hidden dark:block" src="/images/figures/attribute-variant-axis-dark.svg" alt="The Size axis attribute maps each value to a Medusa option value, S to optval_01JD8Q1 and so on, and the Linen shirt selects only S and M, so its variants behind it are S and M." />
11
11
 
12
- A `multi_select` attribute marked `is_variant_axis` is more than a descriptor.
13
- It defines a dimension along which a product varies, such as Size or Color. When
14
- such an attribute is created, the `ProductAttribute` model records the id of a native
15
- Medusa `ProductOption` in its `product_option_id` field, and each of its
16
- `ProductAttributeValue`s records the matching `ProductOptionValue` id in
17
- `product_option_value_id`.
12
+ ## The option mirror
13
+
14
+ When you create a `multi_select` attribute with `is_variant_axis: true`, `createProductAttributesWorkflow` first creates a Medusa `ProductOption` (id prefix `opt`) titled with the attribute’s name. It then stores the option’s id on the attribute and each option value’s id on the matching attribute value.
15
+
16
+ | Attribute side | Field | Medusa side |
17
+ | --- | --- | --- |
18
+ | `ProductAttribute` | `product_option_id` | `ProductOption` |
19
+ | `ProductAttributeValue` | `product_option_value_id` | `ProductOptionValue` (id prefix `optval`) |
20
+
21
+ The workflows keep the two sides in step. Renaming the attribute renames the option, and adding, renaming, or deleting values rewrites the option’s value list. Deleting the attribute deletes the option.
18
22
 
19
23
  ```ts
20
24
  await createProductAttributesWorkflow(container).run({
@@ -22,7 +26,7 @@ await createProductAttributesWorkflow(container).run({
22
26
  attributes: [
23
27
  {
24
28
  name: "Size",
25
- type: "multi_select",
29
+ type: AttributeType.MULTI_SELECT,
26
30
  is_variant_axis: true,
27
31
  values: [{ name: "S" }, { name: "M" }, { name: "L" }],
28
32
  },
@@ -31,31 +35,49 @@ await createProductAttributesWorkflow(container).run({
31
35
  })
32
36
  ```
33
37
 
34
- Because the attribute mirrors a real `ProductOption`, the values a product
35
- selects along that axis are exactly what Medusa uses to generate its variants.
38
+ This run creates the `Size` attribute, a global `Size` option with values `S`, `M`, and `L`, and three attribute values pointing at those option values.
36
39
 
37
- ## Mirror links
40
+ ## Global options and exclusive options
38
41
 
39
- The attribute catalog and Medusa's product options are two separate modules, so
40
- the relationship is kept as a pair of **read-only mirror links**:
42
+ Since Medusa 2.16, a product option can be shared by several products, and products attach to it. Mercur uses both kinds of option Medusa offers:
41
43
 
42
- | Mirror | FK on the attribute side | Points to |
43
- | --- | --- | --- |
44
- | Attribute → option | `ProductAttribute.product_option_id` | `ProductOption` |
45
- | Value → option value | `ProductAttributeValue.product_option_value_id` | `ProductOptionValue` |
44
+ | Axis | Option created | `is_exclusive` | Shared by |
45
+ | --- | --- | --- | --- |
46
+ | Global axis attribute | When the attribute is created | `false` | Every product that attaches the axis |
47
+ | Inline axis attribute | When it’s added to one product | `true` | Only that product |
48
+
49
+ Medusa keeps titles of non-exclusive options unique, so two global axis attributes can’t share a name. Inline axes use exclusive options, so two products can each have their own “Cut” axis.
50
+
51
+ ## A product picks a subset of values
52
+
53
+ A global axis holds every value the marketplace supports, but one product rarely uses all of them. When you attach the axis with `value_ids`, the workflow links the option to the product with only the matching option values, and it also links those attribute values to the product through the pivot link.
54
+
55
+ ```ts
56
+ await createAndLinkProductAttributesToProductWorkflow(container).run({
57
+ input: {
58
+ product_id: "prod_01JD8Q1234567",
59
+ add: [
60
+ {
61
+ id: "pattr_01JD8Q1234567",
62
+ value_ids: ["pattrval_01JD8Q1234567", "pattrval_01JD8Q1234568"],
63
+ },
64
+ ],
65
+ },
66
+ })
67
+ ```
68
+
69
+ The product now has the `Size` option restricted to two values. You change the subset later with an `update` entry. The workflow moves the option values and the pivot links together.
46
70
 
47
- Both are 1:1 and have no pivot table. The foreign key lives on the attribute
48
- record itself.
71
+ ## Variants bind to the option by title
72
+
73
+ Attaching an axis doesn’t create variants. You create variants yourself, and each one names its option values by option title, for example `options: { Size: "M" }`. Mercur’s `createProductsWorkflow` takes `attributes` and `variants` together, attaches the axes first, and then creates the variants against them.
49
74
 
50
75
  <Note>
51
- The mirror links are **read-only**. They're resolved from the FK on the
52
- attribute record; you never write the relationship through the link. The
53
- workflows keep the option and the attribute in step whenever an axis attribute
54
- or its values change.
76
+ A product with no axis still needs an option for Medusa to accept variants. `createProductsWorkflow` gives every new product a placeholder `__default__` option and removes it once a real axis is attached.
55
77
  </Note>
56
78
 
57
- <Tip>
58
- Only `multi_select` attributes can be variant axes. The other four types
59
- (`single_select`, `text`, `unit`, `toggle`) describe a product but never
60
- generate variants.
61
- </Tip>
79
+ ## Related
80
+
81
+ - [Create a variant axis and build variants on it](/platform/attribute/guides/create-a-variant-axis)
82
+ - [The read-only mirror links](/platform/attribute/reference/links)
83
+ - [Global and inline attributes](/platform/attribute/concepts/global-vs-inline)
@@ -1,69 +1,112 @@
1
1
  ---
2
2
  title: "Attach attributes to a product"
3
3
  sidebarTitle: "Attach to a product"
4
- description: "Attach, detach, and update a product's attributes in one batch call."
4
+ description: "Attach, detach, and update a product's attributes in one run of the batch workflow."
5
5
  ---
6
6
 
7
- In this guide, you'll learn how to manage all of a product's attributes from
8
- server code through a single batch workflow.
7
+ Manage every attribute on a product from server code with one workflow, `createAndLinkProductAttributesToProductWorkflow`, the same engine behind the admin attribute batch route.
9
8
 
10
- Mercur exposes `createAndLinkProductAttributesToProductWorkflow`, the engine
11
- behind the product attribute batch endpoint. One call can attach new attributes,
12
- detach existing ones, and update selections, applied in the order
13
- **remove → add → update** so a same-call remove and re-add of one attribute
14
- resolves correctly.
9
+ ## Before you begin
10
+
11
+ You need the following:
12
+
13
+ - **A product**: the id of a product in the shared catalog, such as `prod_01JD8Q1234567`
14
+ - **Global attributes**: the ids of the catalog attributes and values you want to attach, created as in [Create a catalog attribute](/platform/attribute/guides/create-an-attribute)
15
15
 
16
16
  ## Run the batch workflow
17
17
 
18
- ```ts title="src/api/custom/products/[id]/attributes/route.ts"
19
- import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
20
- import { createAndLinkProductAttributesToProductWorkflow } from "@mercurjs/core/workflows"
21
-
22
- export async function POST(req: MedusaRequest, res: MedusaResponse) {
23
- await createAndLinkProductAttributesToProductWorkflow(req.scope).run({
24
- input: {
25
- product_id: req.params.id,
26
- add: [
27
- // Existing select attribute: link chosen values
28
- { id: "pattr_material", value_ids: ["pattrval_cotton"] },
29
- // Existing text / unit / toggle attribute: set a scalar
30
- { id: "pattr_thread_count", value: 400 },
31
- // Inline attribute created and attached in one step
32
- { title: "Gift wrap", type: "toggle", value: true },
18
+ <Steps>
19
+ <Step title="Send add, remove, and update in one input">
20
+ The workflow applies `remove`, then `add`, then `update`, so removing and re-adding the same attribute in one run works.
21
+
22
+ ```ts title="src/api/admin/catalog-import/products/[id]/route.ts"
23
+ import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
24
+ import {
25
+ createAndLinkProductAttributesToProductWorkflow,
26
+ } from "@mercurjs/core/workflows"
27
+
28
+ export async function POST(req: MedusaRequest, res: MedusaResponse) {
29
+ await createAndLinkProductAttributesToProductWorkflow(req.scope).run({
30
+ input: {
31
+ product_id: req.params.id,
32
+ add: [
33
+ { id: "pattr_01JD8Q1234561", value_ids: ["pattrval_01JD8Q1234561"] },
34
+ { id: "pattr_01JD8Q1234562", value: 400 },
35
+ { title: "Gift wrap", value: true },
36
+ ],
37
+ remove: ["pattr_01JD8Q1234563"],
38
+ },
39
+ })
40
+
41
+ res.sendStatus(200)
42
+ }
43
+ ```
44
+
45
+ The first entry picks a value of a select attribute, the second sets a `unit` attribute to `400`, and the third creates an inline `toggle` attribute on this product.
46
+ </Step>
47
+ <Step title="Read the attributes back">
48
+ A product holds its selections as linked values. Read them through the `product_attribute_values` link, and read inline attributes through `scoped_attributes`.
49
+
50
+ ```ts
51
+ const query = req.scope.resolve("query")
52
+
53
+ const { data: [product] } = await query.graph({
54
+ entity: "product",
55
+ fields: [
56
+ "product_attribute_values.name",
57
+ "product_attribute_values.attribute.name",
58
+ "scoped_attributes.name",
33
59
  ],
34
- remove: ["pattr_legacy_field"],
35
- update: [{ id: "pattr_color", add: ["pattrval_blue"], remove: ["pattrval_red"] }],
36
- },
37
- })
60
+ filters: { id: req.params.id },
61
+ })
62
+ ```
38
63
 
39
- res.sendStatus(200)
40
- }
41
- ```
64
+ Inline attributes also show up under `product_attribute_values`, because their values link to the product the same way.
65
+ </Step>
66
+ </Steps>
42
67
 
43
- ## The three operations
68
+ ## Choose the add form
44
69
 
45
- Each entry in `add` is one of the `ProductAttributeBatchAdd` forms:
70
+ Each `add` entry is a `ProductAttributeBatchAdd`. The keys you pass decide what the workflow does:
46
71
 
47
72
  | Form | Shape | Effect |
48
73
  | --- | --- | --- |
49
- | Existing select / axis | `{ id, value_ids }` | Links the referenced values to the product |
50
- | Existing text / unit / toggle | `{ id, value }` | `text`/`unit` create and link a value; `toggle` links the seeded `true`/`false` value |
51
- | Inline axis | `{ title, values, is_variant_axis: true }` | Creates an exclusive option, a scoped attribute, and the value mirror |
52
- | Inline non-axis | `{ title, type, value \| values }` | Creates a scoped attribute plus its value(s) and links them |
53
-
54
- `remove` takes attribute ids to detach; `update` carries
55
- `ProductAttributeBatchUpdate` entries adjusting an existing selection.
56
-
57
- <Note>
58
- For a variant-axis attribute, `value_ids` is the per-product **subset** of the
59
- mirror option's values. The product's variants are generated from exactly that
60
- subset. See [Variant axes](/platform/attribute/concepts/variant-axes).
61
- </Note>
62
-
63
- <Tip>
64
- The batch workflow composes the lower-level
65
- `addProductAttributesToProductWorkflow`,
66
- `removeProductAttributesFromProductWorkflow`, and
67
- `updateProductAttributesOnProductWorkflow`. Reach for those directly when you
68
- only need one of the three operations.
69
- </Tip>
74
+ | Existing select | `{ id, value_ids }` | Links the chosen values to the product |
75
+ | Existing axis | `{ id, value_ids }` | Attaches the global option with only those values, and links the values |
76
+ | Existing text or unit | `{ id, value }` | Creates a value named `String(value)` and links it |
77
+ | Existing toggle | `{ id, value }` | Links the seeded `"true"` or `"false"` value |
78
+ | Inline attribute | `{ title, type?, value }` or `{ title, type?, values }` | Creates a product-scoped attribute and its values, and links them |
79
+ | Inline axis | `{ title, values, is_variant_axis: true }` | Creates an exclusive option, a scoped `multi_select` attribute, and the mirrored values |
80
+
81
+ ## Change a selection with update
82
+
83
+ An `update` entry targets one attribute by `id`. What `add` and `remove` mean depends on the attribute:
84
+
85
+ - **Global select or axis**: `add` and `remove` hold attribute value ids, and an axis also moves the option values
86
+ - **Global select or axis, full set**: `value_ids` is the complete set the product should hold, and the workflow works out the difference
87
+ - **Inline axis**: `add` holds new values such as `{ value: "XL" }`, and `remove` holds attribute value ids to delete
88
+ - **Text, unit, or toggle**: `value` replaces the current value
89
+ - **Inline rename**: `title` renames an inline attribute, and it’s ignored for global ones
90
+
91
+ ```ts
92
+ update: [
93
+ { id: "pattr_01JD8Q1234561", value_ids: ["pattrval_01JD8Q1234562"] },
94
+ { id: "pattr_01JD8Q1234564", title: "Fit", add: [{ value: "Relaxed" }] },
95
+ ]
96
+ ```
97
+
98
+ The first entry swaps the product to a different value. The second renames an inline axis and adds a value to its exclusive option.
99
+
100
+ ## Handle a required attribute
101
+
102
+ Removing an attribute with `is_required: true` fails with a `NOT_ALLOWED` error, and nothing in the run is applied. The check skips attributes you remove and re-add in the same run, so you can replace a required attribute’s selection in one call.
103
+
104
+ ## Route vendor edits through review
105
+
106
+ Running the batch workflow writes to the product immediately. For changes a store makes, run `productEditUpdateAttributesWorkflow` with the same `add`, `remove`, and `update` plus `created_by`. It stages a product change for review, and the change runs the batch workflow when it’s confirmed. See [Product edits](/platform/product-edit/overview).
107
+
108
+ ## Next steps
109
+
110
+ - [Create a variant axis and build variants on it](/platform/attribute/guides/create-a-variant-axis)
111
+ - [Every attachment workflow and its input](/platform/attribute/reference/workflows)
112
+ - [The links behind product attributes](/platform/attribute/reference/links)