Skip to content

Conversation

@willbouch
Copy link
Contributor

@willbouch willbouch commented Oct 22, 2025

This is the SERVER-SIDE part of the PR. The client-side PR can be found here. To test via snapshot, it will be easier to fo it from the client-side PR, since it is based on this one.

WHAT
This PR handles an important and heavily requested redesign in the Core. For context, the relationship between Product and ProductOptions currently is 1-many. We want to change that to a many-many relationship, which will allow linking multiple Products to the same ProductOption. This will be useful to implement filters on the storefront for example.

HOW
Here is an image of the redesign:
CleanShot_2025-10-21_at_13 27 29

To clarify, the pivot table between product_option_value and the other pivot table will be used to enable linking only a subset of the ProdutOptionValues to a Product. For example, a ProductOption named Color could have 10 colors defined, and I might have a Product that only exists in 5 colors. In that case, to avoid making many Color options, the user will have the ability to only choose the colors they want for that product

In this PR, you will find:

  1. Brand new endpoints to interact with product options (CRUD)
  2. Endpoint to link options with products
  3. Modification to the product service to account for the modified relationship

I will leave comments in the PR to make the review a bit easier

WHAT IS LEFT
I decided to put the stuff I needed to have a "functional" frontend, but there are still things that are missing that I will address in another PR

  • The functionality to save a subset of the values
  • Error scenarios (block assigning exclusive option to another product, block deleting a global option assigned to a product)

Note

Redesigns product options to a many‑to‑many model with CRUD/admin APIs, linking workflows, value ranking, and updated types, events, and migrations.

  • Data Model & Migrations
    • Convert ProductProductOption to many‑to‑many via product_product_option pivot; add is_exclusive on product_option and rank on product_option_value.
  • Core Service (product module)
    • Add link/unlink APIs: addProductOptionToProduct/removeProductOptionFromProduct.
    • Support creating/updating options with ranks and is_exclusive; preserve options on product soft delete; validate variant option combinations against linked options.
    • Product create/update now accepts existing option IDs, and updates handle linking/unlinking.
  • Workflows (core-flows)
    • New steps/workflows: link-product-options-to-product, create-and-link-product-options-to-product, process-product-options-for-import (transforms optionsoption_ids).
    • Enhance create/update-product-options to handle ranks; batch products workflow pipes option processing on update.
  • HTTP Admin API (medusa)
    • New GET/POST/DELETE /admin/product-options and GET/POST/DELETE /admin/product-options/:id (CRUD with is_exclusive, ranks).
    • New POST /admin/products/:id/options to add/remove existing or create‑and‑link options; GET /admin/products/:id/options lists by product.
    • Product update deprecates options payload; use option_ids.
  • Types & Events
    • Update DTOs/query params to include is_exclusive, ranks, option_ids, and product‑option link types.
    • Add productProductOption event namespace; adjust event emissions for link/unlink.
  • Tests
    • Add/adjust integration tests for product options CRUD, linking, imports, and fulfillment; update expectations for new fields and behaviors.

Written by Cursor Bugbot for commit 8041f0e. This will update automatically on new commits. Configure here.

@changeset-bot
Copy link

changeset-bot bot commented Oct 22, 2025

🦋 Changeset detected

Latest commit: 8041f0e

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 74 packages
Name Type
@medusajs/medusa Major
@medusajs/product Major
@medusajs/core-flows Major
@medusajs/types Major
@medusajs/test-utils Major
@medusajs/medusa-oas-cli Major
integration-tests-http Patch
@medusajs/draft-order Major
@medusajs/framework Major
@medusajs/js-sdk Major
@medusajs/modules-sdk Major
@medusajs/orchestration Major
@medusajs/utils Major
@medusajs/workflows-sdk Major
@medusajs/admin-bundler Major
@medusajs/dashboard Major
@medusajs/analytics Major
@medusajs/api-key Major
@medusajs/auth Major
@medusajs/caching Major
@medusajs/cart Major
@medusajs/currency Major
@medusajs/customer Major
@medusajs/file Major
@medusajs/fulfillment Major
@medusajs/index Major
@medusajs/inventory Major
@medusajs/link-modules Major
@medusajs/locking Major
@medusajs/notification Major
@medusajs/order Major
@medusajs/payment Major
@medusajs/pricing Major
@medusajs/promotion Major
@medusajs/region Major
@medusajs/sales-channel Major
@medusajs/settings Major
@medusajs/stock-location Major
@medusajs/store Major
@medusajs/tax Major
@medusajs/user Major
@medusajs/workflow-engine-inmemory Major
@medusajs/workflow-engine-redis Major
@medusajs/oas-github-ci Major
@medusajs/cache-inmemory Major
@medusajs/cache-redis Major
@medusajs/event-bus-local Major
@medusajs/event-bus-redis Major
@medusajs/analytics-local Major
@medusajs/analytics-posthog Major
@medusajs/auth-emailpass Major
@medusajs/auth-github Major
@medusajs/auth-google Major
@medusajs/caching-redis Major
@medusajs/file-local Major
@medusajs/file-s3 Major
@medusajs/fulfillment-manual Major
@medusajs/locking-postgres Major
@medusajs/locking-redis Major
@medusajs/notification-local Major
@medusajs/notification-sendgrid Major
@medusajs/payment-stripe Major
@medusajs/cli Major
@medusajs/deps Major
@medusajs/telemetry Major
@medusajs/admin-sdk Major
@medusajs/admin-shared Major
@medusajs/admin-vite-plugin Major
@medusajs/icons Major
@medusajs/toolbox Major
@medusajs/ui-preset Major
create-medusa-app Major
medusa-dev-cli Major
@medusajs/ui Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel
Copy link

vercel bot commented Oct 22, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

8 Skipped Deployments
Project Deployment Preview Comments Updated (UTC)
api-reference Ignored Ignored Nov 14, 2025 8:25pm
api-reference-v2 Ignored Ignored Preview Nov 14, 2025 8:25pm
cloud-docs Ignored Ignored Preview Nov 14, 2025 8:25pm
docs-ui Ignored Ignored Preview Nov 14, 2025 8:25pm
docs-v2 Ignored Ignored Preview Nov 14, 2025 8:25pm
medusa-docs Ignored Ignored Preview Nov 14, 2025 8:25pm
resources-docs Ignored Ignored Preview Nov 14, 2025 8:25pm
user-guide Ignored Ignored Preview Nov 14, 2025 8:25pm

@NicolasGorga
Copy link
Contributor

Hey Will, one thing i noticed is if the pivot entity is not defined between product and product option, additional columns can't be added to the pivot table, like for example the rank field, to be able to specify for a given product, the order you want its options to show.

@willbouch
Copy link
Contributor Author

Hey Will, one thing i noticed is if the pivot entity is not defined between product and product option, additional columns can't be added to the pivot table, like for example the rank field, to be able to specify for a given product, the order you want its options to show.

The rank field will be on product_option_value and also on the pivot table between product_option_value and product_option_option. Will take care of that later but you are right. I guess that I will have to create a model for the pivot table anyway, so will do it now

@willbouch willbouch changed the title chore(product,types): change relationship between product and option to many-to-many WIP chore(product,types): change relationship between product and option to many-to-many Oct 23, 2025
@willbouch willbouch changed the title WIP chore(product,types): change relationship between product and option to many-to-many WIP Oct 23, 2025
@willbouch willbouch changed the title WIP WIP - feat(): product options redesign (server-side) Oct 28, 2025
@willbouch willbouch changed the title WIP - feat(): product options redesign (server-side) WIP - feat(medusa,product,core-flows,types): product options redesign (server-side) Nov 3, 2025
@olivermrbl
Copy link
Contributor

/snapshot-this

@github-actions
Copy link
Contributor

github-actions bot commented Nov 6, 2025

🚀 A snapshot release has been made for this PR

Test the snapshots by updating your package.json with the newly published versions:

yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]
yarn add @medusajs/[email protected]

Latest commit: 3ca1e1d

Copy link
Contributor

@fPolic fPolic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work @willbouch! Left a few questions, but looks good overall!

{
title: "option2",
values: ["D", "E"],
is_exclusive: true,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

q: do we validate is_exclusive when assigning products

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not at the moment, you are right, I had planned to do most the error validation scenarios like that in the next PRs but forgot to add it in the list. Added it to the list

) => {
const id = req.params.id

await deleteProductOptionsWorkflow(req.scope).run({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

q: do we validate if a global option can be deleted if there are linked products?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not at the moment, good point. I will do that with the other error scenarios. Added it to the list

$or: pairs,
}
)
await this.productProductOptionService_.delete(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

q: what happens with variants that have values of the removed option?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the moment nothing. I think this wil also be something that I need to do in upcoming PRs. The notion of cleaning up when deleting an option. I will have a discussion with you guys before that to make sure we align on the behaviour

Copy link
Contributor

@olivermrbl olivermrbl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is epic work @willbouch!

// get product_id from the first product in the products array of the first option
...(options.length && options[0].products?.length
? { product_id: options[0].products[0].id }
: {}),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Variants Lack Product Identity

The code attempts to extract product_id from options[0].products[0].id, but the products relation isn't loaded when productRepository_.deepUpdate calls validateVariantOptions. The repository only populates options and options.values relations, leaving products as an uninitialized Collection with length 0. This causes the condition to evaluate false, preventing variants from receiving a product_id, which breaks the filtering logic in assignOptionsToVariants that relies on variant.product_id to match options.

Fix in Cursor Fix in Web

})
}
}),
option_ids: z.array(IdAssociation).optional(),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: API option_ids Schema Conflict

The option_ids validator expects z.array(IdAssociation) requiring objects like [{ id: "opt_1" }], but the HTTP type definition at packages/core/types/src/http/product/admin/payloads.ts line 462 and service implementation expect string[] like ["opt_1", "opt_2"]. This mismatch causes the validator to reject valid API requests following the type definition, and if bypassed, would cause the service logic to fail when iterating over objects instead of strings.

Fix in Cursor Fix in Web

})
}
}),
option_ids: z.array(IdAssociation).optional(),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Type Mismatch Blocks Product Update Flow

The option_ids validator expects z.array(IdAssociation) which validates { id: string }[], but the service implementation at product-module-service.ts lines 2027-2028 and 2074 expects a flat array of strings string[]. This mismatch causes the service to attempt operations like flatMap and new Set(product.option_ids) on an array of objects instead of strings, breaking the product update logic when option_ids are provided.

Fix in Cursor Fix in Web

"updateRule": "cascade"
}
},
"foreignKeys": {},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Migration Snapshot Foreign Key Mismatch

The migration snapshot for product_product_option table has empty foreignKeys object, but the corresponding migration Migration20251022153442.ts creates foreign key constraints product_product_option_product_id_foreign and product_product_option_product_option_id_foreign. The snapshot should include these foreign keys to match the actual database schema created by the migration, otherwise there will be a mismatch between the snapshot and the actual database state.

Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants