feat: Improve how options are defined and handled for product (#7007)
* feat: Improve how options are defined and handled for product * fix: Updating option values supports passing without id
This commit is contained in:
@@ -806,10 +806,8 @@ medusaIntegrationTestRunner({
|
|||||||
() =>
|
() =>
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: expect.stringMatching(/^varopt_*/),
|
id: expect.stringMatching(/^optval_*/),
|
||||||
option_value: expect.objectContaining({
|
value: "100",
|
||||||
value: "100",
|
|
||||||
}),
|
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
),
|
),
|
||||||
@@ -910,10 +908,8 @@ medusaIntegrationTestRunner({
|
|||||||
() =>
|
() =>
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: expect.stringMatching(/^varopt_*/),
|
id: expect.stringMatching(/^optval_*/),
|
||||||
option_value: expect.objectContaining({
|
value: "large",
|
||||||
value: "large",
|
|
||||||
}),
|
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
),
|
),
|
||||||
@@ -963,10 +959,8 @@ medusaIntegrationTestRunner({
|
|||||||
() =>
|
() =>
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: expect.stringMatching(/^varopt_*/),
|
id: expect.stringMatching(/^optval_*/),
|
||||||
option_value: expect.objectContaining({
|
value: "green",
|
||||||
value: "green",
|
|
||||||
}),
|
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
),
|
),
|
||||||
@@ -1394,21 +1388,17 @@ medusaIntegrationTestRunner({
|
|||||||
() =>
|
() =>
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: expect.stringMatching(/^varopt_*/),
|
id: expect.stringMatching(/^optval_*/),
|
||||||
option_value: expect.objectContaining({
|
value: "large",
|
||||||
value: "large",
|
option: expect.objectContaining({
|
||||||
option: expect.objectContaining({
|
title: "size",
|
||||||
title: "size",
|
|
||||||
}),
|
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: expect.stringMatching(/^varopt_*/),
|
id: expect.stringMatching(/^optval_*/),
|
||||||
option_value: expect.objectContaining({
|
value: "green",
|
||||||
value: "green",
|
option: expect.objectContaining({
|
||||||
option: expect.objectContaining({
|
title: "color",
|
||||||
title: "color",
|
|
||||||
}),
|
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
@@ -1661,12 +1651,10 @@ medusaIntegrationTestRunner({
|
|||||||
() =>
|
() =>
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: expect.stringMatching(/^varopt_*/),
|
id: expect.stringMatching(/^optval_*/),
|
||||||
option_value: expect.objectContaining({
|
value: "large",
|
||||||
value: "large",
|
option: expect.objectContaining({
|
||||||
option: expect.objectContaining({
|
title: "size",
|
||||||
title: "size",
|
|
||||||
}),
|
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
|||||||
+2
-2
@@ -82,7 +82,7 @@ export const useProductVariantTableColumns = (product?: Product) => {
|
|||||||
),
|
),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const variantOpt: any = row.original.options.find(
|
const variantOpt: any = row.original.options.find(
|
||||||
(opt: any) => opt.option_value.option_id === option.id
|
(opt: any) => opt.option_id === option.id
|
||||||
)
|
)
|
||||||
if (!variantOpt) {
|
if (!variantOpt) {
|
||||||
return <PlaceholderCell />
|
return <PlaceholderCell />
|
||||||
@@ -94,7 +94,7 @@ export const useProductVariantTableColumns = (product?: Product) => {
|
|||||||
size="2xsmall"
|
size="2xsmall"
|
||||||
className="flex min-w-[20px] items-center justify-center"
|
className="flex min-w-[20px] items-center justify-center"
|
||||||
>
|
>
|
||||||
{variantOpt.option_value.value}
|
{variantOpt.value}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
+2
-4
@@ -53,10 +53,8 @@ export const ProductEditVariantForm = ({
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { handleSuccess } = useRouteModal()
|
const { handleSuccess } = useRouteModal()
|
||||||
const defaultOptions = product.options.reduce((acc: any, option: any) => {
|
const defaultOptions = product.options.reduce((acc: any, option: any) => {
|
||||||
const varOpt = variant.options.find(
|
const varOpt = variant.options.find((o: any) => o.option_id === option.id)
|
||||||
(o: any) => o.option_value.option_id === option.id
|
acc[option.title] = varOpt?.value
|
||||||
)
|
|
||||||
acc[option.title] = varOpt?.option_value?.value
|
|
||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { LoaderFunctionArgs } from "react-router-dom"
|
||||||
|
|
||||||
|
import { queryClient } from "../../../lib/medusa"
|
||||||
|
import { productsQueryKeys } from "../../../hooks/api/products"
|
||||||
|
import { client } from "../../../lib/client"
|
||||||
|
|
||||||
|
const queryKey = (id: string) => {
|
||||||
|
return [productsQueryKeys.detail(id)]
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryFn = async (id: string) => {
|
||||||
|
const productRes = await client.products.retrieve(id)
|
||||||
|
return {
|
||||||
|
initialData: productRes,
|
||||||
|
isStockAndInventoryEnabled: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const editProductVariantQuery = (id: string) => ({
|
||||||
|
queryKey: queryKey(id),
|
||||||
|
queryFn: async () => queryFn(id),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const editProductVariantLoader = async ({
|
||||||
|
params,
|
||||||
|
}: LoaderFunctionArgs) => {
|
||||||
|
const id = params.id
|
||||||
|
const query = editProductVariantQuery(id!)
|
||||||
|
|
||||||
|
return (
|
||||||
|
queryClient.getQueryData<ReturnType<typeof queryFn>>(query.queryKey) ??
|
||||||
|
(await queryClient.fetchQuery(query))
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -24,7 +24,6 @@ export const defaultAdminProductsVariantFields = [
|
|||||||
"barcode",
|
"barcode",
|
||||||
"*prices",
|
"*prices",
|
||||||
"*options",
|
"*options",
|
||||||
"*options.option_value",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
export const retrieveVariantConfig = {
|
export const retrieveVariantConfig = {
|
||||||
@@ -85,7 +84,6 @@ export const defaultAdminProductFields = [
|
|||||||
"*variants",
|
"*variants",
|
||||||
"*variants.prices",
|
"*variants.prices",
|
||||||
"*variants.options",
|
"*variants.options",
|
||||||
"*variants.options.option_value",
|
|
||||||
"*sales_channels",
|
"*sales_channels",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
+31
-8
@@ -28,6 +28,10 @@ moduleIntegrationTestRunner({
|
|||||||
title: "size",
|
title: "size",
|
||||||
values: ["large", "small"],
|
values: ["large", "small"],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "color",
|
||||||
|
values: ["red", "blue"],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -182,7 +186,7 @@ moduleIntegrationTestRunner({
|
|||||||
expect(productVariant.title).toEqual("new test")
|
expect(productVariant.title).toEqual("new test")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should update the options of a variant successfully", async () => {
|
it("should upsert the options of a variant successfully", async () => {
|
||||||
await service.upsertVariants([
|
await service.upsertVariants([
|
||||||
{
|
{
|
||||||
id: variantOne.id,
|
id: variantOne.id,
|
||||||
@@ -191,12 +195,33 @@ moduleIntegrationTestRunner({
|
|||||||
])
|
])
|
||||||
|
|
||||||
const productVariant = await service.retrieveVariant(variantOne.id, {
|
const productVariant = await service.retrieveVariant(variantOne.id, {
|
||||||
relations: ["options", "options.option_value", "options.variant"],
|
relations: ["options"],
|
||||||
})
|
})
|
||||||
expect(productVariant.options).toEqual(
|
expect(productVariant.options).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
option_value: expect.objectContaining({ value: "small" }),
|
value: "small",
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should do a partial update on the options of a variant successfully", async () => {
|
||||||
|
await service.updateVariants(variantOne.id, {
|
||||||
|
options: { size: "small", color: "red" },
|
||||||
|
})
|
||||||
|
|
||||||
|
const productVariant = await service.retrieveVariant(variantOne.id, {
|
||||||
|
relations: ["options"],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(productVariant.options).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
value: "small",
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
value: "red",
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
@@ -222,7 +247,7 @@ moduleIntegrationTestRunner({
|
|||||||
const beforeDeletedVariants = await service.listVariants(
|
const beforeDeletedVariants = await service.listVariants(
|
||||||
{ id: variantOne.id },
|
{ id: variantOne.id },
|
||||||
{
|
{
|
||||||
relations: ["options", "options.option_value", "options.variant"],
|
relations: ["options"],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -230,7 +255,7 @@ moduleIntegrationTestRunner({
|
|||||||
const deletedVariants = await service.listVariants(
|
const deletedVariants = await service.listVariants(
|
||||||
{ id: variantOne.id },
|
{ id: variantOne.id },
|
||||||
{
|
{
|
||||||
relations: ["options", "options.option_value", "options.variant"],
|
relations: ["options"],
|
||||||
withDeleted: true,
|
withDeleted: true,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -239,9 +264,7 @@ moduleIntegrationTestRunner({
|
|||||||
expect(deletedVariants[0].deleted_at).not.toBeNull()
|
expect(deletedVariants[0].deleted_at).not.toBeNull()
|
||||||
|
|
||||||
for (const variantOption of deletedVariants[0].options) {
|
for (const variantOption of deletedVariants[0].options) {
|
||||||
expect(variantOption.deleted_at).not.toBeNull()
|
expect(variantOption?.deleted_at).toBeNull()
|
||||||
// The value itself should not be affected
|
|
||||||
expect(variantOption?.option_value?.deleted_at).toBeNull()
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
+69
-34
@@ -54,7 +54,6 @@ moduleIntegrationTestRunner({
|
|||||||
let productTwo: Product
|
let productTwo: Product
|
||||||
let productCategoryOne: ProductCategory
|
let productCategoryOne: ProductCategory
|
||||||
let productCategoryTwo: ProductCategory
|
let productCategoryTwo: ProductCategory
|
||||||
let variantTwo: ProductVariant
|
|
||||||
let productTypeOne: ProductType
|
let productTypeOne: ProductType
|
||||||
let productTypeTwo: ProductType
|
let productTypeTwo: ProductType
|
||||||
let images = [{ url: "image-1" }]
|
let images = [{ url: "image-1" }]
|
||||||
@@ -112,43 +111,57 @@ moduleIntegrationTestRunner({
|
|||||||
productCategoryOne = categories[0]
|
productCategoryOne = categories[0]
|
||||||
productCategoryTwo = categories[1]
|
productCategoryTwo = categories[1]
|
||||||
|
|
||||||
productOne = testManager.create(Product, {
|
productOne = service.create({
|
||||||
id: "product-1",
|
id: "product-1",
|
||||||
title: "product 1",
|
title: "product 1",
|
||||||
status: ProductTypes.ProductStatus.PUBLISHED,
|
status: ProductTypes.ProductStatus.PUBLISHED,
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
id: "variant-1",
|
||||||
|
title: "variant 1",
|
||||||
|
inventory_quantity: 10,
|
||||||
|
},
|
||||||
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
productTwo = testManager.create(Product, {
|
productTwo = service.create({
|
||||||
id: "product-2",
|
id: "product-2",
|
||||||
title: "product 2",
|
title: "product 2",
|
||||||
status: ProductTypes.ProductStatus.PUBLISHED,
|
status: ProductTypes.ProductStatus.PUBLISHED,
|
||||||
categories: [productCategoryOne],
|
categories: [{ id: productCategoryOne.id }],
|
||||||
collection_id: productCollectionOne.id,
|
collection_id: productCollectionOne.id,
|
||||||
tags: tagsData,
|
tags: tagsData,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
title: "size",
|
||||||
|
values: ["large", "small"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "color",
|
||||||
|
values: ["red", "blue"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
id: "variant-2",
|
||||||
|
title: "variant 2",
|
||||||
|
inventory_quantity: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "variant-3",
|
||||||
|
title: "variant 3",
|
||||||
|
inventory_quantity: 10,
|
||||||
|
options: {
|
||||||
|
size: "small",
|
||||||
|
color: "red",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
testManager.create(ProductVariant, {
|
const res = await Promise.all([productOne, productTwo])
|
||||||
id: "variant-1",
|
productOne = res[0]
|
||||||
title: "variant 1",
|
productTwo = res[1]
|
||||||
inventory_quantity: 10,
|
|
||||||
product: productOne,
|
|
||||||
})
|
|
||||||
|
|
||||||
variantTwo = testManager.create(ProductVariant, {
|
|
||||||
id: "variant-2",
|
|
||||||
title: "variant 2",
|
|
||||||
inventory_quantity: 10,
|
|
||||||
product: productTwo,
|
|
||||||
})
|
|
||||||
|
|
||||||
testManager.create(ProductVariant, {
|
|
||||||
id: "variant-3",
|
|
||||||
title: "variant 3",
|
|
||||||
inventory_quantity: 10,
|
|
||||||
product: productTwo,
|
|
||||||
})
|
|
||||||
|
|
||||||
await testManager.persistAndFlush([productOne, productTwo])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should update a product and upsert relations that are not created yet", async () => {
|
it("should update a product and upsert relations that are not created yet", async () => {
|
||||||
@@ -250,9 +263,7 @@ moduleIntegrationTestRunner({
|
|||||||
options: expect.arrayContaining([
|
options: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
option_value: expect.objectContaining({
|
value: data.options[0].values[0],
|
||||||
value: data.options[0].values[0],
|
|
||||||
}),
|
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
}),
|
}),
|
||||||
@@ -435,7 +446,7 @@ moduleIntegrationTestRunner({
|
|||||||
// Note: VariantThree is already assigned to productTwo, that should be deleted
|
// Note: VariantThree is already assigned to productTwo, that should be deleted
|
||||||
variants: [
|
variants: [
|
||||||
{
|
{
|
||||||
id: variantTwo.id,
|
id: productTwo.variants[0].id,
|
||||||
title: "updated-variant",
|
title: "updated-variant",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -456,7 +467,7 @@ moduleIntegrationTestRunner({
|
|||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
variants: expect.arrayContaining([
|
variants: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: variantTwo.id,
|
id: productTwo.variants[0].id,
|
||||||
title: "updated-variant",
|
title: "updated-variant",
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@@ -468,6 +479,32 @@ moduleIntegrationTestRunner({
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should do a partial update on the options of a variant successfully", async () => {
|
||||||
|
await service.update(productTwo.id, {
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
id: "variant-3",
|
||||||
|
options: { size: "small", color: "blue" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const fetchedProduct = await service.retrieve(productTwo.id, {
|
||||||
|
relations: ["variants", "variants.options"],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(fetchedProduct.variants[0].options).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
value: "small",
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
value: "blue",
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
it("should createa variant with id that was passed if it does not exist", async () => {
|
it("should createa variant with id that was passed if it does not exist", async () => {
|
||||||
const updateData = {
|
const updateData = {
|
||||||
id: productTwo.id,
|
id: productTwo.id,
|
||||||
@@ -583,9 +620,7 @@ moduleIntegrationTestRunner({
|
|||||||
options: expect.arrayContaining([
|
options: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
option_value: expect.objectContaining({
|
value: data.options[0].values[0],
|
||||||
value: data.options[0].values[0],
|
|
||||||
}),
|
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -112,17 +112,27 @@
|
|||||||
"length": 6,
|
"length": 6,
|
||||||
"default": "now()",
|
"default": "now()",
|
||||||
"mappedType": "datetime"
|
"mappedType": "datetime"
|
||||||
|
},
|
||||||
|
"deleted_at": {
|
||||||
|
"name": "deleted_at",
|
||||||
|
"type": "timestamptz",
|
||||||
|
"unsigned": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"primary": false,
|
||||||
|
"nullable": true,
|
||||||
|
"length": 6,
|
||||||
|
"mappedType": "datetime"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "product_category",
|
"name": "product_category",
|
||||||
"schema": "public",
|
"schema": "public",
|
||||||
"indexes": [
|
"indexes": [
|
||||||
{
|
{
|
||||||
"keyName": "IDX_product_category_path",
|
|
||||||
"columnNames": [
|
"columnNames": [
|
||||||
"mpath"
|
"deleted_at"
|
||||||
],
|
],
|
||||||
"composite": false,
|
"composite": false,
|
||||||
|
"keyName": "IDX_product_category_deleted_at",
|
||||||
"primary": false,
|
"primary": false,
|
||||||
"unique": false
|
"unique": false
|
||||||
},
|
},
|
||||||
@@ -314,15 +324,6 @@
|
|||||||
"name": "image",
|
"name": "image",
|
||||||
"schema": "public",
|
"schema": "public",
|
||||||
"indexes": [
|
"indexes": [
|
||||||
{
|
|
||||||
"columnNames": [
|
|
||||||
"url"
|
|
||||||
],
|
|
||||||
"composite": false,
|
|
||||||
"keyName": "IDX_product_image_url",
|
|
||||||
"primary": false,
|
|
||||||
"unique": false
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"columnNames": [
|
"columnNames": [
|
||||||
"deleted_at"
|
"deleted_at"
|
||||||
@@ -754,15 +755,6 @@
|
|||||||
"name": "product",
|
"name": "product",
|
||||||
"schema": "public",
|
"schema": "public",
|
||||||
"indexes": [
|
"indexes": [
|
||||||
{
|
|
||||||
"columnNames": [
|
|
||||||
"type_id"
|
|
||||||
],
|
|
||||||
"composite": false,
|
|
||||||
"keyName": "IDX_product_type_id",
|
|
||||||
"primary": false,
|
|
||||||
"unique": false
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"columnNames": [
|
"columnNames": [
|
||||||
"deleted_at"
|
"deleted_at"
|
||||||
@@ -996,15 +988,6 @@
|
|||||||
"name": "product_option_value",
|
"name": "product_option_value",
|
||||||
"schema": "public",
|
"schema": "public",
|
||||||
"indexes": [
|
"indexes": [
|
||||||
{
|
|
||||||
"columnNames": [
|
|
||||||
"option_id"
|
|
||||||
],
|
|
||||||
"composite": false,
|
|
||||||
"keyName": "IDX_product_option_value_option_id",
|
|
||||||
"primary": false,
|
|
||||||
"unique": false
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"columnNames": [
|
"columnNames": [
|
||||||
"deleted_at"
|
"deleted_at"
|
||||||
@@ -1458,15 +1441,6 @@
|
|||||||
"name": "product_variant",
|
"name": "product_variant",
|
||||||
"schema": "public",
|
"schema": "public",
|
||||||
"indexes": [
|
"indexes": [
|
||||||
{
|
|
||||||
"columnNames": [
|
|
||||||
"product_id"
|
|
||||||
],
|
|
||||||
"composite": false,
|
|
||||||
"keyName": "IDX_product_variant_product_id",
|
|
||||||
"primary": false,
|
|
||||||
"unique": false
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"columnNames": [
|
"columnNames": [
|
||||||
"deleted_at"
|
"deleted_at"
|
||||||
@@ -1505,8 +1479,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": {
|
"columns": {
|
||||||
"id": {
|
"variant_id": {
|
||||||
"name": "id",
|
"name": "variant_id",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"unsigned": false,
|
"unsigned": false,
|
||||||
"autoincrement": false,
|
"autoincrement": false,
|
||||||
@@ -1520,88 +1494,26 @@
|
|||||||
"unsigned": false,
|
"unsigned": false,
|
||||||
"autoincrement": false,
|
"autoincrement": false,
|
||||||
"primary": false,
|
"primary": false,
|
||||||
"nullable": true,
|
|
||||||
"mappedType": "text"
|
|
||||||
},
|
|
||||||
"variant_id": {
|
|
||||||
"name": "variant_id",
|
|
||||||
"type": "text",
|
|
||||||
"unsigned": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"primary": false,
|
|
||||||
"nullable": true,
|
|
||||||
"mappedType": "text"
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"name": "created_at",
|
|
||||||
"type": "timestamptz",
|
|
||||||
"unsigned": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"primary": false,
|
|
||||||
"nullable": false,
|
"nullable": false,
|
||||||
"length": 6,
|
"mappedType": "text"
|
||||||
"default": "now()",
|
|
||||||
"mappedType": "datetime"
|
|
||||||
},
|
|
||||||
"updated_at": {
|
|
||||||
"name": "updated_at",
|
|
||||||
"type": "timestamptz",
|
|
||||||
"unsigned": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"primary": false,
|
|
||||||
"nullable": false,
|
|
||||||
"length": 6,
|
|
||||||
"default": "now()",
|
|
||||||
"mappedType": "datetime"
|
|
||||||
},
|
|
||||||
"deleted_at": {
|
|
||||||
"name": "deleted_at",
|
|
||||||
"type": "timestamptz",
|
|
||||||
"unsigned": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"primary": false,
|
|
||||||
"nullable": true,
|
|
||||||
"length": 6,
|
|
||||||
"mappedType": "datetime"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "product_variant_option",
|
"name": "product_variant_option",
|
||||||
"schema": "public",
|
"schema": "public",
|
||||||
"indexes": [
|
"indexes": [
|
||||||
{
|
|
||||||
"columnNames": [
|
|
||||||
"deleted_at"
|
|
||||||
],
|
|
||||||
"composite": false,
|
|
||||||
"keyName": "IDX_product_variant_option_deleted_at",
|
|
||||||
"primary": false,
|
|
||||||
"unique": false
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"keyName": "product_variant_option_pkey",
|
"keyName": "product_variant_option_pkey",
|
||||||
"columnNames": [
|
"columnNames": [
|
||||||
"id"
|
"variant_id",
|
||||||
|
"option_value_id"
|
||||||
],
|
],
|
||||||
"composite": false,
|
"composite": true,
|
||||||
"primary": true,
|
"primary": true,
|
||||||
"unique": true
|
"unique": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"checks": [],
|
"checks": [],
|
||||||
"foreignKeys": {
|
"foreignKeys": {
|
||||||
"product_variant_option_option_value_id_foreign": {
|
|
||||||
"constraintName": "product_variant_option_option_value_id_foreign",
|
|
||||||
"columnNames": [
|
|
||||||
"option_value_id"
|
|
||||||
],
|
|
||||||
"localTableName": "public.product_variant_option",
|
|
||||||
"referencedColumnNames": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"referencedTableName": "public.product_option_value",
|
|
||||||
"deleteRule": "cascade",
|
|
||||||
"updateRule": "cascade"
|
|
||||||
},
|
|
||||||
"product_variant_option_variant_id_foreign": {
|
"product_variant_option_variant_id_foreign": {
|
||||||
"constraintName": "product_variant_option_variant_id_foreign",
|
"constraintName": "product_variant_option_variant_id_foreign",
|
||||||
"columnNames": [
|
"columnNames": [
|
||||||
@@ -1614,6 +1526,19 @@
|
|||||||
"referencedTableName": "public.product_variant",
|
"referencedTableName": "public.product_variant",
|
||||||
"deleteRule": "cascade",
|
"deleteRule": "cascade",
|
||||||
"updateRule": "cascade"
|
"updateRule": "cascade"
|
||||||
|
},
|
||||||
|
"product_variant_option_option_value_id_foreign": {
|
||||||
|
"constraintName": "product_variant_option_option_value_id_foreign",
|
||||||
|
"columnNames": [
|
||||||
|
"option_value_id"
|
||||||
|
],
|
||||||
|
"localTableName": "public.product_variant_option",
|
||||||
|
"referencedColumnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"referencedTableName": "public.product_option_value",
|
||||||
|
"deleteRule": "cascade",
|
||||||
|
"updateRule": "cascade"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,15 +42,12 @@ export class InitialSetup20240315083440 extends Migration {
|
|||||||
this.addSql('create unique index if not exists "IDX_option_value_option_id_unique" on "product_option_value" (option_id, value) where deleted_at is null;')
|
this.addSql('create unique index if not exists "IDX_option_value_option_id_unique" on "product_option_value" (option_id, value) where deleted_at is null;')
|
||||||
this.addSql('create index if not exists "IDX_product_option_value_deleted_at" on "product_option_value" ("deleted_at");');
|
this.addSql('create index if not exists "IDX_product_option_value_deleted_at" on "product_option_value" ("deleted_at");');
|
||||||
|
|
||||||
this.addSql('create table if not exists "product_variant_option" ("id" text not null, "option_value_id" text null, "variant_id" text null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "product_variant_option_pkey" primary key ("id"));');
|
|
||||||
this.addSql('create unique index if not exists "IDX_variant_option_option_value_unique" on "product_variant_option" (variant_id, option_value_id) where deleted_at is null;')
|
|
||||||
this.addSql('create index if not exists "IDX_product_variant_option_deleted_at" on "product_variant_option" ("deleted_at");');
|
|
||||||
|
|
||||||
this.addSql('create table if not exists "image" ("id" text not null, "url" text not null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "image_pkey" primary key ("id"));');
|
this.addSql('create table if not exists "image" ("id" text not null, "url" text not null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "image_pkey" primary key ("id"));');
|
||||||
this.addSql('create index if not exists "IDX_product_image_url" on "image" ("url") where deleted_at is null;');
|
this.addSql('create index if not exists "IDX_product_image_url" on "image" ("url") where deleted_at is null;');
|
||||||
this.addSql('create index if not exists "IDX_product_image_deleted_at" on "image" ("deleted_at");');
|
this.addSql('create index if not exists "IDX_product_image_deleted_at" on "image" ("deleted_at");');
|
||||||
|
|
||||||
this.addSql('create table if not exists "product_tag" ("id" text not null, "value" text not null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "product_tag_pkey" primary key ("id"));');
|
this.addSql('create table if not exists "product_tag" ("id" text not null, "value" text not null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "product_tag_pkey" primary key ("id"));');
|
||||||
|
|
||||||
// TODO: We need to modify upsertWithReplace to handle unique constraints
|
// TODO: We need to modify upsertWithReplace to handle unique constraints
|
||||||
// this.addSql('create unique index if not exists "IDX_tag_value_unique" on "product_tag" (value) where deleted_at is null;')
|
// this.addSql('create unique index if not exists "IDX_tag_value_unique" on "product_tag" (value) where deleted_at is null;')
|
||||||
this.addSql('create index if not exists "IDX_product_tag_deleted_at" on "product_tag" ("deleted_at");');
|
this.addSql('create index if not exists "IDX_product_tag_deleted_at" on "product_tag" ("deleted_at");');
|
||||||
@@ -72,7 +69,7 @@ export class InitialSetup20240315083440 extends Migration {
|
|||||||
this.addSql('create table if not exists "product_tags" ("product_id" text not null, "product_tag_id" text not null, constraint "product_tags_pkey" primary key ("product_id", "product_tag_id"));');
|
this.addSql('create table if not exists "product_tags" ("product_id" text not null, "product_tag_id" text not null, constraint "product_tags_pkey" primary key ("product_id", "product_tag_id"));');
|
||||||
this.addSql('create table if not exists "product_images" ("product_id" text not null, "image_id" text not null, constraint "product_images_pkey" primary key ("product_id", "image_id"));');
|
this.addSql('create table if not exists "product_images" ("product_id" text not null, "image_id" text not null, constraint "product_images_pkey" primary key ("product_id", "image_id"));');
|
||||||
this.addSql('create table if not exists "product_category_product" ("product_id" text not null, "product_category_id" text not null, constraint "product_category_product_pkey" primary key ("product_id", "product_category_id"));');
|
this.addSql('create table if not exists "product_category_product" ("product_id" text not null, "product_category_id" text not null, constraint "product_category_product_pkey" primary key ("product_id", "product_category_id"));');
|
||||||
|
this.addSql('create table if not exists "product_variant_option" ("variant_id" text not null, "option_value_id" text not null, constraint "product_variant_option_pkey" primary key ("variant_id", "option_value_id"));');
|
||||||
|
|
||||||
/* --- FOREIGN KEYS AND CONSTRAINTS --- */
|
/* --- FOREIGN KEYS AND CONSTRAINTS --- */
|
||||||
this.addSql('alter table if exists "product" add constraint "product_collection_id_foreign" foreign key ("collection_id") references "product_collection" ("id") on update cascade on delete set null;');
|
this.addSql('alter table if exists "product" add constraint "product_collection_id_foreign" foreign key ("collection_id") references "product_collection" ("id") on update cascade on delete set null;');
|
||||||
@@ -84,8 +81,8 @@ export class InitialSetup20240315083440 extends Migration {
|
|||||||
|
|
||||||
this.addSql('alter table if exists "product_option_value" add constraint "product_option_value_option_id_foreign" foreign key ("option_id") references "product_option" ("id") on update cascade on delete cascade;');
|
this.addSql('alter table if exists "product_option_value" add constraint "product_option_value_option_id_foreign" foreign key ("option_id") references "product_option" ("id") on update cascade on delete cascade;');
|
||||||
|
|
||||||
this.addSql('alter table if exists "product_variant_option" add constraint "product_variant_option_option_value_id_foreign" foreign key ("option_value_id") references "product_option_value" ("id") on update cascade on delete cascade;');
|
|
||||||
this.addSql('alter table if exists "product_variant_option" add constraint "product_variant_option_variant_id_foreign" foreign key ("variant_id") references "product_variant" ("id") on update cascade on delete cascade;');
|
this.addSql('alter table if exists "product_variant_option" add constraint "product_variant_option_variant_id_foreign" foreign key ("variant_id") references "product_variant" ("id") on update cascade on delete cascade;');
|
||||||
|
this.addSql('alter table if exists "product_variant_option" add constraint "product_variant_option_option_value_id_foreign" foreign key ("option_value_id") references "product_option_value" ("id") on update cascade on delete cascade;');
|
||||||
|
|
||||||
this.addSql('alter table if exists "product_images" add constraint "product_images_product_id_foreign" foreign key ("product_id") references "product" ("id") on update cascade on delete cascade;');
|
this.addSql('alter table if exists "product_images" add constraint "product_images_product_id_foreign" foreign key ("product_id") references "product" ("id") on update cascade on delete cascade;');
|
||||||
this.addSql('alter table if exists "product_images" add constraint "product_images_image_id_foreign" foreign key ("image_id") references "image" ("id") on update cascade on delete cascade;');
|
this.addSql('alter table if exists "product_images" add constraint "product_images_image_id_foreign" foreign key ("image_id") references "image" ("id") on update cascade on delete cascade;');
|
||||||
|
|||||||
@@ -6,5 +6,4 @@ export { default as ProductType } from "./product-type"
|
|||||||
export { default as ProductVariant } from "./product-variant"
|
export { default as ProductVariant } from "./product-variant"
|
||||||
export { default as ProductOption } from "./product-option"
|
export { default as ProductOption } from "./product-option"
|
||||||
export { default as ProductOptionValue } from "./product-option-value"
|
export { default as ProductOptionValue } from "./product-option-value"
|
||||||
export { default as ProductVariantOption } from "./product-variant-option"
|
|
||||||
export { default as Image } from "./product-image"
|
export { default as Image } from "./product-image"
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ import {
|
|||||||
Entity,
|
Entity,
|
||||||
Filter,
|
Filter,
|
||||||
Index,
|
Index,
|
||||||
|
ManyToMany,
|
||||||
ManyToOne,
|
ManyToOne,
|
||||||
OnInit,
|
OnInit,
|
||||||
OneToMany,
|
|
||||||
PrimaryKey,
|
PrimaryKey,
|
||||||
Property,
|
Property,
|
||||||
} from "@mikro-orm/core"
|
} from "@mikro-orm/core"
|
||||||
import { ProductOption, ProductVariantOption } from "./index"
|
import { ProductOption, ProductVariant } from "./index"
|
||||||
|
|
||||||
const optionValueOptionIdIndexName = "IDX_option_value_option_id_unique"
|
const optionValueOptionIdIndexName = "IDX_option_value_option_id_unique"
|
||||||
const optionValueOptionIdIndexStatement = createPsqlIndexStatementHelper({
|
const optionValueOptionIdIndexStatement = createPsqlIndexStatementHelper({
|
||||||
@@ -51,8 +51,8 @@ class ProductOptionValue {
|
|||||||
})
|
})
|
||||||
option: ProductOption | null
|
option: ProductOption | null
|
||||||
|
|
||||||
@OneToMany(() => ProductVariantOption, (value) => value.option_value, {})
|
@ManyToMany(() => ProductVariant, (variant) => variant.options)
|
||||||
variant_options = new Collection<ProductVariantOption>(this)
|
variants = new Collection<ProductVariant>(this)
|
||||||
|
|
||||||
@Property({ columnType: "jsonb", nullable: true })
|
@Property({ columnType: "jsonb", nullable: true })
|
||||||
metadata?: Record<string, unknown> | null
|
metadata?: Record<string, unknown> | null
|
||||||
|
|||||||
@@ -1,92 +0,0 @@
|
|||||||
import {
|
|
||||||
DALUtils,
|
|
||||||
createPsqlIndexStatementHelper,
|
|
||||||
generateEntityId,
|
|
||||||
} from "@medusajs/utils"
|
|
||||||
import {
|
|
||||||
BeforeCreate,
|
|
||||||
Entity,
|
|
||||||
Filter,
|
|
||||||
Index,
|
|
||||||
ManyToOne,
|
|
||||||
OnInit,
|
|
||||||
PrimaryKey,
|
|
||||||
Property,
|
|
||||||
} from "@mikro-orm/core"
|
|
||||||
import { ProductOptionValue, ProductVariant } from "./index"
|
|
||||||
|
|
||||||
const variantOptionValueIndexName = "IDX_variant_option_option_value_unique"
|
|
||||||
const variantOptionValueIndexStatement = createPsqlIndexStatementHelper({
|
|
||||||
name: variantOptionValueIndexName,
|
|
||||||
tableName: "product_variant_option",
|
|
||||||
columns: ["variant_id", "option_value_id"],
|
|
||||||
unique: true,
|
|
||||||
where: "deleted_at IS NULL",
|
|
||||||
})
|
|
||||||
|
|
||||||
variantOptionValueIndexStatement.MikroORMIndex()
|
|
||||||
@Entity({ tableName: "product_variant_option" })
|
|
||||||
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
|
|
||||||
class ProductVariantOption {
|
|
||||||
@PrimaryKey({ columnType: "text" })
|
|
||||||
id!: string
|
|
||||||
|
|
||||||
@ManyToOne(() => ProductOptionValue, {
|
|
||||||
columnType: "text",
|
|
||||||
nullable: true,
|
|
||||||
fieldName: "option_value_id",
|
|
||||||
mapToPk: true,
|
|
||||||
onDelete: "cascade",
|
|
||||||
})
|
|
||||||
option_value_id!: string
|
|
||||||
|
|
||||||
@ManyToOne(() => ProductOptionValue, {
|
|
||||||
persist: false,
|
|
||||||
nullable: true,
|
|
||||||
})
|
|
||||||
option_value!: ProductOptionValue
|
|
||||||
|
|
||||||
@ManyToOne(() => ProductVariant, {
|
|
||||||
columnType: "text",
|
|
||||||
nullable: true,
|
|
||||||
fieldName: "variant_id",
|
|
||||||
mapToPk: true,
|
|
||||||
onDelete: "cascade",
|
|
||||||
})
|
|
||||||
variant_id: string | null
|
|
||||||
|
|
||||||
@ManyToOne(() => ProductVariant, {
|
|
||||||
persist: false,
|
|
||||||
nullable: true,
|
|
||||||
})
|
|
||||||
variant!: ProductVariant
|
|
||||||
|
|
||||||
@Property({
|
|
||||||
onCreate: () => new Date(),
|
|
||||||
columnType: "timestamptz",
|
|
||||||
defaultRaw: "now()",
|
|
||||||
})
|
|
||||||
created_at: Date
|
|
||||||
|
|
||||||
@Property({
|
|
||||||
onCreate: () => new Date(),
|
|
||||||
onUpdate: () => new Date(),
|
|
||||||
columnType: "timestamptz",
|
|
||||||
defaultRaw: "now()",
|
|
||||||
})
|
|
||||||
updated_at: Date
|
|
||||||
|
|
||||||
@Index({ name: "IDX_product_variant_option_deleted_at" })
|
|
||||||
@Property({ columnType: "timestamptz", nullable: true })
|
|
||||||
deleted_at?: Date
|
|
||||||
|
|
||||||
@OnInit()
|
|
||||||
@BeforeCreate()
|
|
||||||
onInit() {
|
|
||||||
this.id = generateEntityId(this.id, "varopt")
|
|
||||||
this.variant_id ??= this.variant?.id ?? null
|
|
||||||
this.option_value_id ??= this.option_value?.id ?? null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ProductVariantOption
|
|
||||||
@@ -12,14 +12,14 @@ import {
|
|||||||
Entity,
|
Entity,
|
||||||
Filter,
|
Filter,
|
||||||
Index,
|
Index,
|
||||||
|
ManyToMany,
|
||||||
ManyToOne,
|
ManyToOne,
|
||||||
OneToMany,
|
OneToMany,
|
||||||
OnInit,
|
OnInit,
|
||||||
PrimaryKey,
|
PrimaryKey,
|
||||||
Property,
|
Property,
|
||||||
} from "@mikro-orm/core"
|
} from "@mikro-orm/core"
|
||||||
import { Product } from "@models"
|
import { Product, ProductOptionValue } from "@models"
|
||||||
import ProductVariantOption from "./product-variant-option"
|
|
||||||
|
|
||||||
const variantSkuIndexName = "IDX_product_variant_sku_unique"
|
const variantSkuIndexName = "IDX_product_variant_sku_unique"
|
||||||
const variantSkuIndexStatement = createPsqlIndexStatementHelper({
|
const variantSkuIndexStatement = createPsqlIndexStatementHelper({
|
||||||
@@ -162,14 +162,13 @@ class ProductVariant {
|
|||||||
})
|
})
|
||||||
product: Product | null
|
product: Product | null
|
||||||
|
|
||||||
@OneToMany(
|
@ManyToMany(() => ProductOptionValue, "variants", {
|
||||||
() => ProductVariantOption,
|
owner: true,
|
||||||
(variantOption) => variantOption.variant,
|
pivotTable: "product_variant_option",
|
||||||
{
|
joinColumn: "variant_id",
|
||||||
cascade: [Cascade.PERSIST, "soft-remove" as any],
|
inverseJoinColumn: "option_value_id",
|
||||||
}
|
})
|
||||||
)
|
options = new Collection<ProductOptionValue>(this)
|
||||||
options = new Collection<ProductVariantOption>(this)
|
|
||||||
|
|
||||||
@Property({
|
@Property({
|
||||||
onCreate: () => new Date(),
|
onCreate: () => new Date(),
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import {
|
|||||||
ProductTag,
|
ProductTag,
|
||||||
ProductType,
|
ProductType,
|
||||||
ProductVariant,
|
ProductVariant,
|
||||||
ProductVariantOption,
|
|
||||||
} from "@models"
|
} from "@models"
|
||||||
import {
|
import {
|
||||||
ProductCategoryService,
|
ProductCategoryService,
|
||||||
@@ -66,7 +65,6 @@ type InjectedDependencies = {
|
|||||||
productTypeService: ProductTypeService<any>
|
productTypeService: ProductTypeService<any>
|
||||||
productOptionService: ProductOptionService<any>
|
productOptionService: ProductOptionService<any>
|
||||||
productOptionValueService: ModulesSdkTypes.InternalModuleService<any>
|
productOptionValueService: ModulesSdkTypes.InternalModuleService<any>
|
||||||
productVariantOptionService: ModulesSdkTypes.InternalModuleService<any>
|
|
||||||
eventBusModuleService?: IEventBusModuleService
|
eventBusModuleService?: IEventBusModuleService
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,11 +75,6 @@ const generateMethodForModels = [
|
|||||||
{ model: ProductTag, singular: "Tag", plural: "Tags" },
|
{ model: ProductTag, singular: "Tag", plural: "Tags" },
|
||||||
{ model: ProductType, singular: "Type", plural: "Types" },
|
{ model: ProductType, singular: "Type", plural: "Types" },
|
||||||
{ model: ProductVariant, singular: "Variant", plural: "Variants" },
|
{ model: ProductVariant, singular: "Variant", plural: "Variants" },
|
||||||
{
|
|
||||||
model: ProductVariantOption,
|
|
||||||
singular: "VariantOption",
|
|
||||||
plural: "VariantOptions",
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
export default class ProductModuleService<
|
export default class ProductModuleService<
|
||||||
@@ -93,8 +86,7 @@ export default class ProductModuleService<
|
|||||||
TProductImage extends Image = Image,
|
TProductImage extends Image = Image,
|
||||||
TProductType extends ProductType = ProductType,
|
TProductType extends ProductType = ProductType,
|
||||||
TProductOption extends ProductOption = ProductOption,
|
TProductOption extends ProductOption = ProductOption,
|
||||||
TProductOptionValue extends ProductOptionValue = ProductOptionValue,
|
TProductOptionValue extends ProductOptionValue = ProductOptionValue
|
||||||
TProductVariantOption extends ProductVariantOption = ProductVariantOption
|
|
||||||
>
|
>
|
||||||
extends ModulesSdkUtils.abstractModuleServiceFactory<
|
extends ModulesSdkUtils.abstractModuleServiceFactory<
|
||||||
InjectedDependencies,
|
InjectedDependencies,
|
||||||
@@ -130,11 +122,6 @@ export default class ProductModuleService<
|
|||||||
singular: "Variant"
|
singular: "Variant"
|
||||||
plural: "Variants"
|
plural: "Variants"
|
||||||
}
|
}
|
||||||
VariantOption: {
|
|
||||||
dto: ProductTypes.ProductVariantOptionDTO
|
|
||||||
singular: "VariantOption"
|
|
||||||
plural: "VariantOptions"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
>(Product, generateMethodForModels, entityNameToLinkableKeysMap)
|
>(Product, generateMethodForModels, entityNameToLinkableKeysMap)
|
||||||
implements ProductTypes.IProductModuleService
|
implements ProductTypes.IProductModuleService
|
||||||
@@ -154,8 +141,6 @@ export default class ProductModuleService<
|
|||||||
protected readonly productTypeService_: ProductTypeService<TProductType>
|
protected readonly productTypeService_: ProductTypeService<TProductType>
|
||||||
protected readonly productOptionService_: ProductOptionService<TProductOption>
|
protected readonly productOptionService_: ProductOptionService<TProductOption>
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
protected readonly productVariantOptionService_: ModulesSdkTypes.InternalModuleService<TProductVariantOption>
|
|
||||||
// eslint-disable-next-line max-len
|
|
||||||
protected readonly productOptionValueService_: ModulesSdkTypes.InternalModuleService<TProductOptionValue>
|
protected readonly productOptionValueService_: ModulesSdkTypes.InternalModuleService<TProductOptionValue>
|
||||||
protected readonly eventBusModuleService_?: IEventBusModuleService
|
protected readonly eventBusModuleService_?: IEventBusModuleService
|
||||||
|
|
||||||
@@ -171,7 +156,6 @@ export default class ProductModuleService<
|
|||||||
productTypeService,
|
productTypeService,
|
||||||
productOptionService,
|
productOptionService,
|
||||||
productOptionValueService,
|
productOptionValueService,
|
||||||
productVariantOptionService,
|
|
||||||
eventBusModuleService,
|
eventBusModuleService,
|
||||||
}: InjectedDependencies,
|
}: InjectedDependencies,
|
||||||
protected readonly moduleDeclaration: InternalModuleDeclaration
|
protected readonly moduleDeclaration: InternalModuleDeclaration
|
||||||
@@ -190,7 +174,6 @@ export default class ProductModuleService<
|
|||||||
this.productTypeService_ = productTypeService
|
this.productTypeService_ = productTypeService
|
||||||
this.productOptionService_ = productOptionService
|
this.productOptionService_ = productOptionService
|
||||||
this.productOptionValueService_ = productOptionValueService
|
this.productOptionValueService_ = productOptionValueService
|
||||||
this.productVariantOptionService_ = productVariantOptionService
|
|
||||||
this.eventBusModuleService_ = eventBusModuleService
|
this.eventBusModuleService_ = eventBusModuleService
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -357,7 +340,7 @@ export default class ProductModuleService<
|
|||||||
const variantIdsToUpdate = data.map(({ id }) => id)
|
const variantIdsToUpdate = data.map(({ id }) => id)
|
||||||
const variants = await this.productVariantService_.list(
|
const variants = await this.productVariantService_.list(
|
||||||
{ id: variantIdsToUpdate },
|
{ id: variantIdsToUpdate },
|
||||||
{ relations: ["options"], take: null },
|
{ take: null },
|
||||||
sharedContext
|
sharedContext
|
||||||
)
|
)
|
||||||
if (variants.length !== data.length) {
|
if (variants.length !== data.length) {
|
||||||
@@ -686,26 +669,63 @@ export default class ProductModuleService<
|
|||||||
@MedusaContext() sharedContext: Context = {}
|
@MedusaContext() sharedContext: Context = {}
|
||||||
): Promise<ProductOption[]> {
|
): Promise<ProductOption[]> {
|
||||||
// Validation step
|
// Validation step
|
||||||
const normalizedInput = data.map((opt) => {
|
if (data.some((option) => !option.id)) {
|
||||||
return {
|
|
||||||
...opt,
|
|
||||||
...(opt.values
|
|
||||||
? {
|
|
||||||
values: opt.values.map((v) => {
|
|
||||||
return typeof v === "string" ? { value: v } : v
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
} as UpdateProductOptionInput
|
|
||||||
})
|
|
||||||
|
|
||||||
if (normalizedInput.some((option) => !option.id)) {
|
|
||||||
throw new MedusaError(
|
throw new MedusaError(
|
||||||
MedusaError.Types.INVALID_DATA,
|
MedusaError.Types.INVALID_DATA,
|
||||||
"Tried to update options without specifying an ID"
|
"Tried to update options without specifying an ID"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const dbOptions = await this.productOptionService_.list(
|
||||||
|
{ id: data.map(({ id }) => id) },
|
||||||
|
{ take: null, relations: ["values"] },
|
||||||
|
sharedContext
|
||||||
|
)
|
||||||
|
|
||||||
|
if (dbOptions.length !== data.length) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
`Cannot update non-existing options with ids: ${arrayDifference(
|
||||||
|
data.map(({ id }) => id),
|
||||||
|
dbOptions.map(({ id }) => id)
|
||||||
|
).join(", ")}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data normalization
|
||||||
|
const normalizedInput = data.map((opt) => {
|
||||||
|
const dbValues = dbOptions.find(({ id }) => id === opt.id)?.values || []
|
||||||
|
const normalizedValues = opt.values?.map((v) => {
|
||||||
|
return typeof v === "string" ? { value: v } : v
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
...opt,
|
||||||
|
...(normalizedValues
|
||||||
|
? {
|
||||||
|
// Oftentimes the options are only passed by value without an id, even if they exist in the DB
|
||||||
|
values: normalizedValues.map((normVal) => {
|
||||||
|
if ("id" in normVal) {
|
||||||
|
return normVal
|
||||||
|
}
|
||||||
|
|
||||||
|
const dbVal = dbValues.find(
|
||||||
|
(dbVal) => dbVal.value === normVal.value
|
||||||
|
)
|
||||||
|
if (!dbVal) {
|
||||||
|
return normVal
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: dbVal.id,
|
||||||
|
value: normVal.value,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
} as UpdateProductOptionInput
|
||||||
|
})
|
||||||
|
|
||||||
return await this.productOptionService_.upsertWithReplace(
|
return await this.productOptionService_.upsertWithReplace(
|
||||||
normalizedInput,
|
normalizedInput,
|
||||||
{ relations: ["values"] },
|
{ relations: ["values"] },
|
||||||
@@ -1188,7 +1208,7 @@ export default class ProductModuleService<
|
|||||||
sharedContext
|
sharedContext
|
||||||
)
|
)
|
||||||
|
|
||||||
// Since we handle the options and variants outside of the product upsert, we need to clean up manuallys
|
// Since we handle the options and variants outside of the product upsert, we need to clean up manually
|
||||||
await this.productOptionService_.delete(
|
await this.productOptionService_.delete(
|
||||||
{
|
{
|
||||||
product_id: upsertedProduct.id,
|
product_id: upsertedProduct.id,
|
||||||
@@ -1345,8 +1365,7 @@ export default class ProductModuleService<
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
variant_id: variant.id,
|
id: optionValue.id,
|
||||||
option_value_id: optionValue.id,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -237,7 +237,7 @@ export interface ProductVariantDTO {
|
|||||||
*
|
*
|
||||||
* @expandable
|
* @expandable
|
||||||
*/
|
*/
|
||||||
options: ProductVariantOptionDTO[]
|
options: ProductOptionValueDTO[]
|
||||||
/**
|
/**
|
||||||
* Holds custom data in key-value pairs.
|
* Holds custom data in key-value pairs.
|
||||||
*/
|
*/
|
||||||
@@ -554,45 +554,6 @@ export interface ProductOptionDTO {
|
|||||||
deleted_at?: string | Date
|
deleted_at?: string | Date
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProductVariantOptionDTO {
|
|
||||||
/**
|
|
||||||
* The ID of the product variant option.
|
|
||||||
*/
|
|
||||||
id: string
|
|
||||||
/**
|
|
||||||
* The value of the product variant option.
|
|
||||||
*
|
|
||||||
* @expandable
|
|
||||||
*/
|
|
||||||
option_value?: ProductOptionValueDTO | null
|
|
||||||
/**
|
|
||||||
* The value of the product variant option id.
|
|
||||||
*/
|
|
||||||
option_value_id?: string | null
|
|
||||||
/**
|
|
||||||
* The associated product variant.
|
|
||||||
*
|
|
||||||
* @expandable
|
|
||||||
*/
|
|
||||||
variant?: ProductVariantDTO | null
|
|
||||||
/**
|
|
||||||
* The associated product variant id.
|
|
||||||
*/
|
|
||||||
variant_id?: string | null
|
|
||||||
/**
|
|
||||||
* When the product variant option was created.
|
|
||||||
*/
|
|
||||||
created_at: string | Date
|
|
||||||
/**
|
|
||||||
* When the product variant option was updated.
|
|
||||||
*/
|
|
||||||
updated_at: string | Date
|
|
||||||
/**
|
|
||||||
* When the product variant option was deleted.
|
|
||||||
*/
|
|
||||||
deleted_at?: string | Date
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @interface
|
* @interface
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ export const dbErrorMapper = (err: Error) => {
|
|||||||
throw new MedusaError(MedusaError.Types.NOT_FOUND, err.message)
|
throw new MedusaError(MedusaError.Types.NOT_FOUND, err.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (err instanceof UniqueConstraintViolationException) {
|
if (
|
||||||
|
err instanceof UniqueConstraintViolationException ||
|
||||||
|
(err as any).code === "23505"
|
||||||
|
) {
|
||||||
const info = getConstraintInfo(err)
|
const info = getConstraintInfo(err)
|
||||||
if (!info) {
|
if (!info) {
|
||||||
throw err
|
throw err
|
||||||
@@ -27,7 +30,10 @@ export const dbErrorMapper = (err: Error) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (err instanceof NotNullConstraintViolationException) {
|
if (
|
||||||
|
err instanceof NotNullConstraintViolationException ||
|
||||||
|
(err as any).code === "23502"
|
||||||
|
) {
|
||||||
throw new MedusaError(
|
throw new MedusaError(
|
||||||
MedusaError.Types.INVALID_DATA,
|
MedusaError.Types.INVALID_DATA,
|
||||||
`Cannot set field '${(err as any).column}' of ${upperCaseFirst(
|
`Cannot set field '${(err as any).column}' of ${upperCaseFirst(
|
||||||
@@ -36,7 +42,10 @@ export const dbErrorMapper = (err: Error) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (err instanceof InvalidFieldNameException) {
|
if (
|
||||||
|
err instanceof InvalidFieldNameException ||
|
||||||
|
(err as any).code === "42703"
|
||||||
|
) {
|
||||||
const userFriendlyMessage = err.message.match(/(column.*)/)?.[0]
|
const userFriendlyMessage = err.message.match(/(column.*)/)?.[0]
|
||||||
throw new MedusaError(
|
throw new MedusaError(
|
||||||
MedusaError.Types.INVALID_DATA,
|
MedusaError.Types.INVALID_DATA,
|
||||||
@@ -44,7 +53,10 @@ export const dbErrorMapper = (err: Error) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (err instanceof ForeignKeyConstraintViolationException) {
|
if (
|
||||||
|
err instanceof ForeignKeyConstraintViolationException ||
|
||||||
|
(err as any).code === "23503"
|
||||||
|
) {
|
||||||
const info = getConstraintInfo(err)
|
const info = getConstraintInfo(err)
|
||||||
throw new MedusaError(
|
throw new MedusaError(
|
||||||
MedusaError.Types.NOT_FOUND,
|
MedusaError.Types.NOT_FOUND,
|
||||||
|
|||||||
+26
-25
@@ -656,8 +656,7 @@ describe("mikroOrmRepository", () => {
|
|||||||
expect(listedEntities[0].entity3.getItems()).toEqual(
|
expect(listedEntities[0].entity3.getItems()).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
// title: "en3-1",
|
title: "en3-1",
|
||||||
title: "newen3-1",
|
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
title: "en3-4",
|
title: "en3-4",
|
||||||
@@ -744,8 +743,7 @@ describe("mikroOrmRepository", () => {
|
|||||||
expect(listedEntities[0].entity3.getItems()).toEqual(
|
expect(listedEntities[0].entity3.getItems()).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
// title: "en3-1",
|
title: "en3-1",
|
||||||
title: "newen3-1",
|
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
title: "en3-4",
|
title: "en3-4",
|
||||||
@@ -817,31 +815,34 @@ describe("mikroOrmRepository", () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
// it("should correctly handle many-to-many upserts with a uniqueness constriant on a non-primary key", async () => {
|
it("should correctly handle many-to-many upserts with a uniqueness constriant on a non-primary key", async () => {
|
||||||
// const entity1 = {
|
const entity1 = {
|
||||||
// id: "1",
|
id: "1",
|
||||||
// title: "en1",
|
title: "en1",
|
||||||
// entity3: [{ title: "en3-1" }, { title: "en3-2" }] as any,
|
entity3: [
|
||||||
// }
|
{ id: "4", title: "en3-1" },
|
||||||
|
{ id: "5", title: "en3-2" },
|
||||||
|
] as any,
|
||||||
|
}
|
||||||
|
|
||||||
// await manager1().upsertWithReplace([entity1], {
|
await manager1().upsertWithReplace([entity1], {
|
||||||
// relations: ["entity3"],
|
relations: ["entity3"],
|
||||||
// })
|
})
|
||||||
|
|
||||||
// await manager1().upsertWithReplace([{ ...entity1, id: "2" }], {
|
await manager1().upsertWithReplace([{ ...entity1, id: "2" }], {
|
||||||
// relations: ["entity3"],
|
relations: ["entity3"],
|
||||||
// })
|
})
|
||||||
|
|
||||||
// const listedEntities = await manager1().find({
|
const listedEntities = await manager1().find({
|
||||||
// where: {},
|
where: {},
|
||||||
// options: { populate: ["entity3"] },
|
options: { populate: ["entity3"] },
|
||||||
// })
|
})
|
||||||
|
|
||||||
// expect(listedEntities).toHaveLength(2)
|
expect(listedEntities).toHaveLength(2)
|
||||||
// expect(listedEntities[0].entity3.getItems()).toEqual(
|
expect(listedEntities[0].entity3.getItems()).toEqual(
|
||||||
// listedEntities[1].entity3.getItems()
|
listedEntities[1].entity3.getItems()
|
||||||
// )
|
)
|
||||||
// })
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("error mapping", () => {
|
describe("error mapping", () => {
|
||||||
|
|||||||
@@ -602,8 +602,7 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
|
|||||||
return normalizedData
|
return normalizedData
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Currently we will also do an update of the data on the other side of the many-to-many relationship. Reevaluate if we should avoid that.
|
await this.upsertMany_(manager, relation.type, normalizedData, true)
|
||||||
await this.upsertMany_(manager, relation.type, normalizedData)
|
|
||||||
|
|
||||||
const pivotData = normalizedData.map((currModel) => {
|
const pivotData = normalizedData.map((currModel) => {
|
||||||
return {
|
return {
|
||||||
@@ -721,39 +720,17 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
|
|||||||
return { id: (created as any).id, ...data }
|
return { id: (created as any).id, ...data }
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async upsertManyToOneRelations_(
|
|
||||||
manager: SqlEntityManager,
|
|
||||||
upsertsPerType: Record<string, any[]>
|
|
||||||
) {
|
|
||||||
const typesToUpsert = Object.entries(upsertsPerType)
|
|
||||||
if (!typesToUpsert.length) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
await promiseAll(
|
|
||||||
typesToUpsert.map(([type, data]) => {
|
|
||||||
return this.upsertMany_(manager, type, data)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
).flat()
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async upsertMany_(
|
protected async upsertMany_(
|
||||||
manager: SqlEntityManager,
|
manager: SqlEntityManager,
|
||||||
entityName: string,
|
entityName: string,
|
||||||
entries: any[]
|
entries: any[],
|
||||||
|
skipUpdate: boolean = false
|
||||||
) {
|
) {
|
||||||
const existingEntities: any[] = await manager.find(
|
const selectQb = manager.qb(entityName)
|
||||||
entityName,
|
const existingEntities: any[] = await selectQb.select("*").where({
|
||||||
{
|
id: { $in: entries.map((d) => d.id) },
|
||||||
id: { $in: entries.map((d) => d.id) },
|
})
|
||||||
},
|
|
||||||
{
|
|
||||||
populate: [],
|
|
||||||
disableIdentityMap: true,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
const existingEntitiesMap = new Map(
|
const existingEntitiesMap = new Map(
|
||||||
existingEntities.map((e) => [e.id, e])
|
existingEntities.map((e) => [e.id, e])
|
||||||
)
|
)
|
||||||
@@ -762,12 +739,21 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
|
|||||||
|
|
||||||
await promiseAll(
|
await promiseAll(
|
||||||
entries.map(async (data) => {
|
entries.map(async (data) => {
|
||||||
orderedEntities.push(data)
|
|
||||||
const existingEntity = existingEntitiesMap.get(data.id)
|
const existingEntity = existingEntitiesMap.get(data.id)
|
||||||
|
orderedEntities.push(data)
|
||||||
if (existingEntity) {
|
if (existingEntity) {
|
||||||
|
if (skipUpdate) {
|
||||||
|
return
|
||||||
|
}
|
||||||
await manager.nativeUpdate(entityName, { id: data.id }, data)
|
await manager.nativeUpdate(entityName, { id: data.id }, data)
|
||||||
} else {
|
} else {
|
||||||
await manager.insert(entityName, data)
|
const qb = manager.qb(entityName)
|
||||||
|
if (skipUpdate) {
|
||||||
|
await qb.insert(data).onConflict().ignore().execute()
|
||||||
|
} else {
|
||||||
|
await manager.insert(entityName, data)
|
||||||
|
// await manager.insert(entityName, data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user