feat(medusa-js, medusa-react, medusa): Prepare API for admin implementations (#3110)

********What********
Add `joinSalesChannels util to stock locations

Add the following endpoints to medusa-react
- inventory items
    - mutations
        - update
        - delete
        - update location level
        - delete location level
        - create location level
    - queries
        - list inventory items
        - get inventory item
        - list location levels
- Stock locations
    - mutations
        - create stock location
        - update stock location
        - delete stock location
    - queries
        - list stock locations
        - get stock locatoin
- Variants
    - queries
        - get inventory
- Reservations
    - mutations
        - create reservation
        - update reservation
        - delete reservation
    - queries
        - list reservations
        - get reservation
- sales channels 
  - mutations
    - associate location with sc
    - remove location association

**Why**
- Update clients to reflect new api endpoints in the core with inventory modules

Co-authored-by: Philip Korsholm <88927411+pKorsholm@users.noreply.github.com>
This commit is contained in:
Rares StefanandPhilip Korsholm authored and GitHub committed 2023-02-16 08:49:48 +00:00
1 parent 121b42acfe
commit 12d304307a
48 files changed
+1756 -57

No files matched your search

+1
View File
@@ -43,6 +43,7 @@ export * from "./routes/admin/publishable-api-keys"
export * from "./routes/admin/regions"
export * from "./routes/admin/return-reasons"
export * from "./routes/admin/returns"
export * from "./routes/admin/reservations"
export * from "./routes/admin/sales-channels"
export * from "./routes/admin/shipping-options"
export * from "./routes/admin/shipping-profiles"
@@ -82,7 +82,6 @@ export default async (req, res) => {
* @schema AdminPostReservationsReq
* type: object
* required:
* - line_item_id
* - location_id
* - inventory_item_id
* - quantity
@@ -109,3 +109,4 @@ export * from "./create-reservation"
export * from "./delete-reservation"
export * from "./get-reservation"
export * from "./update-reservation"
export * from "./list-reservations"
@@ -15,6 +15,7 @@ import { AdminGetSalesChannelsParams } from "./list-sales-channels"
import { AdminPostSalesChannelsSalesChannelReq } from "./update-sales-channel"
import { AdminPostSalesChannelsChannelStockLocationsReq } from "./associate-stock-location"
import { AdminDeleteSalesChannelsChannelStockLocationsReq } from "./remove-stock-location"
import { checkRegisteredModules } from "../../../middlewares/check-registered-modules"
const route = Router()
@@ -47,11 +48,19 @@ export default (app) => {
)
salesChannelRouter.post(
"/stock-locations",
checkRegisteredModules({
stockLocationService:
"Stock Locations are not enabled. Please add a Stock Location module to enable this functionality.",
}),
transformBody(AdminPostSalesChannelsChannelStockLocationsReq),
middlewares.wrap(require("./associate-stock-location").default)
)
salesChannelRouter.delete(
"/stock-locations",
checkRegisteredModules({
stockLocationService:
"Stock Locations are not enabled. Please add a Stock Location module to enable this functionality.",
}),
transformBody(AdminDeleteSalesChannelsChannelStockLocationsReq),
middlewares.wrap(require("./remove-stock-location").default)
)
@@ -1,6 +1,11 @@
import { IStockLocationService } from "../../../../interfaces"
import { Request, Response } from "express"
import { FindParams } from "../../../../types/common"
import { joinSalesChannels } from "./utils/join-sales-channels"
import {
SalesChannelLocationService,
SalesChannelService,
} from "../../../../services"
/**
* @oas [get] /stock-locations/{id}
@@ -50,7 +55,34 @@ export default async (req: Request, res: Response) => {
const locationService: IStockLocationService = req.scope.resolve(
"stockLocationService"
)
const stockLocation = await locationService.retrieve(id, req.retrieveConfig)
const channelLocationService: SalesChannelLocationService = req.scope.resolve(
"salesChannelLocationService"
)
const salesChannelService: SalesChannelService = req.scope.resolve(
"salesChannelService"
)
const { retrieveConfig } = req
const includeSalesChannels =
!!retrieveConfig.relations?.includes("sales_channels")
if (includeSalesChannels) {
retrieveConfig.relations = retrieveConfig.relations?.filter(
(r) => r !== "sales_channels"
)
}
let stockLocation = await locationService.retrieve(id, retrieveConfig)
if (includeSalesChannels) {
const [location] = await joinSalesChannels(
[stockLocation],
channelLocationService,
salesChannelService
)
stockLocation = location
}
res.status(200).json({ stock_location: stockLocation })
}
@@ -1,7 +1,10 @@
import { Router } from "express"
import "reflect-metadata"
import { DeleteResponse, PaginatedResponse } from "../../../../types/common"
import { StockLocationDTO } from "../../../../types/stock-location"
import {
StockLocationDTO,
StockLocationExpandedDTO,
} from "../../../../types/stock-location"
import middlewares, {
transformBody,
transformQuery,
@@ -113,10 +116,10 @@ export type AdminStockLocationsDeleteRes = DeleteResponse
* type: object
* properties:
* stock_location:
* $ref: "#/components/schemas/StockLocationDTO"
* $ref: "#/components/schemas/StockLocationExpandedDTO"
*/
export type AdminStockLocationsRes = {
stock_location: StockLocationDTO
stock_location: StockLocationExpandedDTO
}
/**
@@ -126,7 +129,7 @@ export type AdminStockLocationsRes = {
* stock_locations:
* type: array
* items:
* $ref: "#/components/schemas/StockLocationDTO"
* $ref: "#/components/schemas/StockLocationExpandedDTO"
* count:
* type: integer
* description: The total number of items available
@@ -138,7 +141,7 @@ export type AdminStockLocationsRes = {
* description: The number of items per page
*/
export type AdminStockLocationsListRes = PaginatedResponse & {
stock_locations: StockLocationDTO[]
stock_locations: StockLocationExpandedDTO[]
}
export * from "./list-stock-locations"
@@ -4,6 +4,11 @@ import { IsType } from "../../../../utils/validators/is-type"
import { IStockLocationService } from "../../../../interfaces"
import { extendedFindParamsMixin } from "../../../../types/common"
import { Request, Response } from "express"
import {
SalesChannelLocationService,
SalesChannelService,
} from "../../../../services"
import { joinSalesChannels } from "./utils/join-sales-channels"
/**
* @oas [get] /stock-locations
@@ -133,15 +138,38 @@ export default async (req: Request, res: Response) => {
const stockLocationService: IStockLocationService = req.scope.resolve(
"stockLocationService"
)
const channelLocationService: SalesChannelLocationService = req.scope.resolve(
"salesChannelLocationService"
)
const salesChannelService: SalesChannelService = req.scope.resolve(
"salesChannelService"
)
const { filterableFields, listConfig } = req
const { skip, take } = listConfig
const [locations, count] = await stockLocationService.listAndCount(
const includeSalesChannels =
!!listConfig.relations?.includes("sales_channels")
if (includeSalesChannels) {
listConfig.relations = listConfig.relations?.filter(
(r) => r !== "sales_channels"
)
}
let [locations, count] = await stockLocationService.listAndCount(
filterableFields,
listConfig
)
if (includeSalesChannels) {
locations = await joinSalesChannels(
locations,
channelLocationService,
salesChannelService
)
}
res.status(200).json({
stock_locations: locations,
count,
@@ -0,0 +1,31 @@
import {
SalesChannelLocationService,
SalesChannelService,
} from "../../../../../services"
import {
StockLocationDTO,
StockLocationExpandedDTO,
} from "../../../../../types/stock-location"
const joinSalesChannels = async (
locations: StockLocationDTO[],
channelLocationService: SalesChannelLocationService,
salesChannelService: SalesChannelService
): Promise<StockLocationExpandedDTO[]> => {
return await Promise.all(
locations.map(async (location: StockLocationExpandedDTO) => {
const salesChannelIds = await channelLocationService.listSalesChannelIds(
location.id
)
const [salesChannels] = await salesChannelService.listAndCount({
id: salesChannelIds,
})
location.sales_channels = salesChannels
return location
})
)
}
export { joinSalesChannels }
@@ -1,11 +1,9 @@
import { FulfillmentProvider, PaymentProvider, Store } from "../../../../models"
import {
FulfillmentProviderService,
PaymentProviderService,
StoreService,
} from "../../../../services"
import { FeatureFlagsResponse } from "../../../../types/feature-flags"
import { ModulesResponse } from "../../../../types/modules"
import { ExtendedStoreDTO } from "../../../../types/store"
import { FlagRouter } from "../../../../utils/flag-router"
import { ModulesHelper } from "../../../../utils/module-helper"
@@ -77,12 +75,7 @@ export default async (req, res) => {
const data = (await storeService.retrieve({
relations,
})) as Store & {
payment_providers: PaymentProvider[]
fulfillment_providers: FulfillmentProvider[]
feature_flags: FeatureFlagsResponse
modules: ModulesResponse
}
})) as ExtendedStoreDTO
data.feature_flags = featureFlagRouter.listFlags()
data.modules = modulesHelper.modules
@@ -1,6 +1,7 @@
import { Router } from "express"
import { PaymentProvider, Store, TaxProvider } from "./../../../../"
import { PaymentProvider, TaxProvider } from "./../../../../"
import middlewares from "../../../middlewares"
import { ExtendedStoreDTO } from "../../../../types/store"
const route = Router()
@@ -34,10 +35,10 @@ export default (app) => {
* type: object
* properties:
* store:
* $ref: "#/components/schemas/Store"
* $ref: "#/components/schemas/ExtendedStoreDTO"
*/
export type AdminStoresRes = {
store: Store
store: ExtendedStoreDTO
}
/**
@@ -72,7 +72,9 @@ export default async (req, res) => {
const inventoryService: IInventoryService =
req.scope.resolve("inventoryService")
const channelLocationService: SalesChannelLocationService = req.scope.resolve(
"salesChannelLocationService"
)
const channelService: SalesChannelService = req.scope.resolve(
"salesChannelService"
)
@@ -91,11 +93,17 @@ export default async (req, res) => {
sales_channel_availability: [],
}
const [channels] = await channelService.listAndCount(
{},
{
relations: ["locations"],
}
const [rawChannels] = await channelService.listAndCount({})
const channels: SalesChannelDTO[] = await Promise.all(
rawChannels.map(async (channel) => {
const locationIds = await channelLocationService.listLocationIds(
channel.id
)
return {
...channel,
locations: locationIds,
}
})
)
const inventory =
@@ -116,7 +124,7 @@ export default async (req, res) => {
const quantity = await inventoryService.retrieveAvailableQuantity(
inventory[0].id,
channel.locations.map((loc) => loc.id)
channel.locations
)
return {
@@ -133,6 +141,10 @@ export default async (req, res) => {
})
}
type SalesChannelDTO = Omit<SalesChannel, "beforeInsert" | "locations"> & {
locations: string[]
}
type ResponseInventoryItem = Partial<InventoryItemDTO> & {
location_levels?: InventoryLevelDTO[]
}
@@ -105,9 +105,9 @@ class ProductVariantInventoryService extends TransactionBaseService {
return true
}
let locations: string[] = []
let locationIds: string[] = []
if (context.salesChannelId) {
locations = await this.salesChannelLocationService_.listLocations(
locationIds = await this.salesChannelLocationService_.listLocationIds(
context.salesChannelId
)
} else {
@@ -115,7 +115,7 @@ class ProductVariantInventoryService extends TransactionBaseService {
{},
{ select: ["id"] }
)
locations = stockLocations.map((l) => l.id)
locationIds = stockLocations.map((l) => l.id)
}
const hasInventory = await Promise.all(
@@ -125,7 +125,7 @@ class ProductVariantInventoryService extends TransactionBaseService {
.withTransaction(manager)
.confirmInventory(
inventoryPart.inventory_item_id,
locations,
locationIds,
itemQuantity
)
})
@@ -382,7 +382,7 @@ class ProductVariantInventoryService extends TransactionBaseService {
if (!isDefined(locationId) && context.salesChannelId) {
const locations = await this.salesChannelLocationService_
.withTransaction(manager)
.listLocations(context.salesChannelId)
.listLocationIds(context.salesChannelId)
if (!locations.length) {
throw new MedusaError(
@@ -40,13 +40,13 @@ class SalesChannelInventoryService {
salesChannelId: string,
inventoryItemId: string
): Promise<number> {
const locations = await this.salesChannelLocationService_.listLocations(
const locationIds = await this.salesChannelLocationService_.listLocationIds(
salesChannelId
)
return await this.inventoryService_.retrieveAvailableQuantity(
inventoryItemId,
locations
locationIds
)
}
}
@@ -101,7 +101,7 @@ class SalesChannelLocationService extends TransactionBaseService {
* @param salesChannelId - The ID of the sales channel.
* @returns A promise that resolves with an array of location IDs.
*/
async listLocations(salesChannelId: string): Promise<string[]> {
async listLocationIds(salesChannelId: string): Promise<string[]> {
const manager = this.transactionManager_ || this.manager_
const salesChannel = await this.salesChannelService_
.withTransaction(manager)
@@ -109,10 +109,28 @@ class SalesChannelLocationService extends TransactionBaseService {
const locations = await manager.find(SalesChannelLocation, {
where: { sales_channel_id: salesChannel.id },
select: ["location_id"],
})
return locations.map((l) => l.location_id)
}
/**
* Lists the sales channels associated with a stock location.
* @param {string} salesChannelId - The ID of the stock location.
* @returns {Promise<string[]>} A promise that resolves with an array of sales channel IDs.
*/
async listSalesChannelIds(locationId: string): Promise<string[]> {
const manager = this.transactionManager_ || this.manager_
const location = await this.stockLocationService.retrieve(locationId)
const salesChannelLocations = await manager.find(SalesChannelLocation, {
where: { location_id: location.id },
select: ["sales_channel_id"],
})
return salesChannelLocations.map((l) => l.sales_channel_id)
}
}
export default SalesChannelLocationService
@@ -3,6 +3,22 @@ export interface IFlagRouter {
listFlags: () => FeatureFlagsResponse
}
/**
* @schema FeatureFlagsResponse
* type: array
* items:
* type: object
* required:
* - key
* - value
* properties:
* key:
* description: The key of the feature flag.
* type: string
* value:
* description: The value of the feature flag.
* type: boolean
*/
export type FeatureFlagsResponse = {
key: string
value: boolean
+16
View File
@@ -1,3 +1,19 @@
/**
* @schema ModulesResponse
* type: array
* items:
* type: object
* required:
* - module
* - resolution
* properties:
* module:
* description: The key of the module.
* type: string
* resolution:
* description: The resolution path of the module or false if module is not installed.
* type: string
*/
export type ModulesResponse = {
module: string
resolution: string | false
@@ -1,3 +1,4 @@
import { SalesChannel } from "../models"
import { StringComparisonOperator } from "./common"
/**
@@ -137,6 +138,19 @@ export type StockLocationDTO = {
deleted_at: string | Date | null
}
/**
* @schema StockLocationExpandedDTO
* allOf:
* - $ref: "#/components/schemas/StockLocationDTO"
* - type: object
* properties:
* sales_channels:
* $ref: "#/components/schemas/SalesChannel"
*/
export type StockLocationExpandedDTO = StockLocationDTO & {
sales_channels?: SalesChannel[]
}
export type FilterableStockLocationProps = {
id?: string | string[]
name?: string | string[] | StringComparisonOperator
+32
View File
@@ -1,3 +1,7 @@
import { Store, PaymentProvider, FulfillmentProvider } from "../models"
import { FeatureFlagsResponse } from "./feature-flags"
import { ModulesResponse } from "./modules"
export type UpdateStoreInput = {
name?: string
swap_link_template?: string
@@ -8,3 +12,31 @@ export type UpdateStoreInput = {
metadata?: Record<string, unknown>
default_sales_channel_id?: string
}
/**
* @schema ExtendedStoreDTO
* allOf:
* - $ref: "#/components/schemas/Store"
* - type: object
* required:
* - payment_providers
* - fulfillment_providers
* - feature_flags
* - modules
* properties:
* payment_providers:
* $ref: "#/components/schemas/PaymentProvider"
* fulfillment_providers:
* $ref: "#/components/schemas/FulfillmentProvider"
* feature_flags:
* $ref: "#/components/schemas/FeatureFlagsResponse"
* modules:
* $ref: "#/components/schemas/ModulesResponse"
*
*/
export type ExtendedStoreDTO = Store & {
payment_providers: PaymentProvider[]
fulfillment_providers: FulfillmentProvider[]
feature_flags: FeatureFlagsResponse
modules: ModulesResponse
}