Feat(inventory-next, medusa): Add List inventory items endpoint (#6694)

* code for list inventory items

* fix integration tests

* move integration test

* calculate reserved and stocked quantities in db

* update lockfile
This commit is contained in:
Philip Korsholm
2024-03-14 14:48:34 +01:00
committed by GitHub
parent 3dd55efd15
commit c3f8c30ba6
14 changed files with 1247 additions and 16 deletions

View File

@@ -0,0 +1,726 @@
import { IInventoryServiceNext, IStockLocationService } from "@medusajs/types"
import { ContainerRegistrationKeys } from "@medusajs/utils"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { createAdminUser } from "../../../helpers/create-admin-user"
import { remoteQueryObjectFromString } from "@medusajs/utils"
const { medusaIntegrationTestRunner } = require("medusa-test-utils")
jest.setTimeout(30000)
const { simpleProductFactory } = require("../../../factories")
const adminHeaders = { headers: { "x-medusa-access-token": "test_token" } }
medusaIntegrationTestRunner({
env: {
MEDUSA_FF_MEDUSA_V2: true,
},
testSuite: ({ dbConnection, getContainer, api }) => {
let appContainer
let shutdownServer
let service: IInventoryServiceNext
let variantId
let inventoryItems
let locationId
let location2Id
let location3Id
beforeEach(async () => {
appContainer = getContainer()
await createAdminUser(dbConnection, adminHeaders, appContainer)
service = appContainer.resolve(ModuleRegistrationName.INVENTORY)
})
describe("Inventory Items", () => {
it.skip("should create, update and delete the inventory location levels", async () => {
const inventoryItemId = inventoryItems[0].id
await api.post(
`/admin/inventory-items/${inventoryItemId}/location-levels`,
{
location_id: locationId,
stocked_quantity: 17,
incoming_quantity: 2,
},
adminHeaders
)
const inventoryService = appContainer.resolve("inventoryService")
const stockLevel = await inventoryService.retrieveInventoryLevel(
inventoryItemId,
locationId
)
expect(stockLevel.location_id).toEqual(locationId)
expect(stockLevel.inventory_item_id).toEqual(inventoryItemId)
expect(stockLevel.stocked_quantity).toEqual(17)
expect(stockLevel.incoming_quantity).toEqual(2)
await api.post(
`/admin/inventory-items/${inventoryItemId}/location-levels/${locationId}`,
{
stocked_quantity: 21,
incoming_quantity: 0,
},
adminHeaders
)
const newStockLevel = await inventoryService.retrieveInventoryLevel(
inventoryItemId,
locationId
)
expect(newStockLevel.stocked_quantity).toEqual(21)
expect(newStockLevel.incoming_quantity).toEqual(0)
await api.delete(
`/admin/inventory-items/${inventoryItemId}/location-levels/${locationId}`,
adminHeaders
)
const invLevel = await inventoryService
.retrieveInventoryLevel(inventoryItemId, locationId)
.catch((e) => e)
expect(invLevel.message).toEqual(
`Inventory level for item ${inventoryItemId} and location ${locationId} not found`
)
})
it.skip("should update the inventory item", async () => {
const inventoryItemId = inventoryItems[0].id
const response = await api.post(
`/admin/inventory-items/${inventoryItemId}`,
{
mid_code: "updated mid_code",
weight: 120,
},
adminHeaders
)
expect(response.data.inventory_item).toEqual(
expect.objectContaining({
origin_country: "UK",
hs_code: "hs001",
mid_code: "updated mid_code",
weight: 120,
length: 100,
height: 200,
width: 150,
})
)
})
it.skip("should fail to update the location level to negative quantity", async () => {
const inventoryItemId = inventoryItems[0].id
await api.post(
`/admin/inventory-items/${inventoryItemId}/location-levels`,
{
location_id: locationId,
stocked_quantity: 17,
incoming_quantity: 2,
},
adminHeaders
)
const res = await api
.post(
`/admin/inventory-items/${inventoryItemId}/location-levels/${locationId}`,
{
incoming_quantity: -1,
stocked_quantity: -1,
},
adminHeaders
)
.catch((error) => error)
expect(res.response.status).toEqual(400)
expect(res.response.data).toEqual({
type: "invalid_data",
message:
"incoming_quantity must not be less than 0, stocked_quantity must not be less than 0",
})
})
it.skip("should retrieve the inventory item", async () => {
const inventoryItemId = inventoryItems[0].id
await api.post(
`/admin/inventory-items/${inventoryItemId}/location-levels`,
{
location_id: locationId,
stocked_quantity: 15,
incoming_quantity: 5,
},
adminHeaders
)
await api.post(
`/admin/inventory-items/${inventoryItemId}/location-levels`,
{
location_id: location2Id,
stocked_quantity: 7,
incoming_quantity: 0,
},
adminHeaders
)
const response = await api.get(
`/admin/inventory-items/${inventoryItemId}`,
adminHeaders
)
expect(response.data).toEqual({
inventory_item: expect.objectContaining({
height: 200,
hs_code: "hs001",
id: inventoryItemId,
length: 100,
location_levels: [
expect.objectContaining({
available_quantity: 15,
deleted_at: null,
id: expect.any(String),
incoming_quantity: 5,
inventory_item_id: inventoryItemId,
location_id: locationId,
metadata: null,
reserved_quantity: 0,
stocked_quantity: 15,
}),
expect.objectContaining({
available_quantity: 7,
deleted_at: null,
id: expect.any(String),
incoming_quantity: 0,
inventory_item_id: inventoryItemId,
location_id: location2Id,
metadata: null,
reserved_quantity: 0,
stocked_quantity: 7,
}),
],
material: "material",
metadata: null,
mid_code: "mids",
origin_country: "UK",
requires_shipping: true,
sku: "MY_SKU",
weight: 300,
width: 150,
}),
})
})
it.skip("should create the inventory item using the api", async () => {
const product = await simpleProductFactory(dbConnection, {})
const productRes = await api.get(
`/admin/products/${product.id}`,
adminHeaders
)
const variantId = productRes.data.product.variants[0].id
let variantInventoryRes = await api.get(
`/admin/variants/${variantId}/inventory`,
adminHeaders
)
expect(variantInventoryRes.data).toEqual({
variant: {
id: variantId,
inventory: [],
sales_channel_availability: [],
},
})
expect(variantInventoryRes.status).toEqual(200)
const inventoryItemCreateRes = await api.post(
`/admin/inventory-items`,
{ variant_id: variantId, sku: "attach_this_to_variant" },
adminHeaders
)
variantInventoryRes = await api.get(
`/admin/variants/${variantId}/inventory`,
adminHeaders
)
expect(variantInventoryRes.data).toEqual({
variant: expect.objectContaining({
id: variantId,
inventory: [
expect.objectContaining({
...inventoryItemCreateRes.data.inventory_item,
}),
],
}),
})
expect(variantInventoryRes.status).toEqual(200)
})
it.skip("should list the location levels based on id param constraint", async () => {
const inventoryItemId = inventoryItems[0].id
await api.post(
`/admin/inventory-items/${inventoryItemId}/location-levels`,
{
location_id: location2Id,
stocked_quantity: 10,
},
adminHeaders
)
await api.post(
`/admin/inventory-items/${inventoryItemId}/location-levels`,
{
location_id: location3Id,
stocked_quantity: 5,
},
adminHeaders
)
const result = await api.get(
`/admin/inventory-items/${inventoryItemId}/location-levels?location_id[]=${location2Id}`,
adminHeaders
)
expect(result.status).toEqual(200)
expect(result.data.inventory_item.location_levels).toHaveLength(1)
expect(result.data.inventory_item.location_levels[0]).toEqual(
expect.objectContaining({
stocked_quantity: 10,
})
)
})
describe.skip("Create inventory item level", () => {
let location1
let location2
beforeEach(async () => {
await service.create([
{
sku: "MY_SKU",
origin_country: "UK",
hs_code: "hs001",
mid_code: "mids",
material: "material",
weight: 300,
length: 100,
height: 200,
width: 150,
},
])
const stockLocationService: IStockLocationService =
appContainer.resolve(ModuleRegistrationName.STOCK_LOCATION)
location1 = await stockLocationService.create({
name: "location-1",
})
location2 = await stockLocationService.create({
name: "location-2",
})
})
it("should list the inventory items", async () => {
const [{ id: inventoryItemId }] = await service.list({})
await api.post(
`/admin/inventory-items/${inventoryItemId}/location-levels`,
{
location_id: location1.id,
stocked_quantity: 10,
},
adminHeaders
)
await api.post(
`/admin/inventory-items/${inventoryItemId}/location-levels`,
{
location_id: location2.id,
stocked_quantity: 5,
},
adminHeaders
)
const levels = await service.listInventoryLevels({
inventory_item_id: inventoryItemId,
})
expect(levels).toHaveLength(2)
expect(levels).toEqual([
expect.objectContaining({
location_id: location1.id,
stocked_quantity: 10,
}),
expect.objectContaining({
location_id: location2.id,
stocked_quantity: 5,
}),
])
})
})
describe.skip("Create inventory items", () => {
it("should create inventory items", async () => {
const createResult = await api.post(
`/admin/products`,
{
title: "Test Product",
variants: [
{
title: "Test Variant w. inventory 2",
sku: "MY_SKU1",
material: "material",
},
],
},
adminHeaders
)
const inventoryItems = await service.list({})
expect(inventoryItems).toHaveLength(0)
const response = await api.post(
`/admin/inventory-items`,
{
sku: "test-sku",
variant_id: createResult.data.product.variants[0].id,
},
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.inventory_item).toEqual(
expect.objectContaining({
sku: "test-sku",
})
)
})
it("should attach inventory items on creation", async () => {
const createResult = await api.post(
`/admin/products`,
{
title: "Test Product",
variants: [
{
title: "Test Variant w. inventory 2",
sku: "MY_SKU1",
material: "material",
},
],
},
adminHeaders
)
const inventoryItems = await service.list({})
expect(inventoryItems).toHaveLength(0)
await api.post(
`/admin/inventory-items`,
{
sku: "test-sku",
variant_id: createResult.data.product.variants[0].id,
},
adminHeaders
)
const remoteQuery = appContainer.resolve(
ContainerRegistrationKeys.REMOTE_QUERY
)
const query = remoteQueryObjectFromString({
entryPoint: "product_variant_inventory_item",
variables: {
variant_id: createResult.data.product.variants[0].id,
},
fields: ["inventory_item_id", "variant_id"],
})
const existingItems = await remoteQuery(query)
expect(existingItems).toHaveLength(1)
expect(existingItems[0].variant_id).toEqual(
createResult.data.product.variants[0].id
)
})
})
describe("List inventory items", () => {
let location1 = "loc_1"
let location2 = "loc_2"
beforeEach(async () => {
await service.create([
{
sku: "MY_SKU",
origin_country: "UK",
hs_code: "hs001",
mid_code: "mids",
material: "material",
weight: 300,
length: 100,
height: 200,
width: 150,
},
])
})
it("should list the inventory items", async () => {
const [{ id: inventoryItemId }] = await service.list({})
await service.createInventoryLevels([
{
inventory_item_id: inventoryItemId,
location_id: location1,
stocked_quantity: 10,
},
{
inventory_item_id: inventoryItemId,
location_id: location2,
stocked_quantity: 5,
},
])
const response = await api.get(`/admin/inventory-items`, adminHeaders)
expect(response.data.inventory_items).toHaveLength(1)
expect(response.data.inventory_items[0]).toEqual(
expect.objectContaining({
id: inventoryItemId,
sku: "MY_SKU",
origin_country: "UK",
hs_code: "hs001",
mid_code: "mids",
material: "material",
weight: 300,
length: 100,
height: 200,
width: 150,
requires_shipping: true,
metadata: null,
location_levels: expect.arrayContaining([
expect.objectContaining({
id: expect.any(String),
inventory_item_id: inventoryItemId,
location_id: location1,
stocked_quantity: 10,
reserved_quantity: 0,
incoming_quantity: 0,
metadata: null,
available_quantity: 10,
}),
expect.objectContaining({
id: expect.any(String),
inventory_item_id: inventoryItemId,
location_id: location2,
stocked_quantity: 5,
reserved_quantity: 0,
incoming_quantity: 0,
metadata: null,
available_quantity: 5,
}),
]),
reserved_quantity: 0,
stocked_quantity: 15,
})
)
})
it("should list the inventory items searching by title, description and sku", async () => {
const inventoryService = appContainer.resolve("inventoryService")
await inventoryService.create([
{
title: "Test Item",
},
{
description: "Test Desc",
},
{
sku: "Test Sku",
},
])
const response = await api.get(
`/admin/inventory-items?q=test`,
adminHeaders
)
expect(response.data.inventory_items).not.toEqual(
expect.arrayContaining([
expect.objectContaining({
sku: "MY_SKU",
}),
])
)
expect(response.data.inventory_items).toHaveLength(3)
expect(response.data.inventory_items).toEqual(
expect.arrayContaining([
expect.objectContaining({
sku: "Test Sku",
}),
expect.objectContaining({
description: "Test Desc",
}),
expect.objectContaining({
title: "Test Item",
}),
])
)
})
})
it.skip("should remove associated levels and reservations when deleting an inventory item", async () => {
const inventoryService = appContainer.resolve("inventoryService")
const invItem2 = await inventoryService.createInventoryItem({
sku: "1234567",
})
const stockRes = await api.post(
`/admin/stock-locations`,
{
name: "Fake Warehouse 1",
},
adminHeaders
)
locationId = stockRes.data.stock_location.id
const level = await inventoryService.createInventoryLevel({
inventory_item_id: invItem2.id,
location_id: locationId,
stocked_quantity: 10,
})
const reservation = await inventoryService.createReservationItem({
inventory_item_id: invItem2.id,
location_id: locationId,
quantity: 5,
})
const [, reservationCount] =
await inventoryService.listReservationItems({
location_id: locationId,
})
expect(reservationCount).toEqual(1)
const [, inventoryLevelCount] =
await inventoryService.listInventoryLevels({
location_id: locationId,
})
expect(inventoryLevelCount).toEqual(1)
const res = await api.delete(
`/admin/stock-locations/${locationId}`,
adminHeaders
)
expect(res.status).toEqual(200)
const [, reservationCountPostDelete] =
await inventoryService.listReservationItems({
location_id: locationId,
})
expect(reservationCountPostDelete).toEqual(0)
const [, inventoryLevelCountPostDelete] =
await inventoryService.listInventoryLevels({
location_id: locationId,
})
expect(inventoryLevelCountPostDelete).toEqual(0)
})
it.skip("should remove the product variant associations when deleting an inventory item", async () => {
await simpleProductFactory(
dbConnection,
{
id: "test-product-new",
variants: [],
},
5
)
const response = await api.post(
`/admin/products/test-product-new/variants`,
{
title: "Test2",
sku: "MY_SKU2",
manage_inventory: true,
options: [
{
option_id: "test-product-new-option",
value: "Blue",
},
],
prices: [{ currency_code: "usd", amount: 100 }],
},
{ headers: { "x-medusa-access-token": "test_token" } }
)
const secondVariantId = response.data.product.variants.find(
(v) => v.sku === "MY_SKU2"
).id
const inventoryService = appContainer.resolve("inventoryService")
const variantInventoryService = appContainer.resolve(
"productVariantInventoryService"
)
const invItem2 = await inventoryService.createInventoryItem({
sku: "123456",
})
await variantInventoryService.attachInventoryItem(
variantId,
invItem2.id,
2
)
await variantInventoryService.attachInventoryItem(
secondVariantId,
invItem2.id,
2
)
expect(
await variantInventoryService.listInventoryItemsByVariant(variantId)
).toHaveLength(2)
expect(
await variantInventoryService.listInventoryItemsByVariant(
secondVariantId
)
).toHaveLength(2)
await api.delete(`/admin/inventory-items/${invItem2.id}`, {
headers: { "x-medusa-access-token": "test_token" },
})
expect(
await variantInventoryService.listInventoryItemsByVariant(variantId)
).toHaveLength(1)
expect(
await variantInventoryService.listInventoryItemsByVariant(
secondVariantId
)
).toHaveLength(1)
})
})
},
})

View File

@@ -4,7 +4,9 @@ const DB_USERNAME = process.env.DB_USERNAME
const DB_PASSWORD = process.env.DB_PASSWORD
const DB_NAME = process.env.DB_TEMP_NAME
const DB_URL = `postgres://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}/${DB_NAME}`
process.env.POSTGRES_URL = DB_URL
process.env.LOG_LEVEL = "error"
const enableMedusaV2 = process.env.MEDUSA_FF_MEDUSA_V2 == "true"
@@ -38,6 +40,8 @@ module.exports = {
medusa_v2: enableMedusaV2,
},
modules: {
workflows: true,
[Modules.AUTH]: {
scope: "internal",
resources: "shared",
@@ -64,6 +68,11 @@ module.exports = {
resources: "shared",
resolve: "@medusajs/inventory",
},
[Modules.PRICING]: {
scope: "internal",
resources: "shared",
resolve: "@medusajs/pricing",
},
[Modules.CACHE]: {
resolve: "@medusajs/cache-inmemory",
options: { ttl: 0 }, // Cache disabled

View File

@@ -3,7 +3,9 @@ import {
Collection,
Entity,
Filter,
Formula,
OnInit,
OnLoad,
OneToMany,
OptionalProps,
PrimaryKey,
@@ -106,7 +108,21 @@ export class InventoryItem {
() => InventoryLevel,
(inventoryLevel) => inventoryLevel.inventory_item
)
inventory_levels = new Collection<InventoryLevel>(this)
location_levels = new Collection<InventoryLevel>(this)
@Formula(
(item) =>
`(SELECT SUM(reserved_quantity) FROM inventory_level il WHERE il.inventory_item_id = ${item}.id)`,
{ lazy: true, serializer: Number, hidden: true }
)
reserved_quantity: number
@Formula(
(item) =>
`(SELECT SUM(stocked_quantity) FROM inventory_level il WHERE il.inventory_item_id = ${item}.id)`,
{ lazy: true, serializer: Number, hidden: true }
)
stocked_quantity: number
@BeforeCreate()
private beforeCreate(): void {

View File

@@ -4,11 +4,12 @@ import {
Filter,
ManyToOne,
OnInit,
OnLoad,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import { DALUtils, isDefined } from "@medusajs/utils"
import { DALUtils } from "@medusajs/utils"
import { InventoryItem } from "./inventory-item"
import { createPsqlIndexStatementHelper } from "@medusajs/utils"
import { generateEntityId } from "@medusajs/utils"
@@ -91,6 +92,8 @@ export class InventoryLevel {
})
inventory_item: InventoryItem
available_quantity: number | null = null
@BeforeCreate()
private beforeCreate(): void {
this.id = generateEntityId(this.id, "ilev")
@@ -101,4 +104,11 @@ export class InventoryLevel {
private onInit(): void {
this.id = generateEntityId(this.id, "ilev")
}
@OnLoad()
private onLoad(): void {
if (isDefined(this.stocked_quantity) && isDefined(this.reserved_quantity)) {
this.available_quantity = this.stocked_quantity - this.reserved_quantity
}
}
}

View File

@@ -1,2 +1,3 @@
export * from "./inventory-level"
export * from "./inventory-item"
export { MikroOrmBaseRepository as BaseRepository } from "@medusajs/utils"

View File

@@ -0,0 +1,63 @@
import { Context, DAL } from "@medusajs/types"
import { InventoryItem, InventoryLevel } from "@models"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { mikroOrmBaseRepositoryFactory } from "@medusajs/utils"
export class InventoryItemRepository extends mikroOrmBaseRepositoryFactory<InventoryItem>(
InventoryItem
) {
async find(
findOptions: DAL.FindOptions<InventoryItem & { q?: string }> = {
where: {},
},
context: Context = {}
): Promise<InventoryItem[]> {
const findOptions_ = { ...findOptions }
findOptions_.options ??= {}
this.applyFreeTextSearchFilters<InventoryItem>(
findOptions_,
this.getFreeTextSearchConstraints
)
return await super.find(findOptions_, context)
}
async findAndCount(
findOptions: DAL.FindOptions<InventoryItem & { q?: string }> = {
where: {},
},
context: Context = {}
): Promise<[InventoryItem[], number]> {
const findOptions_ = { ...findOptions }
findOptions_.options ??= {}
this.applyFreeTextSearchFilters<InventoryItem>(
findOptions_,
this.getFreeTextSearchConstraints
)
return await super.findAndCount(findOptions_, context)
}
protected getFreeTextSearchConstraints(q: string) {
return [
{
description: {
$ilike: `%${q}%`,
},
},
{
title: {
$ilike: `%${q}%`,
},
},
{
sku: {
$ilike: `%${q}%`,
},
},
]
}
}

View File

@@ -15,10 +15,10 @@ import {
InjectManager,
InjectTransactionManager,
InventoryEvents,
isDefined,
MedusaContext,
MedusaError,
ModulesSdkUtils,
isDefined,
partitionArray,
} from "@medusajs/utils"
import { InventoryItem, InventoryLevel, ReservationItem } from "@models"
@@ -100,7 +100,7 @@ export default class InventoryModuleService<
{ location_id: string; inventory_item_id: string }[]
]
const inventoryLevels = await this.inventoryLevelService_.list(
const inventoryLevels = await this.listInventoryLevels(
{
$or: [
{ id: idData.filter(({ id }) => !!id).map((e) => e.id) },

View File

@@ -0,0 +1,40 @@
import * as QueryConfig from "./query-config"
import {
AdminGetInventoryItemsItemParams,
AdminGetInventoryItemsParams,
AdminPostInventoryItemsItemLocationLevelsReq,
AdminPostInventoryItemsReq,
} from "./validators"
import { transformBody, transformQuery } from "../../../api/middlewares"
import { MiddlewareRoute } from "../../../types/middlewares"
import { authenticate } from "../../../utils/authenticate-middleware"
export const adminInventoryRoutesMiddlewares: MiddlewareRoute[] = [
{
method: "ALL",
matcher: "/admin/inventory-items*",
middlewares: [authenticate("admin", ["session", "bearer"])],
},
{
method: ["GET"],
matcher: "/admin/inventory-items",
middlewares: [
transformQuery(
AdminGetInventoryItemsParams,
QueryConfig.listTransformQueryConfig
),
],
},
{
method: ["POST"],
matcher: "/admin/inventory-items/:id/location-levels",
middlewares: [transformBody(AdminPostInventoryItemsItemLocationLevelsReq)],
},
{
method: ["POST"],
matcher: "/admin/inventory-items",
middlewares: [transformBody(AdminPostInventoryItemsReq)],
},
]

View File

@@ -0,0 +1,56 @@
import { InventoryNext } from "@medusajs/types"
export const defaultAdminInventoryItemRelations = []
export const allowedAdminInventoryItemRelations = []
// eslint-disable-next-line max-len
export const defaultAdminLocationLevelFields: (keyof InventoryNext.InventoryLevelDTO)[] =
[
"id",
"inventory_item_id",
"location_id",
"stocked_quantity",
"reserved_quantity",
"incoming_quantity",
"available_quantity",
"metadata",
"created_at",
"updated_at",
]
export const defaultAdminInventoryItemFields = [
"id",
"sku",
"title",
"description",
"thumbnail",
"origin_country",
"hs_code",
"requires_shipping",
"mid_code",
"material",
"weight",
"length",
"height",
"width",
"metadata",
"reserved_quantity",
"stocked_quantity",
"created_at",
"updated_at",
...defaultAdminLocationLevelFields.map(
(field) => `location_levels.${field.toString()}`
),
]
export const retrieveTransformQueryConfig = {
defaultFields: defaultAdminInventoryItemFields,
defaultRelations: defaultAdminInventoryItemRelations,
allowedRelations: allowedAdminInventoryItemRelations,
isList: false,
}
export const listTransformQueryConfig = {
...retrieveTransformQueryConfig,
isList: true,
}

View File

@@ -0,0 +1,38 @@
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "../../../types/routing"
import {
ContainerRegistrationKeys,
remoteQueryObjectFromString,
} from "@medusajs/utils"
// List inventory-items
export const GET = async (
req: AuthenticatedMedusaRequest,
res: MedusaResponse
) => {
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
const query = remoteQueryObjectFromString({
entryPoint: "inventory_items",
variables: {
filters: req.filterableFields,
order: req.listConfig.order,
skip: req.listConfig.skip,
take: req.listConfig.take,
},
fields: [...(req.listConfig.select as string[])],
})
const { rows: inventory_items, metadata } = await remoteQuery({
...query,
})
res.status(200).json({
inventory_items,
count: metadata.count,
offset: metadata.skip,
limit: metadata.take,
})
}

View File

@@ -0,0 +1,268 @@
import {
DateComparisonOperator,
FindParams,
NumericalComparisonOperator,
StringComparisonOperator,
extendedFindParamsMixin,
} from "../../../types/common"
import {
IsBoolean,
IsEmail,
IsNotEmpty,
IsNumber,
IsObject,
IsOptional,
IsString,
ValidateNested,
} from "class-validator"
import { Transform, Type } from "class-transformer"
import { IsType } from "../../../utils"
export class AdminGetInventoryItemsItemParams extends FindParams {}
/**
* Parameters used to filter and configure the pagination of the retrieved inventory items.
*/
export class AdminGetInventoryItemsParams extends extendedFindParamsMixin({
limit: 20,
offset: 0,
}) {
/**
* IDs to filter inventory items by.
*/
@IsOptional()
@IsType([String, [String]])
id?: string | string[]
/**
* Search terms to search inventory items' sku, title, and description.
*/
@IsOptional()
@IsString()
q?: string
/**
* Location IDs to filter inventory items by.
*/
@IsOptional()
@IsType([String, [String]])
location_id?: string | string[]
/**
* SKUs to filter inventory items by.
*/
@IsOptional()
@IsType([String, [String]])
sku?: string | string[]
/**
* Origin countries to filter inventory items by.
*/
@IsOptional()
@IsType([String, [String]])
origin_country?: string | string[]
/**
* MID codes to filter inventory items by.
*/
@IsOptional()
@IsType([String, [String]])
mid_code?: string | string[]
/**
* Materials to filter inventory items by.
*/
@IsOptional()
@IsType([String, [String]])
material?: string | string[]
/**
* String filters to apply to inventory items' `hs_code` field.
*/
@IsOptional()
@IsType([String, [String], StringComparisonOperator])
hs_code?: string | string[] | StringComparisonOperator
/**
* Number filters to apply to inventory items' `weight` field.
*/
@IsOptional()
@IsType([Number, NumericalComparisonOperator])
weight?: number | NumericalComparisonOperator
/**
* Number filters to apply to inventory items' `length` field.
*/
@IsOptional()
@IsType([Number, NumericalComparisonOperator])
length?: number | NumericalComparisonOperator
/**
* Number filters to apply to inventory items' `height` field.
*/
@IsOptional()
@IsType([Number, NumericalComparisonOperator])
height?: number | NumericalComparisonOperator
/**
* Number filters to apply to inventory items' `width` field.
*/
@IsOptional()
@IsType([Number, NumericalComparisonOperator])
width?: number | NumericalComparisonOperator
/**
* Filter inventory items by whether they require shipping.
*/
@IsBoolean()
@IsOptional()
@Transform(({ value }) => value === "true")
requires_shipping?: boolean
}
export class AdminPostInventoryItemsItemLocationLevelsReq {
@IsString()
location_id: string
@IsNumber()
stocked_quantity: number
@IsOptional()
@IsNumber()
incoming_quantity?: number
}
// eslint-disable-next-line
export class AdminPostInventoryItemsItemLocationLevelsParams extends FindParams {}
/**
* @schema AdminPostInventoryItemsReq
* type: object
* description: "The details of the inventory item to create."
* required:
* - variant_id
* properties:
* variant_id:
* description: The ID of the variant to create the inventory item for.
* type: string
* sku:
* description: The unique SKU of the associated Product Variant.
* type: string
* ean:
* description: The EAN number of the item.
* type: string
* upc:
* description: The UPC number of the item.
* type: string
* barcode:
* description: A generic GTIN field for the Product Variant.
* type: string
* hs_code:
* description: The Harmonized System code of the Inventory Item. May be used by Fulfillment Providers to pass customs information to shipping carriers.
* type: string
* inventory_quantity:
* description: The amount of stock kept of the associated Product Variant.
* type: integer
* default: 0
* allow_backorder:
* description: Whether the associated Product Variant can be purchased when out of stock.
* type: boolean
* manage_inventory:
* description: Whether Medusa should keep track of the inventory for the associated Product Variant.
* type: boolean
* default: true
* weight:
* description: The weight of the Inventory Item. May be used in shipping rate calculations.
* type: number
* length:
* description: The length of the Inventory Item. May be used in shipping rate calculations.
* type: number
* height:
* description: The height of the Inventory Item. May be used in shipping rate calculations.
* type: number
* width:
* description: The width of the Inventory Item. May be used in shipping rate calculations.
* type: number
* origin_country:
* description: The country in which the Inventory Item was produced. May be used by Fulfillment Providers to pass customs information to shipping carriers.
* type: string
* mid_code:
* description: The Manufacturers Identification code that identifies the manufacturer of the Inventory Item. May be used by Fulfillment Providers to pass customs information to shipping carriers.
* type: string
* material:
* description: The material and composition that the Inventory Item is made of, May be used by Fulfillment Providers to pass customs information to shipping carriers.
* type: string
* title:
* description: The inventory item's title.
* type: string
* description:
* description: The inventory item's description.
* type: string
* thumbnail:
* description: The inventory item's thumbnail.
* type: string
* metadata:
* description: An optional set of key-value pairs with additional information.
* type: object
* externalDocs:
* description: "Learn about the metadata attribute, and how to delete and update it."
* url: "https://docs.medusajs.com/development/entities/overview#metadata-attribute"
*/
export class AdminPostInventoryItemsReq {
@IsOptional()
@IsString()
variant_id: string
@IsString()
@IsOptional()
sku?: string
@IsString()
@IsOptional()
hs_code?: string
@IsNumber()
@IsOptional()
weight?: number
@IsNumber()
@IsOptional()
length?: number
@IsNumber()
@IsOptional()
height?: number
@IsNumber()
@IsOptional()
width?: number
@IsString()
@IsOptional()
origin_country?: string
@IsString()
@IsOptional()
mid_code?: string
@IsString()
@IsOptional()
material?: string
@IsString()
@IsOptional()
title?: string
@IsString()
@IsOptional()
description?: string
@IsString()
@IsOptional()
thumbnail?: string
@IsObject()
@IsOptional()
metadata?: Record<string, unknown>
}

View File

@@ -5,6 +5,7 @@ import { adminCollectionRoutesMiddlewares } from "./admin/collections/middleware
import { adminCurrencyRoutesMiddlewares } from "./admin/currencies/middlewares"
import { adminCustomerGroupRoutesMiddlewares } from "./admin/customer-groups/middlewares"
import { adminCustomerRoutesMiddlewares } from "./admin/customers/middlewares"
import { adminInventoryRoutesMiddlewares } from "./admin/inventory-items/middlewares"
import { adminInviteRoutesMiddlewares } from "./admin/invites/middlewares"
import { adminPaymentRoutesMiddlewares } from "./admin/payments/middlewares"
import { adminPriceListsRoutesMiddlewares } from "./admin/price-lists/middlewares"
@@ -49,6 +50,7 @@ export const config: MiddlewaresConfig = {
...adminProductRoutesMiddlewares,
...adminPaymentRoutesMiddlewares,
...adminPriceListsRoutesMiddlewares,
...adminInventoryRoutesMiddlewares,
...adminCollectionRoutesMiddlewares,
...adminPricingRoutesMiddlewares,
],

View File

@@ -1,12 +1,3 @@
import {
MedusaApp,
MedusaAppMigrateUp,
MedusaAppOutput,
MedusaModule,
MODULE_PACKAGE_NAMES,
Modules,
ModulesDefinition,
} from "@medusajs/modules-sdk"
import {
CommonTypes,
InternalModuleDeclaration,
@@ -17,12 +8,22 @@ import {
import {
ContainerRegistrationKeys,
FlagRouter,
isObject,
MedusaV2Flag,
isObject,
} from "@medusajs/utils"
import {
MODULE_PACKAGE_NAMES,
MedusaApp,
MedusaAppMigrateUp,
MedusaAppOutput,
MedusaModule,
Modules,
ModulesDefinition,
} from "@medusajs/modules-sdk"
import { asValue } from "awilix"
import { remoteQueryFetchData } from "../utils/remote-query-fetch-data"
import { joinerConfig } from "../joiner-config"
import { remoteQueryFetchData } from "../utils/remote-query-fetch-data"
export function mergeDefaultModules(
modulesConfig: CommonTypes.ConfigModule["modules"]
@@ -74,7 +75,6 @@ export async function migrateMedusaApp(
debug: !!(configModule.projectConfig.database_logging ?? false),
},
}
const configModules = mergeDefaultModules(configModule.modules)
// Apply default options to legacy modules
@@ -90,6 +90,7 @@ export async function migrateMedusaApp(
database: {
type: "postgres",
url: sharedResourcesConfig.database.clientUrl,
clientUrl: sharedResourcesConfig.database.clientUrl,
extra: configModule.projectConfig.database_extra,
schema: configModule.projectConfig.database_schema,
logging: configModule.projectConfig.database_logging,

View File

@@ -46,6 +46,7 @@ export interface InventoryLevelDTO {
stocked_quantity: number
reserved_quantity: number
incoming_quantity: number
available_quantity: number
metadata: Record<string, unknown> | null
created_at: string | Date
updated_at: string | Date