Skip to content

Commit 01f31bc

Browse files
authored
fix(product): delete individual variant metadata fields (#16179)
## Summary **What:** Fix variant metadata updates so an individual field can be deleted without removing or overwriting the remaining fields. **Why:** `ProductModuleService.updateVariants_` uses `upsertWithReplace`, which bypassed the `mergeMetadata` behavior used by other metadata update paths. As a result, an empty-string value was persisted instead of deleting the key. **How:** Merge incoming variant metadata with the persisted metadata before calling `upsertWithReplace`. This preserves unspecified keys and removes keys whose incoming value is an empty string. The change includes unit and integration regression coverage plus a patch changeset. **Testing** - Product module TypeScript build passes. - Product module unit suite passes: 2 tests, 2 passed. - Targeted ESLint checks pass for the implementation and tests. - The new tests and changeset pass Prettier; `git diff --check` passes. - Added PostgreSQL-backed integration coverage for the exact multi-field deletion scenario; it will run in CI because PostgreSQL is not available in the local environment. --- ## Examples ```ts await productModuleService.updateProductVariants(variant.id, { metadata: { obsolete: "", }, }) ``` The `obsolete` key is removed while all other metadata fields remain unchanged. --- ## Checklist - [x] I have added a **changeset** for this PR - [x] The changes are covered by relevant **tests** - [x] I have verified the code works as intended locally - [x] I have linked the related issue(s) if applicable --- ## Additional Context Fixes #16163.
1 parent a42ee08 commit 01f31bc

4 files changed

Lines changed: 104 additions & 5 deletions

File tree

.changeset/friendly-chairs-fix.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@medusajs/product": patch
3+
---
4+
5+
fix(product): allow deleting individual variant metadata fields

packages/modules/product/integration-tests/__tests__/product-module-service/product-variants.spec.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,29 @@ moduleIntegrationTestRunner<IProductModuleService>({
444444
expect(productVariant.title).toEqual("new test")
445445
})
446446

447+
it("should delete one metadata field without removing the others", async () => {
448+
await service.updateProductVariants(variantOne.id, {
449+
metadata: {
450+
keep: "value",
451+
remove: "value",
452+
},
453+
})
454+
455+
await service.updateProductVariants(variantOne.id, {
456+
metadata: {
457+
remove: "",
458+
},
459+
})
460+
461+
const productVariant = await service.retrieveProductVariant(
462+
variantOne.id
463+
)
464+
465+
expect(productVariant.metadata).toEqual({
466+
keep: "value",
467+
})
468+
})
469+
447470
it("should update scalar fields without altering the variant's options", async () => {
448471
// A scalar-only update (no `options` in the payload) skips the option
449472
// resolution/uniqueness/relation-reconcile path. This must NOT wipe or
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import ProductModuleService from "../product-module-service"
2+
3+
describe("ProductModuleService", () => {
4+
it("should merge variant metadata before replacing the entity", async () => {
5+
const productVariantService = {
6+
list: jest.fn().mockResolvedValue([
7+
{
8+
id: "variant_1",
9+
product_id: "product_1",
10+
metadata: {
11+
keep: "value",
12+
remove: "value",
13+
},
14+
},
15+
]),
16+
upsertWithReplace: jest.fn().mockImplementation(async (input) => ({
17+
entities: input,
18+
})),
19+
}
20+
const service = new ProductModuleService(
21+
{
22+
productVariantService,
23+
} as any,
24+
{} as any
25+
)
26+
const context = {
27+
transactionManager: {},
28+
}
29+
30+
await (service as any).updateVariants_(
31+
[
32+
{
33+
id: "variant_1",
34+
metadata: {
35+
remove: "",
36+
},
37+
},
38+
],
39+
context
40+
)
41+
42+
expect(productVariantService.upsertWithReplace).toHaveBeenCalledWith(
43+
[
44+
{
45+
id: "variant_1",
46+
product_id: "product_1",
47+
metadata: {
48+
keep: "value",
49+
},
50+
},
51+
],
52+
{
53+
relations: [],
54+
},
55+
context
56+
)
57+
})
58+
})

packages/modules/product/src/services/product-module-service.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import {
4444
MedusaContext,
4545
MedusaError,
4646
MedusaService,
47+
mergeMetadata,
4748
MessageAggregator,
4849
Modules,
4950
partitionArray,
@@ -659,11 +660,23 @@ export default class ProductModuleService
659660

660661
// Data normalization
661662
const variantsWithProductId: UpdateProductVariantInput[] = variants.map(
662-
(v) => ({
663-
...data.find((d) => d.id === v.id),
664-
id: v.id,
665-
product_id: v.product_id,
666-
})
663+
(variant) => {
664+
const update = data.find(({ id }) => id === variant.id)!
665+
666+
return {
667+
...update,
668+
id: variant.id,
669+
product_id: variant.product_id,
670+
...(isPresent(update.metadata)
671+
? {
672+
metadata: mergeMetadata(
673+
variant.metadata ?? {},
674+
update.metadata!
675+
),
676+
}
677+
: {}),
678+
}
679+
}
667680
)
668681

669682
let productVariantsWithOptions: UpdateProductVariantInput[] =

0 commit comments

Comments
 (0)