diff --git a/.changeset/kind-chairs-cough.md b/.changeset/kind-chairs-cough.md new file mode 100644 index 0000000000..121927b965 --- /dev/null +++ b/.changeset/kind-chairs-cough.md @@ -0,0 +1,5 @@ +--- +"@medusajs/utils": patch +--- + +feat(utils): Backport metadata management diff --git a/integration-tests/http/__tests__/customer/admin/customer.spec.ts b/integration-tests/http/__tests__/customer/admin/customer.spec.ts index 9b2d1af5fa..f58c48044a 100644 --- a/integration-tests/http/__tests__/customer/admin/customer.spec.ts +++ b/integration-tests/http/__tests__/customer/admin/customer.spec.ts @@ -216,7 +216,7 @@ medusaIntegrationTestRunner({ first_name: "newf", last_name: "newl", email: "new@email.com", - metadata: { foo: "bar" }, + metadata: { foo: "bar", bar: "bar", baz: "baz" }, }, adminHeaders ) @@ -230,7 +230,51 @@ medusaIntegrationTestRunner({ first_name: "newf", last_name: "newl", email: "new@email.com", - metadata: { foo: "bar" }, + metadata: { foo: "bar", bar: "bar", baz: "baz" }, + }) + ) + }) + + it("should correctly update customer metadata", async () => { + let response = await api.post( + `/admin/customers/${customer3.id}`, + { + first_name: "newf", + last_name: "newl", + email: "new@email.com", + metadata: { foo: "bar", bar: "bar", baz: "baz" }, + }, + adminHeaders + ) + + expect(response.data.customer).toEqual( + expect.objectContaining({ + first_name: "newf", + last_name: "newl", + email: "new@email.com", + metadata: { foo: "bar", bar: "bar", baz: "baz" }, + }) + ) + + response = await api + .post( + `/admin/customers/${customer3.id}`, + { + metadata: { foo: "", bar: "bar2", baz2: "baz2" }, + }, + adminHeaders + ) + .catch((err) => { + console.log(err) + }) + + expect(response.status).toEqual(200) + expect(response.data.customer).toEqual( + expect.objectContaining({ + first_name: "newf", + last_name: "newl", + email: "new@email.com", + metadata: { bar: "bar2", baz: "baz", baz2: "baz2" }, }) ) }) diff --git a/packages/core/utils/src/common/__tests__/merge-metadata.spec.ts b/packages/core/utils/src/common/__tests__/merge-metadata.spec.ts new file mode 100644 index 0000000000..a9a679323a --- /dev/null +++ b/packages/core/utils/src/common/__tests__/merge-metadata.spec.ts @@ -0,0 +1,40 @@ +import { mergeMetadata } from "../merge-metadata" + +describe("mergeMetadata", () => { + it("should merge simple key-value pairs", () => { + const metadata = { + key1: "value1", + key2: "value2", + } + const metadataToMerge = { + key2: "new-value2", + key3: "value3", + } + + const result = mergeMetadata(metadata, metadataToMerge) + + expect(result).toEqual({ + key1: "value1", + key2: "new-value2", + key3: "value3", + }) + }) + + it("should remove keys with empty string values", () => { + const metadata = { + key1: "value1", + key2: "value2", + key3: "value3", + } + const metadataToMerge = { + key2: "", + } + + const result = mergeMetadata(metadata, metadataToMerge) + + expect(result).toEqual({ + key1: "value1", + key3: "value3", + }) + }) +}) diff --git a/packages/core/utils/src/common/index.ts b/packages/core/utils/src/common/index.ts index 54585cb415..4f81129c3c 100644 --- a/packages/core/utils/src/common/index.ts +++ b/packages/core/utils/src/common/index.ts @@ -81,3 +81,4 @@ export * from "./upper-case-first" export * from "./validate-handle" export * from "./wrap-handler" export * from "./merge-plugin-modules" +export * from "./merge-metadata" diff --git a/packages/core/utils/src/common/merge-metadata.ts b/packages/core/utils/src/common/merge-metadata.ts new file mode 100644 index 0000000000..9c903655e1 --- /dev/null +++ b/packages/core/utils/src/common/merge-metadata.ts @@ -0,0 +1,37 @@ +/** + * Merges two metadata objects. The key from the original metadata object is + * preserved if the key is not present in the metadata to merge. If the key + * is present in the metadata to merge, the value from the metadata to merge + * is used. If the key in the metadata to merge is an empty string, the key + * is removed from the merged metadata object. + * + * @param metadata - The base metadata object. + * @param metadataToMerge - The metadata object to merge. + * @returns The merged metadata object. + */ +export function mergeMetadata( + metadata: Record, + metadataToMerge: Record +) { + const merged = { ...metadata } + + for (const [key, value] of Object.entries(metadataToMerge)) { + if (value === "") { + delete merged[key] + continue + } + + // NOTE: If we want to handle the same behaviour on nested objects. We should then conside arrays as well. + // if (value && typeof value === "object") { + // merged[key] = + // merged[key] && typeof merged[key] === "object" + // ? mergeMetadata(merged[key], value) + // : { ...value } + // continue + // } + + merged[key] = value + } + + return merged +} diff --git a/packages/core/utils/src/modules-sdk/__tests__/medusa-internal-service.ts b/packages/core/utils/src/modules-sdk/__tests__/medusa-internal-service.ts index bc186ad206..83e4137c4a 100644 --- a/packages/core/utils/src/modules-sdk/__tests__/medusa-internal-service.ts +++ b/packages/core/utils/src/modules-sdk/__tests__/medusa-internal-service.ts @@ -45,10 +45,13 @@ describe("Internal Module Service Factory", () => { let instance beforeEach(() => { - jest.clearAllMocks() instance = new IMedusaInternalService(containerMock) }) + afterEach(() => { + jest.clearAllMocks() + }) + it("should throw model id undefined error on retrieve if id is not defined", async () => { const err = await instance.retrieve().catch((e) => e) expect(err.message).toBe("model - id must be defined") @@ -201,7 +204,7 @@ describe("Internal Module Service Factory", () => { updateData, ]) - const result = await instance.update(updateData) + const result = await instance.update({ selector: {}, data: updateData }) expect(result).toEqual([updateData]) }) @@ -223,6 +226,32 @@ describe("Internal Module Service Factory", () => { ]) }) + it("should update entities metadata successfully", async () => { + const updateData = { + id: "1", + name: "UpdatedItem", + metadata: { key1: "", key2: "key2" }, + } + const entitiesToUpdate = [ + { id: "1", name: "Item", metadata: { key1: "value1" } }, + ] + + containerMock[modelRepositoryName].find.mockClear() + containerMock[modelRepositoryName].find.mockResolvedValueOnce( + entitiesToUpdate + ) + + await instance.update({ selector: {}, data: updateData }) + expect( + containerMock[modelRepositoryName].update.mock.calls[0][0][0].update + ).toEqual({ + ...updateData, + metadata: { + key2: "key2", + }, + }) + }) + it("should delete entity successfully", async () => { await instance.delete("1") expect(containerMock[modelRepositoryName].delete).toHaveBeenCalledWith( diff --git a/packages/core/utils/src/modules-sdk/medusa-internal-service.ts b/packages/core/utils/src/modules-sdk/medusa-internal-service.ts index a953f5c1da..403626a81d 100644 --- a/packages/core/utils/src/modules-sdk/medusa-internal-service.ts +++ b/packages/core/utils/src/modules-sdk/medusa-internal-service.ts @@ -17,6 +17,7 @@ import { isString, lowerCaseFirst, MedusaError, + mergeMetadata, } from "../common" import { FreeTextSearchFilterKeyPrefix } from "../dal" import { DmlEntity, toMikroORMEntity } from "../dml" @@ -350,6 +351,16 @@ export function MedusaInternalService< return [] } + // Manage metadata if needed + toUpdateData.forEach(({ entity, update }) => { + if (isPresent(update.metadata)) { + entity.metadata = update.metadata = mergeMetadata( + entity.metadata ?? {}, + update.metadata + ) + } + }) + return await this[propertyRepositoryName].update( toUpdateData, sharedContext