feat(medusa,inventory,types): Expand list-reservation capabilities (#3979)

**What**
- Add filter capabilities to reservation items based on: 
  - description query: "contains", "startsWith", "endsWith", "equals"
  - date querying

**How**
- Introducing a new filtering primitive: "StringSearchOperator" resembling the "dateComparisonOperator"

Fixes CORE-1373
This commit is contained in:
Philip Korsholm
2023-05-24 10:54:25 +01:00
committed by GitHub
parent 3d1eb3f4d4
commit 3a38c84f88
11 changed files with 386 additions and 17 deletions

View File

@@ -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

View File

@@ -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()

View File

@@ -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.
*/

View File

@@ -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)
}

View File

@@ -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,

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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<TWhereKeys extends object, TEntity>(
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<any, TEntity>(objectValue)

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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<TWhereKeys extends object, TEntity>(
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<any, TEntity>(objectValue)