Feat(medusa, medusa-js, medusa-react): Include sales channels in related queries as an optional expand parameter (#1816)
**What** - Add `transformQuery` to get endpoints for product, order and cart - ensure that the default relations (when getting a singular entity) includes sales channels when enabled - Add `EmptyQueryParams` class in common types to prevent query parameters while using `transformQuery` - update product-, order- and cartFactory to include sales channels if provided - remove `packages/medusa/src/controllers/products/admin-list-products.ts` **Testing** - expands sales channel for single order - expands sales channels for orders with expand parameter - returns single product with sales channel - expands sales channels for products with expand parameter - returns cart with sales channel for single cart Fixes CORE-293 Co-authored-by: Sebastian Rindom <7554214+srindom@users.noreply.github.com> Co-authored-by: Adrien de Peretti <25098370+adrien2p@users.noreply.github.com>
This commit is contained in:
@@ -8,6 +8,30 @@ Object {
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`sales channels GET /admin/orders/:id expands sales channel for single 1`] = `
|
||||
Object {
|
||||
"created_at": Any<String>,
|
||||
"deleted_at": null,
|
||||
"description": "test description",
|
||||
"id": Any<String>,
|
||||
"is_disabled": false,
|
||||
"name": "test name",
|
||||
"updated_at": Any<String>,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`sales channels GET /admin/orders?expand=sales_channels expands sales channel with parameter 1`] = `
|
||||
Object {
|
||||
"created_at": Any<String>,
|
||||
"deleted_at": null,
|
||||
"description": "test description",
|
||||
"id": Any<String>,
|
||||
"is_disabled": false,
|
||||
"name": "test name",
|
||||
"updated_at": Any<String>,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`sales channels GET /admin/sales-channels/:id should retrieve the requested sales channel 1`] = `
|
||||
Object {
|
||||
"created_at": Any<String>,
|
||||
@@ -20,6 +44,18 @@ Object {
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`sales channels GET /store/cart/:id with saleschannel returns cart with sales channel for single cart 1`] = `
|
||||
Object {
|
||||
"created_at": Any<String>,
|
||||
"deleted_at": null,
|
||||
"description": "test description",
|
||||
"id": Any<String>,
|
||||
"is_disabled": false,
|
||||
"name": "test name",
|
||||
"updated_at": Any<String>,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`sales channels POST /admin/sales-channels successfully creates a sales channel 1`] = `
|
||||
Object {
|
||||
"sales_channel": ObjectContaining {
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
const path = require("path")
|
||||
|
||||
const { SalesChannel } = require("@medusajs/medusa")
|
||||
|
||||
const { useApi } = require("../../../helpers/use-api")
|
||||
const { useDb } = require("../../../helpers/use-db")
|
||||
|
||||
const adminSeeder = require("../../helpers/admin-seeder")
|
||||
const { simpleSalesChannelFactory } = require("../../factories")
|
||||
|
||||
const {
|
||||
simpleSalesChannelFactory,
|
||||
simpleProductFactory,
|
||||
simpleCartFactory,
|
||||
} = require("../../factories")
|
||||
const { simpleOrderFactory } = require("../../factories")
|
||||
|
||||
const startServerWithEnvironment =
|
||||
require("../../../helpers/start-server-with-environment").default
|
||||
@@ -130,7 +137,6 @@ describe("sales channels", () => {
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
describe("POST /admin/sales-channels", () => {
|
||||
beforeEach(async () => {
|
||||
try {
|
||||
@@ -172,13 +178,10 @@ describe("sales channels", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("GET /admin/sales-channels/:id", () => {})
|
||||
describe("POST /admin/sales-channels/:id", () => {})
|
||||
|
||||
describe("DELETE /admin/sales-channels/:id", () => {
|
||||
let salesChannel
|
||||
|
||||
beforeEach(async() => {
|
||||
beforeEach(async () => {
|
||||
try {
|
||||
await adminSeeder(dbConnection)
|
||||
salesChannel = await simpleSalesChannelFactory(dbConnection, {
|
||||
@@ -194,14 +197,16 @@ describe("sales channels", () => {
|
||||
const db = useDb()
|
||||
await db.teardown()
|
||||
})
|
||||
|
||||
it("should delete the requested sales channel", async() => {
|
||||
it("should delete the requested sales channel", async () => {
|
||||
const api = useApi()
|
||||
|
||||
let deletedSalesChannel = await dbConnection.manager.findOne(SalesChannel, {
|
||||
where: { id: salesChannel.id },
|
||||
withDeleted: true
|
||||
})
|
||||
let deletedSalesChannel = await dbConnection.manager.findOne(
|
||||
SalesChannel,
|
||||
{
|
||||
where: { id: salesChannel.id },
|
||||
withDeleted: true,
|
||||
}
|
||||
)
|
||||
|
||||
expect(deletedSalesChannel.id).toEqual(salesChannel.id)
|
||||
expect(deletedSalesChannel.deleted_at).toEqual(null)
|
||||
@@ -220,20 +225,23 @@ describe("sales channels", () => {
|
||||
|
||||
deletedSalesChannel = await dbConnection.manager.findOne(SalesChannel, {
|
||||
where: { id: salesChannel.id },
|
||||
withDeleted: true
|
||||
withDeleted: true,
|
||||
})
|
||||
|
||||
expect(deletedSalesChannel.id).toEqual(salesChannel.id)
|
||||
expect(deletedSalesChannel.deleted_at).not.toEqual(null)
|
||||
})
|
||||
|
||||
it("should delete the requested sales channel idempotently", async() => {
|
||||
it("should delete the requested sales channel idempotently", async () => {
|
||||
const api = useApi()
|
||||
|
||||
let deletedSalesChannel = await dbConnection.manager.findOne(SalesChannel, {
|
||||
where: { id: salesChannel.id },
|
||||
withDeleted: true
|
||||
})
|
||||
let deletedSalesChannel = await dbConnection.manager.findOne(
|
||||
SalesChannel,
|
||||
{
|
||||
where: { id: salesChannel.id },
|
||||
withDeleted: true,
|
||||
}
|
||||
)
|
||||
|
||||
expect(deletedSalesChannel.id).toEqual(salesChannel.id)
|
||||
expect(deletedSalesChannel.deleted_at).toEqual(null)
|
||||
@@ -247,12 +255,12 @@ describe("sales channels", () => {
|
||||
expect(response.data).toEqual({
|
||||
id: expect.any(String),
|
||||
object: "sales-channel",
|
||||
deleted: true
|
||||
deleted: true,
|
||||
})
|
||||
|
||||
deletedSalesChannel = await dbConnection.manager.findOne(SalesChannel, {
|
||||
where: { id: salesChannel.id },
|
||||
withDeleted: true
|
||||
withDeleted: true,
|
||||
})
|
||||
|
||||
expect(deletedSalesChannel.id).toEqual(salesChannel.id)
|
||||
@@ -267,16 +275,244 @@ describe("sales channels", () => {
|
||||
expect(response.data).toEqual({
|
||||
id: expect.any(String),
|
||||
object: "sales-channel",
|
||||
deleted: true
|
||||
deleted: true,
|
||||
})
|
||||
|
||||
deletedSalesChannel = await dbConnection.manager.findOne(SalesChannel, {
|
||||
where: { id: salesChannel.id },
|
||||
withDeleted: true
|
||||
withDeleted: true,
|
||||
})
|
||||
|
||||
expect(deletedSalesChannel.id).toEqual(salesChannel.id)
|
||||
expect(deletedSalesChannel.deleted_at).not.toEqual(null)
|
||||
})
|
||||
})
|
||||
|
||||
describe("GET /admin/orders/:id", () => {
|
||||
let order
|
||||
beforeEach(async () => {
|
||||
try {
|
||||
await adminSeeder(dbConnection)
|
||||
|
||||
order = await simpleOrderFactory(dbConnection, {
|
||||
sales_channel: {
|
||||
name: "test name",
|
||||
description: "test description",
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
const db = useDb()
|
||||
await db.teardown()
|
||||
})
|
||||
|
||||
it("expands sales channel for single", async () => {
|
||||
const api = useApi()
|
||||
|
||||
const response = await api.get(
|
||||
`/admin/orders/${order.id}`,
|
||||
adminReqConfig
|
||||
)
|
||||
|
||||
expect(response.data.order.sales_channel).toBeTruthy()
|
||||
expect(response.data.order.sales_channel).toMatchSnapshot({
|
||||
id: expect.any(String),
|
||||
name: "test name",
|
||||
description: "test description",
|
||||
is_disabled: false,
|
||||
created_at: expect.any(String),
|
||||
updated_at: expect.any(String),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("GET /admin/orders?expand=sales_channels", () => {
|
||||
beforeEach(async () => {
|
||||
try {
|
||||
await adminSeeder(dbConnection)
|
||||
|
||||
await simpleOrderFactory(dbConnection, {
|
||||
sales_channel: {
|
||||
name: "test name",
|
||||
description: "test description",
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
const db = useDb()
|
||||
await db.teardown()
|
||||
})
|
||||
|
||||
it("expands sales channel with parameter", async () => {
|
||||
const api = useApi()
|
||||
|
||||
const response = await api.get(
|
||||
"/admin/orders?expand=sales_channel",
|
||||
adminReqConfig
|
||||
)
|
||||
|
||||
expect(response.data.orders[0].sales_channel).toBeTruthy()
|
||||
expect(response.data.orders[0].sales_channel).toMatchSnapshot({
|
||||
id: expect.any(String),
|
||||
name: "test name",
|
||||
description: "test description",
|
||||
is_disabled: false,
|
||||
created_at: expect.any(String),
|
||||
updated_at: expect.any(String),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("GET /admin/product/:id", () => {
|
||||
let product
|
||||
beforeEach(async () => {
|
||||
try {
|
||||
await adminSeeder(dbConnection)
|
||||
|
||||
product = await simpleProductFactory(dbConnection, {
|
||||
sales_channels: [
|
||||
{
|
||||
name: "webshop",
|
||||
description: "Webshop sales channel",
|
||||
},
|
||||
{
|
||||
name: "amazon",
|
||||
description: "Amazon sales channel",
|
||||
},
|
||||
],
|
||||
})
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
const db = useDb()
|
||||
await db.teardown()
|
||||
})
|
||||
|
||||
it("returns product with sales channel", async () => {
|
||||
const api = useApi()
|
||||
|
||||
const response = await api
|
||||
.get(`/admin/products/${product.id}`, adminReqConfig)
|
||||
.catch((err) => console.log(err))
|
||||
|
||||
expect(response.data.product.sales_channels).toBeTruthy()
|
||||
expect(response.data.product.sales_channels).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: "webshop",
|
||||
description: "Webshop sales channel",
|
||||
is_disabled: false,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
name: "amazon",
|
||||
description: "Amazon sales channel",
|
||||
is_disabled: false,
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("GET /admin/products?expand[]=sales_channels", () => {
|
||||
beforeEach(async () => {
|
||||
try {
|
||||
await adminSeeder(dbConnection)
|
||||
|
||||
await simpleProductFactory(dbConnection, {
|
||||
sales_channels: [
|
||||
{
|
||||
name: "webshop",
|
||||
description: "Webshop sales channel",
|
||||
},
|
||||
{
|
||||
name: "amazon",
|
||||
description: "Amazon sales channel",
|
||||
},
|
||||
],
|
||||
})
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
const db = useDb()
|
||||
await db.teardown()
|
||||
})
|
||||
|
||||
it("expands sales channel with parameter", async () => {
|
||||
const api = useApi()
|
||||
|
||||
const response = await api.get(
|
||||
"/admin/products?expand=sales_channels",
|
||||
adminReqConfig
|
||||
)
|
||||
|
||||
expect(response.data.products[0].sales_channels).toBeTruthy()
|
||||
expect(response.data.products[0].sales_channels).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: "webshop",
|
||||
description: "Webshop sales channel",
|
||||
is_disabled: false,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
name: "amazon",
|
||||
description: "Amazon sales channel",
|
||||
is_disabled: false,
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("GET /store/cart/:id with saleschannel", () => {
|
||||
let cart
|
||||
beforeEach(async () => {
|
||||
try {
|
||||
await adminSeeder(dbConnection)
|
||||
|
||||
cart = await simpleCartFactory(dbConnection, {
|
||||
sales_channel: {
|
||||
name: "test name",
|
||||
description: "test description",
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
const db = useDb()
|
||||
await db.teardown()
|
||||
})
|
||||
|
||||
it("returns cart with sales channel for single cart", async () => {
|
||||
const api = useApi()
|
||||
|
||||
const response = await api.get(`/store/carts/${cart.id}`, adminReqConfig)
|
||||
|
||||
expect(response.data.cart.sales_channel).toBeTruthy()
|
||||
expect(response.data.cart.sales_channel).toMatchSnapshot({
|
||||
id: expect.any(String),
|
||||
name: "test name",
|
||||
description: "test description",
|
||||
is_disabled: false,
|
||||
created_at: expect.any(String),
|
||||
updated_at: expect.any(String),
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -11,6 +11,10 @@ import {
|
||||
simpleLineItemFactory,
|
||||
} from "./simple-line-item-factory"
|
||||
import { RegionFactoryData, simpleRegionFactory } from "./simple-region-factory"
|
||||
import {
|
||||
SalesChannelFactoryData,
|
||||
simpleSalesChannelFactory,
|
||||
} from "./simple-sales-channel-factory"
|
||||
import {
|
||||
ShippingMethodFactoryData,
|
||||
simpleShippingMethodFactory,
|
||||
@@ -24,6 +28,7 @@ export type CartFactoryData = {
|
||||
line_items?: LineItemFactoryData[]
|
||||
shipping_address?: AddressFactoryData
|
||||
shipping_methods?: ShippingMethodFactoryData[]
|
||||
sales_channel?: SalesChannelFactoryData
|
||||
}
|
||||
|
||||
export const simpleCartFactory = async (
|
||||
@@ -62,6 +67,14 @@ export const simpleCartFactory = async (
|
||||
|
||||
const address = await simpleAddressFactory(connection, data.shipping_address)
|
||||
|
||||
let sales_channel
|
||||
if (typeof data.sales_channel !== "undefined") {
|
||||
sales_channel = await simpleSalesChannelFactory(
|
||||
connection,
|
||||
data.sales_channel
|
||||
)
|
||||
}
|
||||
|
||||
const id = data.id || `simple-cart-${Math.random() * 1000}`
|
||||
const toSave = manager.create(Cart, {
|
||||
id,
|
||||
@@ -70,6 +83,7 @@ export const simpleCartFactory = async (
|
||||
region_id: regionId,
|
||||
customer_id: customerId,
|
||||
shipping_address_id: address.id,
|
||||
sales_channel_id: sales_channel?.id ?? null,
|
||||
})
|
||||
|
||||
const cart = await manager.save(toSave)
|
||||
@@ -79,7 +93,7 @@ export const simpleCartFactory = async (
|
||||
await simpleShippingMethodFactory(connection, { ...sm, cart_id: id })
|
||||
}
|
||||
|
||||
const items = data.line_items
|
||||
const items = data.line_items || []
|
||||
for (const item of items) {
|
||||
await simpleLineItemFactory(connection, { ...item, cart_id: id })
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
PaymentStatus,
|
||||
FulfillmentStatus,
|
||||
} from "@medusajs/medusa"
|
||||
|
||||
import {
|
||||
DiscountFactoryData,
|
||||
simpleDiscountFactory,
|
||||
@@ -24,6 +23,10 @@ import {
|
||||
ShippingMethodFactoryData,
|
||||
simpleShippingMethodFactory,
|
||||
} from "./simple-shipping-method-factory"
|
||||
import {
|
||||
SalesChannelFactoryData,
|
||||
simpleSalesChannelFactory,
|
||||
} from "./simple-sales-channel-factory"
|
||||
|
||||
export type OrderFactoryData = {
|
||||
id?: string
|
||||
@@ -37,6 +40,7 @@ export type OrderFactoryData = {
|
||||
discounts?: DiscountFactoryData[]
|
||||
shipping_address?: AddressFactoryData
|
||||
shipping_methods?: ShippingMethodFactoryData[]
|
||||
sales_channel?: SalesChannelFactoryData
|
||||
}
|
||||
|
||||
export const simpleOrderFactory = async (
|
||||
@@ -79,6 +83,14 @@ export const simpleOrderFactory = async (
|
||||
)
|
||||
}
|
||||
|
||||
let sales_channel
|
||||
if (typeof data.sales_channel !== "undefined") {
|
||||
sales_channel = await simpleSalesChannelFactory(
|
||||
connection,
|
||||
data.sales_channel
|
||||
)
|
||||
}
|
||||
|
||||
const id = data.id || `simple-order-${Math.random() * 1000}`
|
||||
const toSave = manager.create(Order, {
|
||||
id,
|
||||
@@ -92,6 +104,7 @@ export const simpleOrderFactory = async (
|
||||
currency_code: currencyCode,
|
||||
tax_rate: taxRate,
|
||||
shipping_address_id: address.id,
|
||||
sales_channel_id: sales_channel?.id ?? null,
|
||||
})
|
||||
|
||||
const order = await manager.save(toSave)
|
||||
@@ -101,16 +114,17 @@ export const simpleOrderFactory = async (
|
||||
await simpleShippingMethodFactory(connection, { ...sm, order_id: order.id })
|
||||
}
|
||||
|
||||
const items = data.line_items.map((item) => {
|
||||
let adjustments = item?.adjustments || []
|
||||
return {
|
||||
...item,
|
||||
adjustments: adjustments.map((adj) => ({
|
||||
...adj,
|
||||
discount_id: discounts.find((d) => d.code === adj?.discount_code),
|
||||
})),
|
||||
}
|
||||
})
|
||||
const items =
|
||||
data.line_items?.map((item) => {
|
||||
const adjustments = item?.adjustments || []
|
||||
return {
|
||||
...item,
|
||||
adjustments: adjustments.map((adj) => ({
|
||||
...adj,
|
||||
discount_id: discounts.find((d) => d.code === adj?.discount_code),
|
||||
})),
|
||||
}
|
||||
}) || []
|
||||
|
||||
for (const item of items) {
|
||||
await simpleLineItemFactory(connection, { ...item, order_id: id })
|
||||
|
||||
@@ -12,6 +12,10 @@ import {
|
||||
ProductVariantFactoryData,
|
||||
simpleProductVariantFactory,
|
||||
} from "./simple-product-variant-factory"
|
||||
import {
|
||||
SalesChannelFactoryData,
|
||||
simpleSalesChannelFactory,
|
||||
} from "./simple-sales-channel-factory"
|
||||
|
||||
export type ProductFactoryData = {
|
||||
id?: string
|
||||
@@ -22,6 +26,7 @@ export type ProductFactoryData = {
|
||||
tags?: string[]
|
||||
options?: { id: string; title: string }[]
|
||||
variants?: ProductVariantFactoryData[]
|
||||
sales_channels?: SalesChannelFactoryData[]
|
||||
}
|
||||
|
||||
export const simpleProductFactory = async (
|
||||
@@ -43,6 +48,16 @@ export const simpleProductFactory = async (
|
||||
type: ShippingProfileType.GIFT_CARD,
|
||||
})
|
||||
|
||||
let sales_channels
|
||||
if (data.sales_channels) {
|
||||
sales_channels = await Promise.all(
|
||||
data.sales_channels.map(
|
||||
async (salesChannel) =>
|
||||
await simpleSalesChannelFactory(connection, salesChannel)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const prodId = data.id || `simple-product-${Math.random() * 1000}`
|
||||
const productToCreate = {
|
||||
id: prodId,
|
||||
@@ -77,6 +92,8 @@ export const simpleProductFactory = async (
|
||||
|
||||
const toSave = manager.create(Product, productToCreate)
|
||||
|
||||
toSave.sales_channels = sales_channels
|
||||
|
||||
await manager.save(toSave)
|
||||
|
||||
const optionId = `${prodId}-option`
|
||||
|
||||
@@ -12,7 +12,7 @@ const startServerWithEnvironment = async ({ cwd, verbose, env }) => {
|
||||
cwd,
|
||||
})
|
||||
|
||||
Object.entries(env).forEach(([key, value]) => {
|
||||
Object.entries(env).forEach(([key]) => {
|
||||
delete process.env[key]
|
||||
})
|
||||
|
||||
|
||||
@@ -20,4 +20,5 @@ module.exports = {
|
||||
},
|
||||
testEnvironment: `node`,
|
||||
moduleFileExtensions: [`js`, `jsx`, `ts`, `tsx`, `json`],
|
||||
"setupFilesAfterEnv": ["<rootDir>/setupTests.js"]
|
||||
}
|
||||
|
||||
3
packages/medusa/setupTests.js
Normal file
3
packages/medusa/setupTests.js
Normal file
@@ -0,0 +1,3 @@
|
||||
global.afterEach(async () => {
|
||||
await new Promise(resolve => setImmediate(resolve))
|
||||
})
|
||||
@@ -44,6 +44,8 @@ export default (app, container, config) => {
|
||||
})
|
||||
)
|
||||
|
||||
const featureFlagRouter = container.resolve("featureFlagRouter")
|
||||
|
||||
// Unauthenticated routes
|
||||
authRoutes(route)
|
||||
|
||||
@@ -74,9 +76,9 @@ export default (app, container, config) => {
|
||||
inviteRoutes(route)
|
||||
noteRoutes(route)
|
||||
notificationRoutes(route)
|
||||
orderRoutes(route)
|
||||
orderRoutes(route, featureFlagRouter)
|
||||
priceListRoutes(route)
|
||||
productRoutes(route)
|
||||
productRoutes(route, featureFlagRouter)
|
||||
productTagRoutes(route)
|
||||
productTypesRoutes(route)
|
||||
regionRoutes(route)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { defaultAdminOrdersRelations, defaultAdminOrdersFields } from "."
|
||||
import { OrderService } from "../../../../services"
|
||||
|
||||
/**
|
||||
@@ -26,10 +25,7 @@ export default async (req, res) => {
|
||||
|
||||
const orderService: OrderService = req.scope.resolve("orderService")
|
||||
|
||||
const order = await orderService.retrieve(id, {
|
||||
select: defaultAdminOrdersFields,
|
||||
relations: defaultAdminOrdersRelations,
|
||||
})
|
||||
const order = await orderService.retrieve(id, req.retrieveConfig)
|
||||
|
||||
res.json({ order })
|
||||
}
|
||||
|
||||
@@ -1,20 +1,36 @@
|
||||
import { Router } from "express"
|
||||
import "reflect-metadata"
|
||||
import { Order } from "../../../.."
|
||||
import { DeleteResponse, PaginatedResponse } from "../../../../types/common"
|
||||
import middlewares from "../../../middlewares"
|
||||
import {
|
||||
DeleteResponse,
|
||||
EmptyQueryParams,
|
||||
PaginatedResponse,
|
||||
} from "../../../../types/common"
|
||||
import middlewares, { transformQuery } from "../../../middlewares"
|
||||
import { AdminGetOrdersParams } from "./list-orders"
|
||||
import { FlagRouter } from "../../../../utils/flag-router"
|
||||
|
||||
const route = Router()
|
||||
|
||||
export default (app) => {
|
||||
export default (app, featureFlagRouter: FlagRouter) => {
|
||||
app.use("/orders", route)
|
||||
|
||||
const relations = [...defaultAdminOrdersRelations]
|
||||
if (featureFlagRouter.isFeatureEnabled("sales_channels")) {
|
||||
relations.push("sales_channel")
|
||||
}
|
||||
|
||||
/**
|
||||
* List orders
|
||||
*/
|
||||
route.get(
|
||||
"/",
|
||||
middlewares.normalizeQuery(),
|
||||
transformQuery(AdminGetOrdersParams, {
|
||||
defaultRelations: relations,
|
||||
defaultFields: defaultAdminOrdersFields,
|
||||
allowedFields: allowedAdminOrdersFields,
|
||||
isList: true,
|
||||
}),
|
||||
middlewares.wrap(require("./list-orders").default)
|
||||
)
|
||||
|
||||
@@ -23,7 +39,12 @@ export default (app) => {
|
||||
*/
|
||||
route.get(
|
||||
"/:id",
|
||||
middlewares.normalizeQuery(),
|
||||
transformQuery(EmptyQueryParams, {
|
||||
defaultRelations: relations,
|
||||
defaultFields: defaultAdminOrdersFields,
|
||||
allowedFields: allowedAdminOrdersFields,
|
||||
isList: false,
|
||||
}),
|
||||
middlewares.wrap(require("./get-order").default)
|
||||
)
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { defaultAdminOrdersRelations, defaultAdminOrdersFields } from "."
|
||||
import { validator } from "../../../../utils/validator"
|
||||
import { IsNumber, IsOptional, IsString } from "class-validator"
|
||||
import { omit, pick, pickBy } from "lodash"
|
||||
import { pick } from "lodash"
|
||||
import { OrderService } from "../../../../services"
|
||||
import { AdminListOrdersSelector } from "../../../../types/orders"
|
||||
import { Type } from "class-transformer"
|
||||
@@ -47,51 +45,23 @@ import { Type } from "class-transformer"
|
||||
* $ref: "#/components/schemas/order"
|
||||
*/
|
||||
export default async (req, res) => {
|
||||
const value = await validator(AdminGetOrdersParams, req.query)
|
||||
|
||||
const orderService: OrderService = req.scope.resolve("orderService")
|
||||
|
||||
let includeFields: string[] = []
|
||||
if (value.fields) {
|
||||
includeFields = value.fields.split(",")
|
||||
// Ensure created_at is included, since we are sorting on this
|
||||
includeFields.push("created_at")
|
||||
}
|
||||
|
||||
let expandFields: string[] = []
|
||||
if (value.expand) {
|
||||
expandFields = value.expand.split(",")
|
||||
}
|
||||
|
||||
const listConfig = {
|
||||
select: includeFields.length ? includeFields : defaultAdminOrdersFields,
|
||||
relations: expandFields.length ? expandFields : defaultAdminOrdersRelations,
|
||||
skip: value.offset,
|
||||
take: value.limit,
|
||||
order: { created_at: "DESC" },
|
||||
}
|
||||
|
||||
const filterableFields = omit(value, [
|
||||
"limit",
|
||||
"offset",
|
||||
"expand",
|
||||
"fields",
|
||||
"order",
|
||||
])
|
||||
const { skip, take, select, relations } = req.listConfig
|
||||
|
||||
const [orders, count] = await orderService.listAndCount(
|
||||
pickBy(filterableFields, (val) => typeof val !== "undefined"),
|
||||
listConfig
|
||||
req.filterableFields,
|
||||
req.listConfig
|
||||
)
|
||||
|
||||
let data = orders
|
||||
|
||||
const fields = [...includeFields, ...expandFields]
|
||||
const fields = [...select, ...relations]
|
||||
if (fields.length) {
|
||||
data = orders.map((o) => pick(o, fields))
|
||||
}
|
||||
|
||||
res.json({ orders: data, count, offset: value.offset, limit: value.limit })
|
||||
res.json({ orders: data, count, offset: skip, limit: take })
|
||||
}
|
||||
|
||||
export class AdminGetOrdersParams extends AdminListOrdersSelector {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { defaultAdminProductFields, defaultAdminProductRelations } from "."
|
||||
import { ProductService, PricingService } from "../../../../services"
|
||||
|
||||
/**
|
||||
@@ -27,10 +26,7 @@ export default async (req, res) => {
|
||||
const productService: ProductService = req.scope.resolve("productService")
|
||||
const pricingService: PricingService = req.scope.resolve("pricingService")
|
||||
|
||||
const rawProduct = await productService.retrieve(id, {
|
||||
select: defaultAdminProductFields,
|
||||
relations: defaultAdminProductRelations,
|
||||
})
|
||||
const rawProduct = await productService.retrieve(id, req.retrieveConfig)
|
||||
|
||||
const [product] = await pricingService.setProductPrices([rawProduct])
|
||||
|
||||
|
||||
@@ -2,14 +2,21 @@ import { Router } from "express"
|
||||
import "reflect-metadata"
|
||||
import { PricedProduct } from "../../../../types/pricing"
|
||||
import { Product, ProductTag, ProductType } from "../../../.."
|
||||
import { PaginatedResponse } from "../../../../types/common"
|
||||
import middlewares from "../../../middlewares"
|
||||
import { EmptyQueryParams, PaginatedResponse } from "../../../../types/common"
|
||||
import middlewares, { transformQuery } from "../../../middlewares"
|
||||
import { AdminGetProductsParams } from "./list-products"
|
||||
import { FlagRouter } from "../../../../utils/flag-router"
|
||||
|
||||
const route = Router()
|
||||
|
||||
export default (app) => {
|
||||
export default (app, featureFlagRouter: FlagRouter) => {
|
||||
app.use("/products", route)
|
||||
|
||||
const relations = [...defaultAdminProductRelations]
|
||||
if (featureFlagRouter.isFeatureEnabled("sales_channels")) {
|
||||
relations.push("sales_channels")
|
||||
}
|
||||
|
||||
route.post("/", middlewares.wrap(require("./create-product").default))
|
||||
route.post("/:id", middlewares.wrap(require("./update-product").default))
|
||||
route.get("/types", middlewares.wrap(require("./list-types").default))
|
||||
@@ -53,11 +60,25 @@ export default (app) => {
|
||||
"/:id/metadata",
|
||||
middlewares.wrap(require("./set-metadata").default)
|
||||
)
|
||||
route.get(
|
||||
"/:id",
|
||||
transformQuery(EmptyQueryParams, {
|
||||
defaultRelations: relations,
|
||||
defaultFields: defaultAdminProductFields,
|
||||
allowedFields: allowedAdminProductFields,
|
||||
isList: false,
|
||||
}),
|
||||
middlewares.wrap(require("./get-product").default)
|
||||
)
|
||||
|
||||
route.get("/:id", middlewares.wrap(require("./get-product").default))
|
||||
route.get(
|
||||
"/",
|
||||
middlewares.normalizeQuery(),
|
||||
transformQuery(AdminGetProductsParams, {
|
||||
defaultRelations: defaultAdminProductRelations,
|
||||
defaultFields: defaultAdminProductFields,
|
||||
allowedFields: allowedAdminProductFields,
|
||||
isList: true,
|
||||
}),
|
||||
middlewares.wrap(require("./list-products").default)
|
||||
)
|
||||
|
||||
@@ -141,6 +162,7 @@ export const allowedAdminProductRelations = [
|
||||
"tags",
|
||||
"type",
|
||||
"collection",
|
||||
"sales_channels",
|
||||
]
|
||||
|
||||
export type AdminProductsDeleteOptionRes = {
|
||||
|
||||
@@ -8,17 +8,11 @@ import {
|
||||
IsString,
|
||||
ValidateNested,
|
||||
} from "class-validator"
|
||||
import { omit } from "lodash"
|
||||
import { Product, ProductStatus } from "../../../../models/product"
|
||||
import { DateComparisonOperator } from "../../../../types/common"
|
||||
import {
|
||||
allowedAdminProductFields,
|
||||
defaultAdminProductFields,
|
||||
defaultAdminProductRelations,
|
||||
} from "."
|
||||
import listAndCount from "../../../../controllers/products/admin-list-products"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
import { optionalBooleanMapper } from "../../../../utils/validators/is-boolean"
|
||||
import { PricedProduct } from "../../../../types/pricing"
|
||||
import { PricingService, ProductService } from "../../../../services"
|
||||
|
||||
/**
|
||||
* @oas [get] /products
|
||||
@@ -69,32 +63,31 @@ import { optionalBooleanMapper } from "../../../../utils/validators/is-boolean"
|
||||
* $ref: "#/components/schemas/product"
|
||||
*/
|
||||
export default async (req, res) => {
|
||||
const validatedParams = await validator(AdminGetProductsParams, req.query)
|
||||
const productService: ProductService = req.scope.resolve("productService")
|
||||
const pricingService: PricingService = req.scope.resolve("pricingService")
|
||||
|
||||
const filterableFields = omit(validatedParams, [
|
||||
"limit",
|
||||
"offset",
|
||||
"expand",
|
||||
"fields",
|
||||
"order",
|
||||
])
|
||||
const { skip, take, relations } = req.listConfig
|
||||
|
||||
const result = await listAndCount(
|
||||
req.scope,
|
||||
filterableFields,
|
||||
{},
|
||||
{
|
||||
limit: validatedParams.limit ?? 50,
|
||||
offset: validatedParams.offset ?? 0,
|
||||
expand: validatedParams.expand,
|
||||
fields: validatedParams.fields,
|
||||
allowedFields: allowedAdminProductFields,
|
||||
defaultFields: defaultAdminProductFields as (keyof Product)[],
|
||||
defaultRelations: defaultAdminProductRelations,
|
||||
}
|
||||
const [rawProducts, count] = await productService.listAndCount(
|
||||
req.filterableFields,
|
||||
req.listConfig
|
||||
)
|
||||
|
||||
res.json(result)
|
||||
let products: (Product | PricedProduct)[] = rawProducts
|
||||
|
||||
const includesPricing = ["variants", "variants.prices"].every((relation) =>
|
||||
relations?.includes(relation)
|
||||
)
|
||||
if (includesPricing) {
|
||||
products = await pricingService.setProductPrices(rawProducts)
|
||||
}
|
||||
|
||||
res.json({
|
||||
products,
|
||||
count,
|
||||
offset: skip,
|
||||
limit: take,
|
||||
})
|
||||
}
|
||||
|
||||
export class AdminGetProductsPaginationParams {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { defaultStoreCartFields, defaultStoreCartRelations } from "."
|
||||
import { CartService } from "../../../../services"
|
||||
import { decorateLineItemsWithTotals } from "./decorate-line-items-with-totals"
|
||||
|
||||
@@ -43,10 +42,7 @@ export default async (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
cart = await cartService.retrieve(id, {
|
||||
select: defaultStoreCartFields,
|
||||
relations: defaultStoreCartRelations,
|
||||
})
|
||||
cart = await cartService.retrieve(id, req.retrieveConfig)
|
||||
|
||||
const data = await decorateLineItemsWithTotals(cart, req)
|
||||
res.json({ cart: data })
|
||||
|
||||
@@ -1,22 +1,36 @@
|
||||
import { Router } from "express"
|
||||
import "reflect-metadata"
|
||||
import { Cart, Order, Swap } from "../../../../"
|
||||
import { DeleteResponse } from "../../../../types/common"
|
||||
import middlewares from "../../../middlewares"
|
||||
import { DeleteResponse, EmptyQueryParams } from "../../../../types/common"
|
||||
import middlewares, { transformQuery } from "../../../middlewares"
|
||||
const route = Router()
|
||||
|
||||
export default (app, container) => {
|
||||
const middlewareService = container.resolve("middlewareService")
|
||||
const featureFlagRouter = container.resolve("featureFlagRouter")
|
||||
|
||||
app.use("/carts", route)
|
||||
|
||||
const relations = [...defaultStoreCartRelations]
|
||||
if (featureFlagRouter.isFeatureEnabled("sales_channels")) {
|
||||
relations.push("sales_channel")
|
||||
}
|
||||
|
||||
// Inject plugin routes
|
||||
const routers = middlewareService.getRouters("store/carts")
|
||||
for (const router of routers) {
|
||||
route.use("/", router)
|
||||
}
|
||||
|
||||
route.get("/:id", middlewares.wrap(require("./get-cart").default))
|
||||
route.get(
|
||||
"/:id",
|
||||
transformQuery(EmptyQueryParams, {
|
||||
defaultRelations: relations,
|
||||
defaultFields: defaultStoreCartFields,
|
||||
isList: false,
|
||||
}),
|
||||
middlewares.wrap(require("./get-cart").default)
|
||||
)
|
||||
|
||||
route.post(
|
||||
"/",
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
import { AwilixContainer } from "awilix"
|
||||
import { AdminProductsListRes } from "../../api"
|
||||
import { pickBy } from "lodash"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { Product } from "../../models/product"
|
||||
import { ProductService, PricingService } from "../../services"
|
||||
import { getListConfig } from "../../utils/get-query-config"
|
||||
import { FilterableProductProps } from "../../types/product"
|
||||
import { PricedProduct } from "../../types/pricing"
|
||||
|
||||
type ListContext = {
|
||||
limit: number
|
||||
offset: number
|
||||
order?: string
|
||||
fields?: string
|
||||
expand?: string
|
||||
allowedFields?: string[]
|
||||
defaultFields?: (keyof Product)[]
|
||||
defaultRelations?: string[]
|
||||
}
|
||||
|
||||
const listAndCount = async (
|
||||
scope: AwilixContainer,
|
||||
query: FilterableProductProps,
|
||||
body?: object,
|
||||
context: ListContext = { limit: 50, offset: 0 }
|
||||
): Promise<AdminProductsListRes> => {
|
||||
const { limit, offset, allowedFields, defaultFields, defaultRelations } =
|
||||
context
|
||||
|
||||
const productService: ProductService = scope.resolve("productService")
|
||||
const pricingService: PricingService = scope.resolve("pricingService")
|
||||
let includeFields: (keyof Product)[] | undefined
|
||||
if (context.fields) {
|
||||
includeFields = context.fields.split(",") as (keyof Product)[]
|
||||
}
|
||||
|
||||
let expandFields: string[] | undefined
|
||||
if (context.expand) {
|
||||
expandFields = context.expand.split(",")
|
||||
}
|
||||
|
||||
let orderBy: { [k: symbol]: "DESC" | "ASC" } | undefined
|
||||
if (typeof context.order !== "undefined") {
|
||||
let orderField = context.order
|
||||
if (context.order.startsWith("-")) {
|
||||
const [, field] = context.order.split("-")
|
||||
orderField = field
|
||||
orderBy = { [field]: "DESC" }
|
||||
} else {
|
||||
orderBy = { [context.order]: "ASC" }
|
||||
}
|
||||
|
||||
if (!(allowedFields || []).includes(orderField)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Order field must be a valid product field"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const listConfig = getListConfig<Product>(
|
||||
defaultFields ?? [],
|
||||
defaultRelations ?? [],
|
||||
includeFields,
|
||||
expandFields,
|
||||
limit,
|
||||
offset,
|
||||
orderBy
|
||||
)
|
||||
|
||||
const [rawProducts, count] = await productService.listAndCount(
|
||||
pickBy(query, (val) => typeof val !== "undefined"),
|
||||
listConfig
|
||||
)
|
||||
|
||||
let products: (Product | PricedProduct)[] = rawProducts
|
||||
|
||||
const includesPricing = ["variants", "variants.prices"].every((relation) =>
|
||||
listConfig?.relations?.includes(relation)
|
||||
)
|
||||
if (includesPricing) {
|
||||
products = await pricingService.setProductPrices(rawProducts)
|
||||
}
|
||||
|
||||
return {
|
||||
products,
|
||||
count,
|
||||
offset,
|
||||
limit,
|
||||
}
|
||||
}
|
||||
|
||||
export default listAndCount
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
ManyToMany,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
OneToOne,
|
||||
} from "typeorm"
|
||||
import { DbAwareColumn } from "../utils/db-aware-column"
|
||||
import { Image } from "./image"
|
||||
@@ -21,10 +20,7 @@ import { ProductVariant } from "./product-variant"
|
||||
import { ShippingProfile } from "./shipping-profile"
|
||||
import { SoftDeletableEntity } from "../interfaces/models/soft-deletable-entity"
|
||||
import { generateEntityId } from "../utils/generate-entity-id"
|
||||
import {
|
||||
FeatureFlagColumn,
|
||||
FeatureFlagDecorators,
|
||||
} from "../utils/feature-flag-decorators"
|
||||
import { FeatureFlagDecorators } from "../utils/feature-flag-decorators"
|
||||
import { SalesChannel } from "./sales-channel"
|
||||
|
||||
export enum ProductStatus {
|
||||
|
||||
@@ -112,6 +112,8 @@ export type DeleteResponse = {
|
||||
deleted: boolean
|
||||
}
|
||||
|
||||
export class EmptyQueryParams {}
|
||||
|
||||
export class DateComparisonOperator {
|
||||
@IsOptional()
|
||||
@IsDate()
|
||||
|
||||
@@ -1,40 +1,59 @@
|
||||
import { getConfigFile } from "medusa-core-utils"
|
||||
import { getConfigFile } from "medusa-core-utils"
|
||||
import { Column, ColumnOptions, Entity, EntityOptions } from "typeorm"
|
||||
import featureFlagsLoader from "../loaders/feature-flags"
|
||||
import path from "path"
|
||||
import { ConfigModule } from "../types/global"
|
||||
import { FlagRouter } from "./flag-router"
|
||||
|
||||
/**
|
||||
* If that file is required in a non node environment then the setImmediate timer does not exists.
|
||||
* This can happen when a client package require a server based package and that one of the import
|
||||
* require to import that file which is using the setImmediate.
|
||||
* In order to take care of those cases, the setImmediate timer will use the one provided by the api (node)
|
||||
* if possible and will provide a mock in a browser like environment.
|
||||
*/
|
||||
let setImmediate_
|
||||
try {
|
||||
setImmediate_ = setImmediate
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
"[feature-flag-decorator.ts] setImmediate will use a mock, this happen when this file is required in a browser environment and should not impact you"
|
||||
)
|
||||
setImmediate_ = ((callback: () => void | Promise<void>) => callback())
|
||||
}
|
||||
|
||||
export function FeatureFlagColumn(
|
||||
featureFlag: string,
|
||||
columnOptions: ColumnOptions = {}
|
||||
): PropertyDecorator {
|
||||
const featureFlagRouter = getFeatureFlagRouter()
|
||||
return function (target, propertyName) {
|
||||
setImmediate_((): any => {
|
||||
const featureFlagRouter = getFeatureFlagRouter()
|
||||
|
||||
if (!featureFlagRouter.isFeatureEnabled(featureFlag)) {
|
||||
return (): void => {
|
||||
// noop
|
||||
}
|
||||
if (!featureFlagRouter.isFeatureEnabled(featureFlag)) {
|
||||
return
|
||||
}
|
||||
|
||||
Column(columnOptions)(target, propertyName)
|
||||
})
|
||||
}
|
||||
|
||||
return Column(columnOptions)
|
||||
}
|
||||
|
||||
export function FeatureFlagDecorators(
|
||||
featureFlag: string,
|
||||
decorators: PropertyDecorator[]
|
||||
): PropertyDecorator {
|
||||
const featureFlagRouter = getFeatureFlagRouter()
|
||||
return function (target, propertyName) {
|
||||
setImmediate_((): any => {
|
||||
const featureFlagRouter = getFeatureFlagRouter()
|
||||
|
||||
if (!featureFlagRouter.isFeatureEnabled(featureFlag)) {
|
||||
return (): void => {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
return (target: Object, propertyKey: string | symbol): void => {
|
||||
decorators.forEach((decorator) => {
|
||||
decorator(target, propertyKey)
|
||||
if (!featureFlagRouter.isFeatureEnabled(featureFlag)) {
|
||||
return
|
||||
}
|
||||
|
||||
decorators.forEach((decorator: PropertyDecorator) => {
|
||||
decorator(target, propertyName)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -44,12 +63,10 @@ export function FeatureFlagEntity(
|
||||
name?: string,
|
||||
options?: EntityOptions
|
||||
): ClassDecorator {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
return function (target: Function): void {
|
||||
target["isFeatureEnabled"] = function (): boolean {
|
||||
const featureFlagRouter = getFeatureFlagRouter()
|
||||
|
||||
// const featureFlagRouter = featureFlagsLoader(configModule)
|
||||
return featureFlagRouter.isFeatureEnabled(featureFlag)
|
||||
}
|
||||
Entity(name, options)(target)
|
||||
|
||||
@@ -27,14 +27,14 @@ export function getRetrieveConfig<TModel extends BaseEntity>(
|
||||
): FindConfig<TModel> {
|
||||
let includeFields: (keyof TModel)[] = []
|
||||
if (typeof fields !== "undefined") {
|
||||
includeFields = Array
|
||||
.from(new Set([...fields, "id"]))
|
||||
.map(field => (typeof field === "string") ? field.trim() : field) as (keyof TModel)[]
|
||||
includeFields = Array.from(new Set([...fields, "id"])).map((field) =>
|
||||
typeof field === "string" ? field.trim() : field
|
||||
) as (keyof TModel)[]
|
||||
}
|
||||
|
||||
let expandFields: string[] = []
|
||||
if (typeof expand !== "undefined") {
|
||||
expandFields = expand.map(expandRelation => expandRelation.trim())
|
||||
expandFields = expand.map((expandRelation) => expandRelation.trim())
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -106,7 +106,10 @@ export function prepareListQuery<
|
||||
orderBy = { [order]: "ASC" }
|
||||
}
|
||||
|
||||
if (queryConfig?.allowedFields?.length && !queryConfig?.allowedFields.includes(orderField)) {
|
||||
if (
|
||||
queryConfig?.allowedFields?.length &&
|
||||
!queryConfig?.allowedFields.includes(orderField)
|
||||
) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Order field ${orderField} is not valid`
|
||||
|
||||
Reference in New Issue
Block a user