diff --git a/.changeset/real-peas-look.md b/.changeset/real-peas-look.md new file mode 100644 index 0000000000..4f6bdc7531 --- /dev/null +++ b/.changeset/real-peas-look.md @@ -0,0 +1,5 @@ +--- +"@medusajs/utils": patch +--- + +feat(types): add util to transform get response to an update request diff --git a/packages/core-flows/src/promotion/steps/update-campaigns.ts b/packages/core-flows/src/promotion/steps/update-campaigns.ts index 85a03ab857..57d2999ca7 100644 --- a/packages/core-flows/src/promotion/steps/update-campaigns.ts +++ b/packages/core-flows/src/promotion/steps/update-campaigns.ts @@ -1,6 +1,9 @@ import { ModuleRegistrationName } from "@medusajs/modules-sdk" import { IPromotionModuleService, UpdateCampaignDTO } from "@medusajs/types" -import { getSelectsAndRelationsFromObjectArray } from "@medusajs/utils" +import { + convertItemResponseToUpdateRequest, + getSelectsAndRelationsFromObjectArray, +} from "@medusajs/utils" import { StepResponse, createStep } from "@medusajs/workflows-sdk" export const updateCampaignsStepId = "update-campaigns" @@ -19,19 +22,27 @@ export const updateCampaignsStep = createStep( const updatedCampaigns = await promotionModule.updateCampaigns(data) - return new StepResponse(updatedCampaigns, dataBeforeUpdate) + return new StepResponse(updatedCampaigns, { + dataBeforeUpdate, + selects, + relations, + }) }, - async (dataBeforeUpdate, { container }) => { - if (!dataBeforeUpdate) { + async (revertInput, { container }) => { + if (!revertInput) { return } + const { dataBeforeUpdate, selects, relations } = revertInput + const promotionModule = container.resolve( ModuleRegistrationName.PROMOTION ) - // TODO: This still requires some sanitation of data and transformation of - // shapes for manytomany and oneToMany relations. Create a common util. - await promotionModule.updateCampaigns(dataBeforeUpdate) + await promotionModule.updateCampaigns( + dataBeforeUpdate.map((data) => + convertItemResponseToUpdateRequest(data, selects, relations) + ) + ) } ) diff --git a/packages/core-flows/src/promotion/steps/update-promotions.ts b/packages/core-flows/src/promotion/steps/update-promotions.ts index e2f482da84..1ffe3125a1 100644 --- a/packages/core-flows/src/promotion/steps/update-promotions.ts +++ b/packages/core-flows/src/promotion/steps/update-promotions.ts @@ -1,6 +1,9 @@ import { ModuleRegistrationName } from "@medusajs/modules-sdk" import { IPromotionModuleService, UpdatePromotionDTO } from "@medusajs/types" -import { getSelectsAndRelationsFromObjectArray } from "@medusajs/utils" +import { + convertItemResponseToUpdateRequest, + getSelectsAndRelationsFromObjectArray, +} from "@medusajs/utils" import { StepResponse, createStep } from "@medusajs/workflows-sdk" export const updatePromotionsStepId = "update-promotions" @@ -19,19 +22,27 @@ export const updatePromotionsStep = createStep( const updatedPromotions = await promotionModule.update(data) - return new StepResponse(updatedPromotions, dataBeforeUpdate) + return new StepResponse(updatedPromotions, { + dataBeforeUpdate, + selects, + relations, + }) }, - async (dataBeforeUpdate, { container }) => { - if (!dataBeforeUpdate) { + async (revertInput, { container }) => { + if (!revertInput) { return } + const { dataBeforeUpdate, selects, relations } = revertInput + const promotionModule = container.resolve( ModuleRegistrationName.PROMOTION ) - // TODO: This still requires some sanitation of data and transformation of - // shapes for manytomany and oneToMany relations. Create a common util. - await promotionModule.update(dataBeforeUpdate) + await promotionModule.update( + dataBeforeUpdate.map((data) => + convertItemResponseToUpdateRequest(data, selects, relations) + ) + ) } ) diff --git a/packages/utils/src/common/__tests__/convert-item-response-to-update-request.ts b/packages/utils/src/common/__tests__/convert-item-response-to-update-request.ts new file mode 100644 index 0000000000..2ff2d558bc --- /dev/null +++ b/packages/utils/src/common/__tests__/convert-item-response-to-update-request.ts @@ -0,0 +1,59 @@ +import { convertItemResponseToUpdateRequest } from "../convert-item-response-to-update-request" + +describe("convertItemResponseToUpdateRequest", function () { + it("should return true or false for different types of data", function () { + const expectations = [ + { + item: { + id: "test-id", + test_attr: "test-name", + relation_object_with_params: { + id: "test-relation-object-id", + test_attr: "test-object-name", + }, + relation_object_without_params: { + id: "test-relation-object-without-params-id", + }, + relation_array: [ + { + id: "test-relation-array-id", + test_attr: "test-array-name", + }, + ], + }, + selects: [ + "id", + "test_attr", + "relation_object_with_params.id", + "relation_object_with_params.test_attr", + "relation_object_without_params.id", + "relation_array.id", + "relation_array.test_attr", + ], + relations: [ + "relation_object_with_params", + "relation_object_without_params", + "relation_array", + ], + output: { + id: "test-id", + test_attr: "test-name", + relation_object_with_params: { test_attr: "test-object-name" }, + relation_array: [{ id: "test-relation-array-id" }], + relation_object_without_params_id: + "test-relation-object-without-params-id", + }, + }, + ] + + expectations.forEach((expectation) => { + const response = convertItemResponseToUpdateRequest( + expectation.item, + expectation.selects, + expectation.relations + ) + + expect(response).toEqual(expectation.output) + }) + }) +}) diff --git a/packages/utils/src/common/convert-item-response-to-update-request.ts b/packages/utils/src/common/convert-item-response-to-update-request.ts new file mode 100644 index 0000000000..aaea439076 --- /dev/null +++ b/packages/utils/src/common/convert-item-response-to-update-request.ts @@ -0,0 +1,104 @@ +import { isObject } from "../common/is-object" + +interface ItemRecord extends Record { + id: string +} + +export function convertItemResponseToUpdateRequest( + item: ItemRecord, + selects: string[], + relations: string[], + fromManyRelationships: boolean = false +): ItemRecord { + const newItem: ItemRecord = { + id: item.id, + } + + // If item is a child of a many relationship, we just need to pass in the id of the item + if (fromManyRelationships) { + return newItem + } + + for (const [key, value] of Object.entries(item)) { + if (relations.includes(key)) { + const relation = item[key] + + // If the relationship is an object, its either a one to one or many to one relationship + // We typically don't update the parent from the child relationship, we can skip this for now. + // This can be focused on solely for one to one relationships + if (isObject(relation)) { + // If "id" is the only one in the object, underscorize the relation. This is assuming that + // the relationship itself was changed to another item and now we need to revert it to the old item. + if (Object.keys(relation).length === 1 && "id" in relation) { + newItem[`${key}_id`] = relation.id + } + + // If attributes of the relation have been updated, we can assume that this + // was an update operation on the relation. We revert what was updated. + if (Object.keys(relation).length > 1) { + // The ID can be figured out from the relationship, we can delete the ID here + if ("id" in relation) { + delete relation.id + } + + // we just need the selects for the relation, filter it out and remove the parent scope + const filteredSelects = selects + .filter((s) => s.startsWith(key) && !s.includes("id")) + .map(shiftFirstPath) + + // Add the filtered selects to the sanitized object + for (const filteredSelect of filteredSelects) { + newItem[key] = newItem[key] || {} + newItem[key][filteredSelect] = relation[filteredSelect] + } + } + + continue + } + + // If the relation is an array, we can expect this to be a one to many or many to many + // relationships. Recursively call the function until all relations are converted + if (Array.isArray(relation)) { + const newRelationsArray: ItemRecord[] = [] + + for (const rel of relation) { + // Scope selects and relations to ones that are relevant to the current relation + const filteredRelations = relations + .filter((r) => r.startsWith(key)) + .map(shiftFirstPath) + + const filteredSelects = selects + .filter((s) => s.startsWith(key)) + .map(shiftFirstPath) + + newRelationsArray.push( + convertItemResponseToUpdateRequest( + rel, + filteredSelects, + filteredRelations, + true + ) + ) + } + + newItem[key] = newRelationsArray + } + } + + // if the key exists in the selects, we add them to the new sanitized array. + // sanitisation is done because MikroORM adds relationship attributes and other default attributes + // which we do not want to add to the update request + if (selects.includes(key) && !fromManyRelationships) { + newItem[key] = value + } + } + + return newItem +} + +function shiftFirstPath(select) { + const selectArray = select.split(".") + selectArray.shift() + + return selectArray.join(".") +} diff --git a/packages/utils/src/common/index.ts b/packages/utils/src/common/index.ts index 2078870306..0b521bae37 100644 --- a/packages/utils/src/common/index.ts +++ b/packages/utils/src/common/index.ts @@ -3,6 +3,7 @@ export * from "./array-difference" export * from "./build-query" export * from "./camel-to-snake-case" export * from "./container" +export * from "./convert-item-response-to-update-request" export * from "./create-container-like" export * from "./create-psql-index-helper" export * from "./deduplicate"