feat: price list products (#1239)
* feat: add product list for price lists * feat: add product list for price lists * refactor: product list controller * fix: add integration test for price list products * fix: use getListConfig
This commit is contained in:
1 parent
3083aaee81
commit
fb33dbaca3
11 files changed
+566
-67
No files matched your search
@@ -4,6 +4,10 @@ const setupServer = require("../../../helpers/setup-server")
|
|||||||
const { useApi } = require("../../../helpers/use-api")
|
const { useApi } = require("../../../helpers/use-api")
|
||||||
const { useDb, initDb } = require("../../../helpers/use-db")
|
const { useDb, initDb } = require("../../../helpers/use-db")
|
||||||
|
|
||||||
|
const {
|
||||||
|
simpleProductFactory,
|
||||||
|
simplePriceListFactory,
|
||||||
|
} = require("../../factories")
|
||||||
const adminSeeder = require("../../helpers/admin-seeder")
|
const adminSeeder = require("../../helpers/admin-seeder")
|
||||||
const customerSeeder = require("../../helpers/customer-seeder")
|
const customerSeeder = require("../../helpers/customer-seeder")
|
||||||
const priceListSeeder = require("../../helpers/price-list-seeder")
|
const priceListSeeder = require("../../helpers/price-list-seeder")
|
||||||
@@ -750,4 +754,101 @@ describe("/admin/price-lists", () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("GET /admin/price-lists/:id/products", () => {
|
||||||
|
let tag
|
||||||
|
beforeEach(async () => {
|
||||||
|
try {
|
||||||
|
await adminSeeder(dbConnection)
|
||||||
|
|
||||||
|
await simpleProductFactory(
|
||||||
|
dbConnection,
|
||||||
|
{
|
||||||
|
id: "test-prod-1",
|
||||||
|
title: "MedusaHeadphones",
|
||||||
|
variants: [{ id: "test-variant-1" }, { id: "test-variant-2" }],
|
||||||
|
},
|
||||||
|
1
|
||||||
|
)
|
||||||
|
|
||||||
|
const prod = await simpleProductFactory(
|
||||||
|
dbConnection,
|
||||||
|
{
|
||||||
|
id: "test-prod-2",
|
||||||
|
tags: ["test-tag"],
|
||||||
|
variants: [{ id: "test-variant-3" }, { id: "test-variant-4" }],
|
||||||
|
},
|
||||||
|
2
|
||||||
|
)
|
||||||
|
|
||||||
|
tag = prod.tags[0].id
|
||||||
|
|
||||||
|
await simpleProductFactory(
|
||||||
|
dbConnection,
|
||||||
|
{
|
||||||
|
id: "test-prod-3",
|
||||||
|
variants: [{ id: "test-variant-5" }],
|
||||||
|
},
|
||||||
|
3
|
||||||
|
)
|
||||||
|
|
||||||
|
await simplePriceListFactory(dbConnection, {
|
||||||
|
id: "test-list",
|
||||||
|
prices: [
|
||||||
|
{ variant_id: "test-variant-1", currency_code: "usd", amount: 100 },
|
||||||
|
{ variant_id: "test-variant-4", currency_code: "usd", amount: 100 },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
const db = useDb()
|
||||||
|
await db.teardown()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("lists only product 1, 2", async () => {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
const response = await api
|
||||||
|
.get(`/admin/price-lists/test-list/products?order=-created_at`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer test_token",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.warn(err.response.data)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
expect(response.data.count).toEqual(2)
|
||||||
|
expect(response.data.products).toEqual([
|
||||||
|
expect.objectContaining({ id: "test-prod-1" }),
|
||||||
|
expect.objectContaining({ id: "test-prod-2" }),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("lists only product 2", async () => {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
const response = await api
|
||||||
|
.get(`/admin/price-lists/test-list/products?tags[]=${tag}`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer test_token",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.warn(err.response.data)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
expect(response.data.count).toEqual(1)
|
||||||
|
expect(response.data.products).toEqual([
|
||||||
|
expect.objectContaining({ id: "test-prod-2" }),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
@@ -11,3 +11,4 @@ export * from "./simple-tax-rate-factory"
|
|||||||
export * from "./simple-shipping-option-factory"
|
export * from "./simple-shipping-option-factory"
|
||||||
export * from "./simple-shipping-method-factory"
|
export * from "./simple-shipping-method-factory"
|
||||||
export * from "./simple-product-type-tax-rate-factory"
|
export * from "./simple-product-type-tax-rate-factory"
|
||||||
|
export * from "./simple-price-list-factory"
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import {
|
||||||
|
PriceList,
|
||||||
|
MoneyAmount,
|
||||||
|
PriceListType,
|
||||||
|
PriceListStatus,
|
||||||
|
} from "@medusajs/medusa"
|
||||||
|
import faker from "faker"
|
||||||
|
import { Connection } from "typeorm"
|
||||||
|
|
||||||
|
type ProductListPrice = {
|
||||||
|
variant_id: string
|
||||||
|
currency_code: string
|
||||||
|
region_id: string
|
||||||
|
amount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PriceListFactoryData = {
|
||||||
|
id?: string
|
||||||
|
name?: string
|
||||||
|
description?: string
|
||||||
|
type?: PriceListType
|
||||||
|
status?: PriceListStatus
|
||||||
|
starts_at?: Date
|
||||||
|
ends_at?: Date
|
||||||
|
customer_groups?: string[]
|
||||||
|
prices?: ProductListPrice[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const simplePriceListFactory = async (
|
||||||
|
connection: Connection,
|
||||||
|
data: PriceListFactoryData = {},
|
||||||
|
seed?: number
|
||||||
|
): Promise<PriceList> => {
|
||||||
|
if (typeof seed !== "undefined") {
|
||||||
|
faker.seed(seed)
|
||||||
|
}
|
||||||
|
|
||||||
|
const manager = connection.manager
|
||||||
|
|
||||||
|
const listId = data.id || `simple-price-list-${Math.random() * 1000}`
|
||||||
|
const toCreate = {
|
||||||
|
id: listId,
|
||||||
|
name: data.name || faker.commerce.productName(),
|
||||||
|
description: data.description || "Some text",
|
||||||
|
status: data.status || PriceListStatus.ACTIVE,
|
||||||
|
type: data.type || PriceListType.OVERRIDE,
|
||||||
|
starts_at: data.starts_at || null,
|
||||||
|
ends_at: data.ends_at || null,
|
||||||
|
}
|
||||||
|
|
||||||
|
const toSave = manager.create(PriceList, toCreate)
|
||||||
|
const toReturn = await manager.save(toSave)
|
||||||
|
|
||||||
|
if (typeof data.prices !== "undefined") {
|
||||||
|
for (const ma of data.prices) {
|
||||||
|
const factoryData = {
|
||||||
|
...ma,
|
||||||
|
price_list_id: listId,
|
||||||
|
}
|
||||||
|
const toSave = manager.create(MoneyAmount, factoryData)
|
||||||
|
await manager.save(toSave)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return toReturn
|
||||||
|
}
|
||||||
@@ -13,6 +13,11 @@ export default (app) => {
|
|||||||
|
|
||||||
route.get("/", middlewares.wrap(require("./list-price-lists").default))
|
route.get("/", middlewares.wrap(require("./list-price-lists").default))
|
||||||
|
|
||||||
|
route.get(
|
||||||
|
"/:id/products",
|
||||||
|
middlewares.wrap(require("./list-price-list-products").default)
|
||||||
|
)
|
||||||
|
|
||||||
route.post("/", middlewares.wrap(require("./create-price-list").default))
|
route.post("/", middlewares.wrap(require("./create-price-list").default))
|
||||||
|
|
||||||
route.post("/:id", middlewares.wrap(require("./update-price-list").default))
|
route.post("/:id", middlewares.wrap(require("./update-price-list").default))
|
||||||
@@ -65,3 +70,4 @@ export * from "./delete-price-list"
|
|||||||
export * from "./get-price-list"
|
export * from "./get-price-list"
|
||||||
export * from "./list-price-lists"
|
export * from "./list-price-lists"
|
||||||
export * from "./update-price-list"
|
export * from "./update-price-list"
|
||||||
|
export * from "./list-price-list-products"
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
import { Type } from "class-transformer"
|
||||||
|
import { omit } from "lodash"
|
||||||
|
import {
|
||||||
|
IsArray,
|
||||||
|
IsBoolean,
|
||||||
|
IsEnum,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
ValidateNested,
|
||||||
|
} from "class-validator"
|
||||||
|
import { Product } from "../../../../models/product"
|
||||||
|
import { DateComparisonOperator } from "../../../../types/common"
|
||||||
|
import { validator } from "../../../../utils/validator"
|
||||||
|
import { FilterableProductProps } from "../../../../types/product"
|
||||||
|
import {
|
||||||
|
AdminGetProductsPaginationParams,
|
||||||
|
allowedAdminProductFields,
|
||||||
|
defaultAdminProductFields,
|
||||||
|
defaultAdminProductRelations,
|
||||||
|
} from "../products"
|
||||||
|
import listAndCount from "../../../../controllers/products/admin-list-products"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @oas [get] /price-lists/:id/products
|
||||||
|
* operationId: "GetPriceListsPriceListProducts"
|
||||||
|
* summary: "List Product in a Price List"
|
||||||
|
* description: "Retrieves a list of Product that are part of a Price List"
|
||||||
|
* x-authenticated: true
|
||||||
|
* parameters:
|
||||||
|
* - (query) q {string} Query used for searching products.
|
||||||
|
* - (query) id {string} Id of the product to search for.
|
||||||
|
* - (query) status {string[]} Status to search for.
|
||||||
|
* - (query) collection_id {string[]} Collection ids to search for.
|
||||||
|
* - (query) tags {string[]} Tags to search for.
|
||||||
|
* - (query) title {string} to search for.
|
||||||
|
* - (query) description {string} to search for.
|
||||||
|
* - (query) handle {string} to search for.
|
||||||
|
* - (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) 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.
|
||||||
|
* - (query) offset {string} How many products to skip in the result.
|
||||||
|
* - (query) limit {string} Limit the number of products returned.
|
||||||
|
* - (query) expand {string} (Comma separated) Which fields should be expanded in each product of the result.
|
||||||
|
* - (query) fields {string} (Comma separated) Which fields should be included in each product of the result.
|
||||||
|
* tags:
|
||||||
|
* - Product
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: OK
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* properties:
|
||||||
|
* count:
|
||||||
|
* description: The number of Products.
|
||||||
|
* type: integer
|
||||||
|
* offset:
|
||||||
|
* description: The offset of the Product query.
|
||||||
|
* type: integer
|
||||||
|
* limit:
|
||||||
|
* description: The limit of the Product query.
|
||||||
|
* type: integer
|
||||||
|
* products:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* $ref: "#/components/schemas/product"
|
||||||
|
*/
|
||||||
|
export default async (req, res) => {
|
||||||
|
const { id } = req.params
|
||||||
|
|
||||||
|
const validatedParams = await validator(
|
||||||
|
AdminGetPriceListsPriceListProductsParams,
|
||||||
|
req.query
|
||||||
|
)
|
||||||
|
|
||||||
|
req.query.price_list_id = [id]
|
||||||
|
|
||||||
|
const filterableFields: FilterableProductProps = omit(req.query, [
|
||||||
|
"limit",
|
||||||
|
"offset",
|
||||||
|
"expand",
|
||||||
|
"fields",
|
||||||
|
"order",
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = await listAndCount(
|
||||||
|
req.scope,
|
||||||
|
filterableFields,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
limit: validatedParams.limit ?? 50,
|
||||||
|
offset: validatedParams.offset ?? 0,
|
||||||
|
expand: validatedParams.expand,
|
||||||
|
fields: validatedParams.fields,
|
||||||
|
order: validatedParams.order,
|
||||||
|
allowedFields: allowedAdminProductFields,
|
||||||
|
defaultFields: defaultAdminProductFields as (keyof Product)[],
|
||||||
|
defaultRelations: defaultAdminProductRelations,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
res.json(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ProductStatus {
|
||||||
|
DRAFT = "draft",
|
||||||
|
PROPOSED = "proposed",
|
||||||
|
PUBLISHED = "published",
|
||||||
|
REJECTED = "rejected",
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AdminGetPriceListsPriceListProductsParams 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[]
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
title?: string
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
description?: string
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
handle?: string
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Boolean)
|
||||||
|
is_giftcard?: string
|
||||||
|
|
||||||
|
@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
|
||||||
|
}
|
||||||
@@ -8,16 +8,16 @@ import {
|
|||||||
IsString,
|
IsString,
|
||||||
ValidateNested,
|
ValidateNested,
|
||||||
} from "class-validator"
|
} from "class-validator"
|
||||||
import { pickBy, omit } from "lodash"
|
import { omit } from "lodash"
|
||||||
import { MedusaError } from "medusa-core-utils"
|
|
||||||
import { Product } from "../../../../models/product"
|
import { Product } from "../../../../models/product"
|
||||||
|
import { DateComparisonOperator } from "../../../../types/common"
|
||||||
import {
|
import {
|
||||||
allowedAdminProductFields,
|
allowedAdminProductFields,
|
||||||
defaultAdminProductFields,
|
defaultAdminProductFields,
|
||||||
defaultAdminProductRelations,
|
defaultAdminProductRelations,
|
||||||
} from "."
|
} from "."
|
||||||
import { ProductService } from "../../../../services"
|
import listAndCount from "../../../../controllers/products/admin-list-products"
|
||||||
import { FindConfig, DateComparisonOperator } from "../../../../types/common"
|
|
||||||
import { validator } from "../../../../utils/validator"
|
import { validator } from "../../../../utils/validator"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -71,47 +71,6 @@ import { validator } from "../../../../utils/validator"
|
|||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
const validatedParams = await validator(AdminGetProductsParams, req.query)
|
const validatedParams = await validator(AdminGetProductsParams, req.query)
|
||||||
|
|
||||||
const productService: ProductService = req.scope.resolve("productService")
|
|
||||||
|
|
||||||
let includeFields: string[] = []
|
|
||||||
if (validatedParams.fields) {
|
|
||||||
includeFields = validatedParams.fields!.split(",")
|
|
||||||
}
|
|
||||||
|
|
||||||
let expandFields: string[] = []
|
|
||||||
if (validatedParams.expand) {
|
|
||||||
expandFields = validatedParams.expand!.split(",")
|
|
||||||
}
|
|
||||||
|
|
||||||
const listConfig: FindConfig<Product> = {
|
|
||||||
select: (includeFields.length
|
|
||||||
? includeFields
|
|
||||||
: defaultAdminProductFields) as (keyof Product)[],
|
|
||||||
relations: expandFields.length
|
|
||||||
? expandFields
|
|
||||||
: defaultAdminProductRelations,
|
|
||||||
skip: validatedParams.offset,
|
|
||||||
take: validatedParams.limit,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof validatedParams.order !== "undefined") {
|
|
||||||
let orderField = validatedParams.order
|
|
||||||
if (validatedParams.order.startsWith("-")) {
|
|
||||||
const [, field] = validatedParams.order.split("-")
|
|
||||||
orderField = field
|
|
||||||
listConfig.order = { [field]: "DESC" }
|
|
||||||
} else {
|
|
||||||
listConfig.order = { [validatedParams.order]: "ASC" }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!allowedAdminProductFields.includes(orderField)) {
|
|
||||||
throw new MedusaError(
|
|
||||||
MedusaError.Types.INVALID_DATA,
|
|
||||||
"Order field must be a valid product field"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const filterableFields = omit(validatedParams, [
|
const filterableFields = omit(validatedParams, [
|
||||||
"limit",
|
"limit",
|
||||||
"offset",
|
"offset",
|
||||||
@@ -120,17 +79,22 @@ export default async (req, res) => {
|
|||||||
"order",
|
"order",
|
||||||
])
|
])
|
||||||
|
|
||||||
const [products, count] = await productService.listAndCount(
|
const result = await listAndCount(
|
||||||
pickBy(filterableFields, (val) => typeof val !== "undefined"),
|
req.scope,
|
||||||
listConfig
|
filterableFields,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
limit: validatedParams.limit ?? 50,
|
||||||
|
offset: validatedParams.offset ?? 0,
|
||||||
|
expand: validatedParams.expand,
|
||||||
|
fields: validatedParams.fields,
|
||||||
|
allowedFields: allowedAdminProductFields,
|
||||||
|
defaultFields: defaultAdminProductFields as (keyof Product)[],
|
||||||
|
defaultRelations: defaultAdminProductRelations,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
res.json({
|
res.json(result)
|
||||||
products,
|
|
||||||
count,
|
|
||||||
offset: validatedParams.offset,
|
|
||||||
limit: validatedParams.limit,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ProductStatus {
|
export enum ProductStatus {
|
||||||
@@ -181,6 +145,10 @@ export class AdminGetProductsParams extends AdminGetProductsPaginationParams {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
tags?: string[]
|
tags?: string[]
|
||||||
|
|
||||||
|
@IsArray()
|
||||||
|
@IsOptional()
|
||||||
|
price_list_id?: string[]
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
title?: string
|
title?: string
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import { AwilixContainer } from "awilix"
|
||||||
|
import { AdminProductsListRes } from "../../api"
|
||||||
|
import { pickBy } from "lodash"
|
||||||
|
import { MedusaError } from "medusa-core-utils"
|
||||||
|
import { Product } from "../../models/product"
|
||||||
|
import { ProductService } from "../../services"
|
||||||
|
import { getListConfig } from "../../utils/get-query-config"
|
||||||
|
import { FindConfig } from "../../types/common"
|
||||||
|
import { FilterableProductProps } from "../../types/product"
|
||||||
|
|
||||||
|
type ListContext = {
|
||||||
|
limit: number
|
||||||
|
offset: number
|
||||||
|
order?: string
|
||||||
|
fields?: string
|
||||||
|
expand?: string
|
||||||
|
allowedFields?: string[]
|
||||||
|
defaultFields?: (keyof Product)[]
|
||||||
|
defaultRelations?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const listAndCount = async (
|
||||||
|
scope: AwilixContainer,
|
||||||
|
query: FilterableProductProps,
|
||||||
|
body?: object,
|
||||||
|
context: ListContext = { limit: 50, offset: 0 }
|
||||||
|
): Promise<AdminProductsListRes> => {
|
||||||
|
const { limit, offset, allowedFields, defaultFields, defaultRelations } =
|
||||||
|
context
|
||||||
|
|
||||||
|
const productService: ProductService = scope.resolve("productService")
|
||||||
|
let includeFields: (keyof Product)[] | undefined
|
||||||
|
if (context.fields) {
|
||||||
|
includeFields = context.fields.split(",") as (keyof Product)[]
|
||||||
|
}
|
||||||
|
|
||||||
|
let expandFields: string[] | undefined
|
||||||
|
if (context.expand) {
|
||||||
|
expandFields = context.expand.split(",")
|
||||||
|
}
|
||||||
|
|
||||||
|
let orderBy: { [k: symbol]: "DESC" | "ASC" } | undefined
|
||||||
|
if (typeof context.order !== "undefined") {
|
||||||
|
let orderField = context.order
|
||||||
|
if (context.order.startsWith("-")) {
|
||||||
|
const [, field] = context.order.split("-")
|
||||||
|
orderField = field
|
||||||
|
orderBy = { [field]: "DESC" }
|
||||||
|
} else {
|
||||||
|
orderBy = { [context.order]: "ASC" }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(allowedFields || []).includes(orderField)) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
"Order field must be a valid product field"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const listConfig = getListConfig<Product>(
|
||||||
|
defaultFields ?? [],
|
||||||
|
defaultRelations ?? [],
|
||||||
|
includeFields,
|
||||||
|
expandFields,
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
orderBy
|
||||||
|
)
|
||||||
|
|
||||||
|
const [products, count] = await productService.listAndCount(
|
||||||
|
pickBy(query, (val) => typeof val !== "undefined"),
|
||||||
|
listConfig
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
products,
|
||||||
|
count,
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default listAndCount
|
||||||
@@ -4,6 +4,9 @@ export * from "./api"
|
|||||||
// Interfaces
|
// Interfaces
|
||||||
export * from "./interfaces"
|
export * from "./interfaces"
|
||||||
|
|
||||||
|
// Types
|
||||||
|
export * from "./types/price-list"
|
||||||
|
|
||||||
// Models
|
// Models
|
||||||
export * from "./models/shipping-tax-rate"
|
export * from "./models/shipping-tax-rate"
|
||||||
export * from "./models/product-tax-rate"
|
export * from "./models/product-tax-rate"
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
} from "typeorm"
|
} from "typeorm"
|
||||||
import { ProductTag } from ".."
|
import { ProductTag } from ".."
|
||||||
import { Product } from "../models/product"
|
import { Product } from "../models/product"
|
||||||
|
import { PriceList } from "../models/price-list"
|
||||||
|
|
||||||
type DefaultWithoutRelations = Omit<FindManyOptions<Product>, "relations">
|
type DefaultWithoutRelations = Omit<FindManyOptions<Product>, "relations">
|
||||||
|
|
||||||
@@ -16,6 +17,7 @@ type CustomOptions = {
|
|||||||
select?: DefaultWithoutRelations["select"]
|
select?: DefaultWithoutRelations["select"]
|
||||||
where?: DefaultWithoutRelations["where"] & {
|
where?: DefaultWithoutRelations["where"] & {
|
||||||
tags?: FindOperator<ProductTag>
|
tags?: FindOperator<ProductTag>
|
||||||
|
price_list_id?: FindOperator<PriceList>
|
||||||
}
|
}
|
||||||
order?: OrderByCondition
|
order?: OrderByCondition
|
||||||
skip?: number
|
skip?: number
|
||||||
@@ -42,27 +44,50 @@ export class ProductRepository extends Repository<Product> {
|
|||||||
): Promise<[Product[], number]> {
|
): Promise<[Product[], number]> {
|
||||||
const tags = optionsWithoutRelations?.where?.tags
|
const tags = optionsWithoutRelations?.where?.tags
|
||||||
delete optionsWithoutRelations?.where?.tags
|
delete optionsWithoutRelations?.where?.tags
|
||||||
let qb = this.createQueryBuilder("product")
|
|
||||||
|
const price_lists = optionsWithoutRelations?.where?.price_list_id
|
||||||
|
delete optionsWithoutRelations?.where?.price_list_id
|
||||||
|
|
||||||
|
const qb = this.createQueryBuilder("product")
|
||||||
.select(["product.id"])
|
.select(["product.id"])
|
||||||
.skip(optionsWithoutRelations.skip)
|
.skip(optionsWithoutRelations.skip)
|
||||||
.take(optionsWithoutRelations.take)
|
.take(optionsWithoutRelations.take)
|
||||||
|
|
||||||
qb = optionsWithoutRelations.where
|
if (optionsWithoutRelations.where) {
|
||||||
? qb.where(optionsWithoutRelations.where)
|
qb.where(optionsWithoutRelations.where)
|
||||||
: qb
|
}
|
||||||
|
|
||||||
qb = optionsWithoutRelations.order
|
if (optionsWithoutRelations.order) {
|
||||||
? qb.orderBy(optionsWithoutRelations.order)
|
const toSelect: string[] = []
|
||||||
: qb
|
const parsed = Object.entries(optionsWithoutRelations.order).reduce(
|
||||||
|
(acc, [k, v]) => {
|
||||||
|
const key = `product.${k}`
|
||||||
|
toSelect.push(key)
|
||||||
|
acc[key] = v
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
qb.addSelect(toSelect)
|
||||||
|
qb.orderBy(parsed)
|
||||||
|
}
|
||||||
|
|
||||||
if (tags) {
|
if (tags) {
|
||||||
qb = qb
|
qb.leftJoin("product.tags", "tags").andWhere(`tags.id IN (:...tag_ids)`, {
|
||||||
.leftJoinAndSelect("product.tags", "tags")
|
tag_ids: tags.value,
|
||||||
.andWhere(`tags.id IN (:...ids)`, { ids: tags.value })
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (price_lists) {
|
||||||
|
qb.leftJoin("product.variants", "variants")
|
||||||
|
.leftJoin("variants.prices", "ma")
|
||||||
|
.andWhere("ma.price_list_id IN (:...price_list_ids)", {
|
||||||
|
price_list_ids: price_lists.value,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (optionsWithoutRelations.withDeleted) {
|
if (optionsWithoutRelations.withDeleted) {
|
||||||
qb = qb.withDeleted()
|
qb.withDeleted()
|
||||||
}
|
}
|
||||||
|
|
||||||
let entities: Product[]
|
let entities: Product[]
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ class ProductService extends BaseService {
|
|||||||
* by
|
* by
|
||||||
* @param {object} config - object that defines the scope for what should be
|
* @param {object} config - object that defines the scope for what should be
|
||||||
* returned
|
* returned
|
||||||
* @return {[Promise<Product[]>, number]} an array containing the products as
|
* @return {Promise<[Product[], number]>} an array containing the products as
|
||||||
* the first element and the total count of products that matches the query
|
* the first element and the total count of products that matches the query
|
||||||
* as the second element.
|
* as the second element.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
import { ValidateNested } from "class-validator"
|
import { Type } from "class-transformer"
|
||||||
|
import {
|
||||||
|
IsArray,
|
||||||
|
IsBoolean,
|
||||||
|
IsEnum,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
ValidateNested,
|
||||||
|
} from "class-validator"
|
||||||
import { IsType } from "../utils/validators/is-type"
|
import { IsType } from "../utils/validators/is-type"
|
||||||
import { DateComparisonOperator, StringComparisonOperator } from "./common"
|
import { DateComparisonOperator, StringComparisonOperator } from "./common"
|
||||||
|
|
||||||
@@ -9,6 +17,68 @@ export enum ProductStatus {
|
|||||||
REJECTED = "rejected",
|
REJECTED = "rejected",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class FilterableProductProps {
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
id?: string
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
q?: string
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(ProductStatus, { each: true })
|
||||||
|
status?: ProductStatus[]
|
||||||
|
|
||||||
|
@IsArray()
|
||||||
|
@IsOptional()
|
||||||
|
price_list_id?: string[]
|
||||||
|
|
||||||
|
@IsArray()
|
||||||
|
@IsOptional()
|
||||||
|
collection_id?: string[]
|
||||||
|
|
||||||
|
@IsArray()
|
||||||
|
@IsOptional()
|
||||||
|
tags?: string[]
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
title?: string
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
description?: string
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
handle?: string
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Boolean)
|
||||||
|
is_giftcard?: string
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
type?: string
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => DateComparisonOperator)
|
||||||
|
created_at?: DateComparisonOperator
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => DateComparisonOperator)
|
||||||
|
updated_at?: DateComparisonOperator
|
||||||
|
|
||||||
|
@ValidateNested()
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => DateComparisonOperator)
|
||||||
|
deleted_at?: DateComparisonOperator
|
||||||
|
}
|
||||||
|
|
||||||
export class FilterableProductTagProps {
|
export class FilterableProductTagProps {
|
||||||
@ValidateNested()
|
@ValidateNested()
|
||||||
@IsType([String, [String], StringComparisonOperator])
|
@IsType([String, [String], StringComparisonOperator])
|
||||||
|
|||||||
Reference in new issue
Block a user