Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(types): add util to transform get response to an update request #6289

Merged
merged 5 commits into from
Feb 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/real-peas-look.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@medusajs/utils": patch
---

feat(types): add util to transform get response to an update request
25 changes: 18 additions & 7 deletions packages/core-flows/src/promotion/steps/update-campaigns.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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<IPromotionModuleService>(
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)
)
)
}
)
25 changes: 18 additions & 7 deletions packages/core-flows/src/promotion/steps/update-promotions.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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<IPromotionModuleService>(
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)
)
)
}
)
Original file line number Diff line number Diff line change
@@ -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)
})
})
})
104 changes: 104 additions & 0 deletions packages/utils/src/common/convert-item-response-to-update-request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { isObject } from "../common/is-object"

interface ItemRecord extends Record<string, any> {
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(".")
}
1 change: 1 addition & 0 deletions packages/utils/src/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading