feat(medusa): Allow to query product types by discount condition id (#2359)
This commit is contained in:
committed by
GitHub
parent
35df4962f8
commit
19ca18e71c
5
.changeset/lucky-worms-turn.md
Normal file
5
.changeset/lucky-worms-turn.md
Normal 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`] = `
|
||||
Array [
|
||||
Object {
|
||||
"created_at": Any<String>,
|
||||
"id": "test-type",
|
||||
"updated_at": Any<String>,
|
||||
"value": "test-type",
|
||||
},
|
||||
Object {
|
||||
"created_at": Any<String>,
|
||||
"id": "test-type-new",
|
||||
"updated_at": Any<String>,
|
||||
"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 { 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 }),
|
||||
])
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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<ProductType> = {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { EntityRepository, Repository } from "typeorm"
|
||||
import { ProductType } from "../models/product-type"
|
||||
import { ExtendedFindConfig } from "../types/common"
|
||||
|
||||
type UpsertTypeInput = Partial<ProductType> & {
|
||||
value: string
|
||||
}
|
||||
|
||||
@EntityRepository(ProductType)
|
||||
export class ProductTypeRepository extends Repository<ProductType> {
|
||||
async upsertType(type?: UpsertTypeInput): Promise<ProductType | null> {
|
||||
@@ -22,8 +24,35 @@ export class ProductTypeRepository extends Repository<ProductType> {
|
||||
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<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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ProductType> & {
|
||||
q?: string
|
||||
discount_condition_id?: string
|
||||
} = {},
|
||||
config: FindConfig<ProductType> = { skip: 0, take: 20 }
|
||||
): Promise<ProductType[]> {
|
||||
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<ProductType> & {
|
||||
q?: string
|
||||
discount_condition_id?: string
|
||||
} = {},
|
||||
config: FindConfig<ProductType> = { 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<ProductType>): 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)
|
||||
|
||||
@@ -32,6 +32,7 @@ export type PartialPick<T, K extends keyof T> = {
|
||||
export type Writable<T> = {
|
||||
-readonly [key in keyof T]:
|
||||
| T[key]
|
||||
| FindOperator<T[key]>
|
||||
| FindOperator<T[key][]>
|
||||
| FindOperator<string[]>
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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"
|
||||
|
||||
3
packages/medusa/src/utils/is-string.ts
Normal file
3
packages/medusa/src/utils/is-string.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export function isString(val: any): val is string {
|
||||
return val != null && typeof val === "string"
|
||||
}
|
||||
Reference in New Issue
Block a user