feat(medusa,pricing): Cart pricing context with customer group (#10579)
* fix(carts): Fixes cart modifications not accounting for certain price lists (#10493) *What* * Fixes #10490 * Expands any available customer_id into its customer_group_ids for cart updates that add line items. *Why* * Cart updates from the storefront were overriding any valid price lists that were correctly being shown in the storefront's product pages. *How* * Adds a new workflow step that expands an optional customer_id into the customer_group_ids it belongs to. * Uses this step in the addToCartWorkflow and updateLineItemInCartWorkflow workflows. *Testing* * Using medusa-dev to test on a local backend. * Adds integration tests for the addToCart and updateLineItemInCart workflows. Co-authored-by: Riqwan Thamir <rmthamir@gmail.com> * chore: update cart workflows to accept new pricing context * chore: add transfer specs * chore: fix specs * chore: modify types + specs * chore: add data migration + dashboard changes * chore: fix update line item workflow * chore: add changeset + unskip spec --------- Co-authored-by: Sergio Campamá <sergiocampama@gmail.com>
This commit is contained in:
co-authored by
Sergio Campamá
parent
0c49470066
commit
6367bccde8
@@ -1,13 +1,14 @@
|
||||
// Always ensure that cartFieldsForPricingContext is present in cartFieldsForRefreshSteps
|
||||
export const cartFieldsForRefreshSteps = [
|
||||
"id",
|
||||
"currency_code",
|
||||
"quantity",
|
||||
"subtotal",
|
||||
"item_total",
|
||||
"total",
|
||||
"item_subtotal",
|
||||
"shipping_subtotal",
|
||||
"region_id",
|
||||
"currency_code",
|
||||
"metadata",
|
||||
"completed_at",
|
||||
"sales_channel_id",
|
||||
@@ -100,6 +101,22 @@ export const completeCartFields = [
|
||||
"items.variant.inventory_items.inventory.location_levels.stock_locations.sales_channels.name",
|
||||
]
|
||||
|
||||
export const cartFieldsForPricingContext = [
|
||||
"id",
|
||||
"sales_channel_id",
|
||||
"currency_code",
|
||||
"region_id",
|
||||
"shipping_address.city",
|
||||
"shipping_address.country_code",
|
||||
"shipping_address.province",
|
||||
"shipping_address.postal_code",
|
||||
"item_total",
|
||||
"total",
|
||||
"customer.id",
|
||||
"email",
|
||||
"customer.groups.id",
|
||||
]
|
||||
|
||||
export const productVariantsFields = [
|
||||
"id",
|
||||
"title",
|
||||
|
||||
@@ -4,11 +4,12 @@ import {
|
||||
} from "@medusajs/framework/types"
|
||||
import { CartWorkflowEvents } from "@medusajs/framework/utils"
|
||||
import {
|
||||
WorkflowData,
|
||||
createWorkflow,
|
||||
parallelize,
|
||||
transform,
|
||||
WorkflowData,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import { useQueryGraphStep } from "../../common"
|
||||
import { emitEventStep } from "../../common/steps/emit-event"
|
||||
import { useRemoteQueryStep } from "../../common/steps/use-remote-query"
|
||||
import {
|
||||
@@ -18,11 +19,16 @@ import {
|
||||
} from "../steps"
|
||||
import { validateCartStep } from "../steps/validate-cart"
|
||||
import { validateVariantPricesStep } from "../steps/validate-variant-prices"
|
||||
import { productVariantsFields } from "../utils/fields"
|
||||
import {
|
||||
cartFieldsForPricingContext,
|
||||
productVariantsFields,
|
||||
} from "../utils/fields"
|
||||
import { prepareLineItemData } from "../utils/prepare-line-item-data"
|
||||
import { confirmVariantInventoryWorkflow } from "./confirm-variant-inventory"
|
||||
import { refreshCartItemsWorkflow } from "./refresh-cart-items"
|
||||
|
||||
const cartFields = ["completed_at"].concat(cartFieldsForPricingContext)
|
||||
|
||||
export const addToCartWorkflowId = "add-to-cart"
|
||||
/**
|
||||
* This workflow adds items to a cart.
|
||||
@@ -30,30 +36,29 @@ export const addToCartWorkflowId = "add-to-cart"
|
||||
export const addToCartWorkflow = createWorkflow(
|
||||
addToCartWorkflowId,
|
||||
(input: WorkflowData<AddToCartWorkflowInputDTO>) => {
|
||||
validateCartStep(input)
|
||||
const cartQuery = useQueryGraphStep({
|
||||
entity: "cart",
|
||||
filters: { id: input.cart_id },
|
||||
fields: cartFields,
|
||||
options: { throwIfKeyNotFound: true },
|
||||
}).config({ name: "get-cart" })
|
||||
|
||||
const cart = transform({ cartQuery }, ({ cartQuery }) => {
|
||||
return cartQuery.data[0]
|
||||
})
|
||||
|
||||
validateCartStep({ cart })
|
||||
|
||||
const variantIds = transform({ input }, (data) => {
|
||||
return (data.input.items ?? []).map((i) => i.variant_id)
|
||||
})
|
||||
|
||||
// TODO: This is on par with the context used in v1.*, but we can be more flexible.
|
||||
// TODO: create a common workflow to fetch variants and its prices
|
||||
const pricingContext = transform({ cart: input.cart }, (data) => {
|
||||
return {
|
||||
currency_code: data.cart.currency_code,
|
||||
region_id: data.cart.region_id,
|
||||
customer_id: data.cart.customer_id,
|
||||
}
|
||||
})
|
||||
|
||||
const variants = useRemoteQueryStep({
|
||||
entry_point: "variants",
|
||||
fields: productVariantsFields,
|
||||
variables: {
|
||||
id: variantIds,
|
||||
calculated_price: {
|
||||
context: pricingContext,
|
||||
},
|
||||
calculated_price: { context: cart },
|
||||
},
|
||||
throw_if_key_not_found: true,
|
||||
})
|
||||
@@ -73,7 +78,7 @@ export const addToCartWorkflow = createWorkflow(
|
||||
variant.calculated_price.is_calculated_price_tax_inclusive,
|
||||
quantity: item.quantity,
|
||||
metadata: item?.metadata ?? {},
|
||||
cartId: data.input.cart.id,
|
||||
cartId: input.cart_id,
|
||||
}) as CreateLineItemForCartDTO
|
||||
})
|
||||
|
||||
@@ -81,13 +86,13 @@ export const addToCartWorkflow = createWorkflow(
|
||||
})
|
||||
|
||||
const { itemsToCreate = [], itemsToUpdate = [] } = getLineItemActionsStep({
|
||||
id: input.cart.id,
|
||||
id: cart.id,
|
||||
items: lineItems,
|
||||
})
|
||||
|
||||
confirmVariantInventoryWorkflow.runAsStep({
|
||||
input: {
|
||||
sales_channel_id: input.cart.sales_channel_id as string,
|
||||
sales_channel_id: cart.sales_channel_id,
|
||||
variants,
|
||||
items: input.items,
|
||||
itemsToUpdate,
|
||||
@@ -96,22 +101,22 @@ export const addToCartWorkflow = createWorkflow(
|
||||
|
||||
parallelize(
|
||||
createLineItemsStep({
|
||||
id: input.cart.id,
|
||||
id: cart.id,
|
||||
items: itemsToCreate,
|
||||
}),
|
||||
updateLineItemsStep({
|
||||
id: input.cart.id,
|
||||
id: cart.id,
|
||||
items: itemsToUpdate,
|
||||
})
|
||||
)
|
||||
|
||||
refreshCartItemsWorkflow.runAsStep({
|
||||
input: { cart_id: input.cart.id },
|
||||
input: { cart_id: cart.id },
|
||||
})
|
||||
|
||||
emitEventStep({
|
||||
eventName: CartWorkflowEvents.UPDATED,
|
||||
data: { id: input.cart.id },
|
||||
data: { id: cart.id },
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import { useQueryGraphStep, validatePresenceOfStep } from "../../common"
|
||||
import { useRemoteQueryStep } from "../../common/steps/use-remote-query"
|
||||
import { cartFieldsForPricingContext } from "../utils/fields"
|
||||
|
||||
export const listShippingOptionsForCartWorkflowId =
|
||||
"list-shipping-options-for-cart"
|
||||
@@ -26,18 +27,7 @@ export const listShippingOptionsForCartWorkflow = createWorkflow(
|
||||
const cartQuery = useQueryGraphStep({
|
||||
entity: "cart",
|
||||
filters: { id: input.cart_id },
|
||||
fields: [
|
||||
"id",
|
||||
"sales_channel_id",
|
||||
"currency_code",
|
||||
"region_id",
|
||||
"shipping_address.city",
|
||||
"shipping_address.country_code",
|
||||
"shipping_address.province",
|
||||
"shipping_address.postal_code",
|
||||
"item_total",
|
||||
"total",
|
||||
],
|
||||
fields: cartFieldsForPricingContext,
|
||||
options: { throwIfKeyNotFound: true },
|
||||
}).config({ name: "get-cart" })
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { isDefined, PromotionActions } from "@medusajs/framework/utils"
|
||||
import {
|
||||
filterObjectByKeys,
|
||||
isDefined,
|
||||
PromotionActions,
|
||||
} from "@medusajs/framework/utils"
|
||||
import {
|
||||
createWorkflow,
|
||||
transform,
|
||||
@@ -9,6 +13,7 @@ import { useRemoteQueryStep } from "../../common/steps/use-remote-query"
|
||||
import { updateLineItemsStep } from "../steps"
|
||||
import { validateVariantPricesStep } from "../steps/validate-variant-prices"
|
||||
import {
|
||||
cartFieldsForPricingContext,
|
||||
cartFieldsForRefreshSteps,
|
||||
productVariantsFields,
|
||||
} from "../utils/fields"
|
||||
@@ -41,16 +46,9 @@ export const refreshCartItemsWorkflow = createWorkflow(
|
||||
return (data.cart.items ?? []).map((i) => i.variant_id)
|
||||
})
|
||||
|
||||
const pricingContext = transform(
|
||||
{ cart },
|
||||
({ cart: { currency_code, region_id, customer_id } }) => {
|
||||
return {
|
||||
currency_code,
|
||||
region_id,
|
||||
customer_id,
|
||||
}
|
||||
}
|
||||
)
|
||||
const cartPricingContext = transform({ cart }, ({ cart }) => {
|
||||
return filterObjectByKeys(cart, cartFieldsForPricingContext)
|
||||
})
|
||||
|
||||
const variants = useRemoteQueryStep({
|
||||
entry_point: "variants",
|
||||
@@ -58,7 +56,7 @@ export const refreshCartItemsWorkflow = createWorkflow(
|
||||
variables: {
|
||||
id: variantIds,
|
||||
calculated_price: {
|
||||
context: pricingContext,
|
||||
context: cartPricingContext,
|
||||
},
|
||||
},
|
||||
throw_if_key_not_found: true,
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import { useQueryGraphStep } from "../../common"
|
||||
import { updateCartsStep } from "../steps"
|
||||
import { refreshCartItemsWorkflow } from "./refresh-cart-items"
|
||||
|
||||
export const transferCartCustomerWorkflowId = "transfer-cart-customer"
|
||||
/**
|
||||
@@ -65,6 +66,10 @@ export const transferCartCustomerWorkflow = createWorkflow(
|
||||
)
|
||||
|
||||
updateCartsStep(cartInput)
|
||||
|
||||
refreshCartItemsWorkflow.runAsStep({
|
||||
input: { cart_id: input.id },
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,14 +4,20 @@ import {
|
||||
createWorkflow,
|
||||
transform,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import { useQueryGraphStep } from "../../common"
|
||||
import { useRemoteQueryStep } from "../../common/steps/use-remote-query"
|
||||
import { updateLineItemsStepWithSelector } from "../../line-item/steps"
|
||||
import { validateCartStep } from "../steps/validate-cart"
|
||||
import { validateVariantPricesStep } from "../steps/validate-variant-prices"
|
||||
import { productVariantsFields } from "../utils/fields"
|
||||
import {
|
||||
cartFieldsForPricingContext,
|
||||
productVariantsFields,
|
||||
} from "../utils/fields"
|
||||
import { confirmVariantInventoryWorkflow } from "./confirm-variant-inventory"
|
||||
import { refreshCartItemsWorkflow } from "./refresh-cart-items"
|
||||
|
||||
const cartFields = cartFieldsForPricingContext.concat(["items.*"])
|
||||
|
||||
export const updateLineItemInCartWorkflowId = "update-line-item-in-cart"
|
||||
/**
|
||||
* This workflow updates a cart's line item.
|
||||
@@ -19,19 +25,22 @@ export const updateLineItemInCartWorkflowId = "update-line-item-in-cart"
|
||||
export const updateLineItemInCartWorkflow = createWorkflow(
|
||||
updateLineItemInCartWorkflowId,
|
||||
(input: WorkflowData<UpdateLineItemInCartWorkflowInputDTO>) => {
|
||||
validateCartStep(input)
|
||||
const cartQuery = useQueryGraphStep({
|
||||
entity: "cart",
|
||||
filters: { id: input.cart_id },
|
||||
fields: cartFields,
|
||||
options: { throwIfKeyNotFound: true },
|
||||
}).config({ name: "get-cart" })
|
||||
|
||||
const variantIds = transform({ input }, (data) => {
|
||||
return [data.input.item.variant_id]
|
||||
const cart = transform({ cartQuery }, ({ cartQuery }) => cartQuery.data[0])
|
||||
const item = transform({ cart, input }, ({ cart, input }) => {
|
||||
return cart.items.find((i) => i.id === input.item_id)
|
||||
})
|
||||
|
||||
// TODO: This is on par with the context used in v1.*, but we can be more flexible.
|
||||
const pricingContext = transform({ cart: input.cart }, (data) => {
|
||||
return {
|
||||
currency_code: data.cart.currency_code,
|
||||
region_id: data.cart.region_id,
|
||||
customer_id: data.cart.customer_id,
|
||||
}
|
||||
validateCartStep({ cart })
|
||||
|
||||
const variantIds = transform({ item }, ({ item }) => {
|
||||
return [item.variant_id]
|
||||
})
|
||||
|
||||
const variants = useRemoteQueryStep({
|
||||
@@ -40,7 +49,7 @@ export const updateLineItemInCartWorkflow = createWorkflow(
|
||||
variables: {
|
||||
id: variantIds,
|
||||
calculated_price: {
|
||||
context: pricingContext,
|
||||
context: cart,
|
||||
},
|
||||
},
|
||||
throw_if_key_not_found: true,
|
||||
@@ -48,13 +57,13 @@ export const updateLineItemInCartWorkflow = createWorkflow(
|
||||
|
||||
validateVariantPricesStep({ variants })
|
||||
|
||||
const items = transform({ input }, (data) => {
|
||||
return [data.input.item]
|
||||
const items = transform({ item }, ({ item }) => {
|
||||
return [item]
|
||||
})
|
||||
|
||||
confirmVariantInventoryWorkflow.runAsStep({
|
||||
input: {
|
||||
sales_channel_id: input.cart.sales_channel_id as string,
|
||||
sales_channel_id: cart.sales_channel_id,
|
||||
variants,
|
||||
items,
|
||||
},
|
||||
@@ -62,7 +71,6 @@ export const updateLineItemInCartWorkflow = createWorkflow(
|
||||
|
||||
const lineItemUpdate = transform({ input, variants }, (data) => {
|
||||
const variant = data.variants[0]
|
||||
const item = data.input.item
|
||||
|
||||
return {
|
||||
data: {
|
||||
@@ -72,7 +80,7 @@ export const updateLineItemInCartWorkflow = createWorkflow(
|
||||
!!variant.calculated_price.is_calculated_price_tax_inclusive,
|
||||
},
|
||||
selector: {
|
||||
id: item.id,
|
||||
id: data.input.item_id,
|
||||
},
|
||||
}
|
||||
})
|
||||
@@ -80,7 +88,7 @@ export const updateLineItemInCartWorkflow = createWorkflow(
|
||||
updateLineItemsStepWithSelector(lineItemUpdate)
|
||||
|
||||
refreshCartItemsWorkflow.runAsStep({
|
||||
input: { cart_id: input.cart.id },
|
||||
input: { cart_id: input.cart_id },
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@ import { PaymentCollectionDTO } from "../payment"
|
||||
import { ProductDTO } from "../product"
|
||||
import { RegionDTO } from "../region"
|
||||
import { BigNumberInput } from "../totals"
|
||||
import { CartDTO, CartLineItemDTO } from "./common"
|
||||
import { CartDTO } from "./common"
|
||||
import {
|
||||
CreateAddressDTO,
|
||||
UpdateAddressDTO,
|
||||
@@ -44,8 +44,8 @@ export interface CreateCartCreateLineItemDTO {
|
||||
}
|
||||
|
||||
export interface UpdateLineItemInCartWorkflowInputDTO {
|
||||
cart: CartDTO
|
||||
item: CartLineItemDTO
|
||||
cart_id: string
|
||||
item_id: string
|
||||
update: Partial<UpdateLineItemDTO>
|
||||
}
|
||||
|
||||
@@ -80,8 +80,8 @@ export interface CreateCartWorkflowInputDTO {
|
||||
}
|
||||
|
||||
export interface AddToCartWorkflowInputDTO {
|
||||
cart_id: string
|
||||
items: CreateCartCreateLineItemDTO[]
|
||||
cart: CartWorkflowDTO
|
||||
}
|
||||
|
||||
export interface UpdateCartWorkflowInputDTO {
|
||||
|
||||
@@ -2,5 +2,9 @@ export type MedusaPricingContext = {
|
||||
region_id?: string
|
||||
currency_code?: string
|
||||
customer_id?: string
|
||||
customer_group_id?: string[]
|
||||
customer?: {
|
||||
groups?: {
|
||||
id: string
|
||||
}[]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import { filterObjectByKeys } from "../filter-object-by-keys"
|
||||
|
||||
describe("filterObjectByKeys", function () {
|
||||
it("should return an object with only the filtered keys", function () {
|
||||
const cart = {
|
||||
id: "cart_id",
|
||||
customer: {
|
||||
id: "cus_id",
|
||||
groups: [
|
||||
{ id: "group_1", name: "test" },
|
||||
{ id: "group_2", name: "test 2" },
|
||||
],
|
||||
},
|
||||
items: [
|
||||
{
|
||||
product_id: "product-1",
|
||||
product: { id: "product-1" },
|
||||
},
|
||||
{
|
||||
product_id: "product-2",
|
||||
product: { id: "product-2" },
|
||||
},
|
||||
],
|
||||
shipping_method: null,
|
||||
}
|
||||
|
||||
let transformedObject = filterObjectByKeys(cart, [
|
||||
"id",
|
||||
"customer.id",
|
||||
"customer.groups.id",
|
||||
"customer.groups.name",
|
||||
"items.product",
|
||||
])
|
||||
|
||||
expect(transformedObject).toEqual({
|
||||
id: "cart_id",
|
||||
customer: {
|
||||
id: "cus_id",
|
||||
groups: [
|
||||
{
|
||||
id: "group_1",
|
||||
name: "test",
|
||||
},
|
||||
{
|
||||
id: "group_2",
|
||||
name: "test 2",
|
||||
},
|
||||
],
|
||||
},
|
||||
items: [
|
||||
{
|
||||
product: {
|
||||
id: "product-1",
|
||||
},
|
||||
},
|
||||
{
|
||||
product: {
|
||||
id: "product-2",
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
transformedObject = filterObjectByKeys(cart, [
|
||||
"id",
|
||||
"customer.id",
|
||||
"customer.groups.id",
|
||||
"customer.groups.name",
|
||||
])
|
||||
|
||||
expect(transformedObject).toEqual({
|
||||
id: "cart_id",
|
||||
customer: {
|
||||
id: "cus_id",
|
||||
groups: [
|
||||
{
|
||||
id: "group_1",
|
||||
name: "test",
|
||||
},
|
||||
{
|
||||
id: "group_2",
|
||||
name: "test 2",
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
transformedObject = filterObjectByKeys(cart, [
|
||||
"id",
|
||||
"customer.id",
|
||||
"customer.groups.id",
|
||||
])
|
||||
|
||||
expect(transformedObject).toEqual({
|
||||
id: "cart_id",
|
||||
customer: {
|
||||
id: "cus_id",
|
||||
groups: [
|
||||
{
|
||||
id: "group_1",
|
||||
},
|
||||
{
|
||||
id: "group_2",
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
transformedObject = filterObjectByKeys(cart, ["id", "customer.id"])
|
||||
|
||||
expect(transformedObject).toEqual({
|
||||
id: "cart_id",
|
||||
customer: {
|
||||
id: "cus_id",
|
||||
},
|
||||
})
|
||||
|
||||
transformedObject = filterObjectByKeys(cart, ["id"])
|
||||
|
||||
expect(transformedObject).toEqual({
|
||||
id: "cart_id",
|
||||
})
|
||||
|
||||
transformedObject = filterObjectByKeys(cart, [])
|
||||
|
||||
expect(transformedObject).toEqual({})
|
||||
|
||||
transformedObject = filterObjectByKeys(cart, [
|
||||
"doesnotexist.doesnotexist",
|
||||
"shipping_method.city",
|
||||
])
|
||||
|
||||
expect(transformedObject).toEqual({
|
||||
shipping_method: null,
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,36 @@
|
||||
import { flattenObjectToKeyValuePairs } from "../flatten-object-to-key-value-pairs"
|
||||
|
||||
describe("flattenObjectToKeyValuePairs", function () {
|
||||
it("should return only the properties path of the properties that are set to true", function () {
|
||||
const cart = {
|
||||
id: "cart_id",
|
||||
customer: {
|
||||
id: "cus_id",
|
||||
groups: [
|
||||
{ id: "group_1", name: "test" },
|
||||
{ id: "group_2", name: "test 2" },
|
||||
],
|
||||
},
|
||||
items: [
|
||||
{
|
||||
product_id: "product-1",
|
||||
product: { id: "product-1" },
|
||||
},
|
||||
{
|
||||
product_id: "product-2",
|
||||
product: { id: "product-2" },
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const keyValueParis = flattenObjectToKeyValuePairs(cart)
|
||||
expect(keyValueParis).toEqual({
|
||||
id: "cart_id",
|
||||
"customer.id": "cus_id",
|
||||
"customer.groups.id": ["group_1", "group_2"],
|
||||
"customer.groups.name": ["test", "test 2"],
|
||||
"items.product_id": ["product-1", "product-2"],
|
||||
"items.product.id": ["product-1", "product-2"],
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,86 @@
|
||||
import { isDefined } from "./is-defined"
|
||||
|
||||
export function filterObjectByKeys(obj, paths) {
|
||||
function buildObject(paths) {
|
||||
const result = {}
|
||||
|
||||
paths.forEach((path) => {
|
||||
const parts = path.split(".")
|
||||
|
||||
// Handle top-level properties
|
||||
if (parts.length === 1) {
|
||||
const [part] = parts
|
||||
if (obj[part] !== undefined) {
|
||||
result[part] = obj[part]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let current = result
|
||||
let source = obj
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const part = parts[i]
|
||||
const isLast = i === parts.length - 1
|
||||
|
||||
if (!isDefined(current) || source === null) {
|
||||
return
|
||||
}
|
||||
// Initialize the current path if it doesn't exist
|
||||
if (!current[part]) {
|
||||
if (Array.isArray(source[part])) {
|
||||
current[part] = source[part].map(() => ({}))
|
||||
} else if (source[part] === null) {
|
||||
current[part] = null
|
||||
} else if (isDefined(source[part])) {
|
||||
current[part] = {}
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(source[part])) {
|
||||
// Get the array path base (e.g., "customer.groups")
|
||||
const arrayPath = parts.slice(0, i + 1).join(".")
|
||||
// Find all paths that start with this array path
|
||||
const relevantPaths = paths
|
||||
.filter((p) => p.startsWith(arrayPath + "."))
|
||||
.map((p) => p.slice(arrayPath.length + 1)) // Remove the array path prefix
|
||||
|
||||
// Update array items with all relevant properties
|
||||
current[part] = source[part].map((item, idx) => {
|
||||
const existingItem = current[part][idx] || {}
|
||||
relevantPaths.forEach((subPath) => {
|
||||
const value = subPath
|
||||
.split(".")
|
||||
.reduce((obj, key) => obj?.[key], item)
|
||||
if (value !== undefined) {
|
||||
let tempObj = existingItem
|
||||
const keys = subPath.split(".")
|
||||
|
||||
keys.slice(0, -1).forEach((key) => {
|
||||
tempObj[key] = tempObj[key] || {}
|
||||
tempObj = tempObj[key]
|
||||
})
|
||||
|
||||
tempObj[keys[keys.length - 1]] = value
|
||||
}
|
||||
})
|
||||
|
||||
return existingItem
|
||||
})
|
||||
break
|
||||
} else {
|
||||
if (isLast) {
|
||||
current[part] = source[part]
|
||||
} else {
|
||||
current = current[part]
|
||||
source = source[part]
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
return buildObject(paths)
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
type NestedObject = {
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export function flattenObjectToKeyValuePairs(obj: NestedObject): NestedObject {
|
||||
const result: NestedObject = {}
|
||||
|
||||
// Find all paths that contain arrays of objects
|
||||
function findArrayPaths(
|
||||
obj: unknown,
|
||||
currentPath: string[] = []
|
||||
): string[][] {
|
||||
const paths: string[][] = []
|
||||
|
||||
if (!obj || typeof obj !== "object") {
|
||||
return paths
|
||||
}
|
||||
|
||||
// If it's an array of objects, add this path
|
||||
if (Array.isArray(obj) && obj.length > 0 && typeof obj[0] === "object") {
|
||||
paths.push(currentPath)
|
||||
}
|
||||
|
||||
// Check all properties
|
||||
if (typeof obj === "object") {
|
||||
Object.entries(obj as Record<string, unknown>).forEach(([key, value]) => {
|
||||
const newPath = [...currentPath, key]
|
||||
paths.push(...findArrayPaths(value, newPath))
|
||||
})
|
||||
}
|
||||
|
||||
return paths
|
||||
}
|
||||
|
||||
// Extract array values at a specific path
|
||||
function getArrayValues(obj: unknown, path: string[]): unknown[] {
|
||||
const arrayObj = path.reduce((acc: unknown, key: string) => {
|
||||
if (acc && typeof acc === "object") {
|
||||
return (acc as Record<string, unknown>)[key]
|
||||
}
|
||||
return undefined
|
||||
}, obj)
|
||||
|
||||
if (!Array.isArray(arrayObj)) return []
|
||||
|
||||
return arrayObj
|
||||
}
|
||||
|
||||
// Process non-array paths
|
||||
function processRegularPaths(obj: unknown, prefix = ""): void {
|
||||
if (!obj || typeof obj !== "object") {
|
||||
result[prefix] = obj
|
||||
return
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) return
|
||||
|
||||
Object.entries(obj as Record<string, unknown>).forEach(([key, value]) => {
|
||||
const newPrefix = prefix ? `${prefix}.${key}` : key
|
||||
if (value && typeof value === "object" && !Array.isArray(value)) {
|
||||
processRegularPaths(value, newPrefix)
|
||||
} else if (!Array.isArray(value)) {
|
||||
result[newPrefix] = value
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Process the object
|
||||
processRegularPaths(obj)
|
||||
|
||||
// Find and process array paths
|
||||
const arrayPaths = findArrayPaths(obj)
|
||||
arrayPaths.forEach((path) => {
|
||||
const pathStr = path.join(".")
|
||||
const arrayObjects = getArrayValues(obj, path)
|
||||
|
||||
if (Array.isArray(arrayObjects) && arrayObjects.length > 0) {
|
||||
// Get all possible keys from the array objects
|
||||
const keys = new Set<string>()
|
||||
arrayObjects.forEach((item) => {
|
||||
if (item && typeof item === "object") {
|
||||
Object.keys(item as object).forEach((k) => keys.add(k))
|
||||
}
|
||||
})
|
||||
|
||||
// Process each key
|
||||
keys.forEach((key) => {
|
||||
const values = arrayObjects
|
||||
.map((item) => {
|
||||
if (item && typeof item === "object") {
|
||||
return (item as Record<string, unknown>)[key]
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
.filter((v) => v !== undefined)
|
||||
|
||||
if (values.length > 0) {
|
||||
const newPath = `${pathStr}.${key}`
|
||||
if (values.every((v) => typeof v === "object" && !Array.isArray(v))) {
|
||||
// If these are all objects, recursively process them
|
||||
const subObj = { [key]: values }
|
||||
const subResult = flattenObjectToKeyValuePairs(subObj)
|
||||
Object.entries(subResult).forEach(([k, v]) => {
|
||||
const finalPath = `${pathStr}.${k}`
|
||||
result[finalPath] = v
|
||||
})
|
||||
} else {
|
||||
result[newPath] = values
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -18,7 +18,9 @@ export * from "./dynamic-import"
|
||||
export * from "./env-editor"
|
||||
export * from "./errors"
|
||||
export * from "./file-system"
|
||||
export * from "./filter-object-by-keys"
|
||||
export * from "./filter-operator-map"
|
||||
export * from "./flatten-object-to-key-value-pairs"
|
||||
export * from "./generate-entity-id"
|
||||
export * from "./get-caller-file-path"
|
||||
export * from "./get-config-file"
|
||||
|
||||
Reference in New Issue
Block a user