From c717442451cf9fc2e0961edded5b49ea5a78760e Mon Sep 17 00:00:00 2001 From: Adrien de Peretti Date: Tue, 11 Oct 2022 16:14:10 +0200 Subject: [PATCH] feat(medusa): Allow to filter collections by discount condition id (#2411) --- .changeset/brown-cups-draw.md | 5 + .../api/__tests__/admin/colllections.js | 92 +++++++++++++++++++ .../store/__snapshots__/collections.js.snap | 12 +-- .../api/__tests__/store/collections.js | 4 +- .../admin/collections/list-collections.ts | 19 ++-- .../admin/product-types/list-product-types.ts | 3 +- .../collections/__tests__/list-collections.js | 15 +++ .../src/api/routes/store/collections/index.ts | 25 ++++- .../store/collections/list-collections.ts | 12 +-- .../src/repositories/product-collection.ts | 34 ++++++- .../medusa/src/services/product-collection.ts | 40 ++++---- 11 files changed, 210 insertions(+), 51 deletions(-) create mode 100644 .changeset/brown-cups-draw.md diff --git a/.changeset/brown-cups-draw.md b/.changeset/brown-cups-draw.md new file mode 100644 index 0000000000..b184caf1ff --- /dev/null +++ b/.changeset/brown-cups-draw.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa": patch +--- + +feat(medusa): Allow to filter collections by discount condition id diff --git a/integration-tests/api/__tests__/admin/colllections.js b/integration-tests/api/__tests__/admin/colllections.js index a3abda4f35..c650c163e8 100644 --- a/integration-tests/api/__tests__/admin/colllections.js +++ b/integration-tests/api/__tests__/admin/colllections.js @@ -5,8 +5,23 @@ const { initDb, useDb } = require("../../../helpers/use-db") const productSeeder = require("../../helpers/product-seeder") const adminSeeder = require("../../helpers/admin-seeder") +const { + DiscountRuleType, + AllocationType, + DiscountConditionType, + DiscountConditionOperator, +} = require("@medusajs/medusa") +const { IdMap } = require("medusa-test-utils") +const { simpleDiscountFactory } = require("../../factories") jest.setTimeout(30000) + +const adminReqConfig = { + headers: { + Authorization: "Bearer test_token", + }, +} + describe("/admin/collections", () => { let medusaProcess let dbConnection @@ -334,5 +349,82 @@ describe("/admin/collections", () => { offset: 0, }) }) + + it("returns a list of collections filtered by discount condition id", async () => { + const api = useApi() + + const resCollections = await api.get("/admin/collections", adminReqConfig) + + const collection1 = resCollections.data.collections[0] + const collection2 = resCollections.data.collections[1] + + const buildDiscountData = (code, conditionId, collections) => { + return { + code, + rule: { + type: DiscountRuleType.PERCENTAGE, + value: 10, + allocation: AllocationType.TOTAL, + conditions: [ + { + id: conditionId, + type: DiscountConditionType.PRODUCT_COLLECTIONS, + operator: DiscountConditionOperator.IN, + product_collections: collections, + }, + ], + }, + } + } + + const discountConditionId = IdMap.getId("discount-condition-type-1") + await simpleDiscountFactory( + dbConnection, + buildDiscountData("code-1", discountConditionId, [collection1.id]) + ) + + const discountConditionId2 = IdMap.getId("discount-condition-type-2") + await simpleDiscountFactory( + dbConnection, + buildDiscountData("code-2", discountConditionId2, [collection2.id]) + ) + + let res = await api.get( + `/admin/collections?discount_condition_id=${discountConditionId}`, + adminReqConfig + ) + + expect(res.status).toEqual(200) + expect(res.data.collections).toHaveLength(1) + expect(res.data.collections).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: collection1.id }), + ]) + ) + + res = await api.get( + `/admin/collections?discount_condition_id=${discountConditionId2}`, + adminReqConfig + ) + + expect(res.status).toEqual(200) + expect(res.data.collections).toHaveLength(1) + expect(res.data.collections).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: collection2.id }), + ]) + ) + + res = await api.get(`/admin/collections`, adminReqConfig) + + expect(res.status).toEqual(200) + expect(res.data.collections).toHaveLength(3) + expect(res.data.collections).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: collection1.id }), + expect.objectContaining({ id: collection2.id }), + ]) + ) + }) }) }) diff --git a/integration-tests/api/__tests__/store/__snapshots__/collections.js.snap b/integration-tests/api/__tests__/store/__snapshots__/collections.js.snap index 9edcdb7e6e..95526ab005 100644 --- a/integration-tests/api/__tests__/store/__snapshots__/collections.js.snap +++ b/integration-tests/api/__tests__/store/__snapshots__/collections.js.snap @@ -6,10 +6,10 @@ Object { Object { "created_at": Any, "deleted_at": null, - "handle": "test-collection", - "id": "test-collection", + "handle": "test-collection2", + "id": "test-collection2", "metadata": null, - "title": "Test collection", + "title": "Test collection 2", "updated_at": Any, }, Object { @@ -24,10 +24,10 @@ Object { Object { "created_at": Any, "deleted_at": null, - "handle": "test-collection2", - "id": "test-collection2", + "handle": "test-collection", + "id": "test-collection", "metadata": null, - "title": "Test collection 2", + "title": "Test collection", "updated_at": Any, }, ], diff --git a/integration-tests/api/__tests__/store/collections.js b/integration-tests/api/__tests__/store/collections.js index a89d1a05ce..a630f6aca7 100644 --- a/integration-tests/api/__tests__/store/collections.js +++ b/integration-tests/api/__tests__/store/collections.js @@ -66,7 +66,7 @@ describe("/store/collections", () => { expect(response.data).toMatchSnapshot({ collections: [ { - id: "test-collection", + id: "test-collection2", created_at: expect.any(String), updated_at: expect.any(String), }, @@ -76,7 +76,7 @@ describe("/store/collections", () => { updated_at: expect.any(String), }, { - id: "test-collection2", + id: "test-collection", created_at: expect.any(String), updated_at: expect.any(String), }, diff --git a/packages/medusa/src/api/routes/admin/collections/list-collections.ts b/packages/medusa/src/api/routes/admin/collections/list-collections.ts index a443c7a0a7..630b4c4b3f 100644 --- a/packages/medusa/src/api/routes/admin/collections/list-collections.ts +++ b/packages/medusa/src/api/routes/admin/collections/list-collections.ts @@ -1,6 +1,5 @@ import { IsNumber, IsOptional, IsString, ValidateNested } from "class-validator" import { Request, Response } from "express" -import _, { identity } from "lodash" import { DateComparisonOperator } from "../../../../types/common" import ProductCollectionService from "../../../../services/product-collection" @@ -18,6 +17,7 @@ import { Type } from "class-transformer" * - (query) title {string} The title of collections to return. * - (query) handle {string} The handle of collections to return. * - (query) q {string} a search term to search titles and handles. + * - (query) discount_condition_id {string} The discount condition id on which to filter the product collections. * - in: query * name: created_at * description: Date comparison for when resulting collections were created. @@ -143,22 +143,19 @@ export default async (req: Request, res: Response) => { "productCollectionService" ) - const { - validatedQuery: { limit, offset }, - filterableFields, - listConfig, - } = req + const { filterableFields, listConfig } = req + const { skip, take } = listConfig const [collections, count] = await productCollectionService.listAndCount( - _.pickBy(filterableFields, identity), + filterableFields, listConfig ) res.status(200).json({ collections, count, - offset, - limit, + offset: skip, + limit: take, }) } @@ -202,4 +199,8 @@ export class AdminGetCollectionsParams extends AdminGetCollectionsPaginationPara @IsString() @IsOptional() q?: string + + @IsString() + @IsOptional() + discount_condition_id?: string } diff --git a/packages/medusa/src/api/routes/admin/product-types/list-product-types.ts b/packages/medusa/src/api/routes/admin/product-types/list-product-types.ts index aa5f58dbe6..462c5a6552 100644 --- a/packages/medusa/src/api/routes/admin/product-types/list-product-types.ts +++ b/packages/medusa/src/api/routes/admin/product-types/list-product-types.ts @@ -3,7 +3,6 @@ import { StringComparisonOperator, } from "../../../../types/common" import { IsNumber, IsOptional, IsString } from "class-validator" -import { identity, pickBy } from "lodash" import { IsType } from "../../../../utils/validators/is-type" import ProductTypeService from "../../../../services/product-type" @@ -143,7 +142,7 @@ export default async (req, res) => { const { skip, take } = req.listConfig const [types, count] = await typeService.listAndCount( - pickBy(filterableFields, identity), + filterableFields, listConfig ) diff --git a/packages/medusa/src/api/routes/store/collections/__tests__/list-collections.js b/packages/medusa/src/api/routes/store/collections/__tests__/list-collections.js index 0c12d90559..f275838bf8 100644 --- a/packages/medusa/src/api/routes/store/collections/__tests__/list-collections.js +++ b/packages/medusa/src/api/routes/store/collections/__tests__/list-collections.js @@ -36,6 +36,11 @@ describe("GET /store/collections", () => { expect(ProductCollectionServiceMock.listAndCount).toHaveBeenCalledWith( {}, { + order: { + created_at: "DESC", + }, + relations: [], + select: undefined, skip: 0, take: 5, } @@ -60,6 +65,11 @@ describe("GET /store/collections", () => { expect(ProductCollectionServiceMock.listAndCount).toHaveBeenCalledWith( {}, { + order: { + created_at: "DESC", + }, + relations: [], + select: undefined, skip: 10, take: 10, } @@ -84,6 +94,11 @@ describe("GET /store/collections", () => { expect(ProductCollectionServiceMock.listAndCount).toHaveBeenCalledWith( {}, { + order: { + created_at: "DESC", + }, + relations: [], + select: undefined, skip: 10, take: 20, } diff --git a/packages/medusa/src/api/routes/store/collections/index.ts b/packages/medusa/src/api/routes/store/collections/index.ts index 19eb93eee7..4ff9526514 100644 --- a/packages/medusa/src/api/routes/store/collections/index.ts +++ b/packages/medusa/src/api/routes/store/collections/index.ts @@ -1,21 +1,38 @@ import { Router } from "express" -import { PaginatedResponse } from "./../../../../types/common" +import { PaginatedResponse } from "../../../../types/common" import { ProductCollection } from "../../../../" -import middlewares from "../../../middlewares" +import middlewares, { transformQuery } from "../../../middlewares" +import { StoreGetCollectionsParams } from "./list-collections" const route = Router() export default (app) => { app.use("/collections", route) - route.get("/", middlewares.wrap(require("./list-collections").default)) + route.get( + "/", + transformQuery(StoreGetCollectionsParams, { + allowedFields, + isList: true, + }), + middlewares.wrap(require("./list-collections").default) + ) route.get("/:id", middlewares.wrap(require("./get-collection").default)) return app } -export const defaultStoreCollectionFields = ["id", "title", "handle"] export const defaultStoreCollectionRelations = ["products"] +export const allowedFields = [ + "id", + "title", + "handle", + "metadata", + "created_at", + "updated_at", + "deleted_at", + ...defaultStoreCollectionRelations, +] export type StoreCollectionsListRes = PaginatedResponse & { collections: ProductCollection[] diff --git a/packages/medusa/src/api/routes/store/collections/list-collections.ts b/packages/medusa/src/api/routes/store/collections/list-collections.ts index 35bca814b0..37d6550798 100644 --- a/packages/medusa/src/api/routes/store/collections/list-collections.ts +++ b/packages/medusa/src/api/routes/store/collections/list-collections.ts @@ -3,7 +3,6 @@ import { IsInt, IsOptional, ValidateNested } from "class-validator" import { DateComparisonOperator } from "../../../../types/common" import ProductCollectionService from "../../../../services/product-collection" import { Type } from "class-transformer" -import { validator } from "../../../../utils/validator" /** * @oas [get] /collections @@ -105,24 +104,19 @@ import { validator } from "../../../../utils/validator" * $ref: "#/components/responses/500_error" */ export default async (req, res) => { - const validated = await validator(StoreGetCollectionsParams, req.query) - const { limit, offset, ...filterableFields } = validated - const productCollectionService: ProductCollectionService = req.scope.resolve( "productCollectionService" ) - const listConfig = { - skip: offset, - take: limit, - } + const { listConfig, filterableFields } = req + const { skip, take } = req.listConfig const [collections, count] = await productCollectionService.listAndCount( filterableFields, listConfig ) - res.status(200).json({ collections, count, limit, offset }) + res.status(200).json({ collections, count, limit: take, offset: skip }) } export class StoreGetCollectionsParams { diff --git a/packages/medusa/src/repositories/product-collection.ts b/packages/medusa/src/repositories/product-collection.ts index 19eb663d00..2493500797 100644 --- a/packages/medusa/src/repositories/product-collection.ts +++ b/packages/medusa/src/repositories/product-collection.ts @@ -1,6 +1,36 @@ import { EntityRepository, Repository } from "typeorm" -import { ProductCollection } from "../models/product-collection" +import { ProductCollection } from "../models" +import { ExtendedFindConfig } from "../types/common" @EntityRepository(ProductCollection) // eslint-disable-next-line max-len -export class ProductCollectionRepository extends Repository {} +export class ProductCollectionRepository extends Repository { + async findAndCountByDiscountConditionId( + conditionId: string, + query: ExtendedFindConfig> + ): Promise<[ProductCollection[], number]> { + const qb = this.createQueryBuilder("pc") + + if (query?.select) { + qb.select(query.select.map((select) => `pc.${select}`)) + } + + if (query.skip) { + qb.skip(query.skip) + } + + if (query.take) { + qb.take(query.take) + } + + return await qb + .where(query.where) + .innerJoin( + "discount_condition_product_collection", + "dc_pc", + `dc_pc.product_collection_id = pc.id AND dc_pc.condition_id = :dcId`, + { dcId: conditionId } + ) + .getManyAndCount() + } +} diff --git a/packages/medusa/src/services/product-collection.ts b/packages/medusa/src/services/product-collection.ts index 24e71f9210..081f5d3e72 100644 --- a/packages/medusa/src/services/product-collection.ts +++ b/packages/medusa/src/services/product-collection.ts @@ -4,12 +4,12 @@ import { TransactionBaseService } from "../interfaces" import { ProductCollection } from "../models" import { ProductRepository } from "../repositories/product" import { ProductCollectionRepository } from "../repositories/product-collection" -import { ExtendedFindConfig, FindConfig, QuerySelector } from "../types/common" +import { FindConfig } from "../types/common" import { CreateProductCollection, UpdateProductCollection, } from "../types/product-collection" -import { buildQuery, setMetadata } from "../utils" +import { buildQuery, isString, setMetadata } from "../utils" import { formatException } from "../utils/exception-formatter" import EventBusService from "./event-bus" @@ -219,15 +219,14 @@ class ProductCollectionService extends TransactionBaseService { * @return the result of the find operation */ async list( - selector = {}, + selector: Partial & { + q?: string + discount_condition_id?: string + } = {}, config = { skip: 0, take: 20 } ): Promise { - const productCollectionRepo = this.manager_.getCustomRepository( - this.productCollectionRepository_ - ) - - const query = buildQuery(selector, config) - return await productCollectionRepo.find(query) + const [collections] = await this.listAndCount(selector, config) + return collections } /** @@ -237,7 +236,10 @@ class ProductCollectionService extends TransactionBaseService { * @return the result of the find operation */ async listAndCount( - selector: QuerySelector = {}, + selector: Partial & { + q?: string + discount_condition_id?: string + } = {}, config: FindConfig = { skip: 0, take: 20 } ): Promise<[ProductCollection[], number]> { const productCollectionRepo = this.manager_.getCustomRepository( @@ -245,17 +247,12 @@ class ProductCollectionService extends TransactionBaseService { ) let q - if ("q" in selector) { + if (isString(selector.q)) { q = selector.q delete selector.q } - const query = buildQuery( - selector, - config - ) as ExtendedFindConfig & { - where: (qb: any) => void - } + const query = buildQuery(selector, config) if (q) { const where = query.where @@ -278,6 +275,15 @@ class ProductCollectionService extends TransactionBaseService { } } + if (query.where.discount_condition_id) { + const discountConditionId = query.where.discount_condition_id as string + delete query.where.discount_condition_id + return await productCollectionRepo.findAndCountByDiscountConditionId( + discountConditionId, + query + ) + } + return await productCollectionRepo.findAndCount(query) } }