feat(product,dashboard): Allow re-ordering images (#10187)
* migration * fix snapshot * primarykey * init work on dnd * progress * dnd * undo changes * undo changes * undo changes * undo changes * fix firefox issue * lint * lint * lint * add changeset * undo changes to product module * set activator node * init work on service layer * alternative * switch to OneToMany * add tests * progress * update migration * update approach and remove all references to images in product.ts tests * handle delete images on empty array * fix config and order type * update changeset * rm flag * export type and fix type in test * fix type --------- Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
co-authored by
Oli Juhl
parent
b12408dbd8
commit
1659c9be5d
+150
-1
@@ -12,17 +12,18 @@ import {
|
||||
ProductStatus,
|
||||
} from "@medusajs/framework/utils"
|
||||
import {
|
||||
Image,
|
||||
Product,
|
||||
ProductCategory,
|
||||
ProductCollection,
|
||||
ProductType,
|
||||
} from "@models"
|
||||
|
||||
import { UpdateProductInput } from "@types"
|
||||
import {
|
||||
MockEventBusService,
|
||||
moduleIntegrationTestRunner,
|
||||
} from "@medusajs/test-utils"
|
||||
import { UpdateProductInput } from "@types"
|
||||
import {
|
||||
buildProductAndRelationsData,
|
||||
createCollections,
|
||||
@@ -1236,6 +1237,154 @@ moduleIntegrationTestRunner<IProductModuleService>({
|
||||
expect(products).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe("images", function () {
|
||||
it("should create images with correct rank", async () => {
|
||||
const images = [
|
||||
{ url: "image-1" },
|
||||
{ url: "image-2" },
|
||||
{ url: "image-3" },
|
||||
]
|
||||
|
||||
const [product] = await service.createProducts([
|
||||
buildProductAndRelationsData({ images }),
|
||||
])
|
||||
|
||||
expect(product.images).toHaveLength(3)
|
||||
expect(product.images).toEqual([
|
||||
expect.objectContaining({
|
||||
url: "image-1",
|
||||
rank: 0,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
url: "image-2",
|
||||
rank: 1,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
url: "image-3",
|
||||
rank: 2,
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should update images with correct rank", async () => {
|
||||
const images = [
|
||||
{ url: "image-1" },
|
||||
{ url: "image-2" },
|
||||
{ url: "image-3" },
|
||||
]
|
||||
|
||||
const [product] = await service.createProducts([
|
||||
buildProductAndRelationsData({ images }),
|
||||
])
|
||||
|
||||
const reversedImages = [...product.images].reverse()
|
||||
|
||||
const updatedProduct = await service.updateProducts(product.id, {
|
||||
images: reversedImages,
|
||||
})
|
||||
|
||||
expect(updatedProduct.images).toEqual([
|
||||
expect.objectContaining({
|
||||
url: "image-3",
|
||||
rank: 0,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
url: "image-2",
|
||||
rank: 1,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
url: "image-1",
|
||||
rank: 2,
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should retrieve images in the correct order consistently", async () => {
|
||||
const images = Array.from({ length: 1000 }, (_, i) => ({
|
||||
url: `image-${i + 1}`,
|
||||
}))
|
||||
|
||||
const [product] = await service.createProducts([
|
||||
buildProductAndRelationsData({ images }),
|
||||
])
|
||||
|
||||
const retrievedProduct = await service.retrieveProduct(product.id, {
|
||||
relations: ["images"],
|
||||
})
|
||||
|
||||
const retrievedProductAgain = await service.retrieveProduct(product.id, {
|
||||
relations: ["images"],
|
||||
})
|
||||
|
||||
expect(retrievedProduct.images).toEqual(retrievedProductAgain.images)
|
||||
|
||||
expect(retrievedProduct.images).toEqual(
|
||||
Array.from({ length: 1000 }, (_, i) =>
|
||||
expect.objectContaining({
|
||||
url: `image-${i + 1}`,
|
||||
rank: i,
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
service.listAndCountProducts
|
||||
|
||||
// Explicitly verify sequential order
|
||||
retrievedProduct.images.forEach((img, idx) => {
|
||||
if (idx > 0) {
|
||||
expect(img.rank).toBeGreaterThan(retrievedProduct.images[idx - 1].rank)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it("should retrieve images ordered by rank", async () => {
|
||||
const [product] = await service.createProducts([
|
||||
buildProductAndRelationsData({}),
|
||||
])
|
||||
|
||||
const manager = MikroOrmWrapper.forkManager()
|
||||
|
||||
const images = [
|
||||
manager.create(Image, {
|
||||
product_id: product.id,
|
||||
url: "image-one",
|
||||
rank: 1,
|
||||
}),
|
||||
manager.create(Image, {
|
||||
product_id: product.id,
|
||||
url: "image-two",
|
||||
rank: 0,
|
||||
}),
|
||||
manager.create(Image, {
|
||||
product_id: product.id,
|
||||
url: "image-three",
|
||||
rank: 2,
|
||||
}),
|
||||
]
|
||||
|
||||
await manager.persistAndFlush(images)
|
||||
|
||||
const retrievedProduct = await service.retrieveProduct(product.id, {
|
||||
relations: ["images"],
|
||||
})
|
||||
|
||||
expect(retrievedProduct.images).toEqual([
|
||||
expect.objectContaining({
|
||||
url: "image-two",
|
||||
rank: 0,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
url: "image-one",
|
||||
rank: 1,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
url: "image-three",
|
||||
rank: 2,
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { Image, Product, ProductCategory, ProductCollection } from "@models"
|
||||
import { Product, ProductCategory, ProductCollection } from "@models"
|
||||
import {
|
||||
assignCategoriesToProduct,
|
||||
buildProductOnlyData,
|
||||
createCollections,
|
||||
createImages,
|
||||
createProductAndTags,
|
||||
createProductVariants,
|
||||
} from "../__fixtures__/product"
|
||||
@@ -15,13 +14,13 @@ import {
|
||||
ProductStatus,
|
||||
kebabCase,
|
||||
} from "@medusajs/framework/utils"
|
||||
import { moduleIntegrationTestRunner } from "@medusajs/test-utils"
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import {
|
||||
ProductCategoryService,
|
||||
ProductModuleService,
|
||||
ProductService,
|
||||
} from "@services"
|
||||
import { moduleIntegrationTestRunner } from "@medusajs/test-utils"
|
||||
import {
|
||||
categoriesData,
|
||||
productsData,
|
||||
@@ -215,19 +214,12 @@ moduleIntegrationTestRunner<Service>({
|
||||
})
|
||||
|
||||
describe("create", function () {
|
||||
let images: Image[] = []
|
||||
|
||||
beforeEach(async () => {
|
||||
testManager = await MikroOrmWrapper.forkManager()
|
||||
|
||||
images = await createImages(testManager, ["image-1"])
|
||||
})
|
||||
|
||||
it("should create a product", async () => {
|
||||
const data = buildProductOnlyData({
|
||||
images,
|
||||
thumbnail: images[0].url,
|
||||
})
|
||||
const data = buildProductOnlyData()
|
||||
|
||||
const products = await service.create([data])
|
||||
|
||||
@@ -241,25 +233,15 @@ moduleIntegrationTestRunner<Service>({
|
||||
subtitle: data.subtitle,
|
||||
is_giftcard: data.is_giftcard,
|
||||
discountable: data.discountable,
|
||||
thumbnail: images[0].url,
|
||||
status: data.status,
|
||||
images: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: images[0].id,
|
||||
url: images[0].url,
|
||||
}),
|
||||
]),
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("update", function () {
|
||||
let images: Image[] = []
|
||||
|
||||
beforeEach(async () => {
|
||||
testManager = await MikroOrmWrapper.forkManager()
|
||||
images = await createImages(testManager, ["image-1", "image-2"])
|
||||
|
||||
productOne = testManager.create(Product, {
|
||||
id: "product-1",
|
||||
@@ -275,8 +257,6 @@ moduleIntegrationTestRunner<Service>({
|
||||
{
|
||||
id: productOne.id,
|
||||
title: "update test 1",
|
||||
images: images,
|
||||
thumbnail: images[0].url,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -284,24 +264,13 @@ moduleIntegrationTestRunner<Service>({
|
||||
|
||||
expect(products.length).toEqual(1)
|
||||
|
||||
let result = await service.retrieve(productOne.id, {
|
||||
relations: ["images", "thumbnail"],
|
||||
})
|
||||
let result = await service.retrieve(productOne.id)
|
||||
let serialized = JSON.parse(JSON.stringify(result))
|
||||
|
||||
expect(serialized).toEqual(
|
||||
expect.objectContaining({
|
||||
id: productOne.id,
|
||||
title: "update test 1",
|
||||
thumbnail: images[0].url,
|
||||
images: [
|
||||
expect.objectContaining({
|
||||
url: images[0].url,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
url: images[1].url,
|
||||
}),
|
||||
],
|
||||
})
|
||||
)
|
||||
})
|
||||
@@ -750,19 +719,12 @@ moduleIntegrationTestRunner<Service>({
|
||||
})
|
||||
|
||||
describe("softDelete", function () {
|
||||
let images: Image[] = []
|
||||
|
||||
beforeEach(async () => {
|
||||
testManager = await MikroOrmWrapper.forkManager()
|
||||
|
||||
images = await createImages(testManager, ["image-1"])
|
||||
})
|
||||
|
||||
it("should soft delete a product", async () => {
|
||||
const data = buildProductOnlyData({
|
||||
images,
|
||||
thumbnail: images[0].url,
|
||||
})
|
||||
const data = buildProductOnlyData()
|
||||
|
||||
const products = await service.create([data])
|
||||
await service.softDelete(products.map((p) => p.id))
|
||||
@@ -785,19 +747,12 @@ moduleIntegrationTestRunner<Service>({
|
||||
})
|
||||
|
||||
describe("restore", function () {
|
||||
let images: Image[] = []
|
||||
|
||||
beforeEach(async () => {
|
||||
testManager = await MikroOrmWrapper.forkManager()
|
||||
|
||||
images = await createImages(testManager, ["image-1"])
|
||||
})
|
||||
|
||||
it("should restore a soft deleted product", async () => {
|
||||
const data = buildProductOnlyData({
|
||||
images,
|
||||
thumbnail: images[0].url,
|
||||
})
|
||||
const data = buildProductOnlyData()
|
||||
|
||||
const products = await service.create([data])
|
||||
const product = products[0]
|
||||
|
||||
@@ -268,93 +268,6 @@
|
||||
"checks": [],
|
||||
"foreignKeys": {}
|
||||
},
|
||||
{
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"url": {
|
||||
"name": "url",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"metadata": {
|
||||
"name": "metadata",
|
||||
"type": "jsonb",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"mappedType": "json"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"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": "image",
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"columnNames": [
|
||||
"deleted_at"
|
||||
],
|
||||
"composite": false,
|
||||
"keyName": "IDX_product_image_deleted_at",
|
||||
"primary": false,
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"keyName": "image_pkey",
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"composite": false,
|
||||
"primary": true,
|
||||
"unique": true
|
||||
}
|
||||
],
|
||||
"checks": [],
|
||||
"foreignKeys": {}
|
||||
},
|
||||
{
|
||||
"columns": {
|
||||
"id": {
|
||||
@@ -1033,6 +946,126 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"url": {
|
||||
"name": "url",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"metadata": {
|
||||
"name": "metadata",
|
||||
"type": "jsonb",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"mappedType": "json"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"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"
|
||||
},
|
||||
"rank": {
|
||||
"name": "rank",
|
||||
"type": "integer",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"default": "0",
|
||||
"mappedType": "integer"
|
||||
},
|
||||
"product_id": {
|
||||
"name": "product_id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
}
|
||||
},
|
||||
"name": "image",
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"columnNames": [
|
||||
"deleted_at"
|
||||
],
|
||||
"composite": false,
|
||||
"keyName": "IDX_product_image_deleted_at",
|
||||
"primary": false,
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"keyName": "image_pkey",
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"composite": false,
|
||||
"primary": true,
|
||||
"unique": true
|
||||
}
|
||||
],
|
||||
"checks": [],
|
||||
"foreignKeys": {
|
||||
"image_product_id_foreign": {
|
||||
"constraintName": "image_product_id_foreign",
|
||||
"columnNames": [
|
||||
"product_id"
|
||||
],
|
||||
"localTableName": "public.image",
|
||||
"referencedColumnNames": [
|
||||
"id"
|
||||
],
|
||||
"referencedTableName": "public.product",
|
||||
"deleteRule": "cascade",
|
||||
"updateRule": "cascade"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"columns": {
|
||||
"product_id": {
|
||||
@@ -1098,71 +1131,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"columns": {
|
||||
"product_id": {
|
||||
"name": "product_id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"image_id": {
|
||||
"name": "image_id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
}
|
||||
},
|
||||
"name": "product_images",
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"keyName": "product_images_pkey",
|
||||
"columnNames": [
|
||||
"product_id",
|
||||
"image_id"
|
||||
],
|
||||
"composite": true,
|
||||
"primary": true,
|
||||
"unique": true
|
||||
}
|
||||
],
|
||||
"checks": [],
|
||||
"foreignKeys": {
|
||||
"product_images_product_id_foreign": {
|
||||
"constraintName": "product_images_product_id_foreign",
|
||||
"columnNames": [
|
||||
"product_id"
|
||||
],
|
||||
"localTableName": "public.product_images",
|
||||
"referencedColumnNames": [
|
||||
"id"
|
||||
],
|
||||
"referencedTableName": "public.product",
|
||||
"deleteRule": "cascade",
|
||||
"updateRule": "cascade"
|
||||
},
|
||||
"product_images_image_id_foreign": {
|
||||
"constraintName": "product_images_image_id_foreign",
|
||||
"columnNames": [
|
||||
"image_id"
|
||||
],
|
||||
"localTableName": "public.product_images",
|
||||
"referencedColumnNames": [
|
||||
"id"
|
||||
],
|
||||
"referencedTableName": "public.image",
|
||||
"deleteRule": "cascade",
|
||||
"updateRule": "cascade"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"columns": {
|
||||
"product_id": {
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
export class Migration20241122120331 extends Migration {
|
||||
|
||||
async up(): Promise<void> {
|
||||
this.addSql('alter table if exists "image" add column if not exists "rank" integer not null default 0, add column if not exists "product_id" text not null;');
|
||||
|
||||
// Migrate existing relationships
|
||||
this.addSql(`
|
||||
update "image" i
|
||||
set product_id = pi.product_id,
|
||||
rank = (
|
||||
select count(*)
|
||||
from product_images pi2
|
||||
where pi2.product_id = pi.product_id
|
||||
and pi2.image_id <= pi.image_id
|
||||
) - 1
|
||||
from "product_images" pi
|
||||
where pi.image_id = i.id;
|
||||
`);
|
||||
|
||||
this.addSql('alter table if exists "image" add constraint "image_product_id_foreign" foreign key ("product_id") references "product" ("id") on update cascade on delete cascade;');
|
||||
this.addSql('drop table if exists "product_images" cascade;');
|
||||
}
|
||||
|
||||
async down(): Promise<void> {
|
||||
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"));');
|
||||
|
||||
// Migrate relationships back to join table
|
||||
this.addSql(`
|
||||
insert into "product_images" (product_id, image_id)
|
||||
select product_id, id
|
||||
from "image"
|
||||
where product_id is not null;
|
||||
`);
|
||||
|
||||
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 "image" drop constraint if exists "image_product_id_foreign";');
|
||||
this.addSql('alter table if exists "image" drop column if exists "rank";');
|
||||
this.addSql('alter table if exists "image" drop column if exists "product_id";');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
export { default as Product } from "./product"
|
||||
export { default as ProductCategory } from "./product-category"
|
||||
export { default as ProductCollection } from "./product-collection"
|
||||
export { default as Image } from "./product-image"
|
||||
export { default as ProductOption } from "./product-option"
|
||||
export { default as ProductOptionValue } from "./product-option-value"
|
||||
export { default as ProductTag } from "./product-tag"
|
||||
export { default as ProductType } from "./product-type"
|
||||
export { default as ProductVariant } from "./product-variant"
|
||||
export { default as ProductOption } from "./product-option"
|
||||
export { default as ProductOptionValue } from "./product-option-value"
|
||||
export { default as Image } from "./product-image"
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import {
|
||||
BeforeCreate,
|
||||
Collection,
|
||||
Entity,
|
||||
Filter,
|
||||
Index,
|
||||
ManyToMany,
|
||||
ManyToOne,
|
||||
OnInit,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
Rel,
|
||||
} from "@mikro-orm/core"
|
||||
|
||||
import {
|
||||
@@ -58,8 +58,21 @@ class ProductImage {
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at?: Date
|
||||
|
||||
@ManyToMany(() => Product, (product) => product.images)
|
||||
products = new Collection<Product>(this)
|
||||
@Property({ columnType: "integer", default: 0 })
|
||||
rank: number
|
||||
|
||||
@ManyToOne(() => Product, {
|
||||
columnType: "text",
|
||||
onDelete: "cascade",
|
||||
fieldName: "product_id",
|
||||
mapToPk: true,
|
||||
})
|
||||
product_id: string
|
||||
|
||||
@ManyToOne(() => Product, {
|
||||
persist: false,
|
||||
})
|
||||
product: Rel<Product>
|
||||
|
||||
@OnInit()
|
||||
onInit() {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
BeforeCreate,
|
||||
Cascade,
|
||||
Collection,
|
||||
Entity,
|
||||
Enum,
|
||||
@@ -166,11 +167,8 @@ class Product {
|
||||
})
|
||||
tags = new Collection<ProductTag>(this)
|
||||
|
||||
@ManyToMany(() => ProductImage, "products", {
|
||||
owner: true,
|
||||
pivotTable: "product_images",
|
||||
joinColumn: "product_id",
|
||||
inverseJoinColumn: "image_id",
|
||||
@OneToMany(() => ProductImage, (image) => image.product_id, {
|
||||
cascade: [Cascade.PERSIST, Cascade.REMOVE],
|
||||
})
|
||||
images = new Collection<ProductImage>(this)
|
||||
|
||||
|
||||
@@ -128,6 +128,7 @@ type ProductOption {
|
||||
type ProductImage {
|
||||
id: ID!
|
||||
url: String!
|
||||
rank: Int!
|
||||
metadata: JSON
|
||||
created_at: DateTime!
|
||||
updated_at: DateTime!
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
Context,
|
||||
DAL,
|
||||
FindConfig,
|
||||
IEventBusModuleService,
|
||||
InternalModuleDeclaration,
|
||||
ModuleJoinerConfig,
|
||||
@@ -8,10 +9,10 @@ import {
|
||||
ProductTypes,
|
||||
} from "@medusajs/framework/types"
|
||||
import {
|
||||
Image as ProductImage,
|
||||
Product,
|
||||
ProductCategory,
|
||||
ProductCollection,
|
||||
Image as ProductImage,
|
||||
ProductOption,
|
||||
ProductOptionValue,
|
||||
ProductTag,
|
||||
@@ -58,6 +59,7 @@ type InjectedDependencies = {
|
||||
productCategoryService: ProductCategoryService
|
||||
productCollectionService: ModulesSdkTypes.IMedusaInternalService<any>
|
||||
productImageService: ModulesSdkTypes.IMedusaInternalService<any>
|
||||
productImageProductService: ModulesSdkTypes.IMedusaInternalService<any>
|
||||
productTypeService: ModulesSdkTypes.IMedusaInternalService<any>
|
||||
productOptionService: ModulesSdkTypes.IMedusaInternalService<any>
|
||||
productOptionValueService: ModulesSdkTypes.IMedusaInternalService<any>
|
||||
@@ -151,6 +153,74 @@ export default class ProductModuleService
|
||||
return joinerConfig
|
||||
}
|
||||
|
||||
@InjectManager()
|
||||
// @ts-ignore
|
||||
async retrieveProduct(
|
||||
productId: string,
|
||||
config?: FindConfig<ProductTypes.ProductDTO>,
|
||||
@MedusaContext() sharedContext?: Context
|
||||
): Promise<ProductTypes.ProductDTO> {
|
||||
const product = await this.productService_.retrieve(
|
||||
productId,
|
||||
this.getProductFindConfig_(config),
|
||||
sharedContext
|
||||
)
|
||||
|
||||
return this.baseRepository_.serialize<ProductTypes.ProductDTO>(product)
|
||||
}
|
||||
|
||||
@InjectManager()
|
||||
// @ts-ignore
|
||||
async listProducts(
|
||||
filters?: ProductTypes.FilterableProductProps,
|
||||
config?: FindConfig<ProductTypes.ProductDTO>,
|
||||
sharedContext?: Context
|
||||
): Promise<ProductTypes.ProductDTO[]> {
|
||||
const products = await this.productService_.list(
|
||||
filters,
|
||||
this.getProductFindConfig_(config),
|
||||
sharedContext
|
||||
)
|
||||
|
||||
return this.baseRepository_.serialize<ProductTypes.ProductDTO[]>(products)
|
||||
}
|
||||
|
||||
@InjectManager()
|
||||
// @ts-ignore
|
||||
async listAndCountProducts(
|
||||
filters?: ProductTypes.FilterableProductProps,
|
||||
config?: FindConfig<ProductTypes.ProductDTO>,
|
||||
sharedContext?: Context
|
||||
): Promise<[ProductTypes.ProductDTO[], number]> {
|
||||
const [products, count] = await this.productService_.listAndCount(
|
||||
filters,
|
||||
this.getProductFindConfig_(config),
|
||||
sharedContext
|
||||
)
|
||||
const serializedProducts = await this.baseRepository_.serialize<
|
||||
ProductTypes.ProductDTO[]
|
||||
>(products)
|
||||
return [serializedProducts, count]
|
||||
}
|
||||
|
||||
protected getProductFindConfig_(
|
||||
config?: FindConfig<ProductTypes.ProductDTO>
|
||||
): FindConfig<ProductTypes.ProductDTO> {
|
||||
const hasImagesRelation = config?.relations?.includes("images")
|
||||
|
||||
return {
|
||||
...config,
|
||||
order: {
|
||||
...config?.order,
|
||||
...(hasImagesRelation ? {
|
||||
images: {
|
||||
rank: "ASC",
|
||||
},
|
||||
} : {}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
createProductVariants(
|
||||
data: ProductTypes.CreateProductVariantDTO[],
|
||||
@@ -1440,7 +1510,7 @@ export default class ProductModuleService
|
||||
await this.productService_.upsertWithReplace(
|
||||
normalizedInput,
|
||||
{
|
||||
relations: ["images", "tags", "categories"],
|
||||
relations: ["tags", "categories"],
|
||||
},
|
||||
sharedContext
|
||||
)
|
||||
@@ -1480,6 +1550,27 @@ export default class ProductModuleService
|
||||
)
|
||||
upsertedProduct.variants = productVariants
|
||||
}
|
||||
|
||||
if (Array.isArray(product.images)) {
|
||||
if (product.images.length) {
|
||||
const { entities: productImages } =
|
||||
await this.productImageService_.upsertWithReplace(
|
||||
product.images.map((image, rank) => ({
|
||||
...image,
|
||||
product_id: upsertedProduct.id,
|
||||
rank,
|
||||
})),
|
||||
{},
|
||||
sharedContext
|
||||
)
|
||||
upsertedProduct.images = productImages
|
||||
} else {
|
||||
await this.productImageService_.delete(
|
||||
{ product_id: upsertedProduct.id },
|
||||
sharedContext
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
@@ -1506,7 +1597,7 @@ export default class ProductModuleService
|
||||
await this.productService_.upsertWithReplace(
|
||||
normalizedInput,
|
||||
{
|
||||
relations: ["images", "tags", "categories"],
|
||||
relations: ["tags", "categories"],
|
||||
},
|
||||
sharedContext
|
||||
)
|
||||
@@ -1585,6 +1676,27 @@ export default class ProductModuleService
|
||||
sharedContext
|
||||
)
|
||||
}
|
||||
|
||||
if (Array.isArray(product.images)) {
|
||||
if (product.images.length) {
|
||||
const { entities: productImages } =
|
||||
await this.productImageService_.upsertWithReplace(
|
||||
product.images.map((image, rank) => ({
|
||||
...image,
|
||||
product_id: upsertedProduct.id,
|
||||
rank,
|
||||
})),
|
||||
{},
|
||||
sharedContext
|
||||
)
|
||||
upsertedProduct.images = productImages
|
||||
} else {
|
||||
await this.productImageService_.delete(
|
||||
{ product_id: upsertedProduct.id },
|
||||
sharedContext
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user