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:
Adrien de Peretti
2022-07-19 16:54:20 +00:00
committed by GitHub
parent 0e5f0d8cd6
commit 110c995a6a
11 changed files with 168 additions and 96 deletions
@@ -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
}
+1 -1
View File
@@ -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 -1
View File
@@ -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()
} }
+4
View File
@@ -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)
+4
View File
@@ -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)