fix(stock-location,core-flows,types): updates existing address when updating stock location (#10832)

* fix(stock-location,core-flows,types): updates existing address when updating stock location address

* chore: use hasOne instead of hasMany
This commit is contained in:
Riqwan Thamir
2025-01-07 07:55:28 +01:00
committed by GitHub
parent fde73dbfae
commit 99a06102a2
10 changed files with 336 additions and 7 deletions

View File

@@ -0,0 +1,7 @@
---
"@medusajs/stock-location": patch
"@medusajs/core-flows": patch
"@medusajs/types": patch
---
fix(stock-location,core-flows,types): update existing address when updating stock location address

View File

@@ -165,6 +165,58 @@ medusaIntegrationTestRunner({
expect(response.status).toEqual(200)
expect(response.data.stock_location.name).toEqual("new name")
})
it("should update stock location address without creating new addresses", async () => {
const response = await api.post(
`/admin/stock-locations/${location1.id}`,
{
name: "new name",
address: {
address_1: "test",
country_code: "dk",
},
},
adminHeaders
)
const firstAddressId = response.data.stock_location.address.id
expect(response.status).toEqual(200)
expect(response.data.stock_location).toEqual(
expect.objectContaining({
name: "new name",
address: expect.objectContaining({
id: firstAddressId,
address_1: "test",
country_code: "dk",
}),
})
)
const response2 = await api.post(
`/admin/stock-locations/${location1.id}`,
{
name: "new name 2",
address: {
address_1: "test 2",
country_code: "dk",
},
},
adminHeaders
)
expect(response2.status).toEqual(200)
expect(response2.data.stock_location).toEqual(
expect.objectContaining({
name: "new name 2",
address: expect.objectContaining({
id: firstAddressId,
address_1: "test 2",
country_code: "dk",
}),
})
)
})
})
describe("Get stock location", () => {

View File

@@ -0,0 +1,73 @@
import {
IStockLocationService,
UpsertStockLocationAddressInput,
} from "@medusajs/framework/types"
import {
getSelectsAndRelationsFromObjectArray,
promiseAll,
} from "@medusajs/framework/utils"
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"
export const upsertStockLocationAddressesStepId =
"upsert-stock-location-addresses-step"
/**
* This step upserts stock location addresses matching the specified filters.
*/
export const upsertStockLocationAddressesStep = createStep(
upsertStockLocationAddressesStepId,
async (input: UpsertStockLocationAddressInput[], { container }) => {
const stockLocationService = container.resolve<IStockLocationService>(
Modules.STOCK_LOCATION
)
const stockLocationAddressIds = input.map((i) => i.id!).filter(Boolean)
const { selects, relations } = getSelectsAndRelationsFromObjectArray(input)
const dataToUpdate = await stockLocationService.listStockLocationAddresses(
{ id: stockLocationAddressIds },
{ select: selects, relations }
)
const updateIds = dataToUpdate.map((du) => du.id)
const updatedAddresses =
await stockLocationService.upsertStockLocationAddresses(input)
const dataToDelete = updatedAddresses.filter(
(address) => !updateIds.includes(address.id)
)
return new StepResponse(updatedAddresses, { dataToUpdate, dataToDelete })
},
async (revertData, { container }) => {
if (!revertData) {
return
}
const stockLocationService = container.resolve<IStockLocationService>(
Modules.STOCK_LOCATION
)
const promises: any[] = []
if (revertData.dataToDelete) {
promises.push(
stockLocationService.deleteStockLocationAddresses(
revertData.dataToDelete.map((d) => d.id!)
)
)
}
if (revertData.dataToUpdate) {
promises.push(
stockLocationService.upsertStockLocationAddresses(
revertData.dataToUpdate
)
)
}
await promiseAll(promises)
}
)

View File

@@ -1,15 +1,19 @@
import {
FilterableStockLocationProps,
StockLocationDTO,
UpdateStockLocationInput,
FilterableStockLocationProps,
UpsertStockLocationAddressInput,
} from "@medusajs/framework/types"
import {
WorkflowData,
WorkflowResponse,
createWorkflow,
transform,
} from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "../../common"
import { updateStockLocationsStep } from "../steps"
import { upsertStockLocationAddressesStep } from "../steps/upsert-stock-location-addresses"
export interface UpdateStockLocationsWorkflowInput {
selector: FilterableStockLocationProps
@@ -24,6 +28,50 @@ export const updateStockLocationsWorkflow = createWorkflow(
(
input: WorkflowData<UpdateStockLocationsWorkflowInput>
): WorkflowResponse<StockLocationDTO[]> => {
return new WorkflowResponse(updateStockLocationsStep(input))
const stockLocationsQuery = useQueryGraphStep({
entity: "stock_location",
filters: input.selector,
fields: ["id", "address.id"],
}).config({ name: "get-stock-location" })
const stockLocations = transform(
{ stockLocationsQuery },
({ stockLocationsQuery }) => stockLocationsQuery.data
)
const normalizedData = transform(
{ input, stockLocations },
({ input, stockLocations }) => {
const { address, address_id, ...stockLocationInput } = input.update
const addressesInput: UpsertStockLocationAddressInput[] = []
if (address) {
for (const stockLocation of stockLocations) {
if (stockLocation.address?.id) {
addressesInput.push({
id: stockLocation.address?.id!,
...address,
})
} else {
addressesInput.push(address)
}
}
}
return {
stockLocationInput: {
selector: input.selector,
update: stockLocationInput,
},
addressesInput,
}
}
)
upsertStockLocationAddressesStep(normalizedData.addressesInput)
return new WorkflowResponse(
updateStockLocationsStep(normalizedData.stockLocationInput)
)
}
)

View File

@@ -452,3 +452,19 @@ export type UpsertStockLocationInput = Partial<UpdateStockLocationInput> & {
*/
id?: string
}
export type UpdateStockLocationAddressInput = StockLocationAddressInput & {
id: string
}
export type UpsertStockLocationAddressInput = StockLocationAddressInput & {
id?: string
}
export interface FilterableStockLocationAddressProps
extends BaseFilterable<FilterableStockLocationAddressProps> {
/**
* The IDs to filter stock location's address by.
*/
id?: string | string[]
}

View File

@@ -1,14 +1,17 @@
import { FindConfig } from "../common/common"
import { RestoreReturn, SoftDeleteReturn } from "../dal"
import { IModuleService } from "../modules-sdk"
import { Context } from "../shared-context"
import {
CreateStockLocationInput,
FilterableStockLocationAddressProps,
FilterableStockLocationProps,
StockLocationAddressDTO,
StockLocationDTO,
UpdateStockLocationInput,
UpsertStockLocationAddressInput,
UpsertStockLocationInput,
} from "./common"
import { RestoreReturn, SoftDeleteReturn } from "../dal"
import { Context } from "../shared-context"
import { FindConfig } from "../common/common"
import { IModuleService } from "../modules-sdk"
/**
* The main service interface for the Stock Location Module.
@@ -333,4 +336,50 @@ export interface IStockLocationService extends IModuleService {
config?: RestoreReturn<TReturnableLinkableKeys>,
sharedContext?: Context
): Promise<Record<string, string[]> | void>
/**
* This method retrieves a paginated list of stock location addresses based on optional filters and configuration.
*
* @param {FilterableStockLocationAddressProps} selector - The filters to apply on the retrieved stock location address.
* @param {FindConfig<StockLocationAddressDTO>} config - The configurations determining how the stock location address is retrieved. Its properties, such as `select` or `relations`, accept the
* attributes or relations associated with a stock location address.
* @param {Context} context - A context used to share resources, such as transaction manager, between the application and the module.
* @returns {Promise<StockLocationAddressDTO[]>} The list of stock location addressess.
*
*/
listStockLocationAddresses(
selector: FilterableStockLocationAddressProps,
config?: FindConfig<StockLocationAddressDTO>,
context?: Context
): Promise<StockLocationAddressDTO[]>
/**
* This method updates or creates stock location addresses
*
* @param {Partial<UpsertStockLocationAddressInput>[]} data - The list of Make all properties in t optional
* @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module.
* @returns {Promise<StockLocationAddressDTO[]>} The created or updated stock location address
*
* @example
* {example-code}
*/
upsertStockLocationAddresses(
data: UpsertStockLocationAddressInput[],
sharedContext?: Context
): Promise<StockLocationAddressDTO[]>
/**
* This method deletes a stock location address by its ID.
*
* @param {string} id - The ID of the stock location address.
* @param {Context} context - A context used to share resources, such as transaction manager, between the application and the module.
* @returns {Promise<void>} Resolves when the stock location address is deleted successfully.
*
* @example
* await stockLocationModuleService.deleteStockLocationAddresses("sla_123")
*/
deleteStockLocationAddresses(
id: string | string[],
context?: Context
): Promise<void>
}

View File

@@ -227,6 +227,15 @@
"name": "stock_location",
"schema": "public",
"indexes": [
{
"columnNames": [
"address_id"
],
"composite": false,
"keyName": "stock_location_address_id_unique",
"primary": false,
"unique": true
},
{
"keyName": "IDX_stock_location_address_id",
"columnNames": [],

View File

@@ -0,0 +1,15 @@
import { Migration } from "@mikro-orm/migrations"
export class Migration20250106142624 extends Migration {
async up(): Promise<void> {
this.addSql(
'alter table if exists "stock_location" add constraint "stock_location_address_id_unique" unique ("address_id");'
)
}
async down(): Promise<void> {
this.addSql(
'alter table if exists "stock_location" drop constraint if exists "stock_location_address_id_unique";'
)
}
}

View File

@@ -13,7 +13,7 @@ const StockLocationAddress = model
province: model.text().nullable(),
postal_code: model.text().nullable(),
metadata: model.json().nullable(),
stock_locations: model.hasMany(() => StockLocation, {
stock_locations: model.hasOne(() => StockLocation, {
mappedBy: "address",
}),
})

View File

@@ -11,7 +11,9 @@ import {
ModulesSdkTypes,
StockLocationAddressInput,
StockLocationTypes,
UpdateStockLocationAddressInput,
UpdateStockLocationInput,
UpsertStockLocationAddressInput,
UpsertStockLocationInput,
} from "@medusajs/framework/types"
import {
@@ -258,4 +260,62 @@ export default class StockLocationModuleService
) {
return await this.stockLocationAddressService_.update(input, context)
}
async upsertStockLocationAddresses(
data: UpsertStockLocationAddressInput,
context?: Context
): Promise<StockLocationTypes.StockLocationAddressDTO>
async upsertStockLocationAddresses(
data: UpsertStockLocationAddressInput[],
context?: Context
): Promise<StockLocationTypes.StockLocationAddressDTO[]>
@InjectManager()
async upsertStockLocationAddresses(
data: UpsertStockLocationAddressInput | UpsertStockLocationAddressInput[],
@MedusaContext() context: Context = {}
): Promise<
| StockLocationTypes.StockLocationAddressDTO
| StockLocationTypes.StockLocationAddressDTO[]
> {
const input = Array.isArray(data) ? data : [data]
const result = await this.upsertStockLocationAddresses_(input, context)
return await this.baseRepository_.serialize<
| StockLocationTypes.StockLocationAddressDTO[]
| StockLocationTypes.StockLocationAddressDTO
>(Array.isArray(data) ? result : result[0])
}
@InjectTransactionManager()
async upsertStockLocationAddresses_(
input: UpsertStockLocationAddressInput[],
@MedusaContext() context: Context = {}
) {
const toUpdate = input.filter(
(location): location is UpdateStockLocationAddressInput => !!location.id
) as UpdateStockLocationAddressInput[]
const toCreate = input.filter(
(location) => !location.id
) as StockLocationAddressInput[]
const operations: Promise<
| InferEntityType<typeof StockLocationAddress>[]
| InferEntityType<typeof StockLocationAddress>
>[] = []
if (toCreate.length) {
operations.push(
this.stockLocationAddressService_.create(toCreate, context)
)
}
if (toUpdate.length) {
operations.push(
this.stockLocationAddressService_.update(toUpdate, context)
)
}
return (await promiseAll(operations)).flat()
}
}