diff --git a/packages/core-flows/src/region/steps/update-regions.ts b/packages/core-flows/src/region/steps/update-regions.ts index daec4ad790832..b46bab0f5e0ca 100644 --- a/packages/core-flows/src/region/steps/update-regions.ts +++ b/packages/core-flows/src/region/steps/update-regions.ts @@ -2,14 +2,14 @@ import { ModuleRegistrationName } from "@medusajs/modules-sdk" import { FilterableRegionProps, IRegionModuleService, - UpdatableRegionFields, + UpdateRegionDTO, } from "@medusajs/types" import { getSelectsAndRelationsFromObjectArray } from "@medusajs/utils" import { StepResponse, createStep } from "@medusajs/workflows-sdk" type UpdateRegionsStepInput = { selector: FilterableRegionProps - update: UpdatableRegionFields + update: UpdateRegionDTO } export const updateRegionsStepId = "update-region" @@ -42,7 +42,7 @@ export const updateRegionsStep = createStep( ModuleRegistrationName.REGION ) - await service.update( + await service.upsert( prevData.map((r) => ({ id: r.id, name: r.name, diff --git a/packages/core-flows/src/region/workflows/update-regions.ts b/packages/core-flows/src/region/workflows/update-regions.ts index 601fdf7fb1585..50855dc3a7670 100644 --- a/packages/core-flows/src/region/workflows/update-regions.ts +++ b/packages/core-flows/src/region/workflows/update-regions.ts @@ -1,14 +1,14 @@ import { FilterableRegionProps, RegionDTO, - UpdatableRegionFields, + UpdateRegionDTO, } from "@medusajs/types" import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" import { updateRegionsStep } from "../steps" type UpdateRegionsStepInput = { selector: FilterableRegionProps - update: UpdatableRegionFields + update: UpdateRegionDTO } type WorkflowInput = UpdateRegionsStepInput diff --git a/packages/medusa/src/api-v2/admin/regions/[id]/route.ts b/packages/medusa/src/api-v2/admin/regions/[id]/route.ts index 3167c36430336..c0c9d62c05022 100644 --- a/packages/medusa/src/api-v2/admin/regions/[id]/route.ts +++ b/packages/medusa/src/api-v2/admin/regions/[id]/route.ts @@ -2,7 +2,7 @@ import { deleteRegionsWorkflow, updateRegionsWorkflow, } from "@medusajs/core-flows" -import { UpdatableRegionFields } from "@medusajs/types" +import { UpdateRegionDTO } from "@medusajs/types" import { remoteQueryObjectFromString } from "@medusajs/utils" import { MedusaRequest, MedusaResponse } from "../../../../types/routing" import { defaultAdminRegionFields } from "../query-config" @@ -27,7 +27,7 @@ export const POST = async (req: MedusaRequest, res: MedusaResponse) => { const { result, errors } = await updateRegionsWorkflow(req.scope).run({ input: { selector: { id: req.params.id }, - update: req.validatedBody as UpdatableRegionFields, + update: req.validatedBody as UpdateRegionDTO, }, throwOnError: false, }) diff --git a/packages/region/integration-tests/__tests__/region-module.spec.ts b/packages/region/integration-tests/__tests__/region-module.spec.ts index 42c8a365cd2d9..f3077685b6afb 100644 --- a/packages/region/integration-tests/__tests__/region-module.spec.ts +++ b/packages/region/integration-tests/__tests__/region-module.spec.ts @@ -167,6 +167,68 @@ describe("Region Module Service", () => { ) }) + it("should upsert the region successfully", async () => { + const createdRegion = await service.upsert({ + name: "North America", + currency_code: "USD", + countries: ["us", "ca"], + }) + + await service.upsert({ + id: createdRegion.id, + name: "Americas", + currency_code: "MXN", + countries: ["us", "mx"], + }) + + const latestRegion = await service.retrieve(createdRegion.id, { + relations: ["currency", "countries"], + }) + + expect(latestRegion).toMatchObject({ + id: createdRegion.id, + name: "Americas", + currency_code: "mxn", + }) + expect(latestRegion.countries.map((c) => c.iso_2)).toEqual(["mx", "us"]) + }) + + it("should allow mixing create and update operations in upsert", async () => { + const createdRegion = await service.upsert({ + name: "North America", + currency_code: "USD", + countries: ["us", "ca"], + }) + + const upserted = await service.upsert([ + { + id: createdRegion.id, + name: "Americas", + currency_code: "USD", + countries: ["us", "ca"], + }, + { + name: "Central America", + currency_code: "MXN", + countries: ["mx"], + }, + ]) + + expect(upserted).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: createdRegion.id, + name: "Americas", + currency_code: "usd", + }), + expect.objectContaining({ + name: "Central America", + currency_code: "mxn", + }), + ]) + ) + }) + it("should update the region successfully", async () => { const createdRegion = await service.create({ name: "North America", diff --git a/packages/region/src/services/region-module.ts b/packages/region/src/services/region-module.ts index 997cca7f3a16e..5f78948283fb4 100644 --- a/packages/region/src/services/region-module.ts +++ b/packages/region/src/services/region-module.ts @@ -10,14 +10,13 @@ import { RegionCountryDTO, RegionCurrencyDTO, RegionDTO, - UpdatableRegionFields, UpdateRegionDTO, + UpsertRegionDTO, } from "@medusajs/types" import { arrayDifference, InjectManager, InjectTransactionManager, - isObject, isString, MedusaContext, MedusaError, @@ -30,7 +29,7 @@ import { import { Country, Currency, Region } from "@models" -import { CreateCountryDTO, CreateCurrencyDTO } from "@types" +import { CreateCountryDTO, CreateCurrencyDTO, UpdateRegionInput } from "@types" import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config" const COUNTRIES_LIMIT = 1000 @@ -143,54 +142,64 @@ export default class RegionModuleService< return await this.regionService_.create(normalizedDbRegions, sharedContext) } - async update( - selector: FilterableRegionProps, - data: UpdatableRegionFields, + async upsert( + data: UpsertRegionDTO[], sharedContext?: Context ): Promise - async update( - regionId: string, - data: UpdatableRegionFields, + async upsert( + data: UpsertRegionDTO, sharedContext?: Context ): Promise - async update(data: UpdateRegionDTO[]): Promise - @InjectManager("baseRepository_") - async update( - idOrSelectorOrData: string | FilterableRegionProps | UpdateRegionDTO[], - data?: UpdatableRegionFields, + @InjectTransactionManager("baseRepository_") + async upsert( + data: UpsertRegionDTO | UpsertRegionDTO[], @MedusaContext() sharedContext: Context = {} ): Promise { - const updateResult = await this.update_( - idOrSelectorOrData, - data, - sharedContext + const input = Array.isArray(data) ? data : [data] + const forUpdate = input.filter( + (region): region is UpdateRegionInput => !!region.id + ) + const forCreate = input.filter( + (region): region is CreateRegionDTO => !region.id ) - const regions = await this.baseRepository_.serialize< - RegionDTO[] | RegionDTO - >(updateResult) - - return isString(idOrSelectorOrData) ? regions[0] : regions - } + const operations: Promise[] = [] - @InjectTransactionManager("baseRepository_") - protected async update_( - idOrSelectorOrData: string | FilterableRegionProps | UpdateRegionDTO[], - data?: UpdatableRegionFields, - @MedusaContext() sharedContext: Context = {} - ): Promise { - let normalizedInput: UpdateRegionDTO[] = [] - if (isString(idOrSelectorOrData)) { - normalizedInput = [{ id: idOrSelectorOrData, ...data }] + if (forCreate.length) { + operations.push(this.create_(forCreate, sharedContext)) } - - if (Array.isArray(idOrSelectorOrData)) { - normalizedInput = idOrSelectorOrData + if (forUpdate.length) { + operations.push(this.update_(forUpdate, sharedContext)) } - if (isObject(idOrSelectorOrData)) { + const result = (await promiseAll(operations)).flat() + return await this.baseRepository_.serialize( + Array.isArray(data) ? result : result[0] + ) + } + + async update( + id: string, + data: UpdateRegionDTO, + sharedContext?: Context + ): Promise + async update( + selector: FilterableRegionProps, + data: UpdateRegionDTO, + sharedContext?: Context + ): Promise + @InjectManager("baseRepository_") + async update( + idOrSelector: string | FilterableRegionProps, + data: UpdateRegionDTO, + @MedusaContext() sharedContext: Context = {} + ): Promise { + let normalizedInput: UpdateRegionInput[] = [] + if (isString(idOrSelector)) { + normalizedInput = [{ id: idOrSelector, ...data }] + } else { const regions = await this.regionService_.list( - idOrSelectorOrData, + idOrSelector, {}, sharedContext ) @@ -200,7 +209,22 @@ export default class RegionModuleService< ...data, })) } - normalizedInput = RegionModuleService.normalizeInput(normalizedInput) + + const updateResult = await this.update_(normalizedInput, sharedContext) + + const regions = await this.baseRepository_.serialize< + RegionDTO[] | RegionDTO + >(updateResult) + + return isString(idOrSelector) ? regions[0] : regions + } + + @InjectTransactionManager("baseRepository_") + protected async update_( + data: UpdateRegionInput[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + const normalizedInput = RegionModuleService.normalizeInput(data) // If countries are being updated for a region, first make previously set countries' region to null to get to a clean slate. // Somewhat less efficient, but region operations will be very rare, so it is better to go with a simple solution @@ -245,9 +269,7 @@ export default class RegionModuleService< return await this.regionService_.update(normalizedDbRegions, sharedContext) } - private static normalizeInput( - regions: T[] - ): T[] { + private static normalizeInput(regions: T[]): T[] { return regions.map((region) => removeUndefined({ ...region, diff --git a/packages/region/src/types/index.ts b/packages/region/src/types/index.ts index e4f28fb1c6813..f28b0869da103 100644 --- a/packages/region/src/types/index.ts +++ b/packages/region/src/types/index.ts @@ -1,4 +1,4 @@ -import { Logger } from "@medusajs/types" +import { Logger, UpdateRegionDTO } from "@medusajs/types" import { Country } from "@models" export type InitializeModuleInjectableDependencies = { @@ -24,3 +24,5 @@ export type CreateCountryDTO = { name: string display_name: string } + +export type UpdateRegionInput = UpdateRegionDTO & { id: string } diff --git a/packages/types/src/region/mutations.ts b/packages/types/src/region/mutations.ts index d7707288510e7..746ad0d57f602 100644 --- a/packages/types/src/region/mutations.ts +++ b/packages/types/src/region/mutations.ts @@ -22,16 +22,13 @@ export interface CreateRegionDTO { metadata?: Record } -/** - * The attributes to update in the region. - */ -export interface UpdateRegionDTO { +export interface UpsertRegionDTO { /** - * The ID of the region. + * The id of the region in the case of an update */ - id: string + id?: string /** - * The name of the region. + * The target name of the region */ name?: string /** @@ -48,12 +45,9 @@ export interface UpdateRegionDTO { metadata?: Record } -/** - * The updatable fields of a region. - */ -export interface UpdatableRegionFields { +export interface UpdateRegionDTO { /** - * The name of the region. + * The target name of the region */ name?: string /** diff --git a/packages/types/src/region/service.ts b/packages/types/src/region/service.ts index 3eefa490ed060..4b51152ff5168 100644 --- a/packages/types/src/region/service.ts +++ b/packages/types/src/region/service.ts @@ -10,11 +10,7 @@ import { RegionCurrencyDTO, RegionDTO, } from "./common" -import { - CreateRegionDTO, - UpdatableRegionFields, - UpdateRegionDTO, -} from "./mutations" +import { CreateRegionDTO, UpsertRegionDTO, UpdateRegionDTO } from "./mutations" /** * The main service interface for the region module. @@ -45,47 +41,58 @@ export interface IRegionModuleService extends IModuleService { create(data: CreateRegionDTO, sharedContext?: Context): Promise /** - * This method updates existing regions. + * This method updates existing regions, or creates new ones if they don't exist. * - * @param {UpdateRegionDTO[]} data - The attributes to update in each region. - * @returns {Promise} The updated regions. + * @param {UpsertRegionDTO[]} data - The attributes to update or create in each region. + * @returns {Promise} The updated and created regions. * * @example * {example-code} */ - update(data: UpdateRegionDTO[]): Promise + upsert(data: UpsertRegionDTO[], sharedContext?: Context): Promise /** - * This method updates existing regions. + * This method updates an existing region, or creates a new one if it doesn't exist. * - * @param {FilterableRegionProps} selector - The filters to specify which regions should be updated. - * @param {UpdatableRegionFields} data - The details to update in the regions. - * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. - * @returns {Promise} The updated regions. + * @param {UpsertRegionDTO} data - The attributes to update or create for the region. + * @returns {Promise} The updated or created region. * * @example * {example-code} */ - update( - selector: FilterableRegionProps, - data: UpdatableRegionFields, - sharedContext?: Context - ): Promise + upsert(data: UpsertRegionDTO, sharedContext?: Context): Promise /** * This method updates an existing region. * - * @param {string} regionId - The region's ID. - * @param {UpdatableRegionFields} data - The details to update in the regions. + * @param {string} id - The region's ID. + * @param {UpdatableRegionFields} data - The details to update in the region. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The updated region. */ update( - regionId: string, - data: UpdatableRegionFields, + id: string, + data: UpdateRegionDTO, sharedContext?: Context ): Promise + /** + * This method updates existing regions. + * + * @param {FilterableRegionProps} selector - The filters to specify which regions should be updated. + * @param {UpdateRegionDTO} data - The details to update in the regions. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The updated regions. + * + * @example + * {example-code} + */ + update( + selector: FilterableRegionProps, + data: UpdateRegionDTO, + sharedContext?: Context + ): Promise + /** * This method deletes regions by their IDs. *