diff --git a/.changeset/shy-cars-turn.md b/.changeset/shy-cars-turn.md new file mode 100644 index 0000000000..b807ae86d3 --- /dev/null +++ b/.changeset/shy-cars-turn.md @@ -0,0 +1,7 @@ +--- +"@medusajs/stock-location": patch +"@medusajs/medusa": patch +"@medusajs/types": patch +--- + +feat(stock-location, medusa, types): add `q` and `order` query parameters to stock locations list endpoint diff --git a/integration-tests/plugins/__tests__/stock-location/list-stock-locations.spec.ts b/integration-tests/plugins/__tests__/stock-location/list-stock-locations.spec.ts new file mode 100644 index 0000000000..68ff3e4212 --- /dev/null +++ b/integration-tests/plugins/__tests__/stock-location/list-stock-locations.spec.ts @@ -0,0 +1,181 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { useApi } from "../../../environment-helpers/use-api" +import { AxiosInstance } from "axios" +import adminSeeder from "../../../helpers/admin-seeder" + +const path = require("path") + +const { + startBootstrapApp, +} = require("../../../environment-helpers/bootstrap-app") +const { initDb, useDb } = require("../../../environment-helpers/use-db") +const { getContainer } = require("../../../environment-helpers/use-container") +const { useExpressServer } = require("../../../environment-helpers/use-api") + +const adminHeaders = { headers: { "x-medusa-access-token": "test_token" } } + +jest.setTimeout(50000) + +describe("List stock locations", () => { + let shutdownServer + let appContainer + let dbConnection + let addressId + let scId + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..")) + dbConnection = await initDb({ cwd }) + shutdownServer = await startBootstrapApp({ cwd }) + appContainer = getContainer() + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + await shutdownServer() + }) + + afterEach(async () => { + const db = useDb() + return await db.teardown() + }) + + beforeEach(async () => { + await adminSeeder(dbConnection) + + const stockLocationModule = appContainer.resolve( + ModuleRegistrationName.STOCK_LOCATION + ) + + const salesChannelService = appContainer.resolve("salesChannelService") + const salesChannelLocationService = appContainer.resolve( + "salesChannelLocationService" + ) + const { id: sc_id } = await salesChannelService.create({ name: "test" }) + scId = sc_id + + const [{ address_id }, { id }] = await Promise.all([ + stockLocationModule.create({ + name: "Copenhagen", + address: { + address_1: "test street 1", + country_code: "DK", + }, + }), + stockLocationModule.create({ + name: "Berlin", + }), + stockLocationModule.create({ + name: "Paris", + }), + ]) + + await salesChannelLocationService.associateLocation(sc_id, id) + + addressId = address_id + }) + + describe("GET /stock-location", () => { + it("should list all stock locations", async () => { + const api = useApi()! as AxiosInstance + + const result = await api.get("/admin/stock-locations", adminHeaders) + + expect(result.status).toEqual(200) + expect(result.data.stock_locations).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: "Copenhagen", + }), + expect.objectContaining({ + name: "Berlin", + }), + expect.objectContaining({ + name: "Paris", + }), + ]) + ) + }) + + it("should list stock locations filtered by q parameter", async () => { + const api = useApi()! as AxiosInstance + + const result = await api.get( + "/admin/stock-locations?q=open", + adminHeaders + ) + + expect(result.status).toEqual(200) + expect(result.data.count).toEqual(1) + expect(result.data.stock_locations).toEqual([ + expect.objectContaining({ + name: "Copenhagen", + }), + ]) + }) + + it("should list stock locations filtered by addresses", async () => { + const api = useApi()! as AxiosInstance + + const result = await api.get( + `/admin/stock-locations?address_id=${addressId}`, + adminHeaders + ) + + expect(result.status).toEqual(200) + expect(result.data.count).toEqual(1) + expect(result.data.stock_locations).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: "Copenhagen", + }), + ]) + ) + }) + + it("should list stock locations filtered by sales channel Id", async () => { + const api = useApi()! as AxiosInstance + + const result = await api.get( + `/admin/stock-locations?sales_channel_id=${scId}`, + adminHeaders + ) + + expect(result.status).toEqual(200) + expect(result.data.count).toEqual(1) + expect(result.data.stock_locations).toEqual([ + expect.objectContaining({ + name: "Berlin", + }), + ]) + }) + + it("should list stock locations in requested order", async () => { + const api = useApi()! as AxiosInstance + + const nameOrderLocations = [ + expect.objectContaining({ + name: "Berlin", + }), + expect.objectContaining({ + name: "Copenhagen", + }), + expect.objectContaining({ + name: "Paris", + }), + ] + + let result = await api.get( + "/admin/stock-locations?order=name", + adminHeaders + ) + expect(result.status).toEqual(200) + expect(result.data.stock_locations).toEqual(nameOrderLocations) + + result = await api.get("/admin/stock-locations?order=-name", adminHeaders) + expect(result.status).toEqual(200) + expect(result.data.stock_locations).toEqual(nameOrderLocations.reverse()) + }) + }) +}) diff --git a/integration-tests/plugins/__tests__/stock-location/delete-sales-channels.js b/integration-tests/plugins/__tests__/stock-location/service/delete-sales-channels.js similarity index 83% rename from integration-tests/plugins/__tests__/stock-location/delete-sales-channels.js rename to integration-tests/plugins/__tests__/stock-location/service/delete-sales-channels.js index 289993076a..09c95d1a78 100644 --- a/integration-tests/plugins/__tests__/stock-location/delete-sales-channels.js +++ b/integration-tests/plugins/__tests__/stock-location/service/delete-sales-channels.js @@ -2,15 +2,17 @@ const path = require("path") const { startBootstrapApp, -} = require("../../../environment-helpers/bootstrap-app") -const { initDb, useDb } = require("../../../environment-helpers/use-db") +} = require("../../../../environment-helpers/bootstrap-app") +const { initDb, useDb } = require("../../../../environment-helpers/use-db") const { useApi, useExpressServer, -} = require("../../../environment-helpers/use-api") +} = require("../../../../environment-helpers/use-api") -const adminSeeder = require("../../../helpers/admin-seeder") -const { getContainer } = require("../../../environment-helpers/use-container") +const adminSeeder = require("../../../../helpers/admin-seeder") +const { + getContainer, +} = require("../../../../environment-helpers/use-container") jest.setTimeout(30000) @@ -20,7 +22,7 @@ describe("Sales channels", () => { let shutdownServer beforeAll(async () => { - const cwd = path.resolve(path.join(__dirname, "..", "..")) + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) dbConnection = await initDb({ cwd }) shutdownServer = await startBootstrapApp({ cwd }) appContainer = getContainer() diff --git a/integration-tests/plugins/__tests__/stock-location/delete-stock-location.js b/integration-tests/plugins/__tests__/stock-location/service/delete-stock-location.js similarity index 84% rename from integration-tests/plugins/__tests__/stock-location/delete-stock-location.js rename to integration-tests/plugins/__tests__/stock-location/service/delete-stock-location.js index 2262a3f5e3..68dee64e47 100644 --- a/integration-tests/plugins/__tests__/stock-location/delete-stock-location.js +++ b/integration-tests/plugins/__tests__/stock-location/service/delete-stock-location.js @@ -2,15 +2,17 @@ const path = require("path") const { startBootstrapApp, -} = require("../../../environment-helpers/bootstrap-app") -const { initDb, useDb } = require("../../../environment-helpers/use-db") +} = require("../../../../environment-helpers/bootstrap-app") +const { initDb, useDb } = require("../../../../environment-helpers/use-db") const { useApi, useExpressServer, -} = require("../../../environment-helpers/use-api") +} = require("../../../../environment-helpers/use-api") -const adminSeeder = require("../../../helpers/admin-seeder") -const { getContainer } = require("../../../environment-helpers/use-container") +const adminSeeder = require("../../../../helpers/admin-seeder") +const { + getContainer, +} = require("../../../../environment-helpers/use-container") jest.setTimeout(30000) @@ -20,7 +22,7 @@ describe("Sales channels", () => { let shutdownServer beforeAll(async () => { - const cwd = path.resolve(path.join(__dirname, "..", "..")) + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) dbConnection = await initDb({ cwd }) shutdownServer = await startBootstrapApp({ cwd }) appContainer = getContainer() diff --git a/integration-tests/plugins/__tests__/stock-location/sales-channels.js b/integration-tests/plugins/__tests__/stock-location/service/sales-channels.js similarity index 91% rename from integration-tests/plugins/__tests__/stock-location/sales-channels.js rename to integration-tests/plugins/__tests__/stock-location/service/sales-channels.js index 08ef3020c2..ed3a946420 100644 --- a/integration-tests/plugins/__tests__/stock-location/sales-channels.js +++ b/integration-tests/plugins/__tests__/stock-location/service/sales-channels.js @@ -2,15 +2,17 @@ const path = require("path") const { startBootstrapApp, -} = require("../../../environment-helpers/bootstrap-app") -const { initDb, useDb } = require("../../../environment-helpers/use-db") +} = require("../../../../environment-helpers/bootstrap-app") +const { initDb, useDb } = require("../../../../environment-helpers/use-db") const { useApi, useExpressServer, -} = require("../../../environment-helpers/use-api") +} = require("../../../../environment-helpers/use-api") -const adminSeeder = require("../../../helpers/admin-seeder") -const { getContainer } = require("../../../environment-helpers/use-container") +const adminSeeder = require("../../../../helpers/admin-seeder") +const { + getContainer, +} = require("../../../../environment-helpers/use-container") jest.setTimeout(30000) @@ -22,7 +24,7 @@ describe("Sales channels", () => { let shutdownServer beforeAll(async () => { - const cwd = path.resolve(path.join(__dirname, "..", "..")) + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) dbConnection = await initDb({ cwd }) shutdownServer = await startBootstrapApp({ cwd }) appContainer = getContainer() diff --git a/integration-tests/plugins/__tests__/stock-location/service.js b/integration-tests/plugins/__tests__/stock-location/service/service.js similarity index 94% rename from integration-tests/plugins/__tests__/stock-location/service.js rename to integration-tests/plugins/__tests__/stock-location/service/service.js index be40e14c8b..704d1b961b 100644 --- a/integration-tests/plugins/__tests__/stock-location/service.js +++ b/integration-tests/plugins/__tests__/stock-location/service/service.js @@ -2,20 +2,22 @@ const path = require("path") const { startBootstrapApp, -} = require("../../../environment-helpers/bootstrap-app") -const { initDb, useDb } = require("../../../environment-helpers/use-db") -const { getContainer } = require("../../../environment-helpers/use-container") -const { useExpressServer } = require("../../../environment-helpers/use-api") +} = require("../../../../environment-helpers/bootstrap-app") +const { initDb, useDb } = require("../../../../environment-helpers/use-db") +const { + getContainer, +} = require("../../../../environment-helpers/use-container") +const { useExpressServer } = require("../../../../environment-helpers/use-api") jest.setTimeout(30000) -describe("Inventory Module", () => { +describe("Stock Location Module", () => { let appContainer let dbConnection let shutdownServer beforeAll(async () => { - const cwd = path.resolve(path.join(__dirname, "..", "..")) + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) dbConnection = await initDb({ cwd }) shutdownServer = await startBootstrapApp({ cwd }) appContainer = getContainer() diff --git a/packages/medusa/src/api/routes/admin/stock-locations/list-stock-locations.ts b/packages/medusa/src/api/routes/admin/stock-locations/list-stock-locations.ts index fef2f5fa22..9f36dd5eed 100644 --- a/packages/medusa/src/api/routes/admin/stock-locations/list-stock-locations.ts +++ b/packages/medusa/src/api/routes/admin/stock-locations/list-stock-locations.ts @@ -1,5 +1,5 @@ import { IStockLocationService } from "@medusajs/types" -import { IsOptional } from "class-validator" +import { IsOptional, IsString } from "class-validator" import { Request, Response } from "express" import { SalesChannelLocationService, @@ -231,6 +231,13 @@ 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. */ @@ -258,4 +265,11 @@ export class AdminGetStockLocationsParams extends extendedFindParamsMixin({ @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 } diff --git a/packages/stock-location/src/services/stock-location.ts b/packages/stock-location/src/services/stock-location.ts index 3ad855dfe3..c0045a185b 100644 --- a/packages/stock-location/src/services/stock-location.ts +++ b/packages/stock-location/src/services/stock-location.ts @@ -12,12 +12,12 @@ import { } from "@medusajs/types" import { InjectEntityManager, + isDefined, MedusaContext, MedusaError, - isDefined, setMetadata, } from "@medusajs/utils" -import { EntityManager } from "typeorm" +import { EntityManager, FindOptionsWhere, ILike } from "typeorm" import { joinerConfig } from "../joiner-config" import { StockLocation, StockLocationAddress } from "../models" import { buildQuery } from "../utils/build-query" @@ -66,11 +66,8 @@ export default class StockLocationService { config: FindConfig = { relations: [], skip: 0, take: 10 }, context: SharedContext = {} ): Promise { - const manager = context.transactionManager ?? this.manager_ - const locationRepo = manager.getRepository(StockLocation) - - const query = buildQuery(selector, config) - return await locationRepo.find(query) + const [locations] = await this.listAndCount(selector, config, context) + return locations } /** @@ -87,8 +84,27 @@ export default class StockLocationService { ): Promise<[StockLocation[], number]> { const manager = context.transactionManager ?? this.manager_ const locationRepo = manager.getRepository(StockLocation) + let q + if (selector.q) { + q = selector.q + delete selector.q + } const query = buildQuery(selector, config) + + if (q) { + const where = query.where as FindOptionsWhere + + delete where.name + + query.where = [ + { + ...where, + name: ILike(`%${q}%`), + }, + ] + } + return await locationRepo.findAndCount(query) } diff --git a/packages/types/src/stock-location/common.ts b/packages/types/src/stock-location/common.ts index 9bbfe5df48..3a86a2a648 100644 --- a/packages/types/src/stock-location/common.ts +++ b/packages/types/src/stock-location/common.ts @@ -153,10 +153,14 @@ export type StockLocationExpandedDTO = StockLocationDTO & { /** * @interface - * + * * The filters to apply on the retrieved stock locations. */ export type FilterableStockLocationProps = { + /** + * Search parameter for stock location names + */ + q?: string /** * The IDs to filter stock locations by. */