From 19ca18e71c8feea7277e09db3c5e9e6316adb6ab Mon Sep 17 00:00:00 2001 From: Adrien de Peretti Date: Tue, 11 Oct 2022 08:36:08 +0200 Subject: [PATCH] feat(medusa): Allow to query product types by discount condition id (#2359) --- .changeset/lucky-worms-turn.md | 5 + .../admin/__snapshots__/product-type.js.snap | 12 +-- .../api/__tests__/admin/product-type.js | 100 ++++++++++++++++-- .../api/routes/admin/product-types/index.ts | 14 ++- .../admin/product-types/list-product-types.ts | 51 ++------- .../medusa/src/repositories/product-type.ts | 33 +++++- packages/medusa/src/services/product-type.ts | 42 ++++---- packages/medusa/src/types/common.ts | 1 + packages/medusa/src/types/product.ts | 31 +----- packages/medusa/src/utils/index.ts | 1 + packages/medusa/src/utils/is-string.ts | 3 + 11 files changed, 186 insertions(+), 107 deletions(-) create mode 100644 .changeset/lucky-worms-turn.md create mode 100644 packages/medusa/src/utils/is-string.ts diff --git a/.changeset/lucky-worms-turn.md b/.changeset/lucky-worms-turn.md new file mode 100644 index 0000000000..38c2fcc1ee --- /dev/null +++ b/.changeset/lucky-worms-turn.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa": patch +--- + +feat(medusa): Allow to query product types by discount condition id diff --git a/integration-tests/api/__tests__/admin/__snapshots__/product-type.js.snap b/integration-tests/api/__tests__/admin/__snapshots__/product-type.js.snap index a2b42a03c1..cfb4a20199 100644 --- a/integration-tests/api/__tests__/admin/__snapshots__/product-type.js.snap +++ b/integration-tests/api/__tests__/admin/__snapshots__/product-type.js.snap @@ -2,18 +2,18 @@ exports[`/admin/product-types GET /admin/product-types returns a list of product types 1`] = ` Array [ - Object { - "created_at": Any, - "id": "test-type", - "updated_at": Any, - "value": "test-type", - }, Object { "created_at": Any, "id": "test-type-new", "updated_at": Any, "value": "test-type-new", }, + Object { + "created_at": Any, + "id": "test-type", + "updated_at": Any, + "value": "test-type", + }, ] `; diff --git a/integration-tests/api/__tests__/admin/product-type.js b/integration-tests/api/__tests__/admin/product-type.js index 1bf57e717d..1fca3ce9dd 100644 --- a/integration-tests/api/__tests__/admin/product-type.js +++ b/integration-tests/api/__tests__/admin/product-type.js @@ -1,14 +1,29 @@ const path = require("path") +const { IdMap } = require("medusa-test-utils") + const setupServer = require("../../../helpers/setup-server") const { useApi } = require("../../../helpers/use-api") const { initDb, useDb } = require("../../../helpers/use-db") const adminSeeder = require("../../helpers/admin-seeder") const productSeeder = require("../../helpers/product-seeder") +const { + DiscountRuleType, + AllocationType, + DiscountConditionType, + DiscountConditionOperator, +} = require("@medusajs/medusa") +const { simpleDiscountFactory } = require("../../factories") jest.setTimeout(50000) +const adminReqConfig = { + headers: { + Authorization: "Bearer test_token", + }, +} + describe("/admin/product-types", () => { let medusaProcess let dbConnection @@ -41,11 +56,7 @@ describe("/admin/product-types", () => { const api = useApi() const res = await api - .get("/admin/product-types", { - headers: { - Authorization: "Bearer test_token", - }, - }) + .get("/admin/product-types", adminReqConfig) .catch((err) => { console.log(err) }) @@ -64,11 +75,7 @@ describe("/admin/product-types", () => { const api = useApi() const res = await api - .get("/admin/product-types?q=test-type-new", { - headers: { - Authorization: "Bearer test_token", - }, - }) + .get("/admin/product-types?q=test-type-new", adminReqConfig) .catch((err) => { console.log(err) }) @@ -88,5 +95,78 @@ describe("/admin/product-types", () => { // Should only return one type as there is only one match to the search param expect(res.data.product_types).toMatchSnapshot([typeMatch]) }) + + it("returns a list of product type filtered by discount condition id", async () => { + const api = useApi() + + const resTypes = await api.get("/admin/product-types", adminReqConfig) + + const type1 = resTypes.data.product_types[0] + const type2 = resTypes.data.product_types[1] + + const buildDiscountData = (code, conditionId, types) => { + return { + code, + rule: { + type: DiscountRuleType.PERCENTAGE, + value: 10, + allocation: AllocationType.TOTAL, + conditions: [ + { + id: conditionId, + type: DiscountConditionType.PRODUCT_TYPES, + operator: DiscountConditionOperator.IN, + product_types: types, + }, + ], + }, + } + } + + const discountConditionId = IdMap.getId("discount-condition-type-1") + await simpleDiscountFactory( + dbConnection, + buildDiscountData("code-1", discountConditionId, [type1.id]) + ) + + const discountConditionId2 = IdMap.getId("discount-condition-type-2") + await simpleDiscountFactory( + dbConnection, + buildDiscountData("code-2", discountConditionId2, [type2.id]) + ) + + let res = await api.get( + `/admin/product-types?discount_condition_id=${discountConditionId}`, + adminReqConfig + ) + + expect(res.status).toEqual(200) + expect(res.data.product_types).toHaveLength(1) + expect(res.data.product_types).toEqual( + expect.arrayContaining([expect.objectContaining({ id: type1.id })]) + ) + + res = await api.get( + `/admin/product-types?discount_condition_id=${discountConditionId2}`, + adminReqConfig + ) + + expect(res.status).toEqual(200) + expect(res.data.product_types).toHaveLength(1) + expect(res.data.product_types).toEqual( + expect.arrayContaining([expect.objectContaining({ id: type2.id })]) + ) + + res = await api.get(`/admin/product-types`, adminReqConfig) + + expect(res.status).toEqual(200) + expect(res.data.product_types).toHaveLength(2) + expect(res.data.product_types).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: type1.id }), + expect.objectContaining({ id: type2.id }), + ]) + ) + }) }) }) diff --git a/packages/medusa/src/api/routes/admin/product-types/index.ts b/packages/medusa/src/api/routes/admin/product-types/index.ts index 1f35e7e886..c6d99db56e 100644 --- a/packages/medusa/src/api/routes/admin/product-types/index.ts +++ b/packages/medusa/src/api/routes/admin/product-types/index.ts @@ -1,15 +1,25 @@ import { Router } from "express" import { ProductType } from "../../../.." import { PaginatedResponse } from "../../../../types/common" -import middlewares from "../../../middlewares" +import middlewares, { transformQuery } from "../../../middlewares" import "reflect-metadata" +import { AdminGetProductTypesParams } from "./list-product-types" const route = Router() export default (app) => { app.use("/product-types", route) - route.get("/", middlewares.wrap(require("./list-product-types").default)) + route.get( + "/", + transformQuery(AdminGetProductTypesParams, { + defaultFields: defaultAdminProductTypeFields, + defaultRelations: defaultAdminProductTypeRelations, + allowedFields: allowedAdminProductTypeFields, + isList: true, + }), + middlewares.wrap(require("./list-product-types").default) + ) return app } 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 c9381cbd69..aa5f58dbe6 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 @@ -1,23 +1,13 @@ import { DateComparisonOperator, - FindConfig, StringComparisonOperator, } from "../../../../types/common" import { IsNumber, IsOptional, IsString } from "class-validator" -import { - allowedAdminProductTypeFields, - defaultAdminProductTypeFields, - defaultAdminProductTypeRelations, -} from "." -import { identity, omit, pickBy } from "lodash" +import { identity, pickBy } from "lodash" import { IsType } from "../../../../utils/validators/is-type" -import { MedusaError } from "medusa-core-utils" -import { ProductType } from "../../../../models/product-type" import ProductTypeService from "../../../../services/product-type" import { Type } from "class-transformer" -import { validator } from "../../../../utils/validator" -import { isDefined } from "../../../../utils" /** * @oas [get] /product-types @@ -29,6 +19,7 @@ import { isDefined } from "../../../../utils" * - (query) limit=10 {integer} The number of types to return. * - (query) offset=0 {integer} The number of items to skip before the results. * - (query) order {string} The field to sort items by. + * - (query) discount_condition_id {string} The discount condition id on which to filter the product types. * - in: query * name: value * style: form @@ -145,37 +136,11 @@ import { isDefined } from "../../../../utils" * $ref: "#/components/responses/500_error" */ export default async (req, res) => { - const validated = await validator(AdminGetProductTypesParams, req.query) - const typeService: ProductTypeService = req.scope.resolve("productTypeService") - const listConfig: FindConfig = { - select: defaultAdminProductTypeFields as (keyof ProductType)[], - relations: defaultAdminProductTypeRelations, - skip: validated.offset, - take: validated.limit, - } - - if (isDefined(validated.order)) { - let orderField = validated.order - if (validated.order.startsWith("-")) { - const [, field] = validated.order.split("-") - orderField = field - listConfig.order = { [field]: "DESC" } - } else { - listConfig.order = { [validated.order]: "ASC" } - } - - if (!allowedAdminProductTypeFields.includes(orderField)) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "Order field must be a valid product type field" - ) - } - } - - const filterableFields = omit(validated, ["limit", "offset"]) + const { listConfig, filterableFields } = req + const { skip, take } = req.listConfig const [types, count] = await typeService.listAndCount( pickBy(filterableFields, identity), @@ -185,8 +150,8 @@ export default async (req, res) => { res.status(200).json({ product_types: types, count, - offset: validated.offset, - limit: validated.limit, + offset: skip, + limit: take, }) } @@ -227,4 +192,8 @@ export class AdminGetProductTypesParams extends AdminGetProductTypesPaginationPa @IsString() @IsOptional() order?: string + + @IsString() + @IsOptional() + discount_condition_id?: string } diff --git a/packages/medusa/src/repositories/product-type.ts b/packages/medusa/src/repositories/product-type.ts index eb1e9391ea..14178b4710 100644 --- a/packages/medusa/src/repositories/product-type.ts +++ b/packages/medusa/src/repositories/product-type.ts @@ -1,9 +1,11 @@ import { EntityRepository, Repository } from "typeorm" import { ProductType } from "../models/product-type" +import { ExtendedFindConfig } from "../types/common" type UpsertTypeInput = Partial & { value: string } + @EntityRepository(ProductType) export class ProductTypeRepository extends Repository { async upsertType(type?: UpsertTypeInput): Promise { @@ -22,8 +24,35 @@ export class ProductTypeRepository extends Repository { const created = this.create({ value: type.value, }) - const result = await this.save(created) + return await this.save(created) + } - return result + async findAndCountByDiscountConditionId( + conditionId: string, + query: ExtendedFindConfig> + ): Promise<[ProductType[], number]> { + const qb = this.createQueryBuilder("pt") + + if (query?.select) { + qb.select(query.select.map((select) => `pt.${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_type", + "dc_pt", + `dc_pt.product_type_id = pt.id AND dc_pt.condition_id = :dcId`, + { dcId: conditionId } + ) + .getManyAndCount() } } diff --git a/packages/medusa/src/services/product-type.ts b/packages/medusa/src/services/product-type.ts index 111a1bbb67..7137cdf754 100644 --- a/packages/medusa/src/services/product-type.ts +++ b/packages/medusa/src/services/product-type.ts @@ -1,11 +1,10 @@ import { MedusaError } from "medusa-core-utils" -import { EntityManager, ILike, SelectQueryBuilder } from "typeorm" -import { ProductType } from "../models/product-type" +import { EntityManager, ILike } from "typeorm" +import { ProductType } from "../models" import { ProductTypeRepository } from "../repositories/product-type" import { FindConfig } from "../types/common" -import { FilterableProductTypeProps } from "../types/product" import { TransactionBaseService } from "../interfaces" -import { buildQuery } from "../utils" +import { buildQuery, isString } from "../utils" class ProductTypeService extends TransactionBaseService { protected manager_: EntityManager @@ -54,13 +53,14 @@ class ProductTypeService extends TransactionBaseService { * @return the result of the find operation */ async list( - selector: FilterableProductTypeProps = {}, + selector: Partial & { + q?: string + discount_condition_id?: string + } = {}, config: FindConfig = { skip: 0, take: 20 } ): Promise { - const typeRepo = this.manager_.getCustomRepository(this.typeRepository_) - - const query = buildQuery(selector, config) - return await typeRepo.find(query) + const [productTypes] = await this.listAndCount(selector, config) + return productTypes } /** @@ -70,13 +70,16 @@ class ProductTypeService extends TransactionBaseService { * @return the result of the find operation */ async listAndCount( - selector: FilterableProductTypeProps = {}, + selector: Partial & { + q?: string + discount_condition_id?: string + } = {}, config: FindConfig = { skip: 0, take: 20 } ): Promise<[ProductType[], number]> { const typeRepo = this.manager_.getCustomRepository(this.typeRepository_) - let q: string | undefined = undefined - if ("q" in selector) { + let q + if (isString(selector.q)) { q = selector.q delete selector.q } @@ -84,13 +87,16 @@ class ProductTypeService extends TransactionBaseService { const query = buildQuery(selector, config) if (q) { - const where = query.where + query.where.value = ILike(`%${q}%`) + } - delete where.value - - query.where = (qb: SelectQueryBuilder): void => { - qb.where(where).andWhere([{ value: ILike(`%${q}%`) }]) - } + if (query.where.discount_condition_id) { + const discountConditionId = query.where.discount_condition_id as string + delete query.where.discount_condition_id + return await typeRepo.findAndCountByDiscountConditionId( + discountConditionId, + query + ) } return await typeRepo.findAndCount(query) diff --git a/packages/medusa/src/types/common.ts b/packages/medusa/src/types/common.ts index e6b9e85e88..8b6d15eab7 100644 --- a/packages/medusa/src/types/common.ts +++ b/packages/medusa/src/types/common.ts @@ -32,6 +32,7 @@ export type PartialPick = { export type Writable = { -readonly [key in keyof T]: | T[key] + | FindOperator | FindOperator | FindOperator } diff --git a/packages/medusa/src/types/product.ts b/packages/medusa/src/types/product.ts index 308d1894cf..3fcd2f517e 100644 --- a/packages/medusa/src/types/product.ts +++ b/packages/medusa/src/types/product.ts @@ -5,7 +5,7 @@ import { IsEnum, IsOptional, IsString, - ValidateNested + ValidateNested, } from "class-validator" import SalesChannelFeatureFlag from "../loaders/feature-flags/sales-channels" import { Product, ProductOptionValue, ProductStatus } from "../models" @@ -15,7 +15,7 @@ import { IsType } from "../utils/validators/is-type" import { DateComparisonOperator, FindConfig, - StringComparisonOperator + StringComparisonOperator, } from "./common" import { PriceListLoadConfig } from "./price-list" @@ -68,10 +68,7 @@ export class FilterableProductProps { @IsOptional() type?: string - @FeatureFlagDecorators(SalesChannelFeatureFlag.key, [ - IsOptional(), - IsArray(), - ]) + @FeatureFlagDecorators(SalesChannelFeatureFlag.key, [IsOptional(), IsArray()]) sales_channel_id?: string[] @IsOptional() @@ -112,28 +109,6 @@ export class FilterableProductTagProps { q?: string } -export class FilterableProductTypeProps { - @IsOptional() - @IsType([String, [String], StringComparisonOperator]) - id?: string | string[] | StringComparisonOperator - - @IsOptional() - @IsType([String, [String], StringComparisonOperator]) - value?: string | string[] | StringComparisonOperator - - @IsOptional() - @IsType([DateComparisonOperator]) - created_at?: DateComparisonOperator - - @IsOptional() - @IsType([DateComparisonOperator]) - updated_at?: DateComparisonOperator - - @IsString() - @IsOptional() - q?: string -} - /** * Service Level DTOs */ diff --git a/packages/medusa/src/utils/index.ts b/packages/medusa/src/utils/index.ts index 0b72f70c69..f4ae33fe78 100644 --- a/packages/medusa/src/utils/index.ts +++ b/packages/medusa/src/utils/index.ts @@ -4,6 +4,7 @@ export * from "./validate-id" export * from "./generate-entity-id" export * from "./remove-undefined-properties" export * from "./is-defined" +export * from "./is-string" export * from "./calculate-price-tax-amount" export * from "./csv-cell-content-formatter" export * from "./exception-formatter" diff --git a/packages/medusa/src/utils/is-string.ts b/packages/medusa/src/utils/is-string.ts new file mode 100644 index 0000000000..7529d697ad --- /dev/null +++ b/packages/medusa/src/utils/is-string.ts @@ -0,0 +1,3 @@ +export function isString(val: any): val is string { + return val != null && typeof val === "string" +}