feat: Add product and pricing link on create and delete operations (#6740)
Things that remain to be done: 1. Handle product and variant updates 2. Add tests for the workflows independently 3. Align the endpoints to the new code conventions we defined 4. Finish up the update/upsert endpoints for variants All of those can be done in a separate PR, as this is quite large already.
This commit is contained in:
@@ -25,7 +25,7 @@ export const GET = async (
|
||||
const queryObject = remoteQueryObjectFromString({
|
||||
entryPoint: "product_option",
|
||||
variables,
|
||||
fields: req.retrieveConfig.select as string[],
|
||||
fields: req.remoteQueryConfig.fields,
|
||||
})
|
||||
|
||||
const [product_option] = await remoteQuery(queryObject)
|
||||
|
||||
@@ -22,7 +22,7 @@ export const GET = async (
|
||||
skip: req.listConfig.skip,
|
||||
take: req.listConfig.take,
|
||||
},
|
||||
fields: req.listConfig.select as string[],
|
||||
fields: req.remoteQueryConfig.fields,
|
||||
})
|
||||
|
||||
const { rows: product_options, metadata } = await remoteQuery(queryObject)
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
|
||||
import { UpdateProductDTO } from "@medusajs/types"
|
||||
import { remoteQueryObjectFromString } from "@medusajs/utils"
|
||||
import { remapKeysForProduct, remapProduct } from "../helpers"
|
||||
|
||||
export const GET = async (
|
||||
req: AuthenticatedMedusaRequest,
|
||||
@@ -18,15 +19,16 @@ export const GET = async (
|
||||
|
||||
const variables = { id: req.params.id }
|
||||
|
||||
const selectFields = remapKeysForProduct(req.remoteQueryConfig.fields ?? [])
|
||||
const queryObject = remoteQueryObjectFromString({
|
||||
entryPoint: "product",
|
||||
variables,
|
||||
fields: req.retrieveConfig.select as string[],
|
||||
fields: selectFields,
|
||||
})
|
||||
|
||||
const [product] = await remoteQuery(queryObject)
|
||||
|
||||
res.status(200).json({ product })
|
||||
res.status(200).json({ product: remapProduct(product) })
|
||||
}
|
||||
|
||||
export const POST = async (
|
||||
@@ -45,7 +47,7 @@ export const POST = async (
|
||||
throw errors[0].error
|
||||
}
|
||||
|
||||
res.status(200).json({ product: result[0] })
|
||||
res.status(200).json({ product: remapProduct(result[0]) })
|
||||
}
|
||||
|
||||
export const DELETE = async (
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import { UpdateProductVariantDTO } from "@medusajs/types"
|
||||
import { defaultAdminProductsVariantFields } from "../../../query-config"
|
||||
import { remoteQueryObjectFromString } from "@medusajs/utils"
|
||||
import { remapKeysForVariant, remapVariant } from "../../../helpers"
|
||||
|
||||
export const GET = async (
|
||||
req: AuthenticatedMedusaRequest,
|
||||
@@ -26,11 +27,11 @@ export const GET = async (
|
||||
const queryObject = remoteQueryObjectFromString({
|
||||
entryPoint: "variant",
|
||||
variables,
|
||||
fields: req.retrieveConfig.select as string[],
|
||||
fields: remapKeysForVariant(req.remoteQueryConfig.fields ?? []),
|
||||
})
|
||||
|
||||
const [variant] = await remoteQuery(queryObject)
|
||||
res.status(200).json({ variant })
|
||||
res.status(200).json({ variant: remapVariant(variant) })
|
||||
}
|
||||
|
||||
export const POST = async (
|
||||
@@ -55,7 +56,7 @@ export const POST = async (
|
||||
throw errors[0].error
|
||||
}
|
||||
|
||||
res.status(200).json({ variant: result[0] })
|
||||
res.status(200).json({ variant: remapVariant(result[0]) })
|
||||
}
|
||||
|
||||
export const DELETE = async (
|
||||
|
||||
@@ -6,6 +6,12 @@ import {
|
||||
import { CreateProductVariantDTO } from "@medusajs/types"
|
||||
import { createProductVariantsWorkflow } from "@medusajs/core-flows"
|
||||
import { remoteQueryObjectFromString } from "@medusajs/utils"
|
||||
import {
|
||||
remapKeysForProduct,
|
||||
remapKeysForVariant,
|
||||
remapProduct,
|
||||
remapVariant,
|
||||
} from "../../helpers"
|
||||
|
||||
export const GET = async (
|
||||
req: AuthenticatedMedusaRequest,
|
||||
@@ -22,13 +28,13 @@ export const GET = async (
|
||||
skip: req.listConfig.skip,
|
||||
take: req.listConfig.take,
|
||||
},
|
||||
fields: req.listConfig.select as string[],
|
||||
fields: remapKeysForVariant(req.remoteQueryConfig.fields ?? []),
|
||||
})
|
||||
|
||||
const { rows: variants, metadata } = await remoteQuery(queryObject)
|
||||
|
||||
res.json({
|
||||
variants,
|
||||
variants: variants.map(remapVariant),
|
||||
count: metadata.count,
|
||||
offset: metadata.skip,
|
||||
limit: metadata.take,
|
||||
@@ -58,5 +64,15 @@ export const POST = async (
|
||||
throw errors[0].error
|
||||
}
|
||||
|
||||
res.status(200).json({ variant: result[0] })
|
||||
const remoteQuery = req.scope.resolve("remoteQuery")
|
||||
const queryObject = remoteQueryObjectFromString({
|
||||
entryPoint: "product",
|
||||
variables: {
|
||||
filters: { id: productId },
|
||||
},
|
||||
fields: remapKeysForProduct(req.remoteQueryConfig.fields ?? []),
|
||||
})
|
||||
|
||||
const products = await remoteQuery(queryObject)
|
||||
res.status(200).json({ product: remapProduct(products[0]) })
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { ProductDTO, ProductVariantDTO } from "@medusajs/types"
|
||||
|
||||
// The variant had prices before, but that is not part of the price_set money amounts. Do we remap the request and response or not?
|
||||
export const remapKeysForProduct = (selectFields: string[]) => {
|
||||
const productFields = selectFields.filter(
|
||||
(fieldName: string) => !fieldName.startsWith("variants.prices")
|
||||
)
|
||||
const pricingFields = selectFields
|
||||
.filter((fieldName: string) => fieldName.startsWith("variants.prices"))
|
||||
.map((fieldName: string) =>
|
||||
fieldName.replace("variants.prices.", "variants.price_set.money_amounts.")
|
||||
)
|
||||
|
||||
return [...productFields, ...pricingFields]
|
||||
}
|
||||
|
||||
export const remapKeysForVariant = (selectFields: string[]) => {
|
||||
const variantFields = selectFields.filter(
|
||||
(fieldName: string) => !fieldName.startsWith("prices")
|
||||
)
|
||||
const pricingFields = selectFields
|
||||
.filter((fieldName: string) => fieldName.startsWith("prices"))
|
||||
.map((fieldName: string) =>
|
||||
fieldName.replace("prices.", "price_set.money_amounts.")
|
||||
)
|
||||
|
||||
return [...variantFields, ...pricingFields]
|
||||
}
|
||||
|
||||
export const remapProduct = (p: ProductDTO) => {
|
||||
return {
|
||||
...p,
|
||||
variants: p.variants?.map(remapVariant),
|
||||
}
|
||||
}
|
||||
|
||||
export const remapVariant = (v: ProductVariantDTO) => {
|
||||
return {
|
||||
...v,
|
||||
prices: (v as any).price_set?.money_amounts?.map((ma) => ({
|
||||
...ma,
|
||||
variant_id: v.id,
|
||||
})),
|
||||
price_set: undefined,
|
||||
}
|
||||
}
|
||||
@@ -83,7 +83,14 @@ export const adminProductRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
{
|
||||
method: ["POST"],
|
||||
matcher: "/admin/products/:id/variants",
|
||||
middlewares: [transformBody(AdminPostProductsProductVariantsReq)],
|
||||
middlewares: [
|
||||
transformBody(AdminPostProductsProductVariantsReq),
|
||||
// We specify the product here as that's what we return after updating the variant
|
||||
transformQuery(
|
||||
AdminGetProductsProductParams,
|
||||
QueryConfig.retrieveTransformQueryConfig
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
method: ["POST"],
|
||||
|
||||
@@ -22,6 +22,11 @@ export const defaultAdminProductsVariantFields = [
|
||||
"ean",
|
||||
"upc",
|
||||
"barcode",
|
||||
"prices.id",
|
||||
"prices.currency_code",
|
||||
"prices.amount",
|
||||
"prices.created_at",
|
||||
"prices.updated_at",
|
||||
"options.id",
|
||||
"options.option_value.value",
|
||||
"options.option_value.option.title",
|
||||
@@ -55,7 +60,6 @@ export const listOptionConfig = {
|
||||
|
||||
/* export const allowedAdminProductRelations = [
|
||||
"variants",
|
||||
// TODO: Add in next iteration
|
||||
// "variants.prices",
|
||||
"variants.options",
|
||||
"images",
|
||||
|
||||
@@ -11,13 +11,13 @@ import {
|
||||
} from "../../../types/routing"
|
||||
import { listPriceLists } from "../price-lists/queries"
|
||||
import { AdminGetProductsParams } from "./validators"
|
||||
import { remapKeysForProduct, remapProduct } from "./helpers"
|
||||
import { MedusaContainer } from "medusa-core-utils"
|
||||
|
||||
export const GET = async (
|
||||
req: AuthenticatedMedusaRequest<AdminGetProductsParams>,
|
||||
res: MedusaResponse
|
||||
const applyVariantFiltersForPriceList = async (
|
||||
scope: MedusaContainer,
|
||||
filterableFields: AdminGetProductsParams
|
||||
) => {
|
||||
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
|
||||
const filterableFields: AdminGetProductsParams = { ...req.filterableFields }
|
||||
const filterByPriceListIds = filterableFields.price_list_id
|
||||
const priceListVariantIds: string[] = []
|
||||
|
||||
@@ -25,17 +25,17 @@ export const GET = async (
|
||||
// the variant IDs through the price list price sets.
|
||||
if (Array.isArray(filterByPriceListIds)) {
|
||||
const [priceLists] = await listPriceLists({
|
||||
container: req.scope,
|
||||
container: scope,
|
||||
remoteQueryFields: ["price_set_money_amounts.price_set.variant.id"],
|
||||
apiFields: ["prices.variant_id"],
|
||||
variables: { filters: { id: filterByPriceListIds }, skip: 0, take: null },
|
||||
})
|
||||
|
||||
priceListVariantIds.push(
|
||||
...(priceLists
|
||||
...((priceLists
|
||||
.map((priceList) => priceList.prices?.map((price) => price.variant_id))
|
||||
.flat(2)
|
||||
.filter(isString) || [])
|
||||
.filter(isString) || []) as string[])
|
||||
)
|
||||
|
||||
delete filterableFields.price_list_id
|
||||
@@ -50,19 +50,34 @@ export const GET = async (
|
||||
}
|
||||
}
|
||||
|
||||
return filterableFields
|
||||
}
|
||||
|
||||
export const GET = async (
|
||||
req: AuthenticatedMedusaRequest<AdminGetProductsParams>,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
|
||||
let filterableFields: AdminGetProductsParams = { ...req.filterableFields }
|
||||
filterableFields = await applyVariantFiltersForPriceList(
|
||||
req.scope,
|
||||
filterableFields
|
||||
)
|
||||
|
||||
const selectFields = remapKeysForProduct(req.remoteQueryConfig.fields ?? [])
|
||||
const queryObject = remoteQueryObjectFromString({
|
||||
entryPoint: "product",
|
||||
variables: {
|
||||
filters: filterableFields,
|
||||
...req.remoteQueryConfig.pagination,
|
||||
},
|
||||
fields: req.remoteQueryConfig.fields,
|
||||
fields: selectFields,
|
||||
})
|
||||
|
||||
const { rows: products, metadata } = await remoteQuery(queryObject)
|
||||
|
||||
res.json({
|
||||
products,
|
||||
products: products.map(remapProduct),
|
||||
count: metadata.count,
|
||||
offset: metadata.skip,
|
||||
limit: metadata.take,
|
||||
@@ -88,5 +103,5 @@ export const POST = async (
|
||||
throw errors[0].error
|
||||
}
|
||||
|
||||
res.status(200).json({ product: result[0] })
|
||||
res.status(200).json({ product: remapProduct(result[0]) })
|
||||
}
|
||||
|
||||
@@ -5,11 +5,13 @@ import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
IsInt,
|
||||
IsNumber,
|
||||
IsObject,
|
||||
IsOptional,
|
||||
IsString,
|
||||
NotEquals,
|
||||
Validate,
|
||||
ValidateIf,
|
||||
ValidateNested,
|
||||
} from "class-validator"
|
||||
@@ -17,6 +19,7 @@ import { FindParams, extendedFindParamsMixin } from "../../../types/common"
|
||||
import { OperatorMapValidator } from "../../../types/validators/operator-map"
|
||||
import { IsType } from "../../../utils"
|
||||
import { optionalBooleanMapper } from "../../../utils/validators/is-boolean"
|
||||
import { XorConstraint } from "../../../types/validators/xor"
|
||||
|
||||
export class AdminGetProductsProductParams extends FindParams {}
|
||||
export class AdminGetProductsProductVariantsVariantParams extends FindParams {}
|
||||
@@ -537,13 +540,10 @@ export class AdminPostProductsProductVariantsReq {
|
||||
@IsOptional()
|
||||
metadata?: Record<string, unknown>
|
||||
|
||||
// TODO: Add on next iteration, adding temporary field for now
|
||||
// @IsArray()
|
||||
// @ValidateNested({ each: true })
|
||||
// @Type(() => ProductVariantPricesCreateReq)
|
||||
// prices: ProductVariantPricesCreateReq[]
|
||||
@IsArray()
|
||||
prices: any[]
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => ProductVariantPricesCreateReq)
|
||||
prices: ProductVariantPricesCreateReq[]
|
||||
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
@@ -619,12 +619,11 @@ export class AdminPostProductsProductVariantsVariantReq {
|
||||
@IsOptional()
|
||||
metadata?: Record<string, unknown>
|
||||
|
||||
// TODO: Deal with in next iteration
|
||||
// @IsArray()
|
||||
// @IsOptional()
|
||||
// @ValidateNested({ each: true })
|
||||
// @Type(() => ProductVariantPricesUpdateReq)
|
||||
// prices?: ProductVariantPricesUpdateReq[]
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => ProductVariantPricesUpdateReq)
|
||||
prices?: ProductVariantPricesUpdateReq[]
|
||||
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
@@ -679,3 +678,41 @@ export class ProductTypeReq {
|
||||
@IsString()
|
||||
value: string
|
||||
}
|
||||
|
||||
// TODO: Add support for rules
|
||||
export class ProductVariantPricesCreateReq {
|
||||
@IsString()
|
||||
currency_code: string
|
||||
|
||||
@IsInt()
|
||||
amount: number
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
min_quantity?: number
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
max_quantity?: number
|
||||
}
|
||||
|
||||
export class ProductVariantPricesUpdateReq {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
id?: string
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
currency_code?: string
|
||||
|
||||
@IsInt()
|
||||
amount: number
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
min_quantity?: number
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
max_quantity?: number
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user