From 110c995a6a0b7bfeead30aa2f9a6fd0dac180214 Mon Sep 17 00:00:00 2001 From: Adrien de Peretti Date: Tue, 19 Jul 2022 18:54:20 +0200 Subject: [PATCH] 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 --- .../api/__tests__/admin/sales-channels.js | 118 ++++++++++++++++-- .../api/factories/simple-order-factory.ts | 2 +- .../factories/simple-sales-channel-factory.ts | 21 +++- .../api/routes/admin/orders/list-orders.ts | 1 + .../price-lists/list-price-list-products.ts | 10 +- .../routes/admin/products/list-products.ts | 86 +------------ packages/medusa/src/models/product.ts | 2 +- packages/medusa/src/repositories/order.ts | 2 +- packages/medusa/src/repositories/product.ts | 14 +++ packages/medusa/src/types/orders.ts | 4 + packages/medusa/src/types/product.ts | 4 + 11 files changed, 168 insertions(+), 96 deletions(-) diff --git a/integration-tests/api/__tests__/admin/sales-channels.js b/integration-tests/api/__tests__/admin/sales-channels.js index aeb5ce77e2..8c40fdc192 100644 --- a/integration-tests/api/__tests__/admin/sales-channels.js +++ b/integration-tests/api/__tests__/admin/sales-channels.js @@ -12,6 +12,8 @@ const { simpleCartFactory, } = require("../../factories") const { simpleOrderFactory } = require("../../factories") +const orderSeeder = require("../../helpers/order-seeder"); +const productSeeder = require("../../helpers/product-seeder"); const startServerWithEnvironment = require("../../../helpers/start-server-with-environment").default @@ -664,17 +666,15 @@ describe("sales channels", () => { beforeEach(async() => { try { await adminSeeder(dbConnection) - salesChannel = await simpleSalesChannelFactory(dbConnection, { - name: "test name", - description: "test description", - }) product = await simpleProductFactory(dbConnection, { id: "product_1", title: "test title", }) - await dbConnection.manager.query(` - INSERT INTO product_sales_channel VALUES ('${product.id}', '${salesChannel.id}') - `) + salesChannel = await simpleSalesChannelFactory(dbConnection, { + name: "test name", + description: "test description", + products: [product] + }) } catch (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 + }), + ]) + ) + }) + }) + }) }) diff --git a/integration-tests/api/factories/simple-order-factory.ts b/integration-tests/api/factories/simple-order-factory.ts index 70869df543..4b8cc403fb 100644 --- a/integration-tests/api/factories/simple-order-factory.ts +++ b/integration-tests/api/factories/simple-order-factory.ts @@ -46,7 +46,7 @@ export type OrderFactoryData = { export const simpleOrderFactory = async ( connection: Connection, data: OrderFactoryData = {}, - seed: number + seed?: number ): Promise => { if (typeof seed !== "undefined") { faker.seed(seed) diff --git a/integration-tests/api/factories/simple-sales-channel-factory.ts b/integration-tests/api/factories/simple-sales-channel-factory.ts index d623f7f07b..cf7f0f88c3 100644 --- a/integration-tests/api/factories/simple-sales-channel-factory.ts +++ b/integration-tests/api/factories/simple-sales-channel-factory.ts @@ -1,4 +1,4 @@ -import { SalesChannel } from "@medusajs/medusa" +import { Product, SalesChannel } from "@medusajs/medusa" import faker from "faker" import { Connection } from "typeorm" @@ -7,6 +7,7 @@ export type SalesChannelFactoryData = { description?: string is_disabled?: boolean id?: string + products?: Product[], } export const simpleSalesChannelFactory = async ( @@ -20,7 +21,7 @@ export const simpleSalesChannelFactory = async ( const manager = connection.manager - const salesChannel = manager.create(SalesChannel, { + let salesChannel = manager.create(SalesChannel, { id: data.id ?? `simple-id-${Math.random() * 1000}`, name: data.name || faker.name.firstName(), description: data.description || faker.name.lastName(), @@ -28,5 +29,19 @@ export const simpleSalesChannelFactory = async ( 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 } diff --git a/packages/medusa/src/api/routes/admin/orders/list-orders.ts b/packages/medusa/src/api/routes/admin/orders/list-orders.ts index a8f25b8ff3..d63171006c 100644 --- a/packages/medusa/src/api/routes/admin/orders/list-orders.ts +++ b/packages/medusa/src/api/routes/admin/orders/list-orders.ts @@ -23,6 +23,7 @@ import { Type } from "class-transformer" * - (query) region_id {string} to search for. * - (query) currency_code {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) 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. diff --git a/packages/medusa/src/api/routes/admin/price-lists/list-price-list-products.ts b/packages/medusa/src/api/routes/admin/price-lists/list-price-list-products.ts index 85b2dc8807..b8db448e5c 100644 --- a/packages/medusa/src/api/routes/admin/price-lists/list-price-list-products.ts +++ b/packages/medusa/src/api/routes/admin/price-lists/list-price-list-products.ts @@ -9,9 +9,11 @@ import { ValidateNested, } from "class-validator" import { ProductStatus } from "../../../../models" -import { DateComparisonOperator } from "../../../../types/common" +import { + DateComparisonOperator, + extendedFindParamsMixin, +} from "../../../../types/common" import { FilterableProductProps } from "../../../../types/product" -import { AdminGetProductsPaginationParams } from "../products" import PriceListService from "../../../../services/price-list" 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() @IsOptional() id?: string diff --git a/packages/medusa/src/api/routes/admin/products/list-products.ts b/packages/medusa/src/api/routes/admin/products/list-products.ts index a65badea54..37ed39f70d 100644 --- a/packages/medusa/src/api/routes/admin/products/list-products.ts +++ b/packages/medusa/src/api/routes/admin/products/list-products.ts @@ -1,18 +1,9 @@ -import { Transform, Type } from "class-transformer" -import { - IsArray, - 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 { Type } from "class-transformer" +import { IsNumber, IsOptional, IsString } from "class-validator" +import { Product } from "../../../../models" import { PricedProduct } from "../../../../types/pricing" import { PricingService, ProductService } from "../../../../services" +import { FilterableProductProps } from "../../../../types/product" /** * @oas [get] /products @@ -32,6 +23,7 @@ import { PricingService, ProductService } from "../../../../services" * - (query) is_giftcard {string} Search for giftcards using is_giftcard=true. * - (query) type {string} to search for. * - (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) 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. @@ -90,7 +82,7 @@ export default async (req, res) => { }) } -export class AdminGetProductsPaginationParams { +export class AdminGetProductsParams extends FilterableProductProps { @IsNumber() @IsOptional() @Type(() => Number) @@ -109,69 +101,3 @@ export class AdminGetProductsPaginationParams { @IsOptional() 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 -} diff --git a/packages/medusa/src/models/product.ts b/packages/medusa/src/models/product.ts index c67d56290d..08414bfc07 100644 --- a/packages/medusa/src/models/product.ts +++ b/packages/medusa/src/models/product.ts @@ -146,7 +146,7 @@ export class Product extends SoftDeletableEntity { metadata: Record | null @FeatureFlagDecorators("sales_channels", [ - ManyToMany(() => SalesChannel, { cascade: true }), + ManyToMany(() => SalesChannel, { cascade: ["remove", "soft-remove"] }), JoinTable({ name: "product_sales_channel", joinColumn: { diff --git a/packages/medusa/src/repositories/order.ts b/packages/medusa/src/repositories/order.ts index ca0458602a..3d591ebd37 100644 --- a/packages/medusa/src/repositories/order.ts +++ b/packages/medusa/src/repositories/order.ts @@ -1,6 +1,6 @@ import { flatten, groupBy, map, merge } from "lodash" import { EntityRepository, FindManyOptions, Repository } from "typeorm" -import { Order } from "../models/order" +import { Order } from "../models" @EntityRepository(Order) export class OrderRepository extends Repository { diff --git a/packages/medusa/src/repositories/product.ts b/packages/medusa/src/repositories/product.ts index b7f039f516..81a313e249 100644 --- a/packages/medusa/src/repositories/product.ts +++ b/packages/medusa/src/repositories/product.ts @@ -13,6 +13,7 @@ import { Selector, WithRequiredProperty, } from "../types/common" +import { SalesChannel } from "../models"; export type ProductSelector = Omit, "tags"> & { tags: FindOperator @@ -26,6 +27,7 @@ export type DefaultWithoutRelations = Omit< export type FindWithoutRelationsOptions = DefaultWithoutRelations & { where: DefaultWithoutRelations["where"] & { price_list_id?: FindOperator + sales_channel_id?: FindOperator } } @@ -50,6 +52,9 @@ export class ProductRepository extends Repository { const price_lists = 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") .select(["product.id"]) .skip(optionsWithoutRelations.skip) @@ -88,6 +93,15 @@ export class ProductRepository extends Repository { }) } + 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) { qb.withDeleted() } diff --git a/packages/medusa/src/types/orders.ts b/packages/medusa/src/types/orders.ts index b433544d84..6556c33338 100644 --- a/packages/medusa/src/types/orders.ts +++ b/packages/medusa/src/types/orders.ts @@ -99,6 +99,10 @@ export class AdminListOrdersSelector { @IsOptional() tax_rate?: string + @IsArray() + @IsOptional() + sales_channel_id?: string[] + @IsOptional() @ValidateNested() @Type(() => DateComparisonOperator) diff --git a/packages/medusa/src/types/product.ts b/packages/medusa/src/types/product.ts index c9eab96276..ff051bf694 100644 --- a/packages/medusa/src/types/product.ts +++ b/packages/medusa/src/types/product.ts @@ -68,6 +68,10 @@ export class FilterableProductProps { @IsOptional() type?: string + @IsArray() + @IsOptional() + sales_channel_id?: string[] + @IsOptional() @ValidateNested() @Type(() => DateComparisonOperator)