diff --git a/.changeset/smooth-numbers-whisper.md b/.changeset/smooth-numbers-whisper.md new file mode 100644 index 0000000000..cb8fe0c3e1 --- /dev/null +++ b/.changeset/smooth-numbers-whisper.md @@ -0,0 +1,8 @@ +--- +"@medusajs/client-types": patch +"@medusajs/inventory": patch +"@medusajs/medusa": patch +"@medusajs/types": patch +--- + +feat(client-types, inventory, medusa, types): add additional filtering capabilities to list-reservations diff --git a/integration-tests/plugins/__tests__/inventory/reservation-items/index.js b/integration-tests/plugins/__tests__/inventory/reservation-items/index.js index 7906f8ba03..a3053d1b10 100644 --- a/integration-tests/plugins/__tests__/inventory/reservation-items/index.js +++ b/integration-tests/plugins/__tests__/inventory/reservation-items/index.js @@ -204,6 +204,233 @@ describe("Inventory Items endpoints", () => { }) }) + describe("List reservation items", () => { + let item2 + let location2 + let reservation2 + + beforeEach(async () => { + const api = useApi() + const stockRes = await api.post( + `/admin/stock-locations`, + { + name: "Fake Warehouse 1", + }, + adminHeaders + ) + + location2 = stockRes.data.stock_location.id + + await salesChannelLocationService.associateLocation( + "test-channel", + location2 + ) + + const inventoryItem1 = await inventoryService.createInventoryItem({ + sku: "12345", + }) + item2 = inventoryItem1.id + + await inventoryService.createInventoryLevel({ + inventory_item_id: item2, + location_id: location2, + stocked_quantity: 100, + }) + + order = await simpleOrderFactory(dbConnection, { + sales_channel: "test-channel", + line_items: [ + { + variant_id: variantId, + quantity: 2, + id: "line-item-id-2", + }, + ], + shipping_methods: [ + { + shipping_option: { + region_id: regionId, + }, + }, + ], + }) + + const orderRes = await api.get( + `/admin/orders/${order.id}`, + adminHeaders + ) + + reservation2 = await inventoryService.createReservationItem({ + line_item_id: "line-item-id-2", + inventory_item_id: item2, + location_id: location2, + description: "test description", + quantity: 1, + }) + }) + + it("lists reservation items", async () => { + const api = useApi() + + const reservationsRes = await api.get( + `/admin/reservations`, + adminHeaders + ) + expect(reservationsRes.data.reservations.length).toBe(2) + expect(reservationsRes.data.reservations).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: reservationItem.id, + }), + expect.objectContaining({ + id: reservation2.id, + }), + ]) + ) + }) + + describe("Filters reservation items", () => { + it("filters by location", async () => { + const api = useApi() + + const reservationsRes = await api.get( + `/admin/reservations?location_id[]=${locationId}`, + adminHeaders + ) + expect(reservationsRes.data.reservations.length).toBe(1) + expect(reservationsRes.data.reservations[0].location_id).toBe( + locationId + ) + }) + + it("filters by itemID", async () => { + const api = useApi() + + const reservationsRes = await api.get( + `/admin/reservations?inventory_item_id[]=${item2}`, + adminHeaders + ) + expect(reservationsRes.data.reservations.length).toBe(1) + expect(reservationsRes.data.reservations[0].inventory_item_id).toBe( + item2 + ) + }) + + it("filters by quantity", async () => { + const api = useApi() + + const reservationsRes = await api.get( + `/admin/reservations?quantity[gt]=1`, + adminHeaders + ) + + expect(reservationsRes.data.reservations.length).toBe(1) + expect(reservationsRes.data.reservations[0].id).toBe( + reservationItem.id + ) + }) + + it("filters by date", async () => { + const api = useApi() + + const reservationsRes = await api.get( + `/admin/reservations?created_at[gte]=${new Date( + reservation2.created_at + ).toISOString()}`, + adminHeaders + ) + + expect(reservationsRes.data.reservations.length).toBe(1) + expect(reservationsRes.data.reservations[0].id).toBe(reservation2.id) + }) + + it("filters by description using equals", async () => { + const api = useApi() + + const reservationsRes = await api + .get( + `/admin/reservations?description=test%20description`, + adminHeaders + ) + .catch(console.log) + + expect(reservationsRes.data.reservations.length).toBe(1) + expect(reservationsRes.data.reservations[0].id).toBe(reservation2.id) + }) + + it("filters by description using equals removes results", async () => { + const api = useApi() + + const reservationsRes = await api.get( + `/admin/reservations?description=description`, + adminHeaders + ) + + expect(reservationsRes.data.reservations.length).toBe(0) + }) + + it("filters by description using contains", async () => { + const api = useApi() + + const reservationsRes = await api.get( + `/admin/reservations?description[contains]=descri`, + adminHeaders + ) + + expect(reservationsRes.data.reservations.length).toBe(1) + expect(reservationsRes.data.reservations[0].id).toBe(reservation2.id) + }) + + it("filters by description using starts_with", async () => { + const api = useApi() + + const reservationsRes = await api + .get( + `/admin/reservations?description[starts_with]=test`, + adminHeaders + ) + .catch(console.log) + + expect(reservationsRes.data.reservations.length).toBe(1) + expect(reservationsRes.data.reservations[0].id).toBe(reservation2.id) + }) + + it("filters by description using starts_with removes results", async () => { + const api = useApi() + + const reservationsRes = await api.get( + `/admin/reservations?description[starts_with]=description`, + adminHeaders + ) + + expect(reservationsRes.data.reservations.length).toBe(0) + }) + + it("filters by description using ends_with", async () => { + const api = useApi() + + const reservationsRes = await api.get( + `/admin/reservations?description[ends_with]=test`, + adminHeaders + ) + + expect(reservationsRes.data.reservations.length).toBe(0) + }) + + it("filters by description using ends_with removes results", async () => { + const api = useApi() + + const reservationsRes = await api.get( + `/admin/reservations?description[ends_with]=description`, + adminHeaders + ) + + expect(reservationsRes.data.reservations.length).toBe(1) + expect(reservationsRes.data.reservations[0].id).toBe(reservation2.id) + }) + }) + }) + it("lists reservations with inventory_items and line items", async () => { const api = useApi() diff --git a/packages/generated/client-types/src/lib/models/AdminGetReservationsParams.ts b/packages/generated/client-types/src/lib/models/AdminGetReservationsParams.ts index 81fda12ae9..c85d612237 100644 --- a/packages/generated/client-types/src/lib/models/AdminGetReservationsParams.ts +++ b/packages/generated/client-types/src/lib/models/AdminGetReservationsParams.ts @@ -37,6 +37,46 @@ export interface AdminGetReservationsParams { */ gte?: number } + /** + * A param for search reservation descriptions + */ + description?: + | string + | { + /** + * filter by reservation description containing search string. + */ + contains?: string + /** + * filter by reservation description starting with search string. + */ + starts_with?: string + /** + * filter by reservation description ending with search string. + */ + ends_with?: string + } + /** + * Date comparison for when resulting reservations were created. + */ + created_at?: { + /** + * filter by dates less than this date + */ + lt?: string + /** + * filter by dates greater than this date + */ + gt?: string + /** + * filter by dates less than or equal to this date + */ + lte?: string + /** + * filter by dates greater than or equal to this date + */ + gte?: string + } /** * How many Reservations to skip in the result. */ diff --git a/packages/inventory/src/services/reservation-item.ts b/packages/inventory/src/services/reservation-item.ts index 3ae17e9e55..11e6dc6286 100644 --- a/packages/inventory/src/services/reservation-item.ts +++ b/packages/inventory/src/services/reservation-item.ts @@ -60,6 +60,7 @@ export default class ReservationItemService { const itemRepository = manager.getRepository(ReservationItem) const query = buildQuery(selector, config) as FindManyOptions + return await itemRepository.find(query) } @@ -79,6 +80,7 @@ export default class ReservationItemService { const itemRepository = manager.getRepository(ReservationItem) const query = buildQuery(selector, config) as FindManyOptions + return await itemRepository.findAndCount(query) } diff --git a/packages/inventory/src/utils/query.ts b/packages/inventory/src/utils/query.ts index 9506d8c282..6793c1fcfd 100644 --- a/packages/inventory/src/utils/query.ts +++ b/packages/inventory/src/utils/query.ts @@ -1,11 +1,11 @@ +import { EntityManager, FindOptionsWhere, ILike } from "typeorm" import { ExtendedFindConfig, FilterableInventoryItemProps, FindConfig, } from "@medusajs/types" -import { objectToStringPath, buildQuery } from "@medusajs/utils" -import { EntityManager, FindOptionsWhere, ILike } from "typeorm" -import { InventoryItem } from "../models" +import { InventoryItem, ReservationItem } from "../models" +import { buildQuery, objectToStringPath } from "@medusajs/utils" export function getListQuery( manager: EntityManager, diff --git a/packages/medusa/src/api/routes/admin/reservations/list-reservations.ts b/packages/medusa/src/api/routes/admin/reservations/list-reservations.ts index 3f8aa20b3d..a4336d8ac9 100644 --- a/packages/medusa/src/api/routes/admin/reservations/list-reservations.ts +++ b/packages/medusa/src/api/routes/admin/reservations/list-reservations.ts @@ -1,8 +1,10 @@ -import { IsArray, IsOptional, IsString, ValidateNested } from "class-validator" import { + DateComparisonOperator, NumericalComparisonOperator, + StringComparisonOperator, extendedFindParamsMixin, } from "../../../../types/common" +import { IsArray, IsOptional, IsString, ValidateNested } from "class-validator" import { Request, Response } from "express" import { EntityManager } from "typeorm" @@ -65,6 +67,45 @@ import { joinLineItems } from "./utils/join-line-items" * gte: * type: number * description: filter by reservation quantity greater than or equal to this number + * - in: query + * name: description + * description: A param for search reservation descriptions + * schema: + * oneOf: + * - type: string + * - type: object + * properties: + * contains: + * type: string + * description: filter by reservation description containing search string. + * starts_with: + * type: string + * description: filter by reservation description starting with search string. + * ends_with: + * type: string + * description: filter by reservation description ending with search string. + * - in: query + * name: created_at + * description: Date comparison for when resulting reservations were created. + * schema: + * type: object + * properties: + * lt: + * type: string + * description: filter by dates less than this date + * format: date + * gt: + * type: string + * description: filter by dates greater than this date + * format: date + * lte: + * type: string + * description: filter by dates less than or equal to this date + * format: date + * gte: + * type: string + * description: filter by dates greater than or equal to this date + * format: date * - (query) offset=0 {integer} How many Reservations to skip in the result. * - (query) limit=20 {integer} Limit the number of Reservations returned. * - (query) expand {string} (Comma separated) Which fields should be expanded in the product category. @@ -184,4 +225,13 @@ export class AdminGetReservationsParams extends extendedFindParamsMixin({ @ValidateNested() @Type(() => NumericalComparisonOperator) quantity?: NumericalComparisonOperator + + @IsOptional() + @ValidateNested() + @Type(() => DateComparisonOperator) + created_at?: DateComparisonOperator + + @IsOptional() + @IsType([StringComparisonOperator, String]) + description?: string | StringComparisonOperator } diff --git a/packages/medusa/src/types/common.ts b/packages/medusa/src/types/common.ts index e7e2c4a7db..44972e3c6c 100644 --- a/packages/medusa/src/types/common.ts +++ b/packages/medusa/src/types/common.ts @@ -1,12 +1,5 @@ -import { Transform, Type } from "class-transformer" -import { - IsDate, - IsNumber, - IsObject, - IsOptional, - IsString, -} from "class-validator" import "reflect-metadata" + import { FindManyOptions, FindOneOptions, @@ -15,11 +8,22 @@ import { FindOptionsWhere, OrderByCondition, } from "typeorm" +import { + IsDate, + IsNumber, + IsObject, + IsOptional, + IsString, + Validate, +} from "class-validator" +import { Transform, Type } from "class-transformer" + +import { BaseEntity } from "../interfaces" +import { ClassConstructor } from "./global" +import { ExactlyOne } from "./validators/exactly-one" import { FindOptionsOrder } from "typeorm/find-options/FindOptionsOrder" import { FindOptionsRelations } from "typeorm/find-options/FindOptionsRelations" -import { BaseEntity } from "../interfaces" import { transformDate } from "../utils/validators/date-transform" -import { ClassConstructor } from "./global" /** * Utility type used to remove some optional attributes (coming from K) from a type T @@ -162,6 +166,18 @@ export class StringComparisonOperator { @IsString() @IsOptional() lte?: string + + @IsString() + @IsOptional() + contains?: string + + @IsString() + @IsOptional() + starts_with?: string + + @IsString() + @IsOptional() + ends_with?: string } export class NumericalComparisonOperator { diff --git a/packages/medusa/src/utils/build-query.ts b/packages/medusa/src/utils/build-query.ts index 35a6aefcc9..892465e9ed 100644 --- a/packages/medusa/src/utils/build-query.ts +++ b/packages/medusa/src/utils/build-query.ts @@ -5,6 +5,7 @@ import { FindOptionsRelations, FindOptionsSelect, FindOptionsWhere, + ILike, In, IsNull, LessThan, @@ -12,8 +13,9 @@ import { MoreThan, MoreThanOrEqual, } from "typeorm" -import { FindOptionsOrder } from "typeorm/find-options/FindOptionsOrder" import { ExtendedFindConfig, FindConfig } from "../types/common" + +import { FindOptionsOrder } from "typeorm/find-options/FindOptionsOrder" import { isObject } from "./is-object" /** @@ -120,6 +122,15 @@ function buildWhere( case "gte": where[key].push(MoreThanOrEqual(objectValue)) break + case "contains": + where[key].push(ILike(`%${objectValue}%`)) + break + case "starts_with": + where[key].push(ILike(`${objectValue}%`)) + break + case "ends_with": + where[key].push(ILike(`%${objectValue}`)) + break default: if (objectValue != undefined && typeof objectValue === "object") { where[key] = buildWhere(objectValue) diff --git a/packages/types/src/common/common.ts b/packages/types/src/common/common.ts index 532d70f139..265a07f640 100644 --- a/packages/types/src/common/common.ts +++ b/packages/types/src/common/common.ts @@ -6,6 +6,7 @@ import { FindOptionsWhere, OrderByCondition, } from "typeorm" + import { FindOptionsOrder } from "typeorm/find-options/FindOptionsOrder" import { FindOptionsRelations } from "typeorm/find-options/FindOptionsRelations" @@ -138,6 +139,9 @@ export interface StringComparisonOperator { gt?: string gte?: string lte?: string + contains?: string + starts_with?: string + ends_with?: string } export interface NumericalComparisonOperator { diff --git a/packages/types/src/inventory/common.ts b/packages/types/src/inventory/common.ts index 1a2cf447a5..8a656071b7 100644 --- a/packages/types/src/inventory/common.ts +++ b/packages/types/src/inventory/common.ts @@ -204,7 +204,7 @@ export type FilterableReservationItemProps = { line_item_id?: string | string[] inventory_item_id?: string | string[] location_id?: string | string[] - description?: string + description?: string | StringComparisonOperator created_by?: string | string[] quantity?: number | NumericalComparisonOperator } diff --git a/packages/utils/src/common/build-query.ts b/packages/utils/src/common/build-query.ts index 4f3ceb41db..cdb3c8ab19 100644 --- a/packages/utils/src/common/build-query.ts +++ b/packages/utils/src/common/build-query.ts @@ -1,4 +1,3 @@ -import { ExtendedFindConfig, FindConfig } from "@medusajs/types" import { And, FindManyOptions, @@ -6,6 +5,7 @@ import { FindOptionsRelations, FindOptionsSelect, FindOptionsWhere, + ILike, In, IsNull, LessThan, @@ -13,6 +13,8 @@ import { MoreThan, MoreThanOrEqual, } from "typeorm" +import { ExtendedFindConfig, FindConfig } from "@medusajs/types" + import { FindOptionsOrder } from "typeorm/find-options/FindOptionsOrder" import { isObject } from "./is-object" @@ -120,6 +122,15 @@ function buildWhere( case "gte": where[key].push(MoreThanOrEqual(objectValue)) break + case "contains": + where[key].push(ILike(`%${objectValue}%`)) + break + case "starts_with": + where[key].push(ILike(`${objectValue}%`)) + break + case "ends_with": + where[key].push(ILike(`%${objectValue}`)) + break default: if (objectValue != undefined && typeof objectValue === "object") { where[key] = buildWhere(objectValue)