diff --git a/.changeset/silent-files-kiss.md b/.changeset/silent-files-kiss.md new file mode 100644 index 0000000000..87daa01cb5 --- /dev/null +++ b/.changeset/silent-files-kiss.md @@ -0,0 +1,7 @@ +--- +"@medusajs/core-flows": patch +"@medusajs/medusa": patch +"@medusajs/types": patch +--- + +feat(core-flows, medusa): add update stock location endpoint to api-v2 diff --git a/integration-tests/api/__tests__/admin/stock-location/index.spec.ts b/integration-tests/api/__tests__/admin/stock-location/index.spec.ts index c97a8fc7f3..fed384a8f7 100644 --- a/integration-tests/api/__tests__/admin/stock-location/index.spec.ts +++ b/integration-tests/api/__tests__/admin/stock-location/index.spec.ts @@ -56,6 +56,35 @@ medusaIntegrationTestRunner({ }) }) + describe("Update stock locations", () => { + let stockLocationId + + beforeEach(async () => { + const createResponse = await api.post( + `/admin/stock-locations`, + { + name: "test location", + }, + adminHeaders + ) + + stockLocationId = createResponse.data.stock_location.id + }) + + it("should update stock location name", async () => { + const response = await api.post( + `/admin/stock-locations/${stockLocationId}`, + { + name: "new name", + }, + adminHeaders + ) + expect(response.status).toEqual(200) + + expect(response.data.stock_location.name).toEqual("new name") + }) + }) + describe("Get stock location", () => { let locationId const location = { diff --git a/packages/core-flows/src/stock-location/steps/index.ts b/packages/core-flows/src/stock-location/steps/index.ts index f15c405fec..73d2e82ffc 100644 --- a/packages/core-flows/src/stock-location/steps/index.ts +++ b/packages/core-flows/src/stock-location/steps/index.ts @@ -1,2 +1,3 @@ export * from "./create-stock-locations" +export * from "./update-stock-locations" export * from "./delete-stock-locations" diff --git a/packages/core-flows/src/stock-location/steps/update-stock-locations.ts b/packages/core-flows/src/stock-location/steps/update-stock-locations.ts new file mode 100644 index 0000000000..9a3ea1ec66 --- /dev/null +++ b/packages/core-flows/src/stock-location/steps/update-stock-locations.ts @@ -0,0 +1,61 @@ +import { + FilterableStockLocationProps, + IStockLocationServiceNext, + UpdateStockLocationNextInput, +} from "@medusajs/types" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" +import { + convertItemResponseToUpdateRequest, + getSelectsAndRelationsFromObjectArray, +} from "@medusajs/utils" + +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { UpdateStockLocationInput } from "@medusajs/types" + +interface StepInput { + selector: FilterableStockLocationProps + update: UpdateStockLocationInput +} + +export const updateStockLocationsStepId = "update-stock-locations-step" +export const updateStockLocationsStep = createStep( + updateStockLocationsStepId, + async (input: StepInput, { container }) => { + const stockLocationService = container.resolve( + ModuleRegistrationName.STOCK_LOCATION + ) + const { selects, relations } = getSelectsAndRelationsFromObjectArray([ + input.update, + ]) + + const dataBeforeUpdate = await stockLocationService.list(input.selector, { + select: selects, + relations, + }) + + const updatedStockLocations = await stockLocationService.update( + input.selector, + input.update + ) + + return new StepResponse(updatedStockLocations, dataBeforeUpdate) + }, + async (revertInput, { container }) => { + if (!revertInput?.length) { + return + } + + const stockLocationService = container.resolve( + ModuleRegistrationName.STOCK_LOCATION + ) + + await stockLocationService.upsert( + revertInput.map((item) => ({ + id: item.id, + name: item.name, + ...(item.metadata ? { metadata: item.metadata } : {}), + ...(item.address ? { address: item.address } : {}), + })) + ) + } +) diff --git a/packages/core-flows/src/stock-location/workflows/index.ts b/packages/core-flows/src/stock-location/workflows/index.ts index f15c405fec..73d2e82ffc 100644 --- a/packages/core-flows/src/stock-location/workflows/index.ts +++ b/packages/core-flows/src/stock-location/workflows/index.ts @@ -1,2 +1,3 @@ export * from "./create-stock-locations" +export * from "./update-stock-locations" export * from "./delete-stock-locations" diff --git a/packages/core-flows/src/stock-location/workflows/update-stock-locations.ts b/packages/core-flows/src/stock-location/workflows/update-stock-locations.ts new file mode 100644 index 0000000000..2ece07f72d --- /dev/null +++ b/packages/core-flows/src/stock-location/workflows/update-stock-locations.ts @@ -0,0 +1,22 @@ +import { + InventoryNext, + StockLocationDTO, + UpdateStockLocationNextInput, +} from "@medusajs/types" +import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" + +import { FilterableStockLocationProps } from "@medusajs/types" +import { UpdateStockLocationInput } from "@medusajs/types" +import { updateStockLocationsStep } from "../steps" + +interface WorkflowInput { + selector: FilterableStockLocationProps + update: UpdateStockLocationInput +} +export const updateStockLocationsWorkflowId = "update-stock-locations-workflow" +export const updateStockLocationsWorkflow = createWorkflow( + updateStockLocationsWorkflowId, + (input: WorkflowData): WorkflowData => { + return updateStockLocationsStep(input) + } +) diff --git a/packages/medusa/src/api-v2/admin/stock-locations/[id]/route.ts b/packages/medusa/src/api-v2/admin/stock-locations/[id]/route.ts index 678d4b86da..d7326c6cac 100644 --- a/packages/medusa/src/api-v2/admin/stock-locations/[id]/route.ts +++ b/packages/medusa/src/api-v2/admin/stock-locations/[id]/route.ts @@ -4,8 +4,40 @@ import { } from "@medusajs/utils" import { MedusaRequest, MedusaResponse } from "../../../../types/routing" +import { AdminPostStockLocationsLocationReq } from "../validators" import { MedusaError } from "@medusajs/utils" import { deleteStockLocationsWorkflow } from "@medusajs/core-flows" +import { updateStockLocationsWorkflow } from "@medusajs/core-flows" + +export const POST = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + const { id } = req.params + + await updateStockLocationsWorkflow(req.scope).run({ + input: { + selector: { id: req.params.id }, + update: req.validatedBody, + }, + }) + + const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY) + + const [stock_location] = await remoteQuery( + remoteQueryObjectFromString({ + entryPoint: "stock_locations", + variables: { + id, + }, + fields: req.remoteQueryConfig.fields, + }) + ) + + res.status(200).json({ + stock_location, + }) +} export const GET = async (req: MedusaRequest, res: MedusaResponse) => { const { id } = req.params diff --git a/packages/medusa/src/api-v2/admin/stock-locations/middlewares.ts b/packages/medusa/src/api-v2/admin/stock-locations/middlewares.ts index 6e0f77fae9..b26a871d86 100644 --- a/packages/medusa/src/api-v2/admin/stock-locations/middlewares.ts +++ b/packages/medusa/src/api-v2/admin/stock-locations/middlewares.ts @@ -2,6 +2,8 @@ import * as QueryConfig from "./query-config" import { AdminGetStockLocationsLocationParams, + AdminPostStockLocationsLocationParams, + AdminPostStockLocationsLocationReq, AdminPostStockLocationsParams, AdminPostStockLocationsReq, } from "./validators" @@ -27,6 +29,17 @@ export const adminStockLocationRoutesMiddlewares: MiddlewareRoute[] = [ ), ], }, + { + method: ["POST"], + matcher: "/admin/stock-locations/:id", + middlewares: [ + transformBody(AdminPostStockLocationsLocationReq), + transformQuery( + AdminPostStockLocationsLocationParams, + QueryConfig.retrieveTransformQueryConfig + ), + ], + }, { method: ["GET"], matcher: "/admin/stock-locations/:id", diff --git a/packages/medusa/src/api-v2/admin/stock-locations/validators.ts b/packages/medusa/src/api-v2/admin/stock-locations/validators.ts index 5cfeb83a55..3e2c1d3f06 100644 --- a/packages/medusa/src/api-v2/admin/stock-locations/validators.ts +++ b/packages/medusa/src/api-v2/admin/stock-locations/validators.ts @@ -60,7 +60,7 @@ import { IsType } from "../../../utils" * description: Stock location address' province * example: Sinaloa */ -class StockLocationAddress { +class StockLocationCreateAddress { @IsString() address_1: string @@ -124,8 +124,8 @@ export class AdminPostStockLocationsReq { @IsOptional() @ValidateNested() - @Type(() => StockLocationAddress) - address?: StockLocationAddress + @Type(() => StockLocationCreateAddress) + address?: StockLocationCreateAddress @IsOptional() @IsString() @@ -138,4 +138,106 @@ export class AdminPostStockLocationsReq { export class AdminPostStockLocationsParams extends FindParams {} +/** + * The attributes of a stock location address to create or update. + */ +class StockLocationUpdateAddress { + /** + * First line address. + */ + @IsString() + address_1: string + + /** + * Second line address. + */ + @IsOptional() + @IsString() + address_2?: string + + /** + * Company. + */ + @IsOptional() + @IsString() + company?: string + + /** + * City. + */ + @IsOptional() + @IsString() + city?: string + + /** + * Country code. + */ + @IsString() + country_code: string + + /** + * Phone. + */ + @IsOptional() + @IsString() + phone?: string + + /** + * Postal code. + */ + @IsOptional() + @IsString() + postal_code?: string + + /** + * Province. + */ + @IsOptional() + @IsString() + province?: string +} + +/** + * @schema AdminPostStockLocationsLocationReq + * type: object + * description: "The details to update of the stock location." + * properties: + * name: + * description: the name of the stock location + * type: string + * address_id: + * description: the stock location address ID + * type: string + * metadata: + * type: object + * description: An optional key-value map with additional details + * example: {car: "white"} + * externalDocs: + * description: "Learn about the metadata attribute, and how to delete and update it." + * url: "https://docs.medusajs.com/development/entities/overview#metadata-attribute" + * address: + * description: The data of an associated address to create or update. + * $ref: "#/components/schemas/StockLocationAddressInput" + */ +export class AdminPostStockLocationsLocationReq { + @IsOptional() + @IsString() + name?: string + + @IsOptional() + @ValidateNested() + @Type(() => StockLocationUpdateAddress) + address?: StockLocationUpdateAddress + + @IsOptional() + @IsString() + address_id?: string + + @IsObject() + @IsOptional() + metadata?: Record +} + +export class AdminPostStockLocationsLocationParams extends FindParams {} + export class AdminGetStockLocationsLocationParams extends FindParams {} diff --git a/packages/stock-location-next/integration-tests/__tests__/stock-location-module-service.spec.ts b/packages/stock-location-next/integration-tests/__tests__/stock-location-module-service.spec.ts index cf57872c21..5083dc7423 100644 --- a/packages/stock-location-next/integration-tests/__tests__/stock-location-module-service.spec.ts +++ b/packages/stock-location-next/integration-tests/__tests__/stock-location-module-service.spec.ts @@ -107,7 +107,7 @@ moduleIntegrationTestRunner({ id: stockLocation.id, name: "updated location", } - const location = await service.update(data) + const location = await service.upsert(data) expect(location).toEqual(expect.objectContaining(data)) }) @@ -122,7 +122,7 @@ moduleIntegrationTestRunner({ }, } - const location = await service.update(data) + const location = await service.upsert(data) expect(location).toEqual( expect.objectContaining({ diff --git a/packages/stock-location-next/src/services/stock-location.ts b/packages/stock-location-next/src/services/stock-location.ts index a02ac28ef5..f2be165779 100644 --- a/packages/stock-location-next/src/services/stock-location.ts +++ b/packages/stock-location-next/src/services/stock-location.ts @@ -10,16 +10,20 @@ import { ModulesSdkTypes, DAL, IStockLocationServiceNext, + FilterableStockLocationProps, } from "@medusajs/types" import { InjectManager, InjectTransactionManager, MedusaContext, ModulesSdkUtils, + isString, } from "@medusajs/utils" import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config" import { StockLocation, StockLocationAddress } from "../models" import { UpdateStockLocationNextInput } from "@medusajs/types" +import { UpsertStockLocationInput } from "@medusajs/types" +import { promiseAll } from "@medusajs/utils" type InjectedDependencies = { eventBusService: IEventBusService @@ -117,13 +121,65 @@ export default class StockLocationModuleService< return await this.stockLocationService_.create(data, context) } + async upsert( + data: UpsertStockLocationInput, + context?: Context + ): Promise + async upsert( + data: UpsertStockLocationInput[], + context?: Context + ): Promise + + @InjectManager("baseRepository_") + async upsert( + data: UpsertStockLocationInput | UpsertStockLocationInput[], + @MedusaContext() context: Context = {} + ): Promise< + StockLocationTypes.StockLocationDTO | StockLocationTypes.StockLocationDTO[] + > { + const input = Array.isArray(data) ? data : [data] + + const result = await this.upsert_(input, context) + + return await this.baseRepository_.serialize< + | StockLocationTypes.StockLocationDTO[] + | StockLocationTypes.StockLocationDTO + >(Array.isArray(data) ? result : result[0]) + } + + @InjectTransactionManager("baseRepository_") + async upsert_( + input: UpsertStockLocationInput[], + @MedusaContext() context: Context = {} + ) { + const toUpdate = input.filter( + (location): location is UpdateStockLocationNextInput => !!location.id + ) as UpdateStockLocationNextInput[] + const toCreate = input.filter( + (location) => !location.id + ) as CreateStockLocationInput[] + + const operations: Promise[] = [] + + if (toCreate.length) { + operations.push(this.create_(toCreate, context)) + } + if (toUpdate.length) { + operations.push(this.update_(toUpdate, context)) + } + + return (await promiseAll(operations)).flat() + } + update( - data: UpdateStockLocationNextInput, - context: Context + id: string, + input: UpdateStockLocationNextInput, + context?: Context ): Promise update( - data: UpdateStockLocationNextInput[], - context: Context + selector: FilterableStockLocationProps, + input: UpdateStockLocationNextInput, + context?: Context ): Promise /** * Updates an existing stock location. @@ -134,14 +190,21 @@ export default class StockLocationModuleService< */ @InjectManager("baseRepository_") async update( + idOrSelector: string | FilterableStockLocationProps, data: UpdateStockLocationNextInput | UpdateStockLocationNextInput[], @MedusaContext() context: Context = {} ): Promise< StockLocationTypes.StockLocationDTO | StockLocationTypes.StockLocationDTO[] > { - const input = Array.isArray(data) ? data : [data] - - const updated = await this.update_(input, context) + let normalizedInput: + | (UpdateStockLocationNextInput & { id: string })[] + | { data: any; selector: FilterableStockLocationProps } = [] + if (isString(idOrSelector)) { + normalizedInput = [{ id: idOrSelector, ...data }] + } else { + normalizedInput = { data, selector: idOrSelector } + } + const updated = await this.update_(normalizedInput, context) const serialized = await this.baseRepository_.serialize< | StockLocationTypes.StockLocationDTO @@ -153,9 +216,12 @@ export default class StockLocationModuleService< @InjectTransactionManager("baseRepository_") async update_( - data: (UpdateStockLocationInput & { id: string })[], + data: + | UpdateStockLocationNextInput[] + | UpdateStockLocationNextInput + | { data: any; selector: FilterableStockLocationProps }, @MedusaContext() context: Context = {} - ): Promise { + ): Promise { return await this.stockLocationService_.update(data, context) } diff --git a/packages/types/src/stock-location/common.ts b/packages/types/src/stock-location/common.ts index 72fef1d687..6dca9c20a2 100644 --- a/packages/types/src/stock-location/common.ts +++ b/packages/types/src/stock-location/common.ts @@ -1,3 +1,5 @@ +import { BaseFilterable, OperatorMap } from "../dal" + import { StringComparisonOperator } from "../common/common" /** @@ -241,7 +243,8 @@ export type StockLocationExpandedDTO = StockLocationDTO & { * * The filters to apply on the retrieved stock locations. */ -export type FilterableStockLocationProps = { +export interface FilterableStockLocationProps + extends BaseFilterable { /** * Search parameter for stock location names */ @@ -255,7 +258,7 @@ export type FilterableStockLocationProps = { /** * The names to filter stock locations by. */ - name?: string | string[] | StringComparisonOperator + name?: string | string[] | OperatorMap } /** @@ -309,7 +312,7 @@ export type StockLocationAddressInput = { /** * The second line of the stock location address. */ - address_2?: string + address_2?: string | null /** * The country code of the stock location address. @@ -319,27 +322,27 @@ export type StockLocationAddressInput = { /** * The city of the stock location address. */ - city?: string + city?: string | null /** * The phone of the stock location address. */ - phone?: string + phone?: string | null /** * The province of the stock location address. */ - province?: string + province?: string | null /** * The postal code of the stock location address. */ - postal_code?: string + postal_code?: string | null /** * Holds custom data in key-value pairs. */ - metadata?: Record + metadata?: Record | null } /** @@ -435,3 +438,5 @@ export type UpdateStockLocationInput = { export type UpdateStockLocationNextInput = UpdateStockLocationInput & { id: string } + +export type UpsertStockLocationInput = Partial diff --git a/packages/types/src/stock-location/service-next.ts b/packages/types/src/stock-location/service-next.ts index 4387f569e8..6b3744e73d 100644 --- a/packages/types/src/stock-location/service-next.ts +++ b/packages/types/src/stock-location/service-next.ts @@ -4,6 +4,7 @@ import { StockLocationDTO, UpdateStockLocationInput, UpdateStockLocationNextInput, + UpsertStockLocationInput, } from "./common" import { RestoreReturn, SoftDeleteReturn } from "../dal" @@ -251,6 +252,15 @@ export interface IStockLocationServiceNext extends IModuleService { context?: Context ): Promise + upsert( + data: UpsertStockLocationInput[], + sharedContext?: Context + ): Promise + upsert( + data: UpsertStockLocationInput, + sharedContext?: Context + ): Promise + /** * This method is used to update a stock location. * @@ -275,13 +285,15 @@ export interface IStockLocationServiceNext extends IModuleService { * } */ update( - input: UpdateStockLocationNextInput[], - context?: Context - ): Promise - update( - input: UpdateStockLocationNextInput, + id: string, + input: UpdateStockLocationInput, context?: Context ): Promise + update( + selector: FilterableStockLocationProps, + input: UpdateStockLocationInput, + context?: Context + ): Promise /** * This method is used to delete a stock location.