diff --git a/integration-tests/helpers/fixtures.ts b/integration-tests/helpers/fixtures.ts
index 6bd8c777c0..63a3f7807a 100644
--- a/integration-tests/helpers/fixtures.ts
+++ b/integration-tests/helpers/fixtures.ts
@@ -8,7 +8,6 @@ export const getProductFixture = (
status: "draft",
// BREAKING: Images input changed from string[] to {url: string}[]
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[]}[]
options: [
{ title: "size", values: ["large", "small"] },
diff --git a/integration-tests/http/__tests__/product/admin/product-export.spec.ts b/integration-tests/http/__tests__/product/admin/product-export.spec.ts
index bfcb0d9137..e10e0505d0 100644
--- a/integration-tests/http/__tests__/product/admin/product-export.spec.ts
+++ b/integration-tests/http/__tests__/product/admin/product-export.spec.ts
@@ -50,6 +50,9 @@ medusaIntegrationTestRunner({
let baseType
let baseRegion
let baseCategory
+ let baseTag1
+ let baseTag2
+ let newTag
let eventBus: IEventBusModuleService
beforeAll(async () => {
@@ -102,6 +105,22 @@ medusaIntegrationTestRunner({
)
).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 = (
await api.post(
"/admin/products",
@@ -111,6 +130,7 @@ medusaIntegrationTestRunner({
collection_id: baseCollection.id,
type_id: baseType.id,
categories: [{ id: baseCategory.id }],
+ tags: [{ id: baseTag1.id }, { id: baseTag2.id }],
variants: [
{
title: "Test variant",
@@ -166,7 +186,7 @@ medusaIntegrationTestRunner({
getProductFixture({
title: "Proposed product",
status: "proposed",
- tags: [{ value: "new-tag" }],
+ tags: [{ id: newTag.id }],
type_id: baseType.id,
}),
adminHeaders
@@ -258,6 +278,7 @@ medusaIntegrationTestRunner({
"/admin/products",
getProductFixture({
title: "Product with prices",
+ tags: [{ id: baseTag1.id }, { id: baseTag2.id }],
variants: [
{
title: "Test variant",
diff --git a/integration-tests/http/__tests__/product/admin/product-import.spec.ts b/integration-tests/http/__tests__/product/admin/product-import.spec.ts
index da9b5a35e5..172f93011b 100644
--- a/integration-tests/http/__tests__/product/admin/product-import.spec.ts
+++ b/integration-tests/http/__tests__/product/admin/product-import.spec.ts
@@ -33,6 +33,10 @@ medusaIntegrationTestRunner({
let baseProduct
let baseRegion
let baseCategory
+ let baseTag1
+ let baseTag2
+ let baseTag3
+ let newTag
let eventBus: IEventBusModuleService
beforeAll(async () => {
@@ -57,11 +61,32 @@ medusaIntegrationTestRunner({
)
).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 = (
await api.post(
"/admin/products",
getProductFixture({
title: "Base product",
+ tags: [{ id: baseTag1.id }, { id: baseTag2.id }],
}),
adminHeaders
)
diff --git a/integration-tests/http/__tests__/product/admin/product.spec.ts b/integration-tests/http/__tests__/product/admin/product.spec.ts
index 14578f4200..688b5a7c41 100644
--- a/integration-tests/http/__tests__/product/admin/product.spec.ts
+++ b/integration-tests/http/__tests__/product/admin/product.spec.ts
@@ -18,6 +18,9 @@ medusaIntegrationTestRunner({
let publishedCollection
let baseType
+ let baseTag1
+ let baseTag2
+ let newTag
beforeEach(async () => {
await createAdminUser(dbConnection, adminHeaders, getContainer())
@@ -46,6 +49,22 @@ medusaIntegrationTestRunner({
)
).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 = (
await api.post(
"/admin/products",
@@ -54,6 +73,7 @@ medusaIntegrationTestRunner({
collection_id: baseCollection.id,
// BREAKING: Type input changed from {type: {value: string}} to {type_id: string}
type_id: baseType.id,
+ tags: [{ id: baseTag1.id }, { id: baseTag2.id }],
}),
adminHeaders
)
@@ -65,7 +85,7 @@ medusaIntegrationTestRunner({
getProductFixture({
title: "Proposed product",
status: "proposed",
- tags: [{ value: "new-tag" }],
+ tags: [{ id: newTag.id }],
type_id: baseType.id,
}),
adminHeaders
@@ -79,6 +99,7 @@ medusaIntegrationTestRunner({
title: "Published product",
status: "published",
collection_id: publishedCollection.id,
+ tags: [{ id: baseTag1.id }, { id: baseTag2.id }],
}),
adminHeaders
)
@@ -507,7 +528,7 @@ medusaIntegrationTestRunner({
it("returns a list of products with tags", async () => {
const response = await api.get(
- `/admin/products?tags[]=${baseProduct.tags[0].id}`,
+ `/admin/products?tag_id[]=${baseProduct.tags[0].id}`,
adminHeaders
)
@@ -534,7 +555,7 @@ medusaIntegrationTestRunner({
it("returns a list of products with tags in a collection", async () => {
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
)
@@ -1082,6 +1103,7 @@ medusaIntegrationTestRunner({
title: "Test create",
collection_id: baseCollection.id,
type_id: baseType.id,
+ tags: [{ id: baseTag1.id }, { id: baseTag2.id }],
}),
adminHeaders
@@ -1276,7 +1298,7 @@ medusaIntegrationTestRunner({
description: "test-product-description",
images: [{ url: "test-image.png" }, { url: "test-image-2.png" }],
collection_id: baseCollection.id,
- tags: [{ value: "123" }, { value: "456" }],
+ tags: [{ id: baseTag1.id }, { id: baseTag2.id }],
variants: [
{
title: "Test variant",
@@ -1306,7 +1328,7 @@ medusaIntegrationTestRunner({
description: "test-product-description 1",
images: [{ url: "test-image.png" }, { url: "test-image-2.png" }],
collection_id: baseCollection.id,
- tags: [{ value: "123" }, { value: "456" }],
+ tags: [{ id: baseTag1.id }, { id: baseTag2.id }],
variants: [
{
title: "Test variant 1",
@@ -1406,7 +1428,7 @@ medusaIntegrationTestRunner({
],
},
],
- tags: [{ value: "123" }],
+ tags: [{ id: baseTag1.id }],
images: [{ url: "test-image-2.png" }],
status: "published",
}
diff --git a/integration-tests/http/__tests__/product/store/product.spec.ts b/integration-tests/http/__tests__/product/store/product.spec.ts
index d9bf817c5f..42ee397dd3 100644
--- a/integration-tests/http/__tests__/product/store/product.spec.ts
+++ b/integration-tests/http/__tests__/product/store/product.spec.ts
@@ -18,6 +18,7 @@ medusaIntegrationTestRunner({
let store
let appContainer
let collection
+ let tag
let product
let product1
let product2
@@ -496,6 +497,9 @@ medusaIntegrationTestRunner({
adminHeaders
)
).data.collection
+ tag = (
+ await api.post("/admin/product-tags", { value: "tag1" }, adminHeaders)
+ ).data.product_tag
;[product, [variant]] = await createProducts({
title: "test product 1",
collection_id: collection.id,
@@ -504,7 +508,7 @@ medusaIntegrationTestRunner({
{ title: "size", values: ["large", "small"] },
{ title: "color", values: ["green"] },
],
- tags: [{ value: "tag1" }],
+ tags: [{ id: tag.id }],
variants: [
{
title: "test variant 1",
@@ -726,7 +730,7 @@ medusaIntegrationTestRunner({
it("returns a list of products with a given tag", async () => {
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)
diff --git a/integration-tests/modules/__tests__/cart/store/carts.spec.ts b/integration-tests/modules/__tests__/cart/store/carts.spec.ts
index f589ff4624..18ce45bd7c 100644
--- a/integration-tests/modules/__tests__/cart/store/carts.spec.ts
+++ b/integration-tests/modules/__tests__/cart/store/carts.spec.ts
@@ -1899,7 +1899,6 @@ medusaIntegrationTestRunner({
"/admin/products",
{
title: "Test fixture",
- tags: [{ value: "123" }, { value: "456" }],
options: [
{ title: "size", values: ["large", "small"] },
{ title: "color", values: ["green"] },
diff --git a/integration-tests/modules/__tests__/shipping-options/store/shipping-options.spec.ts b/integration-tests/modules/__tests__/shipping-options/store/shipping-options.spec.ts
index b7046a1635..09d9f804c0 100644
--- a/integration-tests/modules/__tests__/shipping-options/store/shipping-options.spec.ts
+++ b/integration-tests/modules/__tests__/shipping-options/store/shipping-options.spec.ts
@@ -65,7 +65,6 @@ medusaIntegrationTestRunner({
"/admin/products",
{
title: "Test fixture",
- tags: [{ value: "123" }, { value: "456" }],
options: [
{ title: "size", values: ["large", "small"] },
{ title: "color", values: ["green"] },
diff --git a/packages/admin-next/dashboard/src/components/layout/notifications/notifications.tsx b/packages/admin-next/dashboard/src/components/layout/notifications/notifications.tsx
index 6786f5f774..32d92ece6b 100644
--- a/packages/admin-next/dashboard/src/components/layout/notifications/notifications.tsx
+++ b/packages/admin-next/dashboard/src/components/layout/notifications/notifications.tsx
@@ -51,8 +51,8 @@ export const Notifications = () => {
}
}, [])
- const handleOnOpen = (isOpen: boolean) => {
- if (isOpen) {
+ const handleOnOpen = (shouldOpen: boolean) => {
+ if (shouldOpen) {
setHasUnread(false)
setOpen(true)
localStorage.setItem(LAST_READ_NOTIFICATION_KEY, new Date().toISOString())
diff --git a/packages/admin-next/dashboard/src/hooks/table/filters/use-product-table-filters.tsx b/packages/admin-next/dashboard/src/hooks/table/filters/use-product-table-filters.tsx
index 8fc0035c6a..1fddaca378 100644
--- a/packages/admin-next/dashboard/src/hooks/table/filters/use-product-table-filters.tsx
+++ b/packages/admin-next/dashboard/src/hooks/table/filters/use-product-table-filters.tsx
@@ -95,7 +95,7 @@ export const useProductTableFilters = (
if (product_tags && !isProductTagExcluded) {
const tagFilter: Filter = {
- key: "tags",
+ key: "tag_id",
label: t("fields.tag"),
type: "select",
multiple: true,
@@ -108,21 +108,6 @@ export const useProductTableFilters = (
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) {
const salesChannelFilter: Filter = {
key: "sales_channel_id",
diff --git a/packages/admin-next/dashboard/src/hooks/table/query/use-product-table-query.tsx b/packages/admin-next/dashboard/src/hooks/table/query/use-product-table-query.tsx
index 1cb7fb1790..b747efc0c6 100644
--- a/packages/admin-next/dashboard/src/hooks/table/query/use-product-table-query.tsx
+++ b/packages/admin-next/dashboard/src/hooks/table/query/use-product-table-query.tsx
@@ -21,7 +21,7 @@ export const useProductTableQuery = ({
"category_id",
"collection_id",
"is_giftcard",
- "tags",
+ "tag_id",
"type_id",
"status",
"id",
@@ -36,7 +36,7 @@ export const useProductTableQuery = ({
updated_at,
category_id,
collection_id,
- tags,
+ tag_id,
type_id,
is_giftcard,
status,
@@ -54,7 +54,7 @@ export const useProductTableQuery = ({
collection_id: collection_id?.split(","),
is_giftcard: is_giftcard ? is_giftcard === "true" : undefined,
order: order,
- tags: tags ? { value: tags.split(",") } : undefined,
+ tag_id: tag_id ? tag_id.split(",") : undefined,
type_id: type_id?.split(","),
status: status?.split(",") as HttpTypes.AdminProductStatus[],
q,
diff --git a/packages/admin-next/dashboard/src/routes/product-tags/product-tag-detail/components/product-tag-product-section/product-tag-product-section.tsx b/packages/admin-next/dashboard/src/routes/product-tags/product-tag-detail/components/product-tag-product-section/product-tag-product-section.tsx
index 67aa0dd410..44b221e0e1 100644
--- a/packages/admin-next/dashboard/src/routes/product-tags/product-tag-detail/components/product-tag-product-section/product-tag-product-section.tsx
+++ b/packages/admin-next/dashboard/src/routes/product-tags/product-tag-detail/components/product-tag-product-section/product-tag-product-section.tsx
@@ -27,7 +27,7 @@ export const ProductTagProductSection = ({
const { products, count, isPending, isError, error } = useProducts({
...searchParams,
- tags: productTag.id,
+ tag_id: productTag.id,
})
const filters = useProductTableFilters(["product_tags"])
diff --git a/packages/admin-next/dashboard/src/routes/products/product-create/utils.ts b/packages/admin-next/dashboard/src/routes/products/product-create/utils.ts
index f4eccadcc4..506de39ee8 100644
--- a/packages/admin-next/dashboard/src/routes/products/product-create/utils.ts
+++ b/packages/admin-next/dashboard/src/routes/products/product-create/utils.ts
@@ -17,7 +17,7 @@ export const normalizeProductFormValues = (
status: values.status,
is_giftcard: false,
tags: values?.tags?.length
- ? values.tags?.map((tag) => ({ value: tag }))
+ ? values.tags?.map((tag) => ({ id: tag }))
: undefined,
sales_channels: values?.sales_channels?.length
? values.sales_channels?.map((sc) => ({ id: sc.id }))
diff --git a/packages/admin-next/dashboard/src/routes/products/product-detail/components/product-organization-section/product-organization-section.tsx b/packages/admin-next/dashboard/src/routes/products/product-detail/components/product-organization-section/product-organization-section.tsx
index 01ecd5bb6a..d37107a4d3 100644
--- a/packages/admin-next/dashboard/src/routes/products/product-detail/components/product-organization-section/product-organization-section.tsx
+++ b/packages/admin-next/dashboard/src/routes/products/product-detail/components/product-organization-section/product-organization-section.tsx
@@ -40,7 +40,7 @@ export const ProductOrganizationSection = ({
product.tags?.length
? product.tags.map((tag) => (
- {tag.value}
+ {tag.value}
))
: undefined
diff --git a/packages/admin-next/dashboard/src/routes/products/product-organization/components/product-organization-form/product-organization-form.tsx b/packages/admin-next/dashboard/src/routes/products/product-organization/components/product-organization-form/product-organization-form.tsx
index 0e6171c04b..83fee51fce 100644
--- a/packages/admin-next/dashboard/src/routes/products/product-organization/components/product-organization-form/product-organization-form.tsx
+++ b/packages/admin-next/dashboard/src/routes/products/product-organization/components/product-organization-form/product-organization-form.tsx
@@ -78,9 +78,9 @@ export const ProductOrganizationForm = ({
collection_id: data.collection_id || undefined,
categories: data.category_ids.map((id) => ({ id })) || undefined,
tags:
- data.tag_ids?.map((t) => {
- t
- }) || undefined,
+ data.tag_ids?.map((t) => ({
+ id: t,
+ })) || undefined,
},
{
onSuccess: ({ product }) => {
diff --git a/packages/core/core-flows/src/product/helpers/normalize-for-import.ts b/packages/core/core-flows/src/product/helpers/normalize-for-import.ts
index 5bedbe1f07..80ff095540 100644
--- a/packages/core/core-flows/src/product/helpers/normalize-for-import.ts
+++ b/packages/core/core-flows/src/product/helpers/normalize-for-import.ts
@@ -1,10 +1,14 @@
+import { ProductTypes } from "@medusajs/types"
import { HttpTypes, RegionTypes } from "@medusajs/types"
import { MedusaError, lowerCaseFirst } from "@medusajs/utils"
// We want to convert the csv data format to a standard DTO format.
export const normalizeForImport = (
rawProducts: object[],
- regions: RegionTypes.RegionDTO[]
+ additional: {
+ regions: RegionTypes.RegionDTO[]
+ tags: ProductTypes.ProductTagDTO[]
+ }
): HttpTypes.AdminCreateProduct[] => {
const productMap = new Map<
string,
@@ -15,13 +19,16 @@ export const normalizeForImport = (
>()
// 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) => {
const productInMap = productMap.get(rawProduct["Product Handle"])
if (!productInMap) {
productMap.set(rawProduct["Product Handle"], {
- product: normalizeProductForImport(rawProduct),
+ product: normalizeProductForImport(rawProduct, tagsMap),
variants: [normalizeVariantForImport(rawProduct, regionsMap)],
})
return
@@ -86,7 +93,8 @@ const booleanFields = [
]
const normalizeProductForImport = (
- rawProduct: object
+ rawProduct: object,
+ tagsMap: Map
): HttpTypes.AdminCreateProduct => {
const response = {}
@@ -108,10 +116,15 @@ const normalizeProductForImport = (
}
if (normalizedKey.startsWith("product_tag_")) {
- response["tags"] = [
- ...(response["tags"] || []),
- { value: normalizedValue },
- ]
+ const tag = tagsMap.get(normalizedValue)
+ if (!tag) {
+ throw new MedusaError(
+ MedusaError.Types.INVALID_DATA,
+ `Tag with value ${normalizedValue} not found`
+ )
+ }
+
+ response["tags"] = [...(response["tags"] || []), { id: tag.id }]
return
}
diff --git a/packages/core/core-flows/src/product/steps/group-products-for-batch.ts b/packages/core/core-flows/src/product/steps/group-products-for-batch.ts
index 64344b27cf..614a642dcc 100644
--- a/packages/core/core-flows/src/product/steps/group-products-for-batch.ts
+++ b/packages/core/core-flows/src/product/steps/group-products-for-batch.ts
@@ -38,9 +38,13 @@ export const groupProductsForBatchStep = createStep(
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.
delete product.id
+ product.variants?.forEach((variant) => {
+ delete (variant as any).id
+ })
+
acc.toCreate.push(product)
return acc
},
diff --git a/packages/core/core-flows/src/product/steps/parse-product-csv.ts b/packages/core/core-flows/src/product/steps/parse-product-csv.ts
index 49aaadea0a..f2125d78ba 100644
--- a/packages/core/core-flows/src/product/steps/parse-product-csv.ts
+++ b/packages/core/core-flows/src/product/steps/parse-product-csv.ts
@@ -52,12 +52,21 @@ export const parseProductCsvStep = createStep(
}
})
- const allRegions = await regionService.listRegions(
- {},
- { select: ["id", "name", "currency_code"], take: null }
- )
+ const [allRegions, allTags] = await Promise.all([
+ regionService.listRegions(
+ {},
+ { 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)
}
)
diff --git a/packages/core/types/src/http/product/admin/payloads.ts b/packages/core/types/src/http/product/admin/payloads.ts
index 7958e8b823..588cde8095 100644
--- a/packages/core/types/src/http/product/admin/payloads.ts
+++ b/packages/core/types/src/http/product/admin/payloads.ts
@@ -65,7 +65,7 @@ export interface AdminCreateProduct {
type_id?: string
collection_id?: string
categories?: { id: string }[]
- tags?: { id?: string; value?: string }[]
+ tags?: { id: string }[]
options?: AdminCreateProductOption[]
variants?: AdminCreateProductVariant[]
sales_channels?: { id: string }[]
@@ -115,9 +115,9 @@ export interface AdminUpdateProduct {
type_id?: string | null
collection_id?: string | null
categories?: { id: string }[]
- tags?: { id?: string; value?: string }[]
+ tags?: { id: string }[]
options?: AdminUpdateProductOption[]
- variants?: AdminCreateProductVariant[]
+ variants?: (AdminCreateProductVariant | AdminUpdateProductVariant)[]
sales_channels?: { id: string }[]
weight?: number | null
length?: number | null
diff --git a/packages/core/types/src/product/common.ts b/packages/core/types/src/product/common.ts
index 8b439210a0..6d88717085 100644
--- a/packages/core/types/src/product/common.ts
+++ b/packages/core/types/src/product/common.ts
@@ -702,25 +702,20 @@ export interface FilterableProductProps
*/
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.
*/
variants?: {
- options: { value: string; option_id: string }
+ options?: { value: string; option_id: string }
}
/**
* Filter a product by the ID of the associated type
*/
type_id?: string | string[]
- /**
- * @deprecated - Use `categories` instead
- * Filter a product by the IDs of their associated categories.
- */
- category_id?: string | string[] | OperatorMap
/**
* Filter a product by the IDs of their associated categories.
*/
@@ -1417,9 +1412,9 @@ export interface CreateProductDTO {
*/
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.
*/
@@ -1533,9 +1528,9 @@ export interface UpdateProductDTO {
*/
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.
*/
diff --git a/packages/medusa/src/api/admin/products/validators.ts b/packages/medusa/src/api/admin/products/validators.ts
index 9f21153942..361ea59e77 100644
--- a/packages/medusa/src/api/admin/products/validators.ts
+++ b/packages/medusa/src/api/admin/products/validators.ts
@@ -1,7 +1,10 @@
import { BatchMethodRequest } from "@medusajs/types"
import { ProductStatus } from "@medusajs/utils"
import { z } from "zod"
-import { GetProductsParams } from "../../utils/common-validators"
+import {
+ GetProductsParams,
+ transformProductParams,
+} from "../../utils/common-validators"
import {
createFindParams,
createOperatorMap,
@@ -38,17 +41,19 @@ export type AdminGetProductsParamsType = z.infer
export const AdminGetProductsParams = createFindParams({
offset: 0,
limit: 50,
-}).merge(
- z
- .object({
- variants: AdminGetProductVariantsParams.optional(),
- price_list_id: z.string().array().optional(),
- status: statusEnum.array().optional(),
- $and: z.lazy(() => AdminGetProductsParams.array()).optional(),
- $or: z.lazy(() => AdminGetProductsParams.array()).optional(),
- })
- .merge(GetProductsParams)
-)
+})
+ .merge(
+ z
+ .object({
+ variants: AdminGetProductVariantsParams.optional(),
+ price_list_id: z.string().array().optional(),
+ status: statusEnum.array().optional(),
+ $and: z.lazy(() => AdminGetProductsParams.array()).optional(),
+ $or: z.lazy(() => AdminGetProductsParams.array()).optional(),
+ })
+ .merge(GetProductsParams)
+ )
+ .transform(transformProductParams)
export type AdminGetProductOptionsParamsType = z.infer<
typeof AdminGetProductOptionsParams
@@ -195,7 +200,7 @@ export const AdminBatchUpdateProductVariant = AdminUpdateProductVariant.extend({
id: z.string(),
})
-export const AdminCreateProductProductCategory = z.object({
+export const IdAssociation = z.object({
id: z.string(),
})
@@ -213,8 +218,8 @@ export const AdminCreateProduct = z
status: statusEnum.nullish().default(ProductStatus.DRAFT),
type_id: z.string().nullish(),
collection_id: z.string().nullish(),
- categories: z.array(AdminCreateProductProductCategory).optional(),
- tags: z.array(AdminUpdateProductTag).optional(),
+ categories: z.array(IdAssociation).optional(),
+ tags: z.array(IdAssociation).optional(),
options: z.array(AdminCreateProductOption).optional(),
variants: z.array(AdminCreateProductVariant).optional(),
sales_channels: z.array(z.object({ id: z.string() })).optional(),
@@ -246,8 +251,8 @@ export const AdminUpdateProduct = z
handle: z.string().nullish(),
type_id: z.string().nullish(),
collection_id: z.string().nullish(),
- categories: z.array(AdminCreateProductProductCategory).optional(),
- tags: z.array(AdminUpdateProductTag).optional(),
+ categories: z.array(IdAssociation).optional(),
+ tags: z.array(IdAssociation).optional(),
sales_channels: z.array(z.object({ id: z.string() })).optional(),
weight: z.number().nullish(),
length: z.number().nullish(),
@@ -271,13 +276,6 @@ export const AdminBatchUpdateProduct = AdminUpdateProduct.extend({
export type AdminExportProductType = z.infer
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({
required_quantity: z.number(),
inventory_item_id: z.string(),
diff --git a/packages/medusa/src/api/store/products/validators.ts b/packages/medusa/src/api/store/products/validators.ts
index 2d3c4ae09d..bb6c3dd89d 100644
--- a/packages/medusa/src/api/store/products/validators.ts
+++ b/packages/medusa/src/api/store/products/validators.ts
@@ -2,6 +2,7 @@ import { z } from "zod"
import {
GetProductsParams,
ProductStatusEnum,
+ transformProductParams,
} from "../../utils/common-validators"
import {
createFindParams,
@@ -45,28 +46,30 @@ export type StoreGetProductsParamsType = z.infer
export const StoreGetProductsParams = createFindParams({
offset: 0,
limit: 50,
-}).merge(
- z
- .object({
- // These are used to populate the tax and pricing context
- region_id: z.string().optional(),
- country_code: z.string().optional(),
- province: z.string().optional(),
- cart_id: z.string().optional(),
+})
+ .merge(
+ z
+ .object({
+ // These are used to populate the tax and pricing context
+ region_id: z.string().optional(),
+ country_code: z.string().optional(),
+ province: z.string().optional(),
+ cart_id: z.string().optional(),
- variants: z
- .object({
- status: ProductStatusEnum.array().optional(),
- options: z
- .object({ value: z.string(), option_id: z.string() })
- .optional(),
- $and: z.lazy(() => StoreGetProductsParams.array()).optional(),
- $or: z.lazy(() => StoreGetProductsParams.array()).optional(),
- })
- .optional(),
- $and: z.lazy(() => StoreGetProductsParams.array()).optional(),
- $or: z.lazy(() => StoreGetProductsParams.array()).optional(),
- })
- .merge(GetProductsParams)
- .strict()
-)
+ variants: z
+ .object({
+ status: ProductStatusEnum.array().optional(),
+ options: z
+ .object({ value: z.string(), option_id: z.string() })
+ .optional(),
+ $and: z.lazy(() => StoreGetProductsParams.array()).optional(),
+ $or: z.lazy(() => StoreGetProductsParams.array()).optional(),
+ })
+ .optional(),
+ $and: z.lazy(() => StoreGetProductsParams.array()).optional(),
+ $or: z.lazy(() => StoreGetProductsParams.array()).optional(),
+ })
+ .merge(GetProductsParams)
+ .strict()
+ )
+ .transform(transformProductParams)
diff --git a/packages/medusa/src/api/utils/common-validators/products/index.ts b/packages/medusa/src/api/utils/common-validators/products/index.ts
index 226f06cf6a..af14d126f9 100644
--- a/packages/medusa/src/api/utils/common-validators/products/index.ts
+++ b/packages/medusa/src/api/utils/common-validators/products/index.ts
@@ -2,21 +2,58 @@ import { ProductStatus } from "@medusajs/utils"
import { z } from "zod"
import { createOperatorMap } from "../../validators"
import { OptionalBooleanValidator } from "../common"
+import { FilterableProductProps } from "@medusajs/types"
export const ProductStatusEnum = z.nativeEnum(ProductStatus)
export const GetProductsParams = z.object({
q: z.string().optional(),
id: z.union([z.string(), z.array(z.string())]).optional(),
- title: z.string().nullish(),
- handle: z.string().nullish(),
+ title: z.string().optional(),
+ handle: z.string().optional(),
is_giftcard: OptionalBooleanValidator,
- category_id: z.union([z.string(), z.array(z.string())]).nullish(),
- sales_channel_id: z.union([z.string(), z.array(z.string())]).nullish(),
- collection_id: z.union([z.string(), z.array(z.string())]).nullish(),
- tags: z.union([z.string(), z.array(z.string())]).optional(),
+ category_id: z.union([z.string(), z.array(z.string())]).optional(),
+ sales_channel_id: z.union([z.string(), z.array(z.string())]).optional(),
+ collection_id: 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(),
created_at: createOperatorMap().optional(),
updated_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
+}
diff --git a/packages/modules/product/integration-tests/__tests__/product-module-service/products.spec.ts b/packages/modules/product/integration-tests/__tests__/product-module-service/products.spec.ts
index 77bf616aac..8b96064d45 100644
--- a/packages/modules/product/integration-tests/__tests__/product-module-service/products.spec.ts
+++ b/packages/modules/product/integration-tests/__tests__/product-module-service/products.spec.ts
@@ -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 {
Product,
@@ -104,6 +108,11 @@ moduleIntegrationTestRunner({
categories.push(await service.createProductCategories(entry))
}
+ const tags: ProductTagDTO[] = []
+ for (const entry of tagsData) {
+ tags.push(await service.createProductTags(entry))
+ }
+
productCategoryOne = categories[0]
productCategoryTwo = categories[1]
@@ -125,7 +134,7 @@ moduleIntegrationTestRunner({
status: ProductStatus.PUBLISHED,
categories: [{ id: productCategoryOne.id }],
collection_id: productCollectionOne.id,
- tags: tagsData,
+ tags: [{ id: tags[0].id }],
options: [
{
title: "size",
@@ -437,6 +446,8 @@ moduleIntegrationTestRunner({
value: "tag 2",
}
+ await service.createProductTags(newTagData)
+
const updateData = {
id: productTwo.id,
categories: [
@@ -446,7 +457,7 @@ moduleIntegrationTestRunner({
],
collection_id: productCollectionTwo.id,
type_id: productTypeTwo.id,
- tags: [newTagData],
+ tags: [{ id: newTagData.id }],
}
await service.upsertProducts([updateData])
diff --git a/packages/modules/product/src/services/index.ts b/packages/modules/product/src/services/index.ts
index b880627471..d287a5893d 100644
--- a/packages/modules/product/src/services/index.ts
+++ b/packages/modules/product/src/services/index.ts
@@ -1,3 +1,2 @@
-export { default as ProductService } from "./product"
export { default as ProductCategoryService } from "./product-category"
export { default as ProductModuleService } from "./product-module-service"
diff --git a/packages/modules/product/src/services/product-module-service.ts b/packages/modules/product/src/services/product-module-service.ts
index 2384d12ff1..b32d5e0fb3 100644
--- a/packages/modules/product/src/services/product-module-service.ts
+++ b/packages/modules/product/src/services/product-module-service.ts
@@ -18,7 +18,7 @@ import {
ProductType,
ProductVariant,
} from "@models"
-import { ProductCategoryService, ProductService } from "@services"
+import { ProductCategoryService } from "@services"
import {
arrayDifference,
@@ -55,7 +55,7 @@ import { joinerConfig } from "./../joiner-config"
type InjectedDependencies = {
baseRepository: DAL.RepositoryService
- productService: ProductService
+ productService: ModulesSdkTypes.IMedusaInternalService
productVariantService: ModulesSdkTypes.IMedusaInternalService
productTagService: ModulesSdkTypes.IMedusaInternalService
productCategoryService: ProductCategoryService
@@ -102,7 +102,7 @@ export default class ProductModuleService
implements ProductTypes.IProductModuleService
{
protected baseRepository_: DAL.RepositoryService
- protected readonly productService_: ProductService
+ protected readonly productService_: ModulesSdkTypes.IMedusaInternalService
protected readonly productVariantService_: ModulesSdkTypes.IMedusaInternalService
protected readonly productCategoryService_: ProductCategoryService
protected readonly productTagService_: ModulesSdkTypes.IMedusaInternalService
@@ -1609,26 +1609,6 @@ export default class ProductModuleService
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) {
const dbOptions = await this.productOptionService_.list(
{ 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) {
;(productData as any).categories = productData.category_ids.map(
(cid) => ({
diff --git a/packages/modules/product/src/services/product.ts b/packages/modules/product/src/services/product.ts
deleted file mode 100644
index e6cedb94d6..0000000000
--- a/packages/modules/product/src/services/product.ts
+++ /dev/null
@@ -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
-
- 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 = {},
- @MedusaContext() sharedContext: Context = {}
- ): Promise {
- return await super.list(
- ProductService.normalizeFilters(filters),
- config,
- sharedContext
- )
- }
-
- @InjectManager("productRepository_")
- async listAndCount(
- filters: ProductTypes.FilterableProductProps = {},
- config: FindConfig = {},
- @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
- }
-}