From 18438a695a9af8c44387d49d9593d803498e2d21 Mon Sep 17 00:00:00 2001 From: Philip Korsholm <88927411+pKorsholm@users.noreply.github.com> Date: Thu, 28 Mar 2024 17:33:26 +0100 Subject: [PATCH] feat(medusa, stock-location-next): add list-stock-locations endpoint to api-v2 (#6788) * initial create * add list for stock locations * add changeset * redo changes for stock locatino module' * add changeset * naming * prep for pr * move integration tests * fix pr feedback * add changeset * update changeset --------- Co-authored-by: Riqwan Thamir --- .changeset/wild-houses-warn.md | 6 ++ .../admin/stock-location/index.spec.ts | 95 +++++++++++++++++++ .../admin/stock-locations/middlewares.ts | 13 +++ .../src/api-v2/admin/stock-locations/route.ts | 22 +++++ .../utils/apply-sales-channel-filter.ts | 49 ++++++++++ .../admin/stock-locations/validators.ts | 49 ++++++++++ .../src/repositories/index.ts | 1 + .../src/repositories/stock-location.ts | 52 ++++++++++ .../stock-location-next/src/services/index.ts | 2 +- ...k-location.ts => stock-location-module.ts} | 0 10 files changed, 288 insertions(+), 1 deletion(-) create mode 100644 .changeset/wild-houses-warn.md create mode 100644 packages/medusa/src/api-v2/admin/stock-locations/utils/apply-sales-channel-filter.ts create mode 100644 packages/stock-location-next/src/repositories/stock-location.ts rename packages/stock-location-next/src/services/{stock-location.ts => stock-location-module.ts} (100%) diff --git a/.changeset/wild-houses-warn.md b/.changeset/wild-houses-warn.md new file mode 100644 index 0000000000..525a18515a --- /dev/null +++ b/.changeset/wild-houses-warn.md @@ -0,0 +1,6 @@ +--- +"@medusajs/stock-location-next": patch +"@medusajs/medusa": patch +--- + +feat(medusa, stock-location-next): add list-stock-locations 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 fed384a8f7..bdf04adb5d 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,101 @@ medusaIntegrationTestRunner({ }) }) + describe("list stock locations", () => { + let location1 + let location2 + beforeEach(async () => { + const location1CreateResponse = await api.post( + `/admin/stock-locations`, + { + name: "Test Location 1", + address: { + address_1: "Test Address", + country_code: "US", + }, + }, + adminHeaders + ) + location1 = location1CreateResponse.data.stock_location + const location2CreateResponse = await api.post( + `/admin/stock-locations`, + { + name: "Test Location 2", + address: { + address_1: "Test Address", + country_code: "US", + }, + }, + adminHeaders + ) + location2 = location2CreateResponse.data.stock_location + }) + + it("should list stock locations", async () => { + const listLocationsResponse = await api.get( + "/admin/stock-locations", + adminHeaders + ) + + expect(listLocationsResponse.status).toEqual(200) + expect(listLocationsResponse.data.stock_locations).toEqual([ + expect.objectContaining(location1), + expect.objectContaining(location2), + ]) + }) + + it("should filter stock locations by name", async () => { + const listLocationsResponse = await api.get( + "/admin/stock-locations?name=Test%20Location%201", + adminHeaders + ) + + expect(listLocationsResponse.status).toEqual(200) + expect(listLocationsResponse.data.stock_locations).toEqual([ + expect.objectContaining(location1), + ]) + }) + + it("should filter stock locations by partial name with q parameter", async () => { + const listLocationsResponse = await api.get( + "/admin/stock-locations?q=ation%201", + adminHeaders + ) + + expect(listLocationsResponse.status).toEqual(200) + expect(listLocationsResponse.data.stock_locations).toEqual([ + expect.objectContaining(location1), + ]) + }) + + it("should filter stock locations on sales_channel_id", async () => { + const remoteLinkService = appContainer.resolve( + ContainerRegistrationKeys.REMOTE_LINK + ) + + await remoteLinkService.create([ + { + [Modules.SALES_CHANNEL]: { + sales_channel_id: "default", + }, + [Modules.STOCK_LOCATION]: { + stock_location_id: location1.id, + }, + }, + ]) + + const listLocationsResponse = await api.get( + "/admin/stock-locations?sales_channel_id=default", + adminHeaders + ) + + expect(listLocationsResponse.status).toEqual(200) + expect(listLocationsResponse.data.stock_locations).toEqual([ + expect.objectContaining(location1), + ]) + }) + }) + describe("Update stock locations", () => { let stockLocationId 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 b26a871d86..f2f5c90f56 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,7 @@ import * as QueryConfig from "./query-config" import { AdminGetStockLocationsLocationParams, + AdminGetStockLocationsParams, AdminPostStockLocationsLocationParams, AdminPostStockLocationsLocationReq, AdminPostStockLocationsParams, @@ -10,6 +11,7 @@ import { import { transformBody, transformQuery } from "../../../api/middlewares" import { MiddlewareRoute } from "../../../types/middlewares" +import { applySalesChannelsFilter } from "./utils/apply-sales-channel-filter" import { authenticate } from "../../../utils/authenticate-middleware" export const adminStockLocationRoutesMiddlewares: MiddlewareRoute[] = [ @@ -29,6 +31,17 @@ export const adminStockLocationRoutesMiddlewares: MiddlewareRoute[] = [ ), ], }, + { + method: ["GET"], + matcher: "/admin/stock-locations", + middlewares: [ + transformQuery( + AdminGetStockLocationsParams, + QueryConfig.listTransformQueryConfig + ), + applySalesChannelsFilter(), + ], + }, { method: ["POST"], matcher: "/admin/stock-locations/:id", diff --git a/packages/medusa/src/api-v2/admin/stock-locations/route.ts b/packages/medusa/src/api-v2/admin/stock-locations/route.ts index 4ad1ed9d4f..2d631cef10 100644 --- a/packages/medusa/src/api-v2/admin/stock-locations/route.ts +++ b/packages/medusa/src/api-v2/admin/stock-locations/route.ts @@ -30,3 +30,25 @@ export const POST = async ( res.status(200).json({ stock_location }) } + +export const GET = async (req: MedusaRequest, res: MedusaResponse) => { + const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY) + + const { rows: stock_locations } = await remoteQuery( + remoteQueryObjectFromString({ + entryPoint: "stock_locations", + variables: { + filters: req.filterableFields, + order: req.listConfig.order, + skip: req.listConfig.skip, + take: req.listConfig.take, + }, + fields: req.remoteQueryConfig.fields, + }) + ) + + res.status(200).json({ + stock_locations, + ...req.remoteQueryConfig.pagination, + }) +} diff --git a/packages/medusa/src/api-v2/admin/stock-locations/utils/apply-sales-channel-filter.ts b/packages/medusa/src/api-v2/admin/stock-locations/utils/apply-sales-channel-filter.ts new file mode 100644 index 0000000000..ffad43bca2 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/stock-locations/utils/apply-sales-channel-filter.ts @@ -0,0 +1,49 @@ +import { + ContainerRegistrationKeys, + remoteQueryObjectFromString, +} from "@medusajs/utils" + +import { AdminGetStockLocationsParams } from "../validators" +import { MedusaRequest } from "../../../../types/routing" +import { Modules } from "@medusajs/modules-sdk" +import { NextFunction } from "express" + +export function applySalesChannelsFilter() { + return async (req: MedusaRequest, _, next: NextFunction) => { + const filterableFields: AdminGetStockLocationsParams = req.filterableFields + + if (!filterableFields.sales_channel_id) { + return next() + } + + const salesChannelIds = Array.isArray(filterableFields.sales_channel_id) + ? filterableFields.sales_channel_id + : [filterableFields.sales_channel_id] + + delete filterableFields.sales_channel_id + + const remoteLinkService = req.scope.resolve( + ContainerRegistrationKeys.REMOTE_LINK + ) + + const stockLocationSalesChannelLinkModuleService = + await remoteLinkService.getLinkModule( + Modules.SALES_CHANNEL, + "sales_channel_id", + Modules.STOCK_LOCATION, + "stock_location_id" + ) + + const stockLocationSalesChannelLinks = + await stockLocationSalesChannelLinkModuleService.list( + { sales_channel_id: salesChannelIds }, + {} + ) + + filterableFields.id = stockLocationSalesChannelLinks.map( + (link) => link.stock_location_id + ) + + return next() + } +} 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 3e2c1d3f06..277e89be20 100644 --- a/packages/medusa/src/api-v2/admin/stock-locations/validators.ts +++ b/packages/medusa/src/api-v2/admin/stock-locations/validators.ts @@ -138,6 +138,55 @@ export class AdminPostStockLocationsReq { export class AdminPostStockLocationsParams extends FindParams {} +/** + * Parameters used to filter and configure the pagination of the retrieved stock locations. + */ +export class AdminGetStockLocationsParams extends extendedFindParamsMixin({ + limit: 20, + offset: 0, +}) { + /** + * Search term to search stock location names. + */ + @IsString() + @IsOptional() + q?: string + + /** + * IDs to filter stock locations by. + */ + @IsOptional() + @IsType([String, [String]]) + id?: string | string[] + + /** + * Names to filter stock locations by. + */ + @IsOptional() + @IsType([String, [String]]) + name?: string | string[] + + /** + * Filter stock locations by the ID of their associated addresses. + */ + @IsOptional() + @IsType([String, [String]]) + address_id?: string | string[] + + /** + * Filter stock locations by the ID of their associated sales channels. + */ + @IsOptional() + @IsType([String, [String]]) + sales_channel_id?: string | string[] + + /** + * The field to sort the data by. By default, the sort order is ascending. To change the order to descending, prefix the field name with `-`. + */ + @IsString() + @IsOptional() + order?: string +} /** * The attributes of a stock location address to create or update. */ diff --git a/packages/stock-location-next/src/repositories/index.ts b/packages/stock-location-next/src/repositories/index.ts index 147c9cc259..824d3612e0 100644 --- a/packages/stock-location-next/src/repositories/index.ts +++ b/packages/stock-location-next/src/repositories/index.ts @@ -1 +1,2 @@ export { MikroOrmBaseRepository as BaseRepository } from "@medusajs/utils" +export { StockLocationRepository } from "./stock-location" diff --git a/packages/stock-location-next/src/repositories/stock-location.ts b/packages/stock-location-next/src/repositories/stock-location.ts new file mode 100644 index 0000000000..b507b20a52 --- /dev/null +++ b/packages/stock-location-next/src/repositories/stock-location.ts @@ -0,0 +1,52 @@ +import { Context, DAL } from "@medusajs/types" + +import { StockLocation } from "@models" +import { mikroOrmBaseRepositoryFactory } from "@medusajs/utils" + +export class StockLocationRepository extends mikroOrmBaseRepositoryFactory( + StockLocation +) { + async find( + findOptions: DAL.FindOptions = { + where: {}, + }, + context: Context + ): Promise { + const findOptions_ = { ...findOptions } + findOptions_.options ??= {} + + this.applyFreeTextSearchFilters( + findOptions_, + this.getFreeTextSearchConstraints + ) + + return await super.find(findOptions_, context) + } + + async findAndCount( + findOptions: DAL.FindOptions = { + where: {}, + }, + context: Context + ): Promise<[StockLocation[], number]> { + const findOptions_ = { ...findOptions } + findOptions_.options ??= {} + + this.applyFreeTextSearchFilters( + findOptions_, + this.getFreeTextSearchConstraints + ) + + return await super.findAndCount(findOptions_, context) + } + + protected getFreeTextSearchConstraints(q: string) { + return [ + { + name: { + $ilike: `%${q}%`, + }, + }, + ] + } +} diff --git a/packages/stock-location-next/src/services/index.ts b/packages/stock-location-next/src/services/index.ts index 3facc2c5c6..9e31254bce 100644 --- a/packages/stock-location-next/src/services/index.ts +++ b/packages/stock-location-next/src/services/index.ts @@ -1 +1 @@ -export { default as StockLocationModuleService } from "./stock-location" +export { default as StockLocationModuleService } from "./stock-location-module" diff --git a/packages/stock-location-next/src/services/stock-location.ts b/packages/stock-location-next/src/services/stock-location-module.ts similarity index 100% rename from packages/stock-location-next/src/services/stock-location.ts rename to packages/stock-location-next/src/services/stock-location-module.ts