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 - } -}