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:
co-authored by
Sebastian Rindom
Adrien de Peretti
parent
fb4cfc3c3c
commit
19f35ba6aa
@@ -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`] = `
|
exports[`sales channels GET /admin/sales-channels/:id should retrieve the requested sales channel 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"created_at": Any<String>,
|
"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`] = `
|
exports[`sales channels POST /admin/sales-channels successfully creates a sales channel 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"sales_channel": ObjectContaining {
|
"sales_channel": ObjectContaining {
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
const path = require("path")
|
const path = require("path")
|
||||||
|
|
||||||
const { SalesChannel } = require("@medusajs/medusa")
|
const { SalesChannel } = require("@medusajs/medusa")
|
||||||
|
|
||||||
const { useApi } = require("../../../helpers/use-api")
|
const { useApi } = require("../../../helpers/use-api")
|
||||||
const { useDb } = require("../../../helpers/use-db")
|
const { useDb } = require("../../../helpers/use-db")
|
||||||
|
|
||||||
const adminSeeder = require("../../helpers/admin-seeder")
|
const adminSeeder = require("../../helpers/admin-seeder")
|
||||||
const { simpleSalesChannelFactory } = require("../../factories")
|
|
||||||
|
const {
|
||||||
|
simpleSalesChannelFactory,
|
||||||
|
simpleProductFactory,
|
||||||
|
simpleCartFactory,
|
||||||
|
} = require("../../factories")
|
||||||
|
const { simpleOrderFactory } = require("../../factories")
|
||||||
|
|
||||||
const startServerWithEnvironment =
|
const startServerWithEnvironment =
|
||||||
require("../../../helpers/start-server-with-environment").default
|
require("../../../helpers/start-server-with-environment").default
|
||||||
@@ -130,7 +137,6 @@ describe("sales channels", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
describe("POST /admin/sales-channels", () => {
|
describe("POST /admin/sales-channels", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
try {
|
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", () => {
|
describe("DELETE /admin/sales-channels/:id", () => {
|
||||||
let salesChannel
|
let salesChannel
|
||||||
|
|
||||||
beforeEach(async() => {
|
beforeEach(async () => {
|
||||||
try {
|
try {
|
||||||
await adminSeeder(dbConnection)
|
await adminSeeder(dbConnection)
|
||||||
salesChannel = await simpleSalesChannelFactory(dbConnection, {
|
salesChannel = await simpleSalesChannelFactory(dbConnection, {
|
||||||
@@ -194,14 +197,16 @@ describe("sales channels", () => {
|
|||||||
const db = useDb()
|
const db = useDb()
|
||||||
await db.teardown()
|
await db.teardown()
|
||||||
})
|
})
|
||||||
|
it("should delete the requested sales channel", async () => {
|
||||||
it("should delete the requested sales channel", async() => {
|
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
|
|
||||||
let deletedSalesChannel = await dbConnection.manager.findOne(SalesChannel, {
|
let deletedSalesChannel = await dbConnection.manager.findOne(
|
||||||
where: { id: salesChannel.id },
|
SalesChannel,
|
||||||
withDeleted: true
|
{
|
||||||
})
|
where: { id: salesChannel.id },
|
||||||
|
withDeleted: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
expect(deletedSalesChannel.id).toEqual(salesChannel.id)
|
expect(deletedSalesChannel.id).toEqual(salesChannel.id)
|
||||||
expect(deletedSalesChannel.deleted_at).toEqual(null)
|
expect(deletedSalesChannel.deleted_at).toEqual(null)
|
||||||
@@ -220,20 +225,23 @@ describe("sales channels", () => {
|
|||||||
|
|
||||||
deletedSalesChannel = await dbConnection.manager.findOne(SalesChannel, {
|
deletedSalesChannel = await dbConnection.manager.findOne(SalesChannel, {
|
||||||
where: { id: salesChannel.id },
|
where: { id: salesChannel.id },
|
||||||
withDeleted: true
|
withDeleted: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(deletedSalesChannel.id).toEqual(salesChannel.id)
|
expect(deletedSalesChannel.id).toEqual(salesChannel.id)
|
||||||
expect(deletedSalesChannel.deleted_at).not.toEqual(null)
|
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()
|
const api = useApi()
|
||||||
|
|
||||||
let deletedSalesChannel = await dbConnection.manager.findOne(SalesChannel, {
|
let deletedSalesChannel = await dbConnection.manager.findOne(
|
||||||
where: { id: salesChannel.id },
|
SalesChannel,
|
||||||
withDeleted: true
|
{
|
||||||
})
|
where: { id: salesChannel.id },
|
||||||
|
withDeleted: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
expect(deletedSalesChannel.id).toEqual(salesChannel.id)
|
expect(deletedSalesChannel.id).toEqual(salesChannel.id)
|
||||||
expect(deletedSalesChannel.deleted_at).toEqual(null)
|
expect(deletedSalesChannel.deleted_at).toEqual(null)
|
||||||
@@ -247,12 +255,12 @@ describe("sales channels", () => {
|
|||||||
expect(response.data).toEqual({
|
expect(response.data).toEqual({
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
object: "sales-channel",
|
object: "sales-channel",
|
||||||
deleted: true
|
deleted: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
deletedSalesChannel = await dbConnection.manager.findOne(SalesChannel, {
|
deletedSalesChannel = await dbConnection.manager.findOne(SalesChannel, {
|
||||||
where: { id: salesChannel.id },
|
where: { id: salesChannel.id },
|
||||||
withDeleted: true
|
withDeleted: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(deletedSalesChannel.id).toEqual(salesChannel.id)
|
expect(deletedSalesChannel.id).toEqual(salesChannel.id)
|
||||||
@@ -267,16 +275,244 @@ describe("sales channels", () => {
|
|||||||
expect(response.data).toEqual({
|
expect(response.data).toEqual({
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
object: "sales-channel",
|
object: "sales-channel",
|
||||||
deleted: true
|
deleted: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
deletedSalesChannel = await dbConnection.manager.findOne(SalesChannel, {
|
deletedSalesChannel = await dbConnection.manager.findOne(SalesChannel, {
|
||||||
where: { id: salesChannel.id },
|
where: { id: salesChannel.id },
|
||||||
withDeleted: true
|
withDeleted: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(deletedSalesChannel.id).toEqual(salesChannel.id)
|
expect(deletedSalesChannel.id).toEqual(salesChannel.id)
|
||||||
expect(deletedSalesChannel.deleted_at).not.toEqual(null)
|
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,
|
simpleLineItemFactory,
|
||||||
} from "./simple-line-item-factory"
|
} from "./simple-line-item-factory"
|
||||||
import { RegionFactoryData, simpleRegionFactory } from "./simple-region-factory"
|
import { RegionFactoryData, simpleRegionFactory } from "./simple-region-factory"
|
||||||
|
import {
|
||||||
|
SalesChannelFactoryData,
|
||||||
|
simpleSalesChannelFactory,
|
||||||
|
} from "./simple-sales-channel-factory"
|
||||||
import {
|
import {
|
||||||
ShippingMethodFactoryData,
|
ShippingMethodFactoryData,
|
||||||
simpleShippingMethodFactory,
|
simpleShippingMethodFactory,
|
||||||
@@ -24,6 +28,7 @@ export type CartFactoryData = {
|
|||||||
line_items?: LineItemFactoryData[]
|
line_items?: LineItemFactoryData[]
|
||||||
shipping_address?: AddressFactoryData
|
shipping_address?: AddressFactoryData
|
||||||
shipping_methods?: ShippingMethodFactoryData[]
|
shipping_methods?: ShippingMethodFactoryData[]
|
||||||
|
sales_channel?: SalesChannelFactoryData
|
||||||
}
|
}
|
||||||
|
|
||||||
export const simpleCartFactory = async (
|
export const simpleCartFactory = async (
|
||||||
@@ -62,6 +67,14 @@ export const simpleCartFactory = async (
|
|||||||
|
|
||||||
const address = await simpleAddressFactory(connection, data.shipping_address)
|
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 id = data.id || `simple-cart-${Math.random() * 1000}`
|
||||||
const toSave = manager.create(Cart, {
|
const toSave = manager.create(Cart, {
|
||||||
id,
|
id,
|
||||||
@@ -70,6 +83,7 @@ export const simpleCartFactory = async (
|
|||||||
region_id: regionId,
|
region_id: regionId,
|
||||||
customer_id: customerId,
|
customer_id: customerId,
|
||||||
shipping_address_id: address.id,
|
shipping_address_id: address.id,
|
||||||
|
sales_channel_id: sales_channel?.id ?? null,
|
||||||
})
|
})
|
||||||
|
|
||||||
const cart = await manager.save(toSave)
|
const cart = await manager.save(toSave)
|
||||||
@@ -79,7 +93,7 @@ export const simpleCartFactory = async (
|
|||||||
await simpleShippingMethodFactory(connection, { ...sm, cart_id: id })
|
await simpleShippingMethodFactory(connection, { ...sm, cart_id: id })
|
||||||
}
|
}
|
||||||
|
|
||||||
const items = data.line_items
|
const items = data.line_items || []
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
await simpleLineItemFactory(connection, { ...item, cart_id: id })
|
await simpleLineItemFactory(connection, { ...item, cart_id: id })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
PaymentStatus,
|
PaymentStatus,
|
||||||
FulfillmentStatus,
|
FulfillmentStatus,
|
||||||
} from "@medusajs/medusa"
|
} from "@medusajs/medusa"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DiscountFactoryData,
|
DiscountFactoryData,
|
||||||
simpleDiscountFactory,
|
simpleDiscountFactory,
|
||||||
@@ -24,6 +23,10 @@ import {
|
|||||||
ShippingMethodFactoryData,
|
ShippingMethodFactoryData,
|
||||||
simpleShippingMethodFactory,
|
simpleShippingMethodFactory,
|
||||||
} from "./simple-shipping-method-factory"
|
} from "./simple-shipping-method-factory"
|
||||||
|
import {
|
||||||
|
SalesChannelFactoryData,
|
||||||
|
simpleSalesChannelFactory,
|
||||||
|
} from "./simple-sales-channel-factory"
|
||||||
|
|
||||||
export type OrderFactoryData = {
|
export type OrderFactoryData = {
|
||||||
id?: string
|
id?: string
|
||||||
@@ -37,6 +40,7 @@ export type OrderFactoryData = {
|
|||||||
discounts?: DiscountFactoryData[]
|
discounts?: DiscountFactoryData[]
|
||||||
shipping_address?: AddressFactoryData
|
shipping_address?: AddressFactoryData
|
||||||
shipping_methods?: ShippingMethodFactoryData[]
|
shipping_methods?: ShippingMethodFactoryData[]
|
||||||
|
sales_channel?: SalesChannelFactoryData
|
||||||
}
|
}
|
||||||
|
|
||||||
export const simpleOrderFactory = async (
|
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 id = data.id || `simple-order-${Math.random() * 1000}`
|
||||||
const toSave = manager.create(Order, {
|
const toSave = manager.create(Order, {
|
||||||
id,
|
id,
|
||||||
@@ -92,6 +104,7 @@ export const simpleOrderFactory = async (
|
|||||||
currency_code: currencyCode,
|
currency_code: currencyCode,
|
||||||
tax_rate: taxRate,
|
tax_rate: taxRate,
|
||||||
shipping_address_id: address.id,
|
shipping_address_id: address.id,
|
||||||
|
sales_channel_id: sales_channel?.id ?? null,
|
||||||
})
|
})
|
||||||
|
|
||||||
const order = await manager.save(toSave)
|
const order = await manager.save(toSave)
|
||||||
@@ -101,16 +114,17 @@ export const simpleOrderFactory = async (
|
|||||||
await simpleShippingMethodFactory(connection, { ...sm, order_id: order.id })
|
await simpleShippingMethodFactory(connection, { ...sm, order_id: order.id })
|
||||||
}
|
}
|
||||||
|
|
||||||
const items = data.line_items.map((item) => {
|
const items =
|
||||||
let adjustments = item?.adjustments || []
|
data.line_items?.map((item) => {
|
||||||
return {
|
const adjustments = item?.adjustments || []
|
||||||
...item,
|
return {
|
||||||
adjustments: adjustments.map((adj) => ({
|
...item,
|
||||||
...adj,
|
adjustments: adjustments.map((adj) => ({
|
||||||
discount_id: discounts.find((d) => d.code === adj?.discount_code),
|
...adj,
|
||||||
})),
|
discount_id: discounts.find((d) => d.code === adj?.discount_code),
|
||||||
}
|
})),
|
||||||
})
|
}
|
||||||
|
}) || []
|
||||||
|
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
await simpleLineItemFactory(connection, { ...item, order_id: id })
|
await simpleLineItemFactory(connection, { ...item, order_id: id })
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ import {
|
|||||||
ProductVariantFactoryData,
|
ProductVariantFactoryData,
|
||||||
simpleProductVariantFactory,
|
simpleProductVariantFactory,
|
||||||
} from "./simple-product-variant-factory"
|
} from "./simple-product-variant-factory"
|
||||||
|
import {
|
||||||
|
SalesChannelFactoryData,
|
||||||
|
simpleSalesChannelFactory,
|
||||||
|
} from "./simple-sales-channel-factory"
|
||||||
|
|
||||||
export type ProductFactoryData = {
|
export type ProductFactoryData = {
|
||||||
id?: string
|
id?: string
|
||||||
@@ -22,6 +26,7 @@ export type ProductFactoryData = {
|
|||||||
tags?: string[]
|
tags?: string[]
|
||||||
options?: { id: string; title: string }[]
|
options?: { id: string; title: string }[]
|
||||||
variants?: ProductVariantFactoryData[]
|
variants?: ProductVariantFactoryData[]
|
||||||
|
sales_channels?: SalesChannelFactoryData[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const simpleProductFactory = async (
|
export const simpleProductFactory = async (
|
||||||
@@ -43,6 +48,16 @@ export const simpleProductFactory = async (
|
|||||||
type: ShippingProfileType.GIFT_CARD,
|
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 prodId = data.id || `simple-product-${Math.random() * 1000}`
|
||||||
const productToCreate = {
|
const productToCreate = {
|
||||||
id: prodId,
|
id: prodId,
|
||||||
@@ -77,6 +92,8 @@ export const simpleProductFactory = async (
|
|||||||
|
|
||||||
const toSave = manager.create(Product, productToCreate)
|
const toSave = manager.create(Product, productToCreate)
|
||||||
|
|
||||||
|
toSave.sales_channels = sales_channels
|
||||||
|
|
||||||
await manager.save(toSave)
|
await manager.save(toSave)
|
||||||
|
|
||||||
const optionId = `${prodId}-option`
|
const optionId = `${prodId}-option`
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const startServerWithEnvironment = async ({ cwd, verbose, env }) => {
|
|||||||
cwd,
|
cwd,
|
||||||
})
|
})
|
||||||
|
|
||||||
Object.entries(env).forEach(([key, value]) => {
|
Object.entries(env).forEach(([key]) => {
|
||||||
delete process.env[key]
|
delete process.env[key]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -20,4 +20,5 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
testEnvironment: `node`,
|
testEnvironment: `node`,
|
||||||
moduleFileExtensions: [`js`, `jsx`, `ts`, `tsx`, `json`],
|
moduleFileExtensions: [`js`, `jsx`, `ts`, `tsx`, `json`],
|
||||||
|
"setupFilesAfterEnv": ["<rootDir>/setupTests.js"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
// Unauthenticated routes
|
||||||
authRoutes(route)
|
authRoutes(route)
|
||||||
|
|
||||||
@@ -74,9 +76,9 @@ export default (app, container, config) => {
|
|||||||
inviteRoutes(route)
|
inviteRoutes(route)
|
||||||
noteRoutes(route)
|
noteRoutes(route)
|
||||||
notificationRoutes(route)
|
notificationRoutes(route)
|
||||||
orderRoutes(route)
|
orderRoutes(route, featureFlagRouter)
|
||||||
priceListRoutes(route)
|
priceListRoutes(route)
|
||||||
productRoutes(route)
|
productRoutes(route, featureFlagRouter)
|
||||||
productTagRoutes(route)
|
productTagRoutes(route)
|
||||||
productTypesRoutes(route)
|
productTypesRoutes(route)
|
||||||
regionRoutes(route)
|
regionRoutes(route)
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { defaultAdminOrdersRelations, defaultAdminOrdersFields } from "."
|
|
||||||
import { OrderService } from "../../../../services"
|
import { OrderService } from "../../../../services"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -26,10 +25,7 @@ export default async (req, res) => {
|
|||||||
|
|
||||||
const orderService: OrderService = req.scope.resolve("orderService")
|
const orderService: OrderService = req.scope.resolve("orderService")
|
||||||
|
|
||||||
const order = await orderService.retrieve(id, {
|
const order = await orderService.retrieve(id, req.retrieveConfig)
|
||||||
select: defaultAdminOrdersFields,
|
|
||||||
relations: defaultAdminOrdersRelations,
|
|
||||||
})
|
|
||||||
|
|
||||||
res.json({ order })
|
res.json({ order })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,36 @@
|
|||||||
import { Router } from "express"
|
import { Router } from "express"
|
||||||
import "reflect-metadata"
|
import "reflect-metadata"
|
||||||
import { Order } from "../../../.."
|
import { Order } from "../../../.."
|
||||||
import { DeleteResponse, PaginatedResponse } from "../../../../types/common"
|
import {
|
||||||
import middlewares from "../../../middlewares"
|
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()
|
const route = Router()
|
||||||
|
|
||||||
export default (app) => {
|
export default (app, featureFlagRouter: FlagRouter) => {
|
||||||
app.use("/orders", route)
|
app.use("/orders", route)
|
||||||
|
|
||||||
|
const relations = [...defaultAdminOrdersRelations]
|
||||||
|
if (featureFlagRouter.isFeatureEnabled("sales_channels")) {
|
||||||
|
relations.push("sales_channel")
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List orders
|
* List orders
|
||||||
*/
|
*/
|
||||||
route.get(
|
route.get(
|
||||||
"/",
|
"/",
|
||||||
middlewares.normalizeQuery(),
|
transformQuery(AdminGetOrdersParams, {
|
||||||
|
defaultRelations: relations,
|
||||||
|
defaultFields: defaultAdminOrdersFields,
|
||||||
|
allowedFields: allowedAdminOrdersFields,
|
||||||
|
isList: true,
|
||||||
|
}),
|
||||||
middlewares.wrap(require("./list-orders").default)
|
middlewares.wrap(require("./list-orders").default)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -23,7 +39,12 @@ export default (app) => {
|
|||||||
*/
|
*/
|
||||||
route.get(
|
route.get(
|
||||||
"/:id",
|
"/:id",
|
||||||
middlewares.normalizeQuery(),
|
transformQuery(EmptyQueryParams, {
|
||||||
|
defaultRelations: relations,
|
||||||
|
defaultFields: defaultAdminOrdersFields,
|
||||||
|
allowedFields: allowedAdminOrdersFields,
|
||||||
|
isList: false,
|
||||||
|
}),
|
||||||
middlewares.wrap(require("./get-order").default)
|
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 { IsNumber, IsOptional, IsString } from "class-validator"
|
||||||
import { omit, pick, pickBy } from "lodash"
|
import { pick } from "lodash"
|
||||||
import { OrderService } from "../../../../services"
|
import { OrderService } from "../../../../services"
|
||||||
import { AdminListOrdersSelector } from "../../../../types/orders"
|
import { AdminListOrdersSelector } from "../../../../types/orders"
|
||||||
import { Type } from "class-transformer"
|
import { Type } from "class-transformer"
|
||||||
@@ -47,51 +45,23 @@ import { Type } from "class-transformer"
|
|||||||
* $ref: "#/components/schemas/order"
|
* $ref: "#/components/schemas/order"
|
||||||
*/
|
*/
|
||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
const value = await validator(AdminGetOrdersParams, req.query)
|
|
||||||
|
|
||||||
const orderService: OrderService = req.scope.resolve("orderService")
|
const orderService: OrderService = req.scope.resolve("orderService")
|
||||||
|
|
||||||
let includeFields: string[] = []
|
const { skip, take, select, relations } = req.listConfig
|
||||||
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 [orders, count] = await orderService.listAndCount(
|
const [orders, count] = await orderService.listAndCount(
|
||||||
pickBy(filterableFields, (val) => typeof val !== "undefined"),
|
req.filterableFields,
|
||||||
listConfig
|
req.listConfig
|
||||||
)
|
)
|
||||||
|
|
||||||
let data = orders
|
let data = orders
|
||||||
|
|
||||||
const fields = [...includeFields, ...expandFields]
|
const fields = [...select, ...relations]
|
||||||
if (fields.length) {
|
if (fields.length) {
|
||||||
data = orders.map((o) => pick(o, fields))
|
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 {
|
export class AdminGetOrdersParams extends AdminListOrdersSelector {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { defaultAdminProductFields, defaultAdminProductRelations } from "."
|
|
||||||
import { ProductService, PricingService } from "../../../../services"
|
import { ProductService, PricingService } from "../../../../services"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -27,10 +26,7 @@ export default async (req, res) => {
|
|||||||
const productService: ProductService = req.scope.resolve("productService")
|
const productService: ProductService = req.scope.resolve("productService")
|
||||||
const pricingService: PricingService = req.scope.resolve("pricingService")
|
const pricingService: PricingService = req.scope.resolve("pricingService")
|
||||||
|
|
||||||
const rawProduct = await productService.retrieve(id, {
|
const rawProduct = await productService.retrieve(id, req.retrieveConfig)
|
||||||
select: defaultAdminProductFields,
|
|
||||||
relations: defaultAdminProductRelations,
|
|
||||||
})
|
|
||||||
|
|
||||||
const [product] = await pricingService.setProductPrices([rawProduct])
|
const [product] = await pricingService.setProductPrices([rawProduct])
|
||||||
|
|
||||||
|
|||||||
@@ -2,14 +2,21 @@ import { Router } from "express"
|
|||||||
import "reflect-metadata"
|
import "reflect-metadata"
|
||||||
import { PricedProduct } from "../../../../types/pricing"
|
import { PricedProduct } from "../../../../types/pricing"
|
||||||
import { Product, ProductTag, ProductType } from "../../../.."
|
import { Product, ProductTag, ProductType } from "../../../.."
|
||||||
import { PaginatedResponse } from "../../../../types/common"
|
import { EmptyQueryParams, PaginatedResponse } from "../../../../types/common"
|
||||||
import middlewares from "../../../middlewares"
|
import middlewares, { transformQuery } from "../../../middlewares"
|
||||||
|
import { AdminGetProductsParams } from "./list-products"
|
||||||
|
import { FlagRouter } from "../../../../utils/flag-router"
|
||||||
|
|
||||||
const route = Router()
|
const route = Router()
|
||||||
|
|
||||||
export default (app) => {
|
export default (app, featureFlagRouter: FlagRouter) => {
|
||||||
app.use("/products", route)
|
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("/", middlewares.wrap(require("./create-product").default))
|
||||||
route.post("/:id", middlewares.wrap(require("./update-product").default))
|
route.post("/:id", middlewares.wrap(require("./update-product").default))
|
||||||
route.get("/types", middlewares.wrap(require("./list-types").default))
|
route.get("/types", middlewares.wrap(require("./list-types").default))
|
||||||
@@ -53,11 +60,25 @@ export default (app) => {
|
|||||||
"/:id/metadata",
|
"/:id/metadata",
|
||||||
middlewares.wrap(require("./set-metadata").default)
|
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(
|
route.get(
|
||||||
"/",
|
"/",
|
||||||
middlewares.normalizeQuery(),
|
transformQuery(AdminGetProductsParams, {
|
||||||
|
defaultRelations: defaultAdminProductRelations,
|
||||||
|
defaultFields: defaultAdminProductFields,
|
||||||
|
allowedFields: allowedAdminProductFields,
|
||||||
|
isList: true,
|
||||||
|
}),
|
||||||
middlewares.wrap(require("./list-products").default)
|
middlewares.wrap(require("./list-products").default)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -141,6 +162,7 @@ export const allowedAdminProductRelations = [
|
|||||||
"tags",
|
"tags",
|
||||||
"type",
|
"type",
|
||||||
"collection",
|
"collection",
|
||||||
|
"sales_channels",
|
||||||
]
|
]
|
||||||
|
|
||||||
export type AdminProductsDeleteOptionRes = {
|
export type AdminProductsDeleteOptionRes = {
|
||||||
|
|||||||
@@ -8,17 +8,11 @@ import {
|
|||||||
IsString,
|
IsString,
|
||||||
ValidateNested,
|
ValidateNested,
|
||||||
} from "class-validator"
|
} from "class-validator"
|
||||||
import { omit } from "lodash"
|
|
||||||
import { Product, ProductStatus } from "../../../../models/product"
|
import { Product, ProductStatus } from "../../../../models/product"
|
||||||
import { DateComparisonOperator } from "../../../../types/common"
|
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 { optionalBooleanMapper } from "../../../../utils/validators/is-boolean"
|
||||||
|
import { PricedProduct } from "../../../../types/pricing"
|
||||||
|
import { PricingService, ProductService } from "../../../../services"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @oas [get] /products
|
* @oas [get] /products
|
||||||
@@ -69,32 +63,31 @@ import { optionalBooleanMapper } from "../../../../utils/validators/is-boolean"
|
|||||||
* $ref: "#/components/schemas/product"
|
* $ref: "#/components/schemas/product"
|
||||||
*/
|
*/
|
||||||
export default async (req, res) => {
|
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, [
|
const { skip, take, relations } = req.listConfig
|
||||||
"limit",
|
|
||||||
"offset",
|
|
||||||
"expand",
|
|
||||||
"fields",
|
|
||||||
"order",
|
|
||||||
])
|
|
||||||
|
|
||||||
const result = await listAndCount(
|
const [rawProducts, count] = await productService.listAndCount(
|
||||||
req.scope,
|
req.filterableFields,
|
||||||
filterableFields,
|
req.listConfig
|
||||||
{},
|
|
||||||
{
|
|
||||||
limit: validatedParams.limit ?? 50,
|
|
||||||
offset: validatedParams.offset ?? 0,
|
|
||||||
expand: validatedParams.expand,
|
|
||||||
fields: validatedParams.fields,
|
|
||||||
allowedFields: allowedAdminProductFields,
|
|
||||||
defaultFields: defaultAdminProductFields as (keyof Product)[],
|
|
||||||
defaultRelations: defaultAdminProductRelations,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
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 {
|
export class AdminGetProductsPaginationParams {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { defaultStoreCartFields, defaultStoreCartRelations } from "."
|
|
||||||
import { CartService } from "../../../../services"
|
import { CartService } from "../../../../services"
|
||||||
import { decorateLineItemsWithTotals } from "./decorate-line-items-with-totals"
|
import { decorateLineItemsWithTotals } from "./decorate-line-items-with-totals"
|
||||||
|
|
||||||
@@ -43,10 +42,7 @@ export default async (req, res) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cart = await cartService.retrieve(id, {
|
cart = await cartService.retrieve(id, req.retrieveConfig)
|
||||||
select: defaultStoreCartFields,
|
|
||||||
relations: defaultStoreCartRelations,
|
|
||||||
})
|
|
||||||
|
|
||||||
const data = await decorateLineItemsWithTotals(cart, req)
|
const data = await decorateLineItemsWithTotals(cart, req)
|
||||||
res.json({ cart: data })
|
res.json({ cart: data })
|
||||||
|
|||||||
@@ -1,22 +1,36 @@
|
|||||||
import { Router } from "express"
|
import { Router } from "express"
|
||||||
import "reflect-metadata"
|
import "reflect-metadata"
|
||||||
import { Cart, Order, Swap } from "../../../../"
|
import { Cart, Order, Swap } from "../../../../"
|
||||||
import { DeleteResponse } from "../../../../types/common"
|
import { DeleteResponse, EmptyQueryParams } from "../../../../types/common"
|
||||||
import middlewares from "../../../middlewares"
|
import middlewares, { transformQuery } from "../../../middlewares"
|
||||||
const route = Router()
|
const route = Router()
|
||||||
|
|
||||||
export default (app, container) => {
|
export default (app, container) => {
|
||||||
const middlewareService = container.resolve("middlewareService")
|
const middlewareService = container.resolve("middlewareService")
|
||||||
|
const featureFlagRouter = container.resolve("featureFlagRouter")
|
||||||
|
|
||||||
app.use("/carts", route)
|
app.use("/carts", route)
|
||||||
|
|
||||||
|
const relations = [...defaultStoreCartRelations]
|
||||||
|
if (featureFlagRouter.isFeatureEnabled("sales_channels")) {
|
||||||
|
relations.push("sales_channel")
|
||||||
|
}
|
||||||
|
|
||||||
// Inject plugin routes
|
// Inject plugin routes
|
||||||
const routers = middlewareService.getRouters("store/carts")
|
const routers = middlewareService.getRouters("store/carts")
|
||||||
for (const router of routers) {
|
for (const router of routers) {
|
||||||
route.use("/", router)
|
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(
|
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,
|
ManyToMany,
|
||||||
ManyToOne,
|
ManyToOne,
|
||||||
OneToMany,
|
OneToMany,
|
||||||
OneToOne,
|
|
||||||
} from "typeorm"
|
} from "typeorm"
|
||||||
import { DbAwareColumn } from "../utils/db-aware-column"
|
import { DbAwareColumn } from "../utils/db-aware-column"
|
||||||
import { Image } from "./image"
|
import { Image } from "./image"
|
||||||
@@ -21,10 +20,7 @@ import { ProductVariant } from "./product-variant"
|
|||||||
import { ShippingProfile } from "./shipping-profile"
|
import { ShippingProfile } from "./shipping-profile"
|
||||||
import { SoftDeletableEntity } from "../interfaces/models/soft-deletable-entity"
|
import { SoftDeletableEntity } from "../interfaces/models/soft-deletable-entity"
|
||||||
import { generateEntityId } from "../utils/generate-entity-id"
|
import { generateEntityId } from "../utils/generate-entity-id"
|
||||||
import {
|
import { FeatureFlagDecorators } from "../utils/feature-flag-decorators"
|
||||||
FeatureFlagColumn,
|
|
||||||
FeatureFlagDecorators,
|
|
||||||
} from "../utils/feature-flag-decorators"
|
|
||||||
import { SalesChannel } from "./sales-channel"
|
import { SalesChannel } from "./sales-channel"
|
||||||
|
|
||||||
export enum ProductStatus {
|
export enum ProductStatus {
|
||||||
|
|||||||
@@ -112,6 +112,8 @@ export type DeleteResponse = {
|
|||||||
deleted: boolean
|
deleted: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class EmptyQueryParams {}
|
||||||
|
|
||||||
export class DateComparisonOperator {
|
export class DateComparisonOperator {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsDate()
|
@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 { Column, ColumnOptions, Entity, EntityOptions } from "typeorm"
|
||||||
import featureFlagsLoader from "../loaders/feature-flags"
|
import featureFlagsLoader from "../loaders/feature-flags"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { ConfigModule } from "../types/global"
|
import { ConfigModule } from "../types/global"
|
||||||
import { FlagRouter } from "./flag-router"
|
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(
|
export function FeatureFlagColumn(
|
||||||
featureFlag: string,
|
featureFlag: string,
|
||||||
columnOptions: ColumnOptions = {}
|
columnOptions: ColumnOptions = {}
|
||||||
): PropertyDecorator {
|
): PropertyDecorator {
|
||||||
const featureFlagRouter = getFeatureFlagRouter()
|
return function (target, propertyName) {
|
||||||
|
setImmediate_((): any => {
|
||||||
|
const featureFlagRouter = getFeatureFlagRouter()
|
||||||
|
|
||||||
if (!featureFlagRouter.isFeatureEnabled(featureFlag)) {
|
if (!featureFlagRouter.isFeatureEnabled(featureFlag)) {
|
||||||
return (): void => {
|
return
|
||||||
// noop
|
}
|
||||||
}
|
|
||||||
|
Column(columnOptions)(target, propertyName)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return Column(columnOptions)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FeatureFlagDecorators(
|
export function FeatureFlagDecorators(
|
||||||
featureFlag: string,
|
featureFlag: string,
|
||||||
decorators: PropertyDecorator[]
|
decorators: PropertyDecorator[]
|
||||||
): PropertyDecorator {
|
): PropertyDecorator {
|
||||||
const featureFlagRouter = getFeatureFlagRouter()
|
return function (target, propertyName) {
|
||||||
|
setImmediate_((): any => {
|
||||||
|
const featureFlagRouter = getFeatureFlagRouter()
|
||||||
|
|
||||||
if (!featureFlagRouter.isFeatureEnabled(featureFlag)) {
|
if (!featureFlagRouter.isFeatureEnabled(featureFlag)) {
|
||||||
return (): void => {
|
return
|
||||||
// noop
|
}
|
||||||
}
|
|
||||||
}
|
decorators.forEach((decorator: PropertyDecorator) => {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
decorator(target, propertyName)
|
||||||
return (target: Object, propertyKey: string | symbol): void => {
|
})
|
||||||
decorators.forEach((decorator) => {
|
|
||||||
decorator(target, propertyKey)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -44,12 +63,10 @@ export function FeatureFlagEntity(
|
|||||||
name?: string,
|
name?: string,
|
||||||
options?: EntityOptions
|
options?: EntityOptions
|
||||||
): ClassDecorator {
|
): ClassDecorator {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
||||||
return function (target: Function): void {
|
return function (target: Function): void {
|
||||||
target["isFeatureEnabled"] = function (): boolean {
|
target["isFeatureEnabled"] = function (): boolean {
|
||||||
const featureFlagRouter = getFeatureFlagRouter()
|
const featureFlagRouter = getFeatureFlagRouter()
|
||||||
|
|
||||||
// const featureFlagRouter = featureFlagsLoader(configModule)
|
|
||||||
return featureFlagRouter.isFeatureEnabled(featureFlag)
|
return featureFlagRouter.isFeatureEnabled(featureFlag)
|
||||||
}
|
}
|
||||||
Entity(name, options)(target)
|
Entity(name, options)(target)
|
||||||
|
|||||||
@@ -27,14 +27,14 @@ export function getRetrieveConfig<TModel extends BaseEntity>(
|
|||||||
): FindConfig<TModel> {
|
): FindConfig<TModel> {
|
||||||
let includeFields: (keyof TModel)[] = []
|
let includeFields: (keyof TModel)[] = []
|
||||||
if (typeof fields !== "undefined") {
|
if (typeof fields !== "undefined") {
|
||||||
includeFields = Array
|
includeFields = Array.from(new Set([...fields, "id"])).map((field) =>
|
||||||
.from(new Set([...fields, "id"]))
|
typeof field === "string" ? field.trim() : field
|
||||||
.map(field => (typeof field === "string") ? field.trim() : field) as (keyof TModel)[]
|
) as (keyof TModel)[]
|
||||||
}
|
}
|
||||||
|
|
||||||
let expandFields: string[] = []
|
let expandFields: string[] = []
|
||||||
if (typeof expand !== "undefined") {
|
if (typeof expand !== "undefined") {
|
||||||
expandFields = expand.map(expandRelation => expandRelation.trim())
|
expandFields = expand.map((expandRelation) => expandRelation.trim())
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -106,7 +106,10 @@ export function prepareListQuery<
|
|||||||
orderBy = { [order]: "ASC" }
|
orderBy = { [order]: "ASC" }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (queryConfig?.allowedFields?.length && !queryConfig?.allowedFields.includes(orderField)) {
|
if (
|
||||||
|
queryConfig?.allowedFields?.length &&
|
||||||
|
!queryConfig?.allowedFields.includes(orderField)
|
||||||
|
) {
|
||||||
throw new MedusaError(
|
throw new MedusaError(
|
||||||
MedusaError.Types.INVALID_DATA,
|
MedusaError.Types.INVALID_DATA,
|
||||||
`Order field ${orderField} is not valid`
|
`Order field ${orderField} is not valid`
|
||||||
|
|||||||
Reference in New Issue
Block a user