diff --git a/integration-tests/api/__tests__/admin/product.js b/integration-tests/api/__tests__/admin/product.js index 111a9265b5..7a70eaf115 100644 --- a/integration-tests/api/__tests__/admin/product.js +++ b/integration-tests/api/__tests__/admin/product.js @@ -42,6 +42,7 @@ describe("/admin/products", () => { const manager = dbConnection.manager; await manager.query(`DELETE FROM "product_option_value"`); await manager.query(`DELETE FROM "product_option"`); + await manager.query(`DELETE FROM "image"`); await manager.query(`DELETE FROM "money_amount"`); await manager.query(`DELETE FROM "product_variant"`); await manager.query(`DELETE FROM "product"`); @@ -62,6 +63,7 @@ describe("/admin/products", () => { title: "Test product", description: "test-product-description", type: { value: "test-type" }, + images: ["test-image.png", "test-image-2.png"], collection_id: "test-collection", tags: [{ value: "123" }, { value: "456" }], options: [{ title: "size" }, { title: "color" }], @@ -91,6 +93,15 @@ describe("/admin/products", () => { expect.objectContaining({ title: "Test product", handle: "test-product", + images: expect.arrayContaining([ + expect.objectContaining({ + url: "test-image.png", + }), + expect.objectContaining({ + url: "test-image-2.png", + }), + ]), + thumbnail: "test-image.png", tags: [ expect.objectContaining({ value: "123", @@ -137,13 +148,15 @@ describe("/admin/products", () => { ); }); - it("updates a product (update tags, delete collection, delete type)", async () => { + it("updates a product (update tags, delete collection, delete type, replaces images)", async () => { const api = useApi(); const payload = { collection_id: null, type: null, tags: [{ value: "123" }], + images: ["test-image-2.png"], + type: { value: "test-type-2" }, }; const response = await api @@ -160,6 +173,12 @@ describe("/admin/products", () => { expect(response.data.product).toEqual( expect.objectContaining({ + images: expect.arrayContaining([ + expect.objectContaining({ + url: "test-image-2.png", + }), + ]), + thumbnail: "test-image-2.png", tags: [ expect.objectContaining({ value: "123", @@ -167,6 +186,9 @@ describe("/admin/products", () => { ], type: null, collection: null, + type: expect.objectContaining({ + value: "test-type-2", + }), }) ); }); diff --git a/integration-tests/api/helpers/product-seeder.js b/integration-tests/api/helpers/product-seeder.js index 14255c729e..79e6f4eb97 100644 --- a/integration-tests/api/helpers/product-seeder.js +++ b/integration-tests/api/helpers/product-seeder.js @@ -6,6 +6,7 @@ const { Product, ShippingProfile, ProductVariant, + Image, } = require("@medusajs/medusa"); module.exports = async (connection, data = {}) => { @@ -36,6 +37,13 @@ module.exports = async (connection, data = {}) => { await manager.save(type); + const image = manager.create(Image, { + id: "test-image", + url: "test-image.png", + }); + + await manager.save(image); + await manager.insert(Region, { id: "test-region", name: "Test Region", @@ -43,7 +51,7 @@ module.exports = async (connection, data = {}) => { tax_rate: 0, }); - await manager.insert(Product, { + const p = manager.create(Product, { id: "test-product", title: "Test product", profile_id: defaultProfile.id, @@ -57,6 +65,10 @@ module.exports = async (connection, data = {}) => { options: [{ id: "test-option", title: "Default value" }], }); + p.images = [image]; + + await manager.save(p); + await manager.insert(ProductVariant, { id: "test-variant", inventory_quantity: 10, diff --git a/packages/medusa/src/models/product.ts b/packages/medusa/src/models/product.ts index 2fb5a58e57..e1f453ae18 100644 --- a/packages/medusa/src/models/product.ts +++ b/packages/medusa/src/models/product.ts @@ -46,7 +46,7 @@ export class Product { @Column({ default: false }) is_giftcard: boolean - @ManyToMany(() => Image) + @ManyToMany(() => Image, { cascade: ["insert"] }) @JoinTable({ name: "product_images", joinColumn: { diff --git a/packages/medusa/src/repositories/image.ts b/packages/medusa/src/repositories/image.ts new file mode 100644 index 0000000000..c8799ca37d --- /dev/null +++ b/packages/medusa/src/repositories/image.ts @@ -0,0 +1,5 @@ +import { EntityRepository, Repository } from "typeorm" +import { Image } from "../models/image" + +@EntityRepository(Image) +export class ImageRepository extends Repository {} diff --git a/packages/medusa/src/services/product.js b/packages/medusa/src/services/product.js index efc7fabde1..fedc1a876e 100644 --- a/packages/medusa/src/services/product.js +++ b/packages/medusa/src/services/product.js @@ -23,6 +23,7 @@ class ProductService extends BaseService { productCollectionService, productTypeRepository, productTagRepository, + imageRepository, }) { super() @@ -52,6 +53,9 @@ class ProductService extends BaseService { /** @private @const {ProductCollectionService} */ this.productTagRepository_ = productTagRepository + + /** @private @const {ImageRepository} */ + this.imageRepository_ = imageRepository } withTransaction(transactionManager) { @@ -69,6 +73,7 @@ class ProductService extends BaseService { productCollectionService: this.productCollectionService_, productTagRepository: this.productTagRepository_, productTypeRepository: this.productTypeRepository_, + imageRepository: this.imageRepository_, }) cloned.transactionManager_ = transactionManager @@ -233,7 +238,7 @@ class ProductService extends BaseService { }) if (existing) { - return existing + return existing.id } const created = productTypeRepository.create(type) @@ -277,10 +282,18 @@ class ProductService extends BaseService { this.productOptionRepository_ ) - const { options, tags, type, ...rest } = productObject + const { options, tags, type, images, ...rest } = productObject + + if (!rest.thumbnail && images && images.length) { + rest.thumbnail = images[0] + } let product = productRepo.create(rest) + if (images && images.length) { + product.images = await this.upsertImages_(images) + } + if (tags) { product.tags = await this.upsertProductTags_(tags) } @@ -310,6 +323,28 @@ class ProductService extends BaseService { }) } + async upsertImages_(images) { + const imageRepository = this.manager_.getCustomRepository( + this.imageRepository_ + ) + + let productImages = [] + for (const img of images) { + const existing = await imageRepository.findOne({ + where: { url: img }, + }) + + if (existing) { + productImages.push(existing) + } else { + const created = imageRepository.create({ url: img }) + productImages.push(created) + } + } + + return productImages + } + /** * Updates a product. Product variant updates should use dedicated methods, * e.g. `addVariant`, etc. The function will throw errors if metadata or @@ -327,7 +362,7 @@ class ProductService extends BaseService { ) const product = await this.retrieve(productId, { - relations: ["variants", "tags"], + relations: ["variants", "tags", "images"], }) const { @@ -344,6 +379,10 @@ class ProductService extends BaseService { product.thumbnail = images[0] } + if (images && images.length) { + product.images = await this.upsertImages_(images) + } + if (metadata) { product.metadata = this.setMetadata_(product, metadata) }