Skip to content
Open
Show file tree
Hide file tree
Changes from 44 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
b7d9de8
chore(product,types): change relationship between product and option …
willbouch Oct 22, 2025
f7de05e
tests
willbouch Oct 23, 2025
2210c08
define model for pivot table
willbouch Oct 23, 2025
cb3dda5
test commit will revert
willbouch Oct 23, 2025
4a62432
Revert "test commit will revert"
willbouch Oct 23, 2025
8fc85ca
model for pivot table
willbouch Oct 23, 2025
a7cbbfe
more
willbouch Oct 23, 2025
af7b374
new product options endpoint
willbouch Oct 24, 2025
c0af764
product options endpoints
willbouch Oct 28, 2025
b4020d6
tests
willbouch Oct 28, 2025
cdd2dec
remove debug
willbouch Oct 28, 2025
5f064d4
small changes in endpoints
willbouch Oct 28, 2025
14e4778
add ranks to product option values
willbouch Oct 29, 2025
60b3430
allow linking to existing option on product creation
willbouch Oct 31, 2025
a97c04e
updates to link endpoint
willbouch Oct 31, 2025
6830b54
merge conflicts
willbouch Nov 3, 2025
f89f19e
Create forty-tables-fetch.md
willbouch Nov 3, 2025
f32e5d3
integration tests
willbouch Nov 3, 2025
72245db
integration tests
willbouch Nov 4, 2025
c35bcd2
product integration tests
willbouch Nov 4, 2025
a0b7ea9
Merge branch 'develop' into chore/add-many-to-many-between-product-an…
willbouch Nov 4, 2025
deff5cc
update validator
willbouch Nov 4, 2025
c914fc1
fix import
willbouch Nov 5, 2025
28ba1d2
Merge branch 'develop' into chore/add-many-to-many-between-product-an…
willbouch Nov 5, 2025
4e0d98b
fix import
willbouch Nov 5, 2025
afb4455
maintain ordering
willbouch Nov 5, 2025
454173f
Merge branch 'develop' into chore/add-many-to-many-between-product-an…
willbouch Nov 5, 2025
5ffe5f6
self review
willbouch Nov 5, 2025
2c58173
pr comments
willbouch Nov 6, 2025
3a9d74e
pr comments
willbouch Nov 6, 2025
39289af
pr comments
willbouch Nov 6, 2025
75662f4
is string
willbouch Nov 6, 2025
1eeb65d
pr comments
willbouch Nov 6, 2025
046d767
query graph
willbouch Nov 6, 2025
df36394
comment about moving error
willbouch Nov 6, 2025
a576bb2
create options in parallel
willbouch Nov 6, 2025
a9d7985
Merge branch 'develop' into chore/add-many-to-many-between-product-an…
olivermrbl Nov 7, 2025
a7fc306
comments
willbouch Nov 7, 2025
c874344
Merge branch 'develop' into chore/add-many-to-many-between-product-an…
willbouch Nov 10, 2025
d1aa1c1
small change in doc
willbouch Nov 12, 2025
50b44c8
migration name
willbouch Nov 12, 2025
6d20893
Merge branch 'develop' into chore/add-many-to-many-between-product-an…
willbouch Nov 14, 2025
1c044f6
Merge branch 'develop' into chore/add-many-to-many-between-product-an…
willbouch Nov 14, 2025
ddd4f36
fix test
willbouch Nov 14, 2025
8041f0e
Merge branch 'develop' into chore/add-many-to-many-between-product-an…
willbouch Nov 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/forty-tables-fetch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@medusajs/medusa": minor
"@medusajs/product": minor
"@medusajs/core-flows": minor
"@medusajs/types": minor
---

feat(medusa,product,core-flows,types): product options redesign (server-side)
2 changes: 1 addition & 1 deletion integration-tests/.env.test
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
DB_HOST=localhost
DB_USERNAME=postgres
DB_PASSWORD=''
LOG_LEVEL=error
LOG_LEVEL=error
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import {
adminHeaders,
createAdminUser,
} from "../../../helpers/create-admin-user"

jest.setTimeout(30000)

medusaIntegrationTestRunner({
env: {},
testSuite: ({ dbConnection, getContainer, api }) => {
let option1
let option2

beforeEach(async () => {
const container = getContainer()
await createAdminUser(dbConnection, adminHeaders, container)

option1 = (
await api.post(
"/admin/product-options",
{
title: "option1",
values: ["A", "B", "C"],
},
adminHeaders
)
).data.product_option

option2 = (
await api.post(
"/admin/product-options",
{
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

},
adminHeaders
)
).data.product_option
})

describe("GET /admin/product-options", () => {
it("should return a list of product options", async () => {
const res = await api.get("/admin/product-options", adminHeaders)

expect(res.status).toEqual(200)
expect(res.data.product_options).toEqual(
expect.arrayContaining([
expect.objectContaining({
title: "option1",
is_exclusive: false,
values: expect.arrayContaining([
expect.objectContaining({ value: "A" }),
expect.objectContaining({ value: "B" }),
expect.objectContaining({ value: "C" }),
]),
}),
expect.objectContaining({
title: "option2",
is_exclusive: true,
values: expect.arrayContaining([
expect.objectContaining({ value: "D" }),
expect.objectContaining({ value: "E" }),
]),
}),
])
)
})

it("should return a list of product options matching free text search param", async () => {
const res = await api.get("/admin/product-options?q=1", adminHeaders)

expect(res.status).toEqual(200)
expect(res.data.product_options.length).toEqual(1)
expect(res.data.product_options).toEqual(
expect.arrayContaining([
expect.objectContaining({ title: "option1" }),
])
)
})

it("should return a list of exclusive product options", async () => {
const res = await api.get(
"/admin/product-options?is_exclusive=false",
adminHeaders
)

expect(res.status).toEqual(200)
expect(res.data.product_options.length).toEqual(1)
expect(res.data.product_options).toEqual(
expect.arrayContaining([
expect.objectContaining({ title: "option1" }),
])
)
})
})

describe("POST /admin/product-options", () => {
it("should create a product option with value ranks", async () => {
const option = (
await api.post(
`/admin/product-options`,
{
title: "option3",
values: ["D", "E"],
ranks: {
E: 1,
D: 2,
},
},
adminHeaders
)
).data.product_option

expect(option).toEqual(
expect.objectContaining({
title: "option3",
is_exclusive: false,
values: expect.arrayContaining([
expect.objectContaining({
value: "D",
rank: 2,
}),
expect.objectContaining({
value: "E",
rank: 1,
}),
]),
})
)
})

it("should throw if a rank is specified for invalid value", async () => {
const error = await api
.post(
`/admin/product-options`,
{
title: "option3",
values: ["D", "E"],
ranks: {
E: 1,
invalid: 2,
},
},
adminHeaders
)
.catch((err) => err)

expect(error.response.status).toEqual(400)
expect(error.response.data.message).toEqual(
'Value "invalid" is assigned a rank but is not defined in the list of values.'
)
})
})

describe("GET /admin/product-options/[id]", () => {
it("should return a product option", async () => {
const res = await api.get(
`/admin/product-options/${option1.id}`,
adminHeaders
)

expect(res.status).toEqual(200)
expect(res.data.product_option.values.length).toEqual(3)
expect(res.data.product_option).toEqual(
expect.objectContaining({
title: "option1",
is_exclusive: false,
values: expect.arrayContaining([
expect.objectContaining({ value: "A" }),
expect.objectContaining({ value: "B" }),
expect.objectContaining({ value: "C" }),
]),
})
)
})
})

describe("POST /admin/product-options/[id]", () => {
it("should update a product option", async () => {
const option = (
await api.post(
`/admin/product-options/${option2.id}`,
{
is_exclusive: false,
},
adminHeaders
)
).data.product_option

expect(option.values.length).toEqual(2)
expect(option).toEqual(
expect.objectContaining({
title: "option2",
is_exclusive: false,
values: expect.arrayContaining([
expect.objectContaining({ value: "D" }),
expect.objectContaining({ value: "E" }),
]),
})
)

const res = await api.get(
"/admin/product-options?is_exclusive=true",
adminHeaders
)

expect(res.status).toEqual(200)
expect(res.data.product_options.length).toEqual(0)
})

it("should update a product value ranks", async () => {
const option = (
await api.post(
`/admin/product-options/${option2.id}`,
{
ranks: {
D: 2,
E: 1,
},
},
adminHeaders
)
).data.product_option

expect(option.values.length).toEqual(2)
expect(option).toEqual(
expect.objectContaining({
title: "option2",
is_exclusive: true,
values: expect.arrayContaining([
expect.objectContaining({
value: "D",
rank: 2,
}),
expect.objectContaining({
value: "E",
rank: 1,
}),
]),
})
)
})

it("should throw when trying to update an option that does not exist", async () => {
const error = await api.post(
`/admin/product-options/iDontExist`,
{
is_exclusive: false,
},
adminHeaders
).catch((e) => e)

expect(error.response.status).toEqual(404)
expect(error.response.data).toEqual({
message: "Product option with id \"iDontExist\" not found",
type: "not_found"
})
})
})

describe("DELETE /admin/product-options/[id]", () => {
it("should delete a product option", async () => {
await api.delete(`/admin/product-options/${option2.id}`, adminHeaders)

const res = await api.get("/admin/product-options", adminHeaders)

expect(res.status).toEqual(200)
expect(res.data.product_options.length).toEqual(1)
})
})
},
})
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { csv2json, json2csv } from "json-2-csv"
import { CommonEvents, Modules } from "@medusajs/utils"
import { IEventBusModuleService, IFileModuleService } from "@medusajs/types"
import {
TestEventUtils,
medusaIntegrationTestRunner,
TestEventUtils,
} from "@medusajs/test-utils"
import {
adminHeaders,
Expand Down
Loading