feat(medusa,types): added store apis for products (#7144)
what: - adds products list api - adds products retrieve api RESOLVES CORE-2016
This commit is contained in:
1 parent
32c2a9d76b
commit
11517f0faf
24 files changed
+980
-48
No files matched your search
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/medusa": patch
|
||||||
|
"@medusajs/types": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
feat(medusa,types): added store apis for products
|
||||||
@@ -0,0 +1,408 @@
|
|||||||
|
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||||
|
import { ApiKeyType, ProductStatus } from "@medusajs/utils"
|
||||||
|
import { medusaIntegrationTestRunner } from "medusa-test-utils"
|
||||||
|
import { createAdminUser } from "../../../../helpers/create-admin-user"
|
||||||
|
import { createDefaultRuleTypes } from "../../../helpers/create-default-rule-types"
|
||||||
|
|
||||||
|
jest.setTimeout(50000)
|
||||||
|
|
||||||
|
const env = { MEDUSA_FF_MEDUSA_V2: true }
|
||||||
|
const adminHeaders = { headers: { "x-medusa-access-token": "test_token" } }
|
||||||
|
|
||||||
|
medusaIntegrationTestRunner({
|
||||||
|
env,
|
||||||
|
testSuite: ({ dbConnection, getContainer, api }) => {
|
||||||
|
describe("Store: Products API", () => {
|
||||||
|
let appContainer
|
||||||
|
let product
|
||||||
|
let product2
|
||||||
|
let product3
|
||||||
|
let product4
|
||||||
|
let variant
|
||||||
|
let variant2
|
||||||
|
let variant3
|
||||||
|
let variant4
|
||||||
|
|
||||||
|
const createProducts = async (data) => {
|
||||||
|
const response = await api.post(
|
||||||
|
"/admin/products?fields=*variants",
|
||||||
|
data,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
return [response.data.product, response.data.product.variants || []]
|
||||||
|
}
|
||||||
|
|
||||||
|
const createSalesChannel = async (data, productIds) => {
|
||||||
|
const response = await api.post(
|
||||||
|
"/admin/sales-channels",
|
||||||
|
data,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const salesChannel = response.data.sales_channel
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/admin/sales-channels/${salesChannel.id}/products`,
|
||||||
|
{ add: productIds },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
return salesChannel
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
appContainer = getContainer()
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await createAdminUser(dbConnection, adminHeaders, appContainer)
|
||||||
|
await createDefaultRuleTypes(appContainer)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("GET /store/products", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
;[product, [variant]] = await createProducts({
|
||||||
|
title: "test product 1",
|
||||||
|
status: ProductStatus.PUBLISHED,
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
title: "test variant 1",
|
||||||
|
prices: [{ amount: 3000, currency_code: "usd" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
;[product2, [variant2]] = await createProducts({
|
||||||
|
title: "test product 2 uniquely",
|
||||||
|
status: ProductStatus.PUBLISHED,
|
||||||
|
variants: [{ title: "test variant 2", prices: [] }],
|
||||||
|
})
|
||||||
|
;[product3, [variant3]] = await createProducts({
|
||||||
|
title: "product not in price list",
|
||||||
|
status: ProductStatus.PUBLISHED,
|
||||||
|
variants: [{ title: "test variant 3", prices: [] }],
|
||||||
|
})
|
||||||
|
;[product4, [variant4]] = await createProducts({
|
||||||
|
title: "draft product",
|
||||||
|
status: ProductStatus.DRAFT,
|
||||||
|
variants: [{ title: "test variant 4", prices: [] }],
|
||||||
|
})
|
||||||
|
|
||||||
|
const defaultSalesChannel = await createSalesChannel(
|
||||||
|
{ name: "default sales channel" },
|
||||||
|
[product.id, product2.id, product3.id, product4.id]
|
||||||
|
)
|
||||||
|
|
||||||
|
const service = appContainer.resolve(ModuleRegistrationName.STORE)
|
||||||
|
const [store] = await service.list()
|
||||||
|
|
||||||
|
if (store) {
|
||||||
|
await service.delete(store.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
await service.create({
|
||||||
|
supported_currency_codes: ["usd", "dkk"],
|
||||||
|
default_currency_code: "usd",
|
||||||
|
default_sales_channel_id: defaultSalesChannel.id,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should list all published products", async () => {
|
||||||
|
let response = await api.get(`/store/products`)
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
expect(response.data.count).toEqual(3)
|
||||||
|
expect(response.data.products).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: product.id,
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
id: product2.id,
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
id: product3.id,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await api.get(`/store/products?q=uniquely`)
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
expect(response.data.count).toEqual(1)
|
||||||
|
expect(response.data.products).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: product2.id,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should list all products for a sales channel", async () => {
|
||||||
|
const salesChannel = await createSalesChannel(
|
||||||
|
{ name: "sales channel test" },
|
||||||
|
[product.id]
|
||||||
|
)
|
||||||
|
|
||||||
|
let response = await api.get(
|
||||||
|
`/store/products?sales_channel_id[]=${salesChannel.id}`
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
expect(response.data.count).toEqual(1)
|
||||||
|
expect(response.data.products).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: product.id,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("with publishable keys", () => {
|
||||||
|
let salesChannel1
|
||||||
|
let salesChannel2
|
||||||
|
let publishableKey1
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
salesChannel1 = await createSalesChannel(
|
||||||
|
{ name: "sales channel test" },
|
||||||
|
[product.id]
|
||||||
|
)
|
||||||
|
|
||||||
|
salesChannel2 = await createSalesChannel(
|
||||||
|
{ name: "sales channel test 2" },
|
||||||
|
[product2.id]
|
||||||
|
)
|
||||||
|
|
||||||
|
const api1Res = await api.post(
|
||||||
|
`/admin/api-keys`,
|
||||||
|
{ title: "Test publishable KEY", type: ApiKeyType.PUBLISHABLE },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
publishableKey1 = api1Res.data.api_key
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/admin/api-keys/${publishableKey1.id}/sales-channels`,
|
||||||
|
{ add: [salesChannel1.id] },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should list all products for a sales channel", async () => {
|
||||||
|
let response = await api.get(
|
||||||
|
`/store/products?sales_channel_id[]=${salesChannel1.id}`,
|
||||||
|
{ headers: { "x-publishable-api-key": publishableKey1.token } }
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
expect(response.data.count).toEqual(1)
|
||||||
|
expect(response.data.products).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: product.id,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should throw error when publishable key is invalid", async () => {
|
||||||
|
let error = await api
|
||||||
|
.get(`/store/products?sales_channel_id[]=does-not-exist`, {
|
||||||
|
headers: { "x-publishable-api-key": "does-not-exist" },
|
||||||
|
})
|
||||||
|
.catch((e) => e)
|
||||||
|
|
||||||
|
expect(error.response.status).toEqual(400)
|
||||||
|
expect(error.response.data).toEqual({
|
||||||
|
message: `Publishable API key not found`,
|
||||||
|
type: "invalid_data",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should throw error when sales channel does not exist", async () => {
|
||||||
|
let error = await api
|
||||||
|
.get(`/store/products?sales_channel_id[]=does-not-exist`, {
|
||||||
|
headers: { "x-publishable-api-key": publishableKey1.token },
|
||||||
|
})
|
||||||
|
.catch((e) => e)
|
||||||
|
|
||||||
|
expect(error.response.status).toEqual(400)
|
||||||
|
expect(error.response.data).toEqual({
|
||||||
|
message: `Invalid sales channel filters provided - does-not-exist`,
|
||||||
|
type: "invalid_data",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should throw error when sales channel not in publishable key", async () => {
|
||||||
|
let error = await api
|
||||||
|
.get(`/store/products?sales_channel_id[]=${salesChannel2.id}`, {
|
||||||
|
headers: { "x-publishable-api-key": publishableKey1.token },
|
||||||
|
})
|
||||||
|
.catch((e) => e)
|
||||||
|
|
||||||
|
expect(error.response.status).toEqual(400)
|
||||||
|
expect(error.response.data).toEqual({
|
||||||
|
message: `Invalid sales channel filters provided - ${salesChannel2.id}`,
|
||||||
|
type: "invalid_data",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should throw error when calculating prices without context", async () => {
|
||||||
|
let error = await api
|
||||||
|
.get(`/store/products?fields=*variants.calculated_price`)
|
||||||
|
.catch((e) => e)
|
||||||
|
|
||||||
|
expect(error.response.status).toEqual(400)
|
||||||
|
expect(error.response.data).toEqual({
|
||||||
|
message:
|
||||||
|
"Pricing parameters (currency_code or region_id) are required to calculate prices",
|
||||||
|
type: "invalid_data",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should list products with prices when context is present", async () => {
|
||||||
|
let response = await api.get(
|
||||||
|
`/store/products?fields=*variants.calculated_price¤cy_code=usd`
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
expect(response.data.count).toEqual(3)
|
||||||
|
expect(response.data.products).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: product.id,
|
||||||
|
variants: [
|
||||||
|
expect.objectContaining({
|
||||||
|
calculated_price: {
|
||||||
|
id: expect.any(String),
|
||||||
|
is_calculated_price_price_list: false,
|
||||||
|
calculated_amount: 3000,
|
||||||
|
is_original_price_price_list: false,
|
||||||
|
original_amount: 3000,
|
||||||
|
currency_code: "usd",
|
||||||
|
calculated_price: {
|
||||||
|
id: expect.any(String),
|
||||||
|
price_list_id: null,
|
||||||
|
price_list_type: null,
|
||||||
|
min_quantity: null,
|
||||||
|
max_quantity: null,
|
||||||
|
},
|
||||||
|
original_price: {
|
||||||
|
id: expect.any(String),
|
||||||
|
price_list_id: null,
|
||||||
|
price_list_type: null,
|
||||||
|
min_quantity: null,
|
||||||
|
max_quantity: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("GET /store/products/:id", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
;[product, [variant]] = await createProducts({
|
||||||
|
title: "test product 1",
|
||||||
|
status: ProductStatus.PUBLISHED,
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
title: "test variant 1",
|
||||||
|
prices: [{ amount: 3000, currency_code: "usd" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const defaultSalesChannel = await createSalesChannel(
|
||||||
|
{ name: "default sales channel" },
|
||||||
|
[product.id]
|
||||||
|
)
|
||||||
|
|
||||||
|
const service = appContainer.resolve(ModuleRegistrationName.STORE)
|
||||||
|
const [store] = await service.list()
|
||||||
|
|
||||||
|
if (store) {
|
||||||
|
await service.delete(store.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
await service.create({
|
||||||
|
supported_currency_codes: ["usd", "dkk"],
|
||||||
|
default_currency_code: "usd",
|
||||||
|
default_sales_channel_id: defaultSalesChannel.id,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should retrieve product successfully", async () => {
|
||||||
|
let response = await api.get(`/store/products/${product.id}`)
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
expect(response.data.product).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: product.id,
|
||||||
|
variants: [
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(String),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should throw error when calculating prices without context", async () => {
|
||||||
|
let error = await api
|
||||||
|
.get(
|
||||||
|
`/store/products/${product.id}?fields=*variants.calculated_price`
|
||||||
|
)
|
||||||
|
.catch((e) => e)
|
||||||
|
|
||||||
|
expect(error.response.status).toEqual(400)
|
||||||
|
expect(error.response.data).toEqual({
|
||||||
|
message:
|
||||||
|
"Pricing parameters (currency_code or region_id) are required to calculate prices",
|
||||||
|
type: "invalid_data",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should get product with prices when context is present", async () => {
|
||||||
|
let response = await api.get(
|
||||||
|
`/store/products/${product.id}?fields=*variants.calculated_price¤cy_code=usd`
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
expect(response.data.product).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: product.id,
|
||||||
|
variants: [
|
||||||
|
expect.objectContaining({
|
||||||
|
calculated_price: {
|
||||||
|
id: expect.any(String),
|
||||||
|
is_calculated_price_price_list: false,
|
||||||
|
calculated_amount: 3000,
|
||||||
|
is_original_price_price_list: false,
|
||||||
|
original_amount: 3000,
|
||||||
|
currency_code: "usd",
|
||||||
|
calculated_price: {
|
||||||
|
id: expect.any(String),
|
||||||
|
price_list_id: null,
|
||||||
|
price_list_type: null,
|
||||||
|
min_quantity: null,
|
||||||
|
max_quantity: null,
|
||||||
|
},
|
||||||
|
original_price: {
|
||||||
|
id: expect.any(String),
|
||||||
|
price_list_id: null,
|
||||||
|
price_list_type: null,
|
||||||
|
min_quantity: null,
|
||||||
|
max_quantity: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ProductStatus } from "@medusajs/utils"
|
import { ProductStatus } from "@medusajs/utils"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import { optionalBooleanMapper } from "../../../utils/validators/is-boolean"
|
import { GetProductsParams } from "../../utils/common-validators"
|
||||||
import {
|
import {
|
||||||
createFindParams,
|
createFindParams,
|
||||||
createOperatorMap,
|
createOperatorMap,
|
||||||
@@ -13,50 +13,6 @@ export const AdminGetProductParams = createSelectParams()
|
|||||||
export const AdminGetProductVariantParams = createSelectParams()
|
export const AdminGetProductVariantParams = createSelectParams()
|
||||||
export const AdminGetProductOptionParams = createSelectParams()
|
export const AdminGetProductOptionParams = createSelectParams()
|
||||||
|
|
||||||
export type AdminGetProductsParamsType = z.infer<typeof AdminGetProductsParams>
|
|
||||||
export const AdminGetProductsParams = createFindParams({
|
|
||||||
offset: 0,
|
|
||||||
limit: 50,
|
|
||||||
}).merge(
|
|
||||||
z.object({
|
|
||||||
q: z.string().optional(),
|
|
||||||
id: z.union([z.string(), z.array(z.string())]).optional(),
|
|
||||||
status: statusEnum.array().optional(),
|
|
||||||
title: z.string().optional(),
|
|
||||||
handle: z.string().optional(),
|
|
||||||
is_giftcard: z.preprocess(
|
|
||||||
(val: any) => optionalBooleanMapper.get(val?.toLowerCase()),
|
|
||||||
z.boolean().optional()
|
|
||||||
),
|
|
||||||
category_id: z.string().array().optional(),
|
|
||||||
price_list_id: z.string().array().optional(),
|
|
||||||
sales_channel_id: z.string().array().optional(),
|
|
||||||
collection_id: z.string().array().optional(),
|
|
||||||
tags: z.string().array().optional(),
|
|
||||||
type_id: z.string().array().optional(),
|
|
||||||
// TODO: Replace this with AdminGetProductVariantsParams when its available
|
|
||||||
variants: z.record(z.unknown()).optional(),
|
|
||||||
created_at: createOperatorMap().optional(),
|
|
||||||
updated_at: createOperatorMap().optional(),
|
|
||||||
deleted_at: createOperatorMap().optional(),
|
|
||||||
$and: z.lazy(() => AdminGetProductsParams.array()).optional(),
|
|
||||||
$or: z.lazy(() => AdminGetProductsParams.array()).optional(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
// TODO: These were part of the products find query, add them once supported
|
|
||||||
// @IsString()
|
|
||||||
// @IsOptional()
|
|
||||||
// discount_condition_id?: string
|
|
||||||
|
|
||||||
// @IsArray()
|
|
||||||
// @IsOptional()
|
|
||||||
// category_id?: string[]
|
|
||||||
|
|
||||||
// @IsBoolean()
|
|
||||||
// @IsOptional()
|
|
||||||
// @Transform(({ value }) => optionalBooleanMapper.get(value.toLowerCase()))
|
|
||||||
// include_category_children?: boolean
|
|
||||||
|
|
||||||
export type AdminGetProductVariantsParamsType = z.infer<
|
export type AdminGetProductVariantsParamsType = z.infer<
|
||||||
typeof AdminGetProductVariantsParams
|
typeof AdminGetProductVariantsParams
|
||||||
>
|
>
|
||||||
@@ -77,6 +33,21 @@ export const AdminGetProductVariantsParams = createFindParams({
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export type AdminGetProductsParamsType = z.infer<typeof AdminGetProductsParams>
|
||||||
|
export const AdminGetProductsParams = createFindParams({
|
||||||
|
offset: 0,
|
||||||
|
limit: 50,
|
||||||
|
}).merge(
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
variants: AdminGetProductVariantsParams.optional(),
|
||||||
|
price_list_id: z.string().array().optional(),
|
||||||
|
$and: z.lazy(() => AdminGetProductsParams.array()).optional(),
|
||||||
|
$or: z.lazy(() => AdminGetProductsParams.array()).optional(),
|
||||||
|
})
|
||||||
|
.merge(GetProductsParams)
|
||||||
|
)
|
||||||
|
|
||||||
export type AdminGetProductOptionsParamsType = z.infer<
|
export type AdminGetProductOptionsParamsType = z.infer<
|
||||||
typeof AdminGetProductOptionsParams
|
typeof AdminGetProductOptionsParams
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import { hooksRoutesMiddlewares } from "./hooks/middlewares"
|
|||||||
import { storeCartRoutesMiddlewares } from "./store/carts/middlewares"
|
import { storeCartRoutesMiddlewares } from "./store/carts/middlewares"
|
||||||
import { storeCurrencyRoutesMiddlewares } from "./store/currencies/middlewares"
|
import { storeCurrencyRoutesMiddlewares } from "./store/currencies/middlewares"
|
||||||
import { storeCustomerRoutesMiddlewares } from "./store/customers/middlewares"
|
import { storeCustomerRoutesMiddlewares } from "./store/customers/middlewares"
|
||||||
|
import { storeProductRoutesMiddlewares } from "./store/products/middlewares"
|
||||||
import { storeRegionRoutesMiddlewares } from "./store/regions/middlewares"
|
import { storeRegionRoutesMiddlewares } from "./store/regions/middlewares"
|
||||||
|
|
||||||
export const config: MiddlewaresConfig = {
|
export const config: MiddlewaresConfig = {
|
||||||
@@ -80,5 +81,6 @@ export const config: MiddlewaresConfig = {
|
|||||||
...adminShippingProfilesMiddlewares,
|
...adminShippingProfilesMiddlewares,
|
||||||
...adminFulfillmentsRoutesMiddlewares,
|
...adminFulfillmentsRoutesMiddlewares,
|
||||||
...adminFulfillmentProvidersRoutesMiddlewares,
|
...adminFulfillmentProvidersRoutesMiddlewares,
|
||||||
|
...storeProductRoutesMiddlewares,
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { isPresent } from "@medusajs/utils"
|
||||||
|
import { MedusaRequest, MedusaResponse } from "../../../../types/routing"
|
||||||
|
import { refetchProduct } from "../helpers"
|
||||||
|
import { StoreGetProductsParamsType } from "../validators"
|
||||||
|
|
||||||
|
export const GET = async (
|
||||||
|
req: MedusaRequest<StoreGetProductsParamsType>,
|
||||||
|
res: MedusaResponse
|
||||||
|
) => {
|
||||||
|
const context = isPresent(req.pricingContext)
|
||||||
|
? {
|
||||||
|
"variants.calculated_price": { context: req.pricingContext },
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
const product = await refetchProduct(
|
||||||
|
{
|
||||||
|
id: req.params.id,
|
||||||
|
context,
|
||||||
|
},
|
||||||
|
req.scope,
|
||||||
|
req.remoteQueryConfig.fields
|
||||||
|
)
|
||||||
|
|
||||||
|
res.json({ product })
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { MedusaContainer } from "@medusajs/types"
|
||||||
|
import { isPresent } from "@medusajs/utils"
|
||||||
|
import { refetchEntity } from "../../utils/refetch-entity"
|
||||||
|
import { StoreGetProductsParamsType } from "./validators"
|
||||||
|
|
||||||
|
// For category filters, we only allow showcasing public and active categories
|
||||||
|
// TODO: This should ideally be done in the middleware, write a generic filter to conditionally
|
||||||
|
// map these values or normalize the filters to the ones expected by remote query
|
||||||
|
export function wrapWithCategoryFilters(filters: StoreGetProductsParamsType) {
|
||||||
|
const categoriesFilter = isPresent(filters.category_id)
|
||||||
|
? {
|
||||||
|
categories: {
|
||||||
|
...filters.category_id,
|
||||||
|
is_internal: false,
|
||||||
|
is_active: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}
|
||||||
|
|
||||||
|
delete filters.category_id
|
||||||
|
|
||||||
|
return {
|
||||||
|
...filters,
|
||||||
|
...categoriesFilter,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const refetchProduct = async (
|
||||||
|
idOrFilter: string | object,
|
||||||
|
scope: MedusaContainer,
|
||||||
|
fields: string[]
|
||||||
|
) => {
|
||||||
|
return await refetchEntity("product", idOrFilter, scope, fields)
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { ProductStatus } from "@medusajs/utils"
|
||||||
|
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types"
|
||||||
|
import { authenticate } from "../../../utils/authenticate-middleware"
|
||||||
|
import { maybeApplyLinkFilter } from "../../utils/maybe-apply-link-filter"
|
||||||
|
import {
|
||||||
|
applyDefaultFilters,
|
||||||
|
filterByValidSalesChannels,
|
||||||
|
setPricingContext,
|
||||||
|
} from "../../utils/middlewares"
|
||||||
|
import { validateAndTransformQuery } from "../../utils/validate-query"
|
||||||
|
import * as QueryConfig from "./query-config"
|
||||||
|
import {
|
||||||
|
StoreGetProductsParams,
|
||||||
|
StoreGetProductsParamsType,
|
||||||
|
} from "./validators"
|
||||||
|
|
||||||
|
export const storeProductRoutesMiddlewares: MiddlewareRoute[] = [
|
||||||
|
{
|
||||||
|
method: "ALL",
|
||||||
|
matcher: "/store/products*",
|
||||||
|
middlewares: [
|
||||||
|
authenticate("store", ["session", "bearer"], {
|
||||||
|
allowUnauthenticated: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: ["GET"],
|
||||||
|
matcher: "/store/products",
|
||||||
|
middlewares: [
|
||||||
|
validateAndTransformQuery(
|
||||||
|
StoreGetProductsParams,
|
||||||
|
QueryConfig.listProductQueryConfig
|
||||||
|
),
|
||||||
|
filterByValidSalesChannels(),
|
||||||
|
maybeApplyLinkFilter({
|
||||||
|
entryPoint: "product_sales_channel",
|
||||||
|
resourceId: "product_id",
|
||||||
|
filterableField: "sales_channel_id",
|
||||||
|
}),
|
||||||
|
applyDefaultFilters<StoreGetProductsParamsType>({
|
||||||
|
status: ProductStatus.PUBLISHED,
|
||||||
|
}),
|
||||||
|
setPricingContext(),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: ["GET"],
|
||||||
|
matcher: "/store/products/:id",
|
||||||
|
middlewares: [
|
||||||
|
validateAndTransformQuery(
|
||||||
|
StoreGetProductsParams,
|
||||||
|
QueryConfig.retrieveProductQueryConfig
|
||||||
|
),
|
||||||
|
filterByValidSalesChannels(),
|
||||||
|
maybeApplyLinkFilter({
|
||||||
|
entryPoint: "product_sales_channel",
|
||||||
|
resourceId: "product_id",
|
||||||
|
filterableField: "sales_channel_id",
|
||||||
|
}),
|
||||||
|
applyDefaultFilters<StoreGetProductsParamsType>({
|
||||||
|
status: ProductStatus.PUBLISHED,
|
||||||
|
}),
|
||||||
|
setPricingContext(),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
export const defaultStoreProductFields = [
|
||||||
|
"id",
|
||||||
|
"title",
|
||||||
|
"subtitle",
|
||||||
|
"description",
|
||||||
|
"handle",
|
||||||
|
"is_giftcard",
|
||||||
|
"discountable",
|
||||||
|
"thumbnail",
|
||||||
|
"collection_id",
|
||||||
|
"type_id",
|
||||||
|
"weight",
|
||||||
|
"length",
|
||||||
|
"height",
|
||||||
|
"width",
|
||||||
|
"hs_code",
|
||||||
|
"origin_country",
|
||||||
|
"mid_code",
|
||||||
|
"material",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"*type",
|
||||||
|
"*collection",
|
||||||
|
"*options",
|
||||||
|
"*options.values",
|
||||||
|
"*tags",
|
||||||
|
"*images",
|
||||||
|
"*variants",
|
||||||
|
"*variants.options",
|
||||||
|
]
|
||||||
|
|
||||||
|
export const retrieveProductQueryConfig = {
|
||||||
|
defaults: defaultStoreProductFields,
|
||||||
|
isList: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const listProductQueryConfig = {
|
||||||
|
...retrieveProductQueryConfig,
|
||||||
|
defaultLimit: 50,
|
||||||
|
isList: true,
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import {
|
||||||
|
ContainerRegistrationKeys,
|
||||||
|
isPresent,
|
||||||
|
remoteQueryObjectFromString,
|
||||||
|
} from "@medusajs/utils"
|
||||||
|
import { MedusaRequest, MedusaResponse } from "../../../types/routing"
|
||||||
|
import { wrapWithCategoryFilters } from "./helpers"
|
||||||
|
import { StoreGetProductsParamsType } from "./validators"
|
||||||
|
|
||||||
|
export const GET = async (
|
||||||
|
req: MedusaRequest<StoreGetProductsParamsType>,
|
||||||
|
res: MedusaResponse
|
||||||
|
) => {
|
||||||
|
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
|
||||||
|
const context = isPresent(req.pricingContext)
|
||||||
|
? {
|
||||||
|
"variants.calculated_price": { context: req.pricingContext },
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
const queryObject = remoteQueryObjectFromString({
|
||||||
|
entryPoint: "product",
|
||||||
|
variables: {
|
||||||
|
filters: wrapWithCategoryFilters(req.filterableFields),
|
||||||
|
...context,
|
||||||
|
...req.remoteQueryConfig.pagination,
|
||||||
|
},
|
||||||
|
fields: req.remoteQueryConfig.fields,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { rows: products, metadata } = await remoteQuery(queryObject)
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
products,
|
||||||
|
count: metadata.count,
|
||||||
|
offset: metadata.skip,
|
||||||
|
limit: metadata.take,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { z } from "zod"
|
||||||
|
import {
|
||||||
|
GetProductsParams,
|
||||||
|
ProductStatusEnum,
|
||||||
|
} from "../../utils/common-validators"
|
||||||
|
import {
|
||||||
|
createFindParams,
|
||||||
|
createOperatorMap,
|
||||||
|
createSelectParams,
|
||||||
|
} from "../../utils/validators"
|
||||||
|
|
||||||
|
export const StoreGetProductParams = createSelectParams()
|
||||||
|
|
||||||
|
export type StoreGetProductVariantsParamsType = z.infer<
|
||||||
|
typeof StoreGetProductVariantsParams
|
||||||
|
>
|
||||||
|
export const StoreGetProductVariantsParams = createFindParams({
|
||||||
|
offset: 0,
|
||||||
|
limit: 50,
|
||||||
|
}).merge(
|
||||||
|
z.object({
|
||||||
|
q: z.string().optional(),
|
||||||
|
id: z.union([z.string(), z.array(z.string())]).optional(),
|
||||||
|
status: ProductStatusEnum.array().optional(),
|
||||||
|
created_at: createOperatorMap().optional(),
|
||||||
|
updated_at: createOperatorMap().optional(),
|
||||||
|
deleted_at: createOperatorMap().optional(),
|
||||||
|
$and: z.lazy(() => StoreGetProductsParams.array()).optional(),
|
||||||
|
$or: z.lazy(() => StoreGetProductsParams.array()).optional(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
export type StoreGetProductsParamsType = z.infer<typeof StoreGetProductsParams>
|
||||||
|
export const StoreGetProductsParams = createFindParams({
|
||||||
|
offset: 0,
|
||||||
|
limit: 50,
|
||||||
|
}).merge(
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
region_id: z.string().optional(),
|
||||||
|
currency_code: z.string().optional(),
|
||||||
|
variants: StoreGetProductVariantsParams.optional(),
|
||||||
|
$and: z.lazy(() => StoreGetProductsParams.array()).optional(),
|
||||||
|
$or: z.lazy(() => StoreGetProductsParams.array()).optional(),
|
||||||
|
})
|
||||||
|
.merge(GetProductsParams)
|
||||||
|
.strict()
|
||||||
|
)
|
||||||
+6
@@ -1,4 +1,5 @@
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
import { optionalBooleanMapper } from "../../../utils/validators/is-boolean"
|
||||||
|
|
||||||
export const AddressPayload = z
|
export const AddressPayload = z
|
||||||
.object({
|
.object({
|
||||||
@@ -24,3 +25,8 @@ export const BigNumberInput = z.union([
|
|||||||
precision: z.number(),
|
precision: z.number(),
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
export const OptionalBooleanValidator = z.preprocess(
|
||||||
|
(val: any) => optionalBooleanMapper.get(val?.toLowerCase()),
|
||||||
|
z.boolean().optional()
|
||||||
|
)
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./common"
|
||||||
|
export * from "./products"
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { ProductStatus } from "@medusajs/utils"
|
||||||
|
import { z } from "zod"
|
||||||
|
import { createOperatorMap } from "../../validators"
|
||||||
|
import { OptionalBooleanValidator } from "../common"
|
||||||
|
|
||||||
|
export const ProductStatusEnum = z.nativeEnum(ProductStatus)
|
||||||
|
|
||||||
|
export const GetProductsParams = z.object({
|
||||||
|
q: z.string().optional(),
|
||||||
|
id: z.union([z.string(), z.array(z.string())]).optional(),
|
||||||
|
title: z.string().optional(),
|
||||||
|
handle: z.string().optional(),
|
||||||
|
is_giftcard: OptionalBooleanValidator,
|
||||||
|
category_id: z.string().array().optional(),
|
||||||
|
sales_channel_id: z.string().array().optional(),
|
||||||
|
collection_id: z.string().array().optional(),
|
||||||
|
tags: z.string().array().optional(),
|
||||||
|
type_id: z.string().array().optional(),
|
||||||
|
created_at: createOperatorMap().optional(),
|
||||||
|
updated_at: createOperatorMap().optional(),
|
||||||
|
deleted_at: createOperatorMap().optional(),
|
||||||
|
})
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { isObject } from "@medusajs/utils"
|
||||||
|
import { NextFunction } from "express"
|
||||||
|
import { MedusaRequest } from "../../../../types/routing"
|
||||||
|
|
||||||
|
export function applyDefaultFilters<TFilter extends object>(filters: TFilter) {
|
||||||
|
return async (req: MedusaRequest, _, next: NextFunction) => {
|
||||||
|
const filterableFields = req.filterableFields || {}
|
||||||
|
|
||||||
|
for (const [filter, filterValue] of Object.entries(filters)) {
|
||||||
|
let existingFilter = filterableFields[filter]
|
||||||
|
|
||||||
|
if (existingFilter && isObject(existingFilter)) {
|
||||||
|
// If an existing filter is found, append to it
|
||||||
|
filterableFields[filter] = {
|
||||||
|
...existingFilter,
|
||||||
|
[filter]: filterValue,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
filterableFields[filter] = filterValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
req.filterableFields = filterableFields
|
||||||
|
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./apply-default-filters"
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./common"
|
||||||
|
export * from "./products"
|
||||||
+79
@@ -0,0 +1,79 @@
|
|||||||
|
import { isPresent, MedusaError } from "@medusajs/utils"
|
||||||
|
import { NextFunction } from "express"
|
||||||
|
import { AuthenticatedMedusaRequest } from "../../../../types/routing"
|
||||||
|
import { refetchEntity } from "../../refetch-entity"
|
||||||
|
|
||||||
|
export function filterByValidSalesChannels() {
|
||||||
|
return async (req: AuthenticatedMedusaRequest, _, next: NextFunction) => {
|
||||||
|
const publishableApiKey = req.get("x-publishable-api-key")
|
||||||
|
const salesChannelIds = req.filterableFields.sales_channel_id as
|
||||||
|
| string[]
|
||||||
|
| undefined
|
||||||
|
|
||||||
|
const store = await refetchEntity("stores", {}, req.scope, [
|
||||||
|
"default_sales_channel_id",
|
||||||
|
])
|
||||||
|
|
||||||
|
if (!store) {
|
||||||
|
try {
|
||||||
|
throw new MedusaError(MedusaError.Types.INVALID_DATA, `Store not found`)
|
||||||
|
} catch (e) {
|
||||||
|
return next(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Always set sales channels in the following priority
|
||||||
|
// - any existing sales chennel ids passed through query params
|
||||||
|
// - if none, we set the default sales channel
|
||||||
|
req.filterableFields.sales_channel_id = salesChannelIds ?? [
|
||||||
|
store.default_sales_channel_id,
|
||||||
|
]
|
||||||
|
|
||||||
|
// Return early if no publishable keys are found
|
||||||
|
if (!isPresent(publishableApiKey)) {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
// When publishable keys are present, we fetch for all sales chennels attached
|
||||||
|
// to the publishable key and validate the sales channel filter against it
|
||||||
|
const apiKey = await refetchEntity(
|
||||||
|
"api_key",
|
||||||
|
{ token: publishableApiKey },
|
||||||
|
req.scope,
|
||||||
|
["id", "sales_channels_link.sales_channel_id"]
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
try {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
`Publishable API key not found`
|
||||||
|
)
|
||||||
|
} catch (e) {
|
||||||
|
return next(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const validSalesChannelIds = apiKey.sales_channels_link.map(
|
||||||
|
(link) => link.sales_channel_id
|
||||||
|
)
|
||||||
|
|
||||||
|
const invalidSalesChannelIds = (salesChannelIds || []).filter(
|
||||||
|
(id) => !validSalesChannelIds.includes(id)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (invalidSalesChannelIds.length) {
|
||||||
|
try {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
`Invalid sales channel filters provided - ${invalidSalesChannelIds.join(
|
||||||
|
", "
|
||||||
|
)}`
|
||||||
|
)
|
||||||
|
} catch (e) {
|
||||||
|
return next(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./filter-by-valid-sales-channels"
|
||||||
|
export * from "./set-pricing-context"
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { MedusaPricingContext } from "@medusajs/types"
|
||||||
|
import { isPresent, MedusaError } from "@medusajs/utils"
|
||||||
|
import { NextFunction } from "express"
|
||||||
|
import { AuthenticatedMedusaRequest } from "../../../../types/routing"
|
||||||
|
import { refetchEntities, refetchEntity } from "../../refetch-entity"
|
||||||
|
|
||||||
|
export function setPricingContext() {
|
||||||
|
return async (req: AuthenticatedMedusaRequest, _, next: NextFunction) => {
|
||||||
|
// If the endpoint doesn't request prices, we can exit early
|
||||||
|
if (
|
||||||
|
!req.remoteQueryConfig.fields.some((field) =>
|
||||||
|
field.startsWith("variants.calculated_price")
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
delete req.filterableFields.region_id
|
||||||
|
delete req.filterableFields.currency_code
|
||||||
|
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = req.filterableFields || {}
|
||||||
|
const pricingContext: MedusaPricingContext = {}
|
||||||
|
const customerId = req.user?.customer_id
|
||||||
|
|
||||||
|
if (query.region_id) {
|
||||||
|
const region = await refetchEntity("region", query.region_id, req.scope, [
|
||||||
|
"id",
|
||||||
|
"currency_code",
|
||||||
|
])
|
||||||
|
|
||||||
|
if (region) {
|
||||||
|
pricingContext.region_id = region.id
|
||||||
|
}
|
||||||
|
|
||||||
|
if (region?.currency_code) {
|
||||||
|
pricingContext.currency_code = region.currency_code
|
||||||
|
}
|
||||||
|
|
||||||
|
delete req.filterableFields.region_id
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a currency code is explicitly passed, we should be using that instead of the
|
||||||
|
// regions currency code
|
||||||
|
if (query.currency_code) {
|
||||||
|
const currency = await refetchEntity(
|
||||||
|
"currency",
|
||||||
|
{ code: query.currency_code },
|
||||||
|
req.scope,
|
||||||
|
["code"]
|
||||||
|
)
|
||||||
|
|
||||||
|
if (currency) {
|
||||||
|
pricingContext.currency_code = currency.code
|
||||||
|
}
|
||||||
|
|
||||||
|
delete req.filterableFields.currency_code
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find all the customer groups the customer is a part of and set
|
||||||
|
if (customerId) {
|
||||||
|
const customerGroups = await refetchEntities(
|
||||||
|
"customer_group",
|
||||||
|
{ customer_id: customerId },
|
||||||
|
req.scope,
|
||||||
|
["id"]
|
||||||
|
)
|
||||||
|
|
||||||
|
pricingContext.customer_group_id = customerGroups.map((cg) => cg.id)
|
||||||
|
|
||||||
|
delete req.filterableFields.customer_id
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a region or currency_code is not present in the context, we will not be able to calculate prices
|
||||||
|
if (!isPresent(pricingContext)) {
|
||||||
|
try {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
`Pricing parameters (currency_code or region_id) are required to calculate prices`
|
||||||
|
)
|
||||||
|
} catch (e) {
|
||||||
|
return next(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
req.pricingContext = pricingContext
|
||||||
|
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { MedusaContainer } from "@medusajs/types"
|
||||||
|
import {
|
||||||
|
ContainerRegistrationKeys,
|
||||||
|
isString,
|
||||||
|
remoteQueryObjectFromString,
|
||||||
|
} from "@medusajs/utils"
|
||||||
|
|
||||||
|
export const refetchEntities = async (
|
||||||
|
entryPoint: string,
|
||||||
|
idOrFilter: string | object,
|
||||||
|
scope: MedusaContainer,
|
||||||
|
fields: string[]
|
||||||
|
) => {
|
||||||
|
const remoteQuery = scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
|
||||||
|
const filters = isString(idOrFilter) ? { id: idOrFilter } : idOrFilter
|
||||||
|
let context: object = {}
|
||||||
|
|
||||||
|
if ("context" in filters) {
|
||||||
|
if (filters.context) {
|
||||||
|
context = filters.context!
|
||||||
|
}
|
||||||
|
|
||||||
|
delete filters.context
|
||||||
|
}
|
||||||
|
|
||||||
|
let variables = { filters, ...context }
|
||||||
|
|
||||||
|
const queryObject = remoteQueryObjectFromString({
|
||||||
|
entryPoint,
|
||||||
|
variables,
|
||||||
|
fields,
|
||||||
|
})
|
||||||
|
|
||||||
|
return await remoteQuery(queryObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const refetchEntity = async (
|
||||||
|
entryPoint: string,
|
||||||
|
idOrFilter: string | object,
|
||||||
|
scope: MedusaContainer,
|
||||||
|
fields: string[]
|
||||||
|
) => {
|
||||||
|
const [entity] = await refetchEntities(entryPoint, idOrFilter, scope, fields)
|
||||||
|
|
||||||
|
return entity
|
||||||
|
}
|
||||||
@@ -44,7 +44,6 @@ export function validateAndTransformQuery<TEntity extends BaseEntity>(
|
|||||||
return async (req: MedusaRequest, _: MedusaResponse, next: NextFunction) => {
|
return async (req: MedusaRequest, _: MedusaResponse, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const query = normalizeQuery(req)
|
const query = normalizeQuery(req)
|
||||||
|
|
||||||
const validated = await zodValidator(zodSchema, query)
|
const validated = await zodValidator(zodSchema, query)
|
||||||
const cnf = queryConfig.isList
|
const cnf = queryConfig.isList
|
||||||
? prepareListQuery(validated, queryConfig)
|
? prepareListQuery(validated, queryConfig)
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import type { NextFunction, Request, Response } from "express"
|
import type { NextFunction, Request, Response } from "express"
|
||||||
import type { Customer, User } from "../models"
|
import type { Customer, User } from "../models"
|
||||||
|
|
||||||
import { MedusaContainer, RequestQueryFields } from "@medusajs/types"
|
import {
|
||||||
import { FindConfig } from "./common"
|
MedusaContainer,
|
||||||
|
MedusaPricingContext,
|
||||||
|
RequestQueryFields,
|
||||||
|
} from "@medusajs/types"
|
||||||
import * as core from "express-serve-static-core"
|
import * as core from "express-serve-static-core"
|
||||||
|
import { FindConfig } from "./common"
|
||||||
|
|
||||||
export interface MedusaRequest<Body = unknown>
|
export interface MedusaRequest<Body = unknown>
|
||||||
extends Request<core.ParamsDictionary, any, Body> {
|
extends Request<core.ParamsDictionary, any, Body> {
|
||||||
@@ -50,6 +54,10 @@ export interface MedusaRequest<Body = unknown>
|
|||||||
session?: any
|
session?: any
|
||||||
rawBody?: any
|
rawBody?: any
|
||||||
requestId?: string
|
requestId?: string
|
||||||
|
/**
|
||||||
|
* An object that carries the context that is used to calculate prices for variants
|
||||||
|
*/
|
||||||
|
pricingContext?: MedusaPricingContext
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthenticatedMedusaRequest<Body = never>
|
export interface AuthenticatedMedusaRequest<Body = never>
|
||||||
|
|||||||
@@ -4,4 +4,5 @@ export * from "./price-list"
|
|||||||
export * from "./price-rule"
|
export * from "./price-rule"
|
||||||
export * from "./price-set"
|
export * from "./price-set"
|
||||||
export * from "./price-set-rule-type"
|
export * from "./price-set-rule-type"
|
||||||
|
export * from "./pricing-context"
|
||||||
export * from "./rule-type"
|
export * from "./rule-type"
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export type MedusaPricingContext = {
|
||||||
|
region_id?: string
|
||||||
|
currency_code?: string
|
||||||
|
customer_id?: string
|
||||||
|
customer_group_id?: string[]
|
||||||
|
}
|
||||||
Reference in new issue
Block a user