feat: Use tag ids instead of values wherever possible (#8394)

This commit is contained in:
Stevche Radevski authored and GitHub committed 2024-08-02 09:22:03 +02:00
1 parent 71f0d24359
commit 3a068c6b27
26 files changed
+259 -229

No files matched your search

-1
View File
@@ -8,7 +8,6 @@ export const getProductFixture = (
status: "draft", status: "draft",
// BREAKING: Images input changed from string[] to {url: string}[] // BREAKING: Images input changed from string[] to {url: string}[]
images: [{ url: "test-image.png" }, { url: "test-image-2.png" }], images: [{ url: "test-image.png" }, { url: "test-image-2.png" }],
tags: [{ value: "123" }, { value: "456" }],
// BREAKING: Options input changed from {title: string}[] to {title: string, values: string[]}[] // BREAKING: Options input changed from {title: string}[] to {title: string, values: string[]}[]
options: [ options: [
{ title: "size", values: ["large", "small"] }, { title: "size", values: ["large", "small"] },
@@ -50,6 +50,9 @@ medusaIntegrationTestRunner({
let baseType let baseType
let baseRegion let baseRegion
let baseCategory let baseCategory
let baseTag1
let baseTag2
let newTag
let eventBus: IEventBusModuleService let eventBus: IEventBusModuleService
beforeAll(async () => { beforeAll(async () => {
@@ -102,6 +105,22 @@ medusaIntegrationTestRunner({
) )
).data.product_category ).data.product_category
baseTag1 = (
await api.post("/admin/product-tags", { value: "123" }, adminHeaders)
).data.product_tag
baseTag2 = (
await api.post("/admin/product-tags", { value: "456" }, adminHeaders)
).data.product_tag
newTag = (
await api.post(
"/admin/product-tags",
{ value: "new-tag" },
adminHeaders
)
).data.product_tag
baseProduct = ( baseProduct = (
await api.post( await api.post(
"/admin/products", "/admin/products",
@@ -111,6 +130,7 @@ medusaIntegrationTestRunner({
collection_id: baseCollection.id, collection_id: baseCollection.id,
type_id: baseType.id, type_id: baseType.id,
categories: [{ id: baseCategory.id }], categories: [{ id: baseCategory.id }],
tags: [{ id: baseTag1.id }, { id: baseTag2.id }],
variants: [ variants: [
{ {
title: "Test variant", title: "Test variant",
@@ -166,7 +186,7 @@ medusaIntegrationTestRunner({
getProductFixture({ getProductFixture({
title: "Proposed product", title: "Proposed product",
status: "proposed", status: "proposed",
tags: [{ value: "new-tag" }], tags: [{ id: newTag.id }],
type_id: baseType.id, type_id: baseType.id,
}), }),
adminHeaders adminHeaders
@@ -258,6 +278,7 @@ medusaIntegrationTestRunner({
"/admin/products", "/admin/products",
getProductFixture({ getProductFixture({
title: "Product with prices", title: "Product with prices",
tags: [{ id: baseTag1.id }, { id: baseTag2.id }],
variants: [ variants: [
{ {
title: "Test variant", title: "Test variant",
@@ -33,6 +33,10 @@ medusaIntegrationTestRunner({
let baseProduct let baseProduct
let baseRegion let baseRegion
let baseCategory let baseCategory
let baseTag1
let baseTag2
let baseTag3
let newTag
let eventBus: IEventBusModuleService let eventBus: IEventBusModuleService
beforeAll(async () => { beforeAll(async () => {
@@ -57,11 +61,32 @@ medusaIntegrationTestRunner({
) )
).data.product_type ).data.product_type
baseTag1 = (
await api.post("/admin/product-tags", { value: "123" }, adminHeaders)
).data.product_tag
baseTag2 = (
await api.post("/admin/product-tags", { value: "123_1" }, adminHeaders)
).data.product_tag
baseTag3 = (
await api.post("/admin/product-tags", { value: "456" }, adminHeaders)
).data.product_tag
newTag = (
await api.post(
"/admin/product-tags",
{ value: "new-tag" },
adminHeaders
)
).data.product_tag
baseProduct = ( baseProduct = (
await api.post( await api.post(
"/admin/products", "/admin/products",
getProductFixture({ getProductFixture({
title: "Base product", title: "Base product",
tags: [{ id: baseTag1.id }, { id: baseTag2.id }],
}), }),
adminHeaders adminHeaders
) )
@@ -18,6 +18,9 @@ medusaIntegrationTestRunner({
let publishedCollection let publishedCollection
let baseType let baseType
let baseTag1
let baseTag2
let newTag
beforeEach(async () => { beforeEach(async () => {
await createAdminUser(dbConnection, adminHeaders, getContainer()) await createAdminUser(dbConnection, adminHeaders, getContainer())
@@ -46,6 +49,22 @@ medusaIntegrationTestRunner({
) )
).data.product_type ).data.product_type
baseTag1 = (
await api.post("/admin/product-tags", { value: "123" }, adminHeaders)
).data.product_tag
baseTag2 = (
await api.post("/admin/product-tags", { value: "456" }, adminHeaders)
).data.product_tag
newTag = (
await api.post(
"/admin/product-tags",
{ value: "new-tag" },
adminHeaders
)
).data.product_tag
baseProduct = ( baseProduct = (
await api.post( await api.post(
"/admin/products", "/admin/products",
@@ -54,6 +73,7 @@ medusaIntegrationTestRunner({
collection_id: baseCollection.id, collection_id: baseCollection.id,
// BREAKING: Type input changed from {type: {value: string}} to {type_id: string} // BREAKING: Type input changed from {type: {value: string}} to {type_id: string}
type_id: baseType.id, type_id: baseType.id,
tags: [{ id: baseTag1.id }, { id: baseTag2.id }],
}), }),
adminHeaders adminHeaders
) )
@@ -65,7 +85,7 @@ medusaIntegrationTestRunner({
getProductFixture({ getProductFixture({
title: "Proposed product", title: "Proposed product",
status: "proposed", status: "proposed",
tags: [{ value: "new-tag" }], tags: [{ id: newTag.id }],
type_id: baseType.id, type_id: baseType.id,
}), }),
adminHeaders adminHeaders
@@ -79,6 +99,7 @@ medusaIntegrationTestRunner({
title: "Published product", title: "Published product",
status: "published", status: "published",
collection_id: publishedCollection.id, collection_id: publishedCollection.id,
tags: [{ id: baseTag1.id }, { id: baseTag2.id }],
}), }),
adminHeaders adminHeaders
) )
@@ -507,7 +528,7 @@ medusaIntegrationTestRunner({
it("returns a list of products with tags", async () => { it("returns a list of products with tags", async () => {
const response = await api.get( const response = await api.get(
`/admin/products?tags[]=${baseProduct.tags[0].id}`, `/admin/products?tag_id[]=${baseProduct.tags[0].id}`,
adminHeaders adminHeaders
) )
@@ -534,7 +555,7 @@ medusaIntegrationTestRunner({
it("returns a list of products with tags in a collection", async () => { it("returns a list of products with tags in a collection", async () => {
const response = await api.get( const response = await api.get(
`/admin/products?collection_id[]=${baseCollection.id}&tags[]=${baseProduct.tags[0].id}`, `/admin/products?collection_id[]=${baseCollection.id}&tag_id[]=${baseProduct.tags[0].id}`,
adminHeaders adminHeaders
) )
@@ -1082,6 +1103,7 @@ medusaIntegrationTestRunner({
title: "Test create", title: "Test create",
collection_id: baseCollection.id, collection_id: baseCollection.id,
type_id: baseType.id, type_id: baseType.id,
tags: [{ id: baseTag1.id }, { id: baseTag2.id }],
}), }),
adminHeaders adminHeaders
@@ -1276,7 +1298,7 @@ medusaIntegrationTestRunner({
description: "test-product-description", description: "test-product-description",
images: [{ url: "test-image.png" }, { url: "test-image-2.png" }], images: [{ url: "test-image.png" }, { url: "test-image-2.png" }],
collection_id: baseCollection.id, collection_id: baseCollection.id,
tags: [{ value: "123" }, { value: "456" }], tags: [{ id: baseTag1.id }, { id: baseTag2.id }],
variants: [ variants: [
{ {
title: "Test variant", title: "Test variant",
@@ -1306,7 +1328,7 @@ medusaIntegrationTestRunner({
description: "test-product-description 1", description: "test-product-description 1",
images: [{ url: "test-image.png" }, { url: "test-image-2.png" }], images: [{ url: "test-image.png" }, { url: "test-image-2.png" }],
collection_id: baseCollection.id, collection_id: baseCollection.id,
tags: [{ value: "123" }, { value: "456" }], tags: [{ id: baseTag1.id }, { id: baseTag2.id }],
variants: [ variants: [
{ {
title: "Test variant 1", title: "Test variant 1",
@@ -1406,7 +1428,7 @@ medusaIntegrationTestRunner({
], ],
}, },
], ],
tags: [{ value: "123" }], tags: [{ id: baseTag1.id }],
images: [{ url: "test-image-2.png" }], images: [{ url: "test-image-2.png" }],
status: "published", status: "published",
} }
@@ -18,6 +18,7 @@ medusaIntegrationTestRunner({
let store let store
let appContainer let appContainer
let collection let collection
let tag
let product let product
let product1 let product1
let product2 let product2
@@ -496,6 +497,9 @@ medusaIntegrationTestRunner({
adminHeaders adminHeaders
) )
).data.collection ).data.collection
tag = (
await api.post("/admin/product-tags", { value: "tag1" }, adminHeaders)
).data.product_tag
;[product, [variant]] = await createProducts({ ;[product, [variant]] = await createProducts({
title: "test product 1", title: "test product 1",
collection_id: collection.id, collection_id: collection.id,
@@ -504,7 +508,7 @@ medusaIntegrationTestRunner({
{ title: "size", values: ["large", "small"] }, { title: "size", values: ["large", "small"] },
{ title: "color", values: ["green"] }, { title: "color", values: ["green"] },
], ],
tags: [{ value: "tag1" }], tags: [{ id: tag.id }],
variants: [ variants: [
{ {
title: "test variant 1", title: "test variant 1",
@@ -726,7 +730,7 @@ medusaIntegrationTestRunner({
it("returns a list of products with a given tag", async () => { it("returns a list of products with a given tag", async () => {
const response = await api.get( const response = await api.get(
`/store/products?tags[]=${product.tags[0].id}` `/store/products?tag_id[]=${product.tags[0].id}`
) )
expect(response.status).toEqual(200) expect(response.status).toEqual(200)
@@ -1899,7 +1899,6 @@ medusaIntegrationTestRunner({
"/admin/products", "/admin/products",
{ {
title: "Test fixture", title: "Test fixture",
tags: [{ value: "123" }, { value: "456" }],
options: [ options: [
{ title: "size", values: ["large", "small"] }, { title: "size", values: ["large", "small"] },
{ title: "color", values: ["green"] }, { title: "color", values: ["green"] },
@@ -65,7 +65,6 @@ medusaIntegrationTestRunner({
"/admin/products", "/admin/products",
{ {
title: "Test fixture", title: "Test fixture",
tags: [{ value: "123" }, { value: "456" }],
options: [ options: [
{ title: "size", values: ["large", "small"] }, { title: "size", values: ["large", "small"] },
{ title: "color", values: ["green"] }, { title: "color", values: ["green"] },
@@ -51,8 +51,8 @@ export const Notifications = () => {
} }
}, []) }, [])
const handleOnOpen = (isOpen: boolean) => { const handleOnOpen = (shouldOpen: boolean) => {
if (isOpen) { if (shouldOpen) {
setHasUnread(false) setHasUnread(false)
setOpen(true) setOpen(true)
localStorage.setItem(LAST_READ_NOTIFICATION_KEY, new Date().toISOString()) localStorage.setItem(LAST_READ_NOTIFICATION_KEY, new Date().toISOString())
@@ -95,7 +95,7 @@ export const useProductTableFilters = (
if (product_tags && !isProductTagExcluded) { if (product_tags && !isProductTagExcluded) {
const tagFilter: Filter = { const tagFilter: Filter = {
key: "tags", key: "tag_id",
label: t("fields.tag"), label: t("fields.tag"),
type: "select", type: "select",
multiple: true, multiple: true,
@@ -108,21 +108,6 @@ export const useProductTableFilters = (
filters = [...filters, tagFilter] filters = [...filters, tagFilter]
} }
// if (product_tags) {
// const tagFilter: Filter = {
// key: "tags",
// label: t("fields.tag"),
// type: "select",
// multiple: true,
// options: product_tags.map((t) => ({
// label: t.value,
// value: t.id,
// })),
// }
// filters = [...filters, tagFilter]
// }
if (sales_channels) { if (sales_channels) {
const salesChannelFilter: Filter = { const salesChannelFilter: Filter = {
key: "sales_channel_id", key: "sales_channel_id",
@@ -21,7 +21,7 @@ export const useProductTableQuery = ({
"category_id", "category_id",
"collection_id", "collection_id",
"is_giftcard", "is_giftcard",
"tags", "tag_id",
"type_id", "type_id",
"status", "status",
"id", "id",
@@ -36,7 +36,7 @@ export const useProductTableQuery = ({
updated_at, updated_at,
category_id, category_id,
collection_id, collection_id,
tags, tag_id,
type_id, type_id,
is_giftcard, is_giftcard,
status, status,
@@ -54,7 +54,7 @@ export const useProductTableQuery = ({
collection_id: collection_id?.split(","), collection_id: collection_id?.split(","),
is_giftcard: is_giftcard ? is_giftcard === "true" : undefined, is_giftcard: is_giftcard ? is_giftcard === "true" : undefined,
order: order, order: order,
tags: tags ? { value: tags.split(",") } : undefined, tag_id: tag_id ? tag_id.split(",") : undefined,
type_id: type_id?.split(","), type_id: type_id?.split(","),
status: status?.split(",") as HttpTypes.AdminProductStatus[], status: status?.split(",") as HttpTypes.AdminProductStatus[],
q, q,
@@ -27,7 +27,7 @@ export const ProductTagProductSection = ({
const { products, count, isPending, isError, error } = useProducts({ const { products, count, isPending, isError, error } = useProducts({
...searchParams, ...searchParams,
tags: productTag.id, tag_id: productTag.id,
}) })
const filters = useProductTableFilters(["product_tags"]) const filters = useProductTableFilters(["product_tags"])
@@ -17,7 +17,7 @@ export const normalizeProductFormValues = (
status: values.status, status: values.status,
is_giftcard: false, is_giftcard: false,
tags: values?.tags?.length tags: values?.tags?.length
? values.tags?.map((tag) => ({ value: tag })) ? values.tags?.map((tag) => ({ id: tag }))
: undefined, : undefined,
sales_channels: values?.sales_channels?.length sales_channels: values?.sales_channels?.length
? values.sales_channels?.map((sc) => ({ id: sc.id })) ? values.sales_channels?.map((sc) => ({ id: sc.id }))
@@ -40,7 +40,7 @@ export const ProductOrganizationSection = ({
product.tags?.length product.tags?.length
? product.tags.map((tag) => ( ? product.tags.map((tag) => (
<Badge key={tag.id} className="w-fit" size="2xsmall" asChild> <Badge key={tag.id} className="w-fit" size="2xsmall" asChild>
<Link to={`/products?tags=${tag.id}`}>{tag.value}</Link> <Link to={`/products?tag_id=${tag.id}`}>{tag.value}</Link>
</Badge> </Badge>
)) ))
: undefined : undefined
@@ -78,9 +78,9 @@ export const ProductOrganizationForm = ({
collection_id: data.collection_id || undefined, collection_id: data.collection_id || undefined,
categories: data.category_ids.map((id) => ({ id })) || undefined, categories: data.category_ids.map((id) => ({ id })) || undefined,
tags: tags:
data.tag_ids?.map((t) => { data.tag_ids?.map((t) => ({
t id: t,
}) || undefined, })) || undefined,
}, },
{ {
onSuccess: ({ product }) => { onSuccess: ({ product }) => {
@@ -1,10 +1,14 @@
import { ProductTypes } from "@medusajs/types"
import { HttpTypes, RegionTypes } from "@medusajs/types" import { HttpTypes, RegionTypes } from "@medusajs/types"
import { MedusaError, lowerCaseFirst } from "@medusajs/utils" import { MedusaError, lowerCaseFirst } from "@medusajs/utils"
// We want to convert the csv data format to a standard DTO format. // We want to convert the csv data format to a standard DTO format.
export const normalizeForImport = ( export const normalizeForImport = (
rawProducts: object[], rawProducts: object[],
regions: RegionTypes.RegionDTO[] additional: {
regions: RegionTypes.RegionDTO[]
tags: ProductTypes.ProductTagDTO[]
}
): HttpTypes.AdminCreateProduct[] => { ): HttpTypes.AdminCreateProduct[] => {
const productMap = new Map< const productMap = new Map<
string, string,
@@ -15,13 +19,16 @@ export const normalizeForImport = (
>() >()
// Currently region names are treated as case-insensitive. // Currently region names are treated as case-insensitive.
const regionsMap = new Map(regions.map((r) => [r.name.toLowerCase(), r])) const regionsMap = new Map(
additional.regions.map((r) => [r.name.toLowerCase(), r])
)
const tagsMap = new Map(additional.tags.map((t) => [t.value, t]))
rawProducts.forEach((rawProduct) => { rawProducts.forEach((rawProduct) => {
const productInMap = productMap.get(rawProduct["Product Handle"]) const productInMap = productMap.get(rawProduct["Product Handle"])
if (!productInMap) { if (!productInMap) {
productMap.set(rawProduct["Product Handle"], { productMap.set(rawProduct["Product Handle"], {
product: normalizeProductForImport(rawProduct), product: normalizeProductForImport(rawProduct, tagsMap),
variants: [normalizeVariantForImport(rawProduct, regionsMap)], variants: [normalizeVariantForImport(rawProduct, regionsMap)],
}) })
return return
@@ -86,7 +93,8 @@ const booleanFields = [
] ]
const normalizeProductForImport = ( const normalizeProductForImport = (
rawProduct: object rawProduct: object,
tagsMap: Map<string, ProductTypes.ProductTagDTO>
): HttpTypes.AdminCreateProduct => { ): HttpTypes.AdminCreateProduct => {
const response = {} const response = {}
@@ -108,10 +116,15 @@ const normalizeProductForImport = (
} }
if (normalizedKey.startsWith("product_tag_")) { if (normalizedKey.startsWith("product_tag_")) {
response["tags"] = [ const tag = tagsMap.get(normalizedValue)
...(response["tags"] || []), if (!tag) {
{ value: normalizedValue }, throw new MedusaError(
] MedusaError.Types.INVALID_DATA,
`Tag with value ${normalizedValue} not found`
)
}
response["tags"] = [...(response["tags"] || []), { id: tag.id }]
return return
} }
@@ -38,9 +38,13 @@ export const groupProductsForBatchStep = createStep(
return acc return acc
} }
// New products will be created with a new ID, even if there is one present in the CSV. // New products and variants will be created with a new ID, even if there is one present in the CSV.
// To add support for creating with predefined IDs we will need to do changes to the upsert method. // To add support for creating with predefined IDs we will need to do changes to the upsert method.
delete product.id delete product.id
product.variants?.forEach((variant) => {
delete (variant as any).id
})
acc.toCreate.push(product) acc.toCreate.push(product)
return acc return acc
}, },
@@ -52,12 +52,21 @@ export const parseProductCsvStep = createStep(
} }
}) })
const allRegions = await regionService.listRegions( const [allRegions, allTags] = await Promise.all([
{}, regionService.listRegions(
{ select: ["id", "name", "currency_code"], take: null } {},
) { select: ["id", "name", "currency_code"], take: null }
),
productService.listProductTags(
{},
{ select: ["id", "value"], take: null }
),
])
const normalizedData = normalizeForImport(v1Normalized, allRegions) const normalizedData = normalizeForImport(v1Normalized, {
regions: allRegions,
tags: allTags,
})
return new StepResponse(normalizedData) return new StepResponse(normalizedData)
} }
) )
@@ -65,7 +65,7 @@ export interface AdminCreateProduct {
type_id?: string type_id?: string
collection_id?: string collection_id?: string
categories?: { id: string }[] categories?: { id: string }[]
tags?: { id?: string; value?: string }[] tags?: { id: string }[]
options?: AdminCreateProductOption[] options?: AdminCreateProductOption[]
variants?: AdminCreateProductVariant[] variants?: AdminCreateProductVariant[]
sales_channels?: { id: string }[] sales_channels?: { id: string }[]
@@ -115,9 +115,9 @@ export interface AdminUpdateProduct {
type_id?: string | null type_id?: string | null
collection_id?: string | null collection_id?: string | null
categories?: { id: string }[] categories?: { id: string }[]
tags?: { id?: string; value?: string }[] tags?: { id: string }[]
options?: AdminUpdateProductOption[] options?: AdminUpdateProductOption[]
variants?: AdminCreateProductVariant[] variants?: (AdminCreateProductVariant | AdminUpdateProductVariant)[]
sales_channels?: { id: string }[] sales_channels?: { id: string }[]
weight?: number | null weight?: number | null
length?: number | null length?: number | null
+7 -12
View File
@@ -702,25 +702,20 @@ export interface FilterableProductProps
*/ */
tags?: { tags?: {
/** /**
* Values to filter product tags by. * Filter a product by the IDs of their associated tags.
*/ */
value?: string[] id?: string[]
} }
/** /**
* Filters on a product's variant properties. * Filters on a product's variant properties.
*/ */
variants?: { variants?: {
options: { value: string; option_id: string } options?: { value: string; option_id: string }
} }
/** /**
* Filter a product by the ID of the associated type * Filter a product by the ID of the associated type
*/ */
type_id?: string | string[] type_id?: string | string[]
/**
* @deprecated - Use `categories` instead
* Filter a product by the IDs of their associated categories.
*/
category_id?: string | string[] | OperatorMap<string>
/** /**
* Filter a product by the IDs of their associated categories. * Filter a product by the IDs of their associated categories.
*/ */
@@ -1417,9 +1412,9 @@ export interface CreateProductDTO {
*/ */
collection_id?: string collection_id?: string
/** /**
* The associated tags to be created or updated. * The tags to be associated with the product.
*/ */
tags?: UpsertProductTagDTO[] tag_ids?: string[]
/** /**
* The product categories to associate with the product. * The product categories to associate with the product.
*/ */
@@ -1533,9 +1528,9 @@ export interface UpdateProductDTO {
*/ */
collection_id?: string | null collection_id?: string | null
/** /**
* The associated tags to create or update. * The tags to associate with the product
*/ */
tags?: UpsertProductTagDTO[] tag_ids?: string[]
/** /**
* The product categories to associate with the product. * The product categories to associate with the product.
*/ */
@@ -1,7 +1,10 @@
import { BatchMethodRequest } from "@medusajs/types" import { BatchMethodRequest } from "@medusajs/types"
import { ProductStatus } from "@medusajs/utils" import { ProductStatus } from "@medusajs/utils"
import { z } from "zod" import { z } from "zod"
import { GetProductsParams } from "../../utils/common-validators" import {
GetProductsParams,
transformProductParams,
} from "../../utils/common-validators"
import { import {
createFindParams, createFindParams,
createOperatorMap, createOperatorMap,
@@ -38,17 +41,19 @@ export type AdminGetProductsParamsType = z.infer<typeof AdminGetProductsParams>
export const AdminGetProductsParams = createFindParams({ export const AdminGetProductsParams = createFindParams({
offset: 0, offset: 0,
limit: 50, limit: 50,
}).merge( })
z .merge(
.object({ z
variants: AdminGetProductVariantsParams.optional(), .object({
price_list_id: z.string().array().optional(), variants: AdminGetProductVariantsParams.optional(),
status: statusEnum.array().optional(), price_list_id: z.string().array().optional(),
$and: z.lazy(() => AdminGetProductsParams.array()).optional(), status: statusEnum.array().optional(),
$or: z.lazy(() => AdminGetProductsParams.array()).optional(), $and: z.lazy(() => AdminGetProductsParams.array()).optional(),
}) $or: z.lazy(() => AdminGetProductsParams.array()).optional(),
.merge(GetProductsParams) })
) .merge(GetProductsParams)
)
.transform(transformProductParams)
export type AdminGetProductOptionsParamsType = z.infer< export type AdminGetProductOptionsParamsType = z.infer<
typeof AdminGetProductOptionsParams typeof AdminGetProductOptionsParams
@@ -195,7 +200,7 @@ export const AdminBatchUpdateProductVariant = AdminUpdateProductVariant.extend({
id: z.string(), id: z.string(),
}) })
export const AdminCreateProductProductCategory = z.object({ export const IdAssociation = z.object({
id: z.string(), id: z.string(),
}) })
@@ -213,8 +218,8 @@ export const AdminCreateProduct = z
status: statusEnum.nullish().default(ProductStatus.DRAFT), status: statusEnum.nullish().default(ProductStatus.DRAFT),
type_id: z.string().nullish(), type_id: z.string().nullish(),
collection_id: z.string().nullish(), collection_id: z.string().nullish(),
categories: z.array(AdminCreateProductProductCategory).optional(), categories: z.array(IdAssociation).optional(),
tags: z.array(AdminUpdateProductTag).optional(), tags: z.array(IdAssociation).optional(),
options: z.array(AdminCreateProductOption).optional(), options: z.array(AdminCreateProductOption).optional(),
variants: z.array(AdminCreateProductVariant).optional(), variants: z.array(AdminCreateProductVariant).optional(),
sales_channels: z.array(z.object({ id: z.string() })).optional(), sales_channels: z.array(z.object({ id: z.string() })).optional(),
@@ -246,8 +251,8 @@ export const AdminUpdateProduct = z
handle: z.string().nullish(), handle: z.string().nullish(),
type_id: z.string().nullish(), type_id: z.string().nullish(),
collection_id: z.string().nullish(), collection_id: z.string().nullish(),
categories: z.array(AdminCreateProductProductCategory).optional(), categories: z.array(IdAssociation).optional(),
tags: z.array(AdminUpdateProductTag).optional(), tags: z.array(IdAssociation).optional(),
sales_channels: z.array(z.object({ id: z.string() })).optional(), sales_channels: z.array(z.object({ id: z.string() })).optional(),
weight: z.number().nullish(), weight: z.number().nullish(),
length: z.number().nullish(), length: z.number().nullish(),
@@ -271,13 +276,6 @@ export const AdminBatchUpdateProduct = AdminUpdateProduct.extend({
export type AdminExportProductType = z.infer<typeof AdminExportProduct> export type AdminExportProductType = z.infer<typeof AdminExportProduct>
export const AdminExportProduct = z.object({}) export const AdminExportProduct = z.object({})
// TODO: Handle in create and update product once ready
// @IsOptional()
// @Type(() => ProductProductCategoryReq)
// @ValidateNested({ each: true })
// @IsArray()
// categories?: ProductProductCategoryReq[]
export const AdminCreateVariantInventoryItem = z.object({ export const AdminCreateVariantInventoryItem = z.object({
required_quantity: z.number(), required_quantity: z.number(),
inventory_item_id: z.string(), inventory_item_id: z.string(),
@@ -2,6 +2,7 @@ import { z } from "zod"
import { import {
GetProductsParams, GetProductsParams,
ProductStatusEnum, ProductStatusEnum,
transformProductParams,
} from "../../utils/common-validators" } from "../../utils/common-validators"
import { import {
createFindParams, createFindParams,
@@ -45,28 +46,30 @@ export type StoreGetProductsParamsType = z.infer<typeof StoreGetProductsParams>
export const StoreGetProductsParams = createFindParams({ export const StoreGetProductsParams = createFindParams({
offset: 0, offset: 0,
limit: 50, limit: 50,
}).merge( })
z .merge(
.object({ z
// These are used to populate the tax and pricing context .object({
region_id: z.string().optional(), // These are used to populate the tax and pricing context
country_code: z.string().optional(), region_id: z.string().optional(),
province: z.string().optional(), country_code: z.string().optional(),
cart_id: z.string().optional(), province: z.string().optional(),
cart_id: z.string().optional(),
variants: z variants: z
.object({ .object({
status: ProductStatusEnum.array().optional(), status: ProductStatusEnum.array().optional(),
options: z options: z
.object({ value: z.string(), option_id: z.string() }) .object({ value: z.string(), option_id: z.string() })
.optional(), .optional(),
$and: z.lazy(() => StoreGetProductsParams.array()).optional(), $and: z.lazy(() => StoreGetProductsParams.array()).optional(),
$or: z.lazy(() => StoreGetProductsParams.array()).optional(), $or: z.lazy(() => StoreGetProductsParams.array()).optional(),
}) })
.optional(), .optional(),
$and: z.lazy(() => StoreGetProductsParams.array()).optional(), $and: z.lazy(() => StoreGetProductsParams.array()).optional(),
$or: z.lazy(() => StoreGetProductsParams.array()).optional(), $or: z.lazy(() => StoreGetProductsParams.array()).optional(),
}) })
.merge(GetProductsParams) .merge(GetProductsParams)
.strict() .strict()
) )
.transform(transformProductParams)
@@ -2,21 +2,58 @@ import { ProductStatus } from "@medusajs/utils"
import { z } from "zod" import { z } from "zod"
import { createOperatorMap } from "../../validators" import { createOperatorMap } from "../../validators"
import { OptionalBooleanValidator } from "../common" import { OptionalBooleanValidator } from "../common"
import { FilterableProductProps } from "@medusajs/types"
export const ProductStatusEnum = z.nativeEnum(ProductStatus) export const ProductStatusEnum = z.nativeEnum(ProductStatus)
export const GetProductsParams = z.object({ export const GetProductsParams = z.object({
q: z.string().optional(), q: z.string().optional(),
id: z.union([z.string(), z.array(z.string())]).optional(), id: z.union([z.string(), z.array(z.string())]).optional(),
title: z.string().nullish(), title: z.string().optional(),
handle: z.string().nullish(), handle: z.string().optional(),
is_giftcard: OptionalBooleanValidator, is_giftcard: OptionalBooleanValidator,
category_id: z.union([z.string(), z.array(z.string())]).nullish(), category_id: z.union([z.string(), z.array(z.string())]).optional(),
sales_channel_id: z.union([z.string(), z.array(z.string())]).nullish(), sales_channel_id: z.union([z.string(), z.array(z.string())]).optional(),
collection_id: z.union([z.string(), z.array(z.string())]).nullish(), collection_id: z.union([z.string(), z.array(z.string())]).optional(),
tags: z.union([z.string(), z.array(z.string())]).optional(), tag_id: z.union([z.string(), z.array(z.string())]).optional(),
type_id: z.union([z.string(), z.array(z.string())]).optional(), type_id: z.union([z.string(), z.array(z.string())]).optional(),
created_at: createOperatorMap().optional(), created_at: createOperatorMap().optional(),
updated_at: createOperatorMap().optional(), updated_at: createOperatorMap().optional(),
deleted_at: createOperatorMap().optional(), deleted_at: createOperatorMap().optional(),
}) })
type HttpProductFilters = FilterableProductProps & {
tag_id?: string | string[]
category_id?: string | string[]
}
export const transformProductParams = (
data: HttpProductFilters
): FilterableProductProps => {
const res = {
...data,
tags: normalizeArray(data, "tag_id"),
categories: normalizeArray(data, "category_id"),
}
delete res.tag_id
delete res.category_id
return res as FilterableProductProps
}
const normalizeArray = (filters: HttpProductFilters, key: string) => {
if (filters[key]) {
if (Array.isArray(filters[key])) {
return {
id: { $in: filters[key] },
}
} else {
return {
id: filters[key] as string,
}
}
}
return undefined
}
@@ -1,4 +1,8 @@
import { IProductModuleService, ProductCategoryDTO } from "@medusajs/types" import {
IProductModuleService,
ProductCategoryDTO,
ProductTagDTO,
} from "@medusajs/types"
import { kebabCase, Modules, ProductStatus } from "@medusajs/utils" import { kebabCase, Modules, ProductStatus } from "@medusajs/utils"
import { import {
Product, Product,
@@ -104,6 +108,11 @@ moduleIntegrationTestRunner<IProductModuleService>({
categories.push(await service.createProductCategories(entry)) categories.push(await service.createProductCategories(entry))
} }
const tags: ProductTagDTO[] = []
for (const entry of tagsData) {
tags.push(await service.createProductTags(entry))
}
productCategoryOne = categories[0] productCategoryOne = categories[0]
productCategoryTwo = categories[1] productCategoryTwo = categories[1]
@@ -125,7 +134,7 @@ moduleIntegrationTestRunner<IProductModuleService>({
status: ProductStatus.PUBLISHED, status: ProductStatus.PUBLISHED,
categories: [{ id: productCategoryOne.id }], categories: [{ id: productCategoryOne.id }],
collection_id: productCollectionOne.id, collection_id: productCollectionOne.id,
tags: tagsData, tags: [{ id: tags[0].id }],
options: [ options: [
{ {
title: "size", title: "size",
@@ -437,6 +446,8 @@ moduleIntegrationTestRunner<IProductModuleService>({
value: "tag 2", value: "tag 2",
} }
await service.createProductTags(newTagData)
const updateData = { const updateData = {
id: productTwo.id, id: productTwo.id,
categories: [ categories: [
@@ -446,7 +457,7 @@ moduleIntegrationTestRunner<IProductModuleService>({
], ],
collection_id: productCollectionTwo.id, collection_id: productCollectionTwo.id,
type_id: productTypeTwo.id, type_id: productTypeTwo.id,
tags: [newTagData], tags: [{ id: newTagData.id }],
} }
await service.upsertProducts([updateData]) await service.upsertProducts([updateData])
@@ -1,3 +1,2 @@
export { default as ProductService } from "./product"
export { default as ProductCategoryService } from "./product-category" export { default as ProductCategoryService } from "./product-category"
export { default as ProductModuleService } from "./product-module-service" export { default as ProductModuleService } from "./product-module-service"
@@ -18,7 +18,7 @@ import {
ProductType, ProductType,
ProductVariant, ProductVariant,
} from "@models" } from "@models"
import { ProductCategoryService, ProductService } from "@services" import { ProductCategoryService } from "@services"
import { import {
arrayDifference, arrayDifference,
@@ -55,7 +55,7 @@ import { joinerConfig } from "./../joiner-config"
type InjectedDependencies = { type InjectedDependencies = {
baseRepository: DAL.RepositoryService baseRepository: DAL.RepositoryService
productService: ProductService productService: ModulesSdkTypes.IMedusaInternalService<any, any>
productVariantService: ModulesSdkTypes.IMedusaInternalService<any, any> productVariantService: ModulesSdkTypes.IMedusaInternalService<any, any>
productTagService: ModulesSdkTypes.IMedusaInternalService<any> productTagService: ModulesSdkTypes.IMedusaInternalService<any>
productCategoryService: ProductCategoryService productCategoryService: ProductCategoryService
@@ -102,7 +102,7 @@ export default class ProductModuleService
implements ProductTypes.IProductModuleService implements ProductTypes.IProductModuleService
{ {
protected baseRepository_: DAL.RepositoryService protected baseRepository_: DAL.RepositoryService
protected readonly productService_: ProductService protected readonly productService_: ModulesSdkTypes.IMedusaInternalService<Product>
protected readonly productVariantService_: ModulesSdkTypes.IMedusaInternalService<ProductVariant> protected readonly productVariantService_: ModulesSdkTypes.IMedusaInternalService<ProductVariant>
protected readonly productCategoryService_: ProductCategoryService protected readonly productCategoryService_: ProductCategoryService
protected readonly productTagService_: ModulesSdkTypes.IMedusaInternalService<ProductTag> protected readonly productTagService_: ModulesSdkTypes.IMedusaInternalService<ProductTag>
@@ -1609,26 +1609,6 @@ export default class ProductModuleService
productData.discountable = false productData.discountable = false
} }
if (productData.tags?.length && productData.tags.some((t) => !t.id)) {
const dbTags = await this.productTagService_.list(
{
value: productData.tags
.map((t) => t.value)
.filter((v) => !!v) as string[],
},
{ take: null },
sharedContext
)
productData.tags = productData.tags.map((tag) => {
const dbTag = dbTags.find((t) => t.value === tag.value)
return {
...tag,
...(dbTag ? { id: dbTag.id } : {}),
}
})
}
if (productData.options?.length) { if (productData.options?.length) {
const dbOptions = await this.productOptionService_.list( const dbOptions = await this.productOptionService_.list(
{ product_id: productData.id }, { product_id: productData.id },
@@ -1652,6 +1632,13 @@ export default class ProductModuleService
}) })
} }
if (productData.tag_ids) {
;(productData as any).tags = productData.tag_ids.map((cid) => ({
id: cid,
}))
delete productData.tag_ids
}
if (productData.category_ids) { if (productData.category_ids) {
;(productData as any).categories = productData.category_ids.map( ;(productData as any).categories = productData.category_ids.map(
(cid) => ({ (cid) => ({
@@ -1,80 +0,0 @@
import {
Context,
DAL,
FilterableProductProps,
FindConfig,
ProductTypes,
} from "@medusajs/types"
import { InjectManager, MedusaContext, ModulesSdkUtils } from "@medusajs/utils"
import { Product } from "@models"
type InjectedDependencies = {
productRepository: DAL.RepositoryService
}
type NormalizedFilterableProductProps = ProductTypes.FilterableProductProps & {
categories?: {
id: string | { $in: string[] }
}
}
export default class ProductService extends ModulesSdkUtils.MedusaInternalService<
InjectedDependencies,
Product
>(Product) {
protected readonly productRepository_: DAL.RepositoryService<Product>
constructor({ productRepository }: InjectedDependencies) {
// @ts-ignore
// eslint-disable-next-line prefer-rest-params
super(...arguments)
this.productRepository_ = productRepository
}
@InjectManager("productRepository_")
async list(
filters: ProductTypes.FilterableProductProps = {},
config: FindConfig<Product> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<Product[]> {
return await super.list(
ProductService.normalizeFilters(filters),
config,
sharedContext
)
}
@InjectManager("productRepository_")
async listAndCount(
filters: ProductTypes.FilterableProductProps = {},
config: FindConfig<any> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<[Product[], number]> {
return await super.listAndCount(
ProductService.normalizeFilters(filters),
config,
sharedContext
)
}
protected static normalizeFilters(
filters: FilterableProductProps = {}
): NormalizedFilterableProductProps {
const normalized = filters as NormalizedFilterableProductProps
if (normalized.category_id) {
if (Array.isArray(normalized.category_id)) {
normalized.categories = {
id: { $in: normalized.category_id },
}
} else {
normalized.categories = {
id: normalized.category_id as string,
}
}
delete normalized.category_id
}
return normalized
}
}