feat(medusa): Allow to query product types by discount condition id (#2359)

This commit is contained in:
Adrien de Peretti authored and GitHub committed 2022-10-11 08:36:08 +02:00
1 parent 35df4962f8
commit 19ca18e71c
11 files changed
+186 -107

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
---
"@medusajs/medusa": patch
---
feat(medusa): Allow to query product types by discount condition id
@@ -2,18 +2,18 @@
exports[`/admin/product-types GET /admin/product-types returns a list of product types 1`] = ` exports[`/admin/product-types GET /admin/product-types returns a list of product types 1`] = `
Array [ Array [
Object {
"created_at": Any<String>,
"id": "test-type",
"updated_at": Any<String>,
"value": "test-type",
},
Object { Object {
"created_at": Any<String>, "created_at": Any<String>,
"id": "test-type-new", "id": "test-type-new",
"updated_at": Any<String>, "updated_at": Any<String>,
"value": "test-type-new", "value": "test-type-new",
}, },
Object {
"created_at": Any<String>,
"id": "test-type",
"updated_at": Any<String>,
"value": "test-type",
},
] ]
`; `;
@@ -1,14 +1,29 @@
const path = require("path") const path = require("path")
const { IdMap } = require("medusa-test-utils")
const setupServer = require("../../../helpers/setup-server") const setupServer = require("../../../helpers/setup-server")
const { useApi } = require("../../../helpers/use-api") const { useApi } = require("../../../helpers/use-api")
const { initDb, useDb } = require("../../../helpers/use-db") const { initDb, useDb } = require("../../../helpers/use-db")
const adminSeeder = require("../../helpers/admin-seeder") const adminSeeder = require("../../helpers/admin-seeder")
const productSeeder = require("../../helpers/product-seeder") const productSeeder = require("../../helpers/product-seeder")
const {
DiscountRuleType,
AllocationType,
DiscountConditionType,
DiscountConditionOperator,
} = require("@medusajs/medusa")
const { simpleDiscountFactory } = require("../../factories")
jest.setTimeout(50000) jest.setTimeout(50000)
const adminReqConfig = {
headers: {
Authorization: "Bearer test_token",
},
}
describe("/admin/product-types", () => { describe("/admin/product-types", () => {
let medusaProcess let medusaProcess
let dbConnection let dbConnection
@@ -41,11 +56,7 @@ describe("/admin/product-types", () => {
const api = useApi() const api = useApi()
const res = await api const res = await api
.get("/admin/product-types", { .get("/admin/product-types", adminReqConfig)
headers: {
Authorization: "Bearer test_token",
},
})
.catch((err) => { .catch((err) => {
console.log(err) console.log(err)
}) })
@@ -64,11 +75,7 @@ describe("/admin/product-types", () => {
const api = useApi() const api = useApi()
const res = await api const res = await api
.get("/admin/product-types?q=test-type-new", { .get("/admin/product-types?q=test-type-new", adminReqConfig)
headers: {
Authorization: "Bearer test_token",
},
})
.catch((err) => { .catch((err) => {
console.log(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 // Should only return one type as there is only one match to the search param
expect(res.data.product_types).toMatchSnapshot([typeMatch]) 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 }),
])
)
})
}) })
}) })
@@ -1,15 +1,25 @@
import { Router } from "express" import { Router } from "express"
import { ProductType } from "../../../.." import { ProductType } from "../../../.."
import { PaginatedResponse } from "../../../../types/common" import { PaginatedResponse } from "../../../../types/common"
import middlewares from "../../../middlewares" import middlewares, { transformQuery } from "../../../middlewares"
import "reflect-metadata" import "reflect-metadata"
import { AdminGetProductTypesParams } from "./list-product-types"
const route = Router() const route = Router()
export default (app) => { export default (app) => {
app.use("/product-types", route) 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 return app
} }
@@ -1,23 +1,13 @@
import { import {
DateComparisonOperator, DateComparisonOperator,
FindConfig,
StringComparisonOperator, StringComparisonOperator,
} from "../../../../types/common" } from "../../../../types/common"
import { IsNumber, IsOptional, IsString } from "class-validator" import { IsNumber, IsOptional, IsString } from "class-validator"
import { import { identity, pickBy } from "lodash"
allowedAdminProductTypeFields,
defaultAdminProductTypeFields,
defaultAdminProductTypeRelations,
} from "."
import { identity, omit, pickBy } from "lodash"
import { IsType } from "../../../../utils/validators/is-type" 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 ProductTypeService from "../../../../services/product-type"
import { Type } from "class-transformer" import { Type } from "class-transformer"
import { validator } from "../../../../utils/validator"
import { isDefined } from "../../../../utils"
/** /**
* @oas [get] /product-types * @oas [get] /product-types
@@ -29,6 +19,7 @@ import { isDefined } from "../../../../utils"
* - (query) limit=10 {integer} The number of types to return. * - (query) limit=10 {integer} The number of types to return.
* - (query) offset=0 {integer} The number of items to skip before the results. * - (query) offset=0 {integer} The number of items to skip before the results.
* - (query) order {string} The field to sort items by. * - (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 * - in: query
* name: value * name: value
* style: form * style: form
@@ -145,37 +136,11 @@ import { isDefined } from "../../../../utils"
* $ref: "#/components/responses/500_error" * $ref: "#/components/responses/500_error"
*/ */
export default async (req, res) => { export default async (req, res) => {
const validated = await validator(AdminGetProductTypesParams, req.query)
const typeService: ProductTypeService = const typeService: ProductTypeService =
req.scope.resolve("productTypeService") req.scope.resolve("productTypeService")
const listConfig: FindConfig<ProductType> = { const { listConfig, filterableFields } = req
select: defaultAdminProductTypeFields as (keyof ProductType)[], const { skip, take } = req.listConfig
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 [types, count] = await typeService.listAndCount( const [types, count] = await typeService.listAndCount(
pickBy(filterableFields, identity), pickBy(filterableFields, identity),
@@ -185,8 +150,8 @@ export default async (req, res) => {
res.status(200).json({ res.status(200).json({
product_types: types, product_types: types,
count, count,
offset: validated.offset, offset: skip,
limit: validated.limit, limit: take,
}) })
} }
@@ -227,4 +192,8 @@ export class AdminGetProductTypesParams extends AdminGetProductTypesPaginationPa
@IsString() @IsString()
@IsOptional() @IsOptional()
order?: string order?: string
@IsString()
@IsOptional()
discount_condition_id?: string
} }
@@ -1,9 +1,11 @@
import { EntityRepository, Repository } from "typeorm" import { EntityRepository, Repository } from "typeorm"
import { ProductType } from "../models/product-type" import { ProductType } from "../models/product-type"
import { ExtendedFindConfig } from "../types/common"
type UpsertTypeInput = Partial<ProductType> & { type UpsertTypeInput = Partial<ProductType> & {
value: string value: string
} }
@EntityRepository(ProductType) @EntityRepository(ProductType)
export class ProductTypeRepository extends Repository<ProductType> { export class ProductTypeRepository extends Repository<ProductType> {
async upsertType(type?: UpsertTypeInput): Promise<ProductType | null> { async upsertType(type?: UpsertTypeInput): Promise<ProductType | null> {
@@ -22,8 +24,35 @@ export class ProductTypeRepository extends Repository<ProductType> {
const created = this.create({ const created = this.create({
value: type.value, value: type.value,
}) })
const result = await this.save(created) return await this.save(created)
}
return result async findAndCountByDiscountConditionId(
conditionId: string,
query: ExtendedFindConfig<ProductType, Partial<ProductType>>
): 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()
} }
} }
+24 -18
View File
@@ -1,11 +1,10 @@
import { MedusaError } from "medusa-core-utils" import { MedusaError } from "medusa-core-utils"
import { EntityManager, ILike, SelectQueryBuilder } from "typeorm" import { EntityManager, ILike } from "typeorm"
import { ProductType } from "../models/product-type" import { ProductType } from "../models"
import { ProductTypeRepository } from "../repositories/product-type" import { ProductTypeRepository } from "../repositories/product-type"
import { FindConfig } from "../types/common" import { FindConfig } from "../types/common"
import { FilterableProductTypeProps } from "../types/product"
import { TransactionBaseService } from "../interfaces" import { TransactionBaseService } from "../interfaces"
import { buildQuery } from "../utils" import { buildQuery, isString } from "../utils"
class ProductTypeService extends TransactionBaseService { class ProductTypeService extends TransactionBaseService {
protected manager_: EntityManager protected manager_: EntityManager
@@ -54,13 +53,14 @@ class ProductTypeService extends TransactionBaseService {
* @return the result of the find operation * @return the result of the find operation
*/ */
async list( async list(
selector: FilterableProductTypeProps = {}, selector: Partial<ProductType> & {
q?: string
discount_condition_id?: string
} = {},
config: FindConfig<ProductType> = { skip: 0, take: 20 } config: FindConfig<ProductType> = { skip: 0, take: 20 }
): Promise<ProductType[]> { ): Promise<ProductType[]> {
const typeRepo = this.manager_.getCustomRepository(this.typeRepository_) const [productTypes] = await this.listAndCount(selector, config)
return productTypes
const query = buildQuery(selector, config)
return await typeRepo.find(query)
} }
/** /**
@@ -70,13 +70,16 @@ class ProductTypeService extends TransactionBaseService {
* @return the result of the find operation * @return the result of the find operation
*/ */
async listAndCount( async listAndCount(
selector: FilterableProductTypeProps = {}, selector: Partial<ProductType> & {
q?: string
discount_condition_id?: string
} = {},
config: FindConfig<ProductType> = { skip: 0, take: 20 } config: FindConfig<ProductType> = { skip: 0, take: 20 }
): Promise<[ProductType[], number]> { ): Promise<[ProductType[], number]> {
const typeRepo = this.manager_.getCustomRepository(this.typeRepository_) const typeRepo = this.manager_.getCustomRepository(this.typeRepository_)
let q: string | undefined = undefined let q
if ("q" in selector) { if (isString(selector.q)) {
q = selector.q q = selector.q
delete selector.q delete selector.q
} }
@@ -84,13 +87,16 @@ class ProductTypeService extends TransactionBaseService {
const query = buildQuery(selector, config) const query = buildQuery(selector, config)
if (q) { if (q) {
const where = query.where query.where.value = ILike(`%${q}%`)
}
delete where.value if (query.where.discount_condition_id) {
const discountConditionId = query.where.discount_condition_id as string
query.where = (qb: SelectQueryBuilder<ProductType>): void => { delete query.where.discount_condition_id
qb.where(where).andWhere([{ value: ILike(`%${q}%`) }]) return await typeRepo.findAndCountByDiscountConditionId(
} discountConditionId,
query
)
} }
return await typeRepo.findAndCount(query) return await typeRepo.findAndCount(query)
+1
View File
@@ -32,6 +32,7 @@ export type PartialPick<T, K extends keyof T> = {
export type Writable<T> = { export type Writable<T> = {
-readonly [key in keyof T]: -readonly [key in keyof T]:
| T[key] | T[key]
| FindOperator<T[key]>
| FindOperator<T[key][]> | FindOperator<T[key][]>
| FindOperator<string[]> | FindOperator<string[]>
} }
+3 -28
View File
@@ -5,7 +5,7 @@ import {
IsEnum, IsEnum,
IsOptional, IsOptional,
IsString, IsString,
ValidateNested ValidateNested,
} from "class-validator" } from "class-validator"
import SalesChannelFeatureFlag from "../loaders/feature-flags/sales-channels" import SalesChannelFeatureFlag from "../loaders/feature-flags/sales-channels"
import { Product, ProductOptionValue, ProductStatus } from "../models" import { Product, ProductOptionValue, ProductStatus } from "../models"
@@ -15,7 +15,7 @@ import { IsType } from "../utils/validators/is-type"
import { import {
DateComparisonOperator, DateComparisonOperator,
FindConfig, FindConfig,
StringComparisonOperator StringComparisonOperator,
} from "./common" } from "./common"
import { PriceListLoadConfig } from "./price-list" import { PriceListLoadConfig } from "./price-list"
@@ -68,10 +68,7 @@ export class FilterableProductProps {
@IsOptional() @IsOptional()
type?: string type?: string
@FeatureFlagDecorators(SalesChannelFeatureFlag.key, [ @FeatureFlagDecorators(SalesChannelFeatureFlag.key, [IsOptional(), IsArray()])
IsOptional(),
IsArray(),
])
sales_channel_id?: string[] sales_channel_id?: string[]
@IsOptional() @IsOptional()
@@ -112,28 +109,6 @@ export class FilterableProductTagProps {
q?: string 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 * Service Level DTOs
*/ */
+1
View File
@@ -4,6 +4,7 @@ export * from "./validate-id"
export * from "./generate-entity-id" export * from "./generate-entity-id"
export * from "./remove-undefined-properties" export * from "./remove-undefined-properties"
export * from "./is-defined" export * from "./is-defined"
export * from "./is-string"
export * from "./calculate-price-tax-amount" export * from "./calculate-price-tax-amount"
export * from "./csv-cell-content-formatter" export * from "./csv-cell-content-formatter"
export * from "./exception-formatter" export * from "./exception-formatter"
+3
View File
@@ -0,0 +1,3 @@
export function isString(val: any): val is string {
return val != null && typeof val === "string"
}