feat(types): add util to transform get response to an update request (#6289)
what:
- add util to transform get response to an update request
RESOLVES CORE-1687
Given an update data object, we can infer the `fields` and `selects` of a data object using a util we already have - `getSelectsAndRelationsFromObjectArray`. Using the `fields` and `selects`, we can call either the module `retrieve` or `list` method to get a snapshot of the data down to its exact attributes. We can pass this data into the revert step.
In the revert step, we just need to convert the snapshot received from `retrieve` or `list` to a shape that the `update` methods will accept. The util that is introduced in this PR aims to do exactly that, so that the revert step looks as simple as:
```
const { snapshotData, selects, relations } = revertInput
await promotionModule.updateCampaigns(
convertItemResponseToUpdateRequest(snapshotData, selects, relations)
)
```
entity before update:
```
Campaign: {
id: "campaign-test-1",
name: "test campaign",
budget: {
total: 2000
},
promotions: [{ id: "promotion-1" }],
rules: [
{ id: "rule-1", operator: "gt", value: "10" }
]
}
```
This is how the util will transform the data for different types of attributes in the object:
simple attributes:
```
invoke:
{
id: "campaign-test-1",
name: "change name",
}
compensate:
{
id: "test-1",
name: "test campaign"
}
```
one to one relationship:
```
invoke:
{
id: "campaign-test-1",
budget: { total: 4000 }
}
compensate:
{
id: "campaign-test-1",
budget: { total: 2000 }
}
```
one to many / many to many relationship:
```
invoke:
{
id: "campaign-test-1",
promotions: [{ id: "promotion-2" }]
rules: [
{ id: "rule-1", operator: "gt", value: "20" },
{ operator: "gt", value: "20" }
]
}
compensate:
{
id: "campaign-test-1",
promotions: [{ id: "promotion-1" }]
rules: [{ id: "rule-1", operator: "gt", value: "20" }]
}
```
all together:
```
invoke:
{
id: "campaign-test-1",
name: "change name",
promotions: [{ id: "promotion-2" }],
budget: { total: 4000 },
rules: [
{ id: "rule-1", operator: "gt", value: "20" },
{ operator: "gt", value: "20" }
]
}
compensate:
{
id: "test-1",
name: "test campaign",
promotions: [{ id: "promotion-1" }],
budget: { total: 2000 },
rules: [{ id: "rule-1", operator: "gt", value: "20" }],
}
```
This commit is contained in:
5
.changeset/real-peas-look.md
Normal file
5
.changeset/real-peas-look.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@medusajs/utils": patch
|
||||
---
|
||||
|
||||
feat(types): add util to transform get response to an update request
|
||||
@@ -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<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)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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<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)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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(".")
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user