feat(medusa): Allow to query order/product by SC (#1867)
**What** Allow to query the products and orders by sales channels **How** Updating the existing end points and repositories (if necessary) to take a new query params that is sales_channel_id as an array of string **Tests** Add new integration tests Fixes CORE-295 Fixes CORE-288
This commit is contained in:
@@ -12,6 +12,8 @@ const {
|
|||||||
simpleCartFactory,
|
simpleCartFactory,
|
||||||
} = require("../../factories")
|
} = require("../../factories")
|
||||||
const { simpleOrderFactory } = require("../../factories")
|
const { simpleOrderFactory } = require("../../factories")
|
||||||
|
const orderSeeder = require("../../helpers/order-seeder");
|
||||||
|
const productSeeder = require("../../helpers/product-seeder");
|
||||||
|
|
||||||
const startServerWithEnvironment =
|
const startServerWithEnvironment =
|
||||||
require("../../../helpers/start-server-with-environment").default
|
require("../../../helpers/start-server-with-environment").default
|
||||||
@@ -664,17 +666,15 @@ describe("sales channels", () => {
|
|||||||
beforeEach(async() => {
|
beforeEach(async() => {
|
||||||
try {
|
try {
|
||||||
await adminSeeder(dbConnection)
|
await adminSeeder(dbConnection)
|
||||||
salesChannel = await simpleSalesChannelFactory(dbConnection, {
|
|
||||||
name: "test name",
|
|
||||||
description: "test description",
|
|
||||||
})
|
|
||||||
product = await simpleProductFactory(dbConnection, {
|
product = await simpleProductFactory(dbConnection, {
|
||||||
id: "product_1",
|
id: "product_1",
|
||||||
title: "test title",
|
title: "test title",
|
||||||
})
|
})
|
||||||
await dbConnection.manager.query(`
|
salesChannel = await simpleSalesChannelFactory(dbConnection, {
|
||||||
INSERT INTO product_sales_channel VALUES ('${product.id}', '${salesChannel.id}')
|
name: "test name",
|
||||||
`)
|
description: "test description",
|
||||||
|
products: [product]
|
||||||
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
}
|
}
|
||||||
@@ -811,4 +811,108 @@ describe("sales channels", () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("/admin/orders using sales channels", () => {
|
||||||
|
describe("GET /admin/orders", () => {
|
||||||
|
let order
|
||||||
|
|
||||||
|
beforeEach(async() => {
|
||||||
|
try {
|
||||||
|
await adminSeeder(dbConnection)
|
||||||
|
order = await simpleOrderFactory(dbConnection, {
|
||||||
|
sales_channel: {
|
||||||
|
name: "test name",
|
||||||
|
description: "test description",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await orderSeeder(dbConnection)
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async() => {
|
||||||
|
const db = useDb()
|
||||||
|
await db.teardown()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should successfully lists orders that belongs to the requested sales channels", async() => {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
const response = await api.get(
|
||||||
|
`/admin/orders?sales_channel_id[]=${order.sales_channel_id}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
authorization: "Bearer test_token",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
expect(response.data.orders.length).toEqual(1)
|
||||||
|
expect(response.data.orders).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: order.id,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("/admin/products using sales channels", () => {
|
||||||
|
describe("GET /admin/products", () => {
|
||||||
|
const productData = {
|
||||||
|
id: "product-sales-channel-1",
|
||||||
|
title: "test description",
|
||||||
|
}
|
||||||
|
let salesChannel
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
try {
|
||||||
|
await productSeeder(dbConnection)
|
||||||
|
await adminSeeder(dbConnection)
|
||||||
|
const product = await simpleProductFactory(dbConnection, productData)
|
||||||
|
salesChannel = await simpleSalesChannelFactory(dbConnection, {
|
||||||
|
name: "test name",
|
||||||
|
description: "test description",
|
||||||
|
products: [product]
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async() => {
|
||||||
|
const db = useDb()
|
||||||
|
await db.teardown()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should returns a list of products that belongs to the requested sales channels", async() => {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
const response = await api
|
||||||
|
.get(`/admin/products?sales_channel_id[]=${salesChannel.id}`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer test_token",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
expect(response.data.products.length).toEqual(1)
|
||||||
|
expect(response.data.products).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: productData.id,
|
||||||
|
title: productData.title
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export type OrderFactoryData = {
|
|||||||
export const simpleOrderFactory = async (
|
export const simpleOrderFactory = async (
|
||||||
connection: Connection,
|
connection: Connection,
|
||||||
data: OrderFactoryData = {},
|
data: OrderFactoryData = {},
|
||||||
seed: number
|
seed?: number
|
||||||
): Promise<Order> => {
|
): Promise<Order> => {
|
||||||
if (typeof seed !== "undefined") {
|
if (typeof seed !== "undefined") {
|
||||||
faker.seed(seed)
|
faker.seed(seed)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { SalesChannel } from "@medusajs/medusa"
|
import { Product, SalesChannel } from "@medusajs/medusa"
|
||||||
import faker from "faker"
|
import faker from "faker"
|
||||||
import { Connection } from "typeorm"
|
import { Connection } from "typeorm"
|
||||||
|
|
||||||
@@ -7,6 +7,7 @@ export type SalesChannelFactoryData = {
|
|||||||
description?: string
|
description?: string
|
||||||
is_disabled?: boolean
|
is_disabled?: boolean
|
||||||
id?: string
|
id?: string
|
||||||
|
products?: Product[],
|
||||||
}
|
}
|
||||||
|
|
||||||
export const simpleSalesChannelFactory = async (
|
export const simpleSalesChannelFactory = async (
|
||||||
@@ -20,7 +21,7 @@ export const simpleSalesChannelFactory = async (
|
|||||||
|
|
||||||
const manager = connection.manager
|
const manager = connection.manager
|
||||||
|
|
||||||
const salesChannel = manager.create(SalesChannel, {
|
let salesChannel = manager.create(SalesChannel, {
|
||||||
id: data.id ?? `simple-id-${Math.random() * 1000}`,
|
id: data.id ?? `simple-id-${Math.random() * 1000}`,
|
||||||
name: data.name || faker.name.firstName(),
|
name: data.name || faker.name.firstName(),
|
||||||
description: data.description || faker.name.lastName(),
|
description: data.description || faker.name.lastName(),
|
||||||
@@ -28,5 +29,19 @@ export const simpleSalesChannelFactory = async (
|
|||||||
typeof data.is_disabled !== undefined ? data.is_disabled : false,
|
typeof data.is_disabled !== undefined ? data.is_disabled : false,
|
||||||
})
|
})
|
||||||
|
|
||||||
return await manager.save(salesChannel)
|
salesChannel = await manager.save(salesChannel)
|
||||||
|
|
||||||
|
if (data.products) {
|
||||||
|
const promises = []
|
||||||
|
for (const product of data.products) {
|
||||||
|
promises.push(
|
||||||
|
manager.query(`
|
||||||
|
INSERT INTO product_sales_channel (product_id, sales_channel_id) VALUES ('${product.id}', '${salesChannel.id}');
|
||||||
|
`)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
await Promise.all(promises)
|
||||||
|
}
|
||||||
|
|
||||||
|
return salesChannel
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { Type } from "class-transformer"
|
|||||||
* - (query) region_id {string} to search for.
|
* - (query) region_id {string} to search for.
|
||||||
* - (query) currency_code {string} to search for.
|
* - (query) currency_code {string} to search for.
|
||||||
* - (query) tax_rate {string} to search for.
|
* - (query) tax_rate {string} to search for.
|
||||||
|
* - (query) sales_chanel_id {string[]} to retrieve products in.
|
||||||
* - (query) cancelled_at {DateComparisonOperator} Date comparison for when resulting orders was cancelled, i.e. less than, greater than etc.
|
* - (query) cancelled_at {DateComparisonOperator} Date comparison for when resulting orders was cancelled, i.e. less than, greater than etc.
|
||||||
* - (query) created_at {DateComparisonOperator} Date comparison for when resulting orders was created, i.e. less than, greater than etc.
|
* - (query) created_at {DateComparisonOperator} Date comparison for when resulting orders was created, i.e. less than, greater than etc.
|
||||||
* - (query) updated_at {DateComparisonOperator} Date comparison for when resulting orders was updated, i.e. less than, greater than etc.
|
* - (query) updated_at {DateComparisonOperator} Date comparison for when resulting orders was updated, i.e. less than, greater than etc.
|
||||||
|
|||||||
@@ -9,9 +9,11 @@ import {
|
|||||||
ValidateNested,
|
ValidateNested,
|
||||||
} from "class-validator"
|
} from "class-validator"
|
||||||
import { ProductStatus } from "../../../../models"
|
import { ProductStatus } from "../../../../models"
|
||||||
import { DateComparisonOperator } from "../../../../types/common"
|
import {
|
||||||
|
DateComparisonOperator,
|
||||||
|
extendedFindParamsMixin,
|
||||||
|
} from "../../../../types/common"
|
||||||
import { FilterableProductProps } from "../../../../types/product"
|
import { FilterableProductProps } from "../../../../types/product"
|
||||||
import { AdminGetProductsPaginationParams } from "../products"
|
|
||||||
import PriceListService from "../../../../services/price-list"
|
import PriceListService from "../../../../services/price-list"
|
||||||
import { Request } from "express"
|
import { Request } from "express"
|
||||||
|
|
||||||
@@ -89,7 +91,9 @@ export default async (req: Request, res) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AdminGetPriceListsPriceListProductsParams extends AdminGetProductsPaginationParams {
|
export class AdminGetPriceListsPriceListProductsParams extends extendedFindParamsMixin(
|
||||||
|
{ limit: 50 }
|
||||||
|
) {
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
id?: string
|
id?: string
|
||||||
|
|||||||
@@ -1,18 +1,9 @@
|
|||||||
import { Transform, Type } from "class-transformer"
|
import { Type } from "class-transformer"
|
||||||
import {
|
import { IsNumber, IsOptional, IsString } from "class-validator"
|
||||||
IsArray,
|
import { Product } from "../../../../models"
|
||||||
IsBoolean,
|
|
||||||
IsEnum,
|
|
||||||
IsNumber,
|
|
||||||
IsOptional,
|
|
||||||
IsString,
|
|
||||||
ValidateNested,
|
|
||||||
} from "class-validator"
|
|
||||||
import { Product, ProductStatus } from "../../../../models/product"
|
|
||||||
import { DateComparisonOperator } from "../../../../types/common"
|
|
||||||
import { optionalBooleanMapper } from "../../../../utils/validators/is-boolean"
|
|
||||||
import { PricedProduct } from "../../../../types/pricing"
|
import { PricedProduct } from "../../../../types/pricing"
|
||||||
import { PricingService, ProductService } from "../../../../services"
|
import { PricingService, ProductService } from "../../../../services"
|
||||||
|
import { FilterableProductProps } from "../../../../types/product"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @oas [get] /products
|
* @oas [get] /products
|
||||||
@@ -32,6 +23,7 @@ import { PricingService, ProductService } from "../../../../services"
|
|||||||
* - (query) is_giftcard {string} Search for giftcards using is_giftcard=true.
|
* - (query) is_giftcard {string} Search for giftcards using is_giftcard=true.
|
||||||
* - (query) type {string} to search for.
|
* - (query) type {string} to search for.
|
||||||
* - (query) order {string} to retrieve products in.
|
* - (query) order {string} to retrieve products in.
|
||||||
|
* - (query) sales_chanel_id {string[]} to retrieve products in.
|
||||||
* - (query) deleted_at {DateComparisonOperator} Date comparison for when resulting products was deleted, i.e. less than, greater than etc.
|
* - (query) deleted_at {DateComparisonOperator} Date comparison for when resulting products was deleted, i.e. less than, greater than etc.
|
||||||
* - (query) created_at {DateComparisonOperator} Date comparison for when resulting products was created, i.e. less than, greater than etc.
|
* - (query) created_at {DateComparisonOperator} Date comparison for when resulting products was created, i.e. less than, greater than etc.
|
||||||
* - (query) updated_at {DateComparisonOperator} Date comparison for when resulting products was updated, i.e. less than, greater than etc.
|
* - (query) updated_at {DateComparisonOperator} Date comparison for when resulting products was updated, i.e. less than, greater than etc.
|
||||||
@@ -90,7 +82,7 @@ export default async (req, res) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AdminGetProductsPaginationParams {
|
export class AdminGetProductsParams extends FilterableProductProps {
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@Type(() => Number)
|
@Type(() => Number)
|
||||||
@@ -109,69 +101,3 @@ export class AdminGetProductsPaginationParams {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
fields?: string
|
fields?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AdminGetProductsParams extends AdminGetProductsPaginationParams {
|
|
||||||
@IsString()
|
|
||||||
@IsOptional()
|
|
||||||
id?: string
|
|
||||||
|
|
||||||
@IsString()
|
|
||||||
@IsOptional()
|
|
||||||
q?: string
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsEnum(ProductStatus, { each: true })
|
|
||||||
status?: ProductStatus[]
|
|
||||||
|
|
||||||
@IsArray()
|
|
||||||
@IsOptional()
|
|
||||||
collection_id?: string[]
|
|
||||||
|
|
||||||
@IsArray()
|
|
||||||
@IsOptional()
|
|
||||||
tags?: string[]
|
|
||||||
|
|
||||||
@IsArray()
|
|
||||||
@IsOptional()
|
|
||||||
price_list_id?: string[]
|
|
||||||
|
|
||||||
@IsString()
|
|
||||||
@IsOptional()
|
|
||||||
title?: string
|
|
||||||
|
|
||||||
@IsString()
|
|
||||||
@IsOptional()
|
|
||||||
description?: string
|
|
||||||
|
|
||||||
@IsString()
|
|
||||||
@IsOptional()
|
|
||||||
handle?: string
|
|
||||||
|
|
||||||
@IsBoolean()
|
|
||||||
@IsOptional()
|
|
||||||
@Transform(({ value }) => optionalBooleanMapper.get(value.toLowerCase()))
|
|
||||||
is_giftcard?: boolean
|
|
||||||
|
|
||||||
@IsString()
|
|
||||||
@IsOptional()
|
|
||||||
type?: string
|
|
||||||
|
|
||||||
@IsString()
|
|
||||||
@IsOptional()
|
|
||||||
order?: string
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@ValidateNested()
|
|
||||||
@Type(() => DateComparisonOperator)
|
|
||||||
created_at?: DateComparisonOperator
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@ValidateNested()
|
|
||||||
@Type(() => DateComparisonOperator)
|
|
||||||
updated_at?: DateComparisonOperator
|
|
||||||
|
|
||||||
@ValidateNested()
|
|
||||||
@IsOptional()
|
|
||||||
@Type(() => DateComparisonOperator)
|
|
||||||
deleted_at?: DateComparisonOperator
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ export class Product extends SoftDeletableEntity {
|
|||||||
metadata: Record<string, unknown> | null
|
metadata: Record<string, unknown> | null
|
||||||
|
|
||||||
@FeatureFlagDecorators("sales_channels", [
|
@FeatureFlagDecorators("sales_channels", [
|
||||||
ManyToMany(() => SalesChannel, { cascade: true }),
|
ManyToMany(() => SalesChannel, { cascade: ["remove", "soft-remove"] }),
|
||||||
JoinTable({
|
JoinTable({
|
||||||
name: "product_sales_channel",
|
name: "product_sales_channel",
|
||||||
joinColumn: {
|
joinColumn: {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { flatten, groupBy, map, merge } from "lodash"
|
import { flatten, groupBy, map, merge } from "lodash"
|
||||||
import { EntityRepository, FindManyOptions, Repository } from "typeorm"
|
import { EntityRepository, FindManyOptions, Repository } from "typeorm"
|
||||||
import { Order } from "../models/order"
|
import { Order } from "../models"
|
||||||
|
|
||||||
@EntityRepository(Order)
|
@EntityRepository(Order)
|
||||||
export class OrderRepository extends Repository<Order> {
|
export class OrderRepository extends Repository<Order> {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
Selector,
|
Selector,
|
||||||
WithRequiredProperty,
|
WithRequiredProperty,
|
||||||
} from "../types/common"
|
} from "../types/common"
|
||||||
|
import { SalesChannel } from "../models";
|
||||||
|
|
||||||
export type ProductSelector = Omit<Selector<Product>, "tags"> & {
|
export type ProductSelector = Omit<Selector<Product>, "tags"> & {
|
||||||
tags: FindOperator<string[]>
|
tags: FindOperator<string[]>
|
||||||
@@ -26,6 +27,7 @@ export type DefaultWithoutRelations = Omit<
|
|||||||
export type FindWithoutRelationsOptions = DefaultWithoutRelations & {
|
export type FindWithoutRelationsOptions = DefaultWithoutRelations & {
|
||||||
where: DefaultWithoutRelations["where"] & {
|
where: DefaultWithoutRelations["where"] & {
|
||||||
price_list_id?: FindOperator<PriceList>
|
price_list_id?: FindOperator<PriceList>
|
||||||
|
sales_channel_id?: FindOperator<SalesChannel>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,6 +52,9 @@ export class ProductRepository extends Repository<Product> {
|
|||||||
const price_lists = optionsWithoutRelations?.where?.price_list_id
|
const price_lists = optionsWithoutRelations?.where?.price_list_id
|
||||||
delete optionsWithoutRelations?.where?.price_list_id
|
delete optionsWithoutRelations?.where?.price_list_id
|
||||||
|
|
||||||
|
const sales_channels = optionsWithoutRelations?.where?.sales_channel_id
|
||||||
|
delete optionsWithoutRelations?.where?.sales_channel_id
|
||||||
|
|
||||||
const qb = this.createQueryBuilder("product")
|
const qb = this.createQueryBuilder("product")
|
||||||
.select(["product.id"])
|
.select(["product.id"])
|
||||||
.skip(optionsWithoutRelations.skip)
|
.skip(optionsWithoutRelations.skip)
|
||||||
@@ -88,6 +93,15 @@ export class ProductRepository extends Repository<Product> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (sales_channels) {
|
||||||
|
qb.innerJoin(
|
||||||
|
"product.sales_channels",
|
||||||
|
"sales_channels",
|
||||||
|
"sales_channels.id IN (:...sales_channels_ids)",
|
||||||
|
{ sales_channels_ids: sales_channels.value }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (optionsWithoutRelations.withDeleted) {
|
if (optionsWithoutRelations.withDeleted) {
|
||||||
qb.withDeleted()
|
qb.withDeleted()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,6 +99,10 @@ export class AdminListOrdersSelector {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
tax_rate?: string
|
tax_rate?: string
|
||||||
|
|
||||||
|
@IsArray()
|
||||||
|
@IsOptional()
|
||||||
|
sales_channel_id?: string[]
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@ValidateNested()
|
@ValidateNested()
|
||||||
@Type(() => DateComparisonOperator)
|
@Type(() => DateComparisonOperator)
|
||||||
|
|||||||
@@ -68,6 +68,10 @@ export class FilterableProductProps {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
type?: string
|
type?: string
|
||||||
|
|
||||||
|
@IsArray()
|
||||||
|
@IsOptional()
|
||||||
|
sales_channel_id?: string[]
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@ValidateNested()
|
@ValidateNested()
|
||||||
@Type(() => DateComparisonOperator)
|
@Type(() => DateComparisonOperator)
|
||||||
|
|||||||
Reference in New Issue
Block a user