From 453297f525bd9f3aaa95bf0b28ff6cd31e6696b4 Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Wed, 18 Oct 2023 15:38:12 +0200 Subject: [PATCH] chore: fix interfaces for product module (#5387) Some changes based on @shahednasser's PR - https://github.com/medusajs/medusa/pull/5341 --- .changeset/gorgeous-fans-wash.md | 6 + .../product-collections.spec.ts | 252 ++++++++++++------ .../product-options.spec.ts | 107 ++++---- packages/product/src/models/product-option.ts | 5 +- .../src/repositories/product-collection.ts | 32 ++- .../src/repositories/product-option.ts | 10 +- .../src/services/product-module-service.ts | 12 +- packages/types/src/product/common.ts | 8 +- 8 files changed, 280 insertions(+), 152 deletions(-) create mode 100644 .changeset/gorgeous-fans-wash.md diff --git a/.changeset/gorgeous-fans-wash.md b/.changeset/gorgeous-fans-wash.md new file mode 100644 index 0000000000..440e1350a8 --- /dev/null +++ b/.changeset/gorgeous-fans-wash.md @@ -0,0 +1,6 @@ +--- +"@medusajs/product": patch +"@medusajs/types": patch +--- + +chore(product,types): fix interfaces for product module diff --git a/packages/product/integration-tests/__tests__/services/product-module-service/product-collections.spec.ts b/packages/product/integration-tests/__tests__/services/product-module-service/product-collections.spec.ts index a521cee195..d1227ef5a5 100644 --- a/packages/product/integration-tests/__tests__/services/product-module-service/product-collections.spec.ts +++ b/packages/product/integration-tests/__tests__/services/product-module-service/product-collections.spec.ts @@ -1,12 +1,11 @@ -import { IProductModuleService } from "@medusajs/types" -import { Product, ProductCollection } from "@models" +import { IProductModuleService, ProductTypes } from "@medusajs/types" import { SqlEntityManager } from "@mikro-orm/postgresql" -import { ProductTypes } from "@medusajs/types" +import { Product, ProductCollection } from "@models" import { initialize } from "../../../../src" -import { DB_URL, TestDatabase } from "../../../utils" -import { createCollections } from "../../../__fixtures__/product" import { EventBusService } from "../../../__fixtures__/event-bus" +import { createCollections } from "../../../__fixtures__/product" +import { DB_URL, TestDatabase } from "../../../utils" describe("ProductModuleService product collections", () => { let service: IProductModuleService @@ -24,14 +23,17 @@ describe("ProductModuleService product collections", () => { repositoryManager = await TestDatabase.forkManager() eventBus = new EventBusService() - service = await initialize({ - database: { - clientUrl: DB_URL, - schema: process.env.MEDUSA_PRODUCT_DB_SCHEMA, + service = await initialize( + { + database: { + clientUrl: DB_URL, + schema: process.env.MEDUSA_PRODUCT_DB_SCHEMA, + }, }, - }, { - eventBusModuleService: eventBus - }) + { + eventBusModuleService: eventBus, + } + ) testManager = await TestDatabase.forkManager() @@ -47,15 +49,18 @@ describe("ProductModuleService product collections", () => { status: ProductTypes.ProductStatus.PUBLISHED, }) - const productCollectionsData = [{ - id: "test-1", - title: "collection 1", - products: [productOne], - },{ - id: "test-2", - title: "collection", - products: [productTwo], - }] + const productCollectionsData = [ + { + id: "test-1", + title: "collection 1", + products: [productOne], + }, + { + id: "test-2", + title: "collection", + products: [productTwo], + }, + ] productCollections = await createCollections( testManager, @@ -65,7 +70,10 @@ describe("ProductModuleService product collections", () => { productCollectionOne = productCollections[0] productCollectionTwo = productCollections[1] - await testManager.persistAndFlush([productCollectionOne, productCollectionTwo]) + await testManager.persistAndFlush([ + productCollectionOne, + productCollectionTwo, + ]) }) afterEach(async () => { @@ -130,7 +138,7 @@ describe("ProductModuleService product collections", () => { expect.objectContaining({ id: "product-1", title: "product 1", - }) + }), ], }), ]) @@ -198,10 +206,12 @@ describe("ProductModuleService product collections", () => { expect.objectContaining({ id: "test-1", title: "collection 1", - products: [expect.objectContaining({ - id: "product-1", - title: "product 1", - })], + products: [ + expect.objectContaining({ + id: "product-1", + title: "product 1", + }), + ], }), ]) }) @@ -215,28 +225,27 @@ describe("ProductModuleService product collections", () => { expect.objectContaining({ id: "test-1", title: "collection 1", - }), + }) ) }) it("should return requested attributes when requested through config", async () => { - const result = await service.retrieveCollection( - productCollectionOne.id, - { - select: ["id", "title", "products.title"], - relations: ["products"], - } - ) + const result = await service.retrieveCollection(productCollectionOne.id, { + select: ["id", "title", "products.title"], + relations: ["products"], + }) expect(result).toEqual( expect.objectContaining({ id: "test-1", title: "collection 1", - products: [expect.objectContaining({ - id: "product-1", - title: "product 1", - })], - }), + products: [ + expect.objectContaining({ + id: "product-1", + title: "product 1", + }), + ], + }) ) }) @@ -249,7 +258,9 @@ describe("ProductModuleService product collections", () => { error = e } - expect(error.message).toEqual("ProductCollection with id: does-not-exist was not found") + expect(error.message).toEqual( + "ProductCollection with id: does-not-exist was not found" + ) }) }) @@ -257,28 +268,26 @@ describe("ProductModuleService product collections", () => { const collectionId = "test-1" it("should delete the product collection given an ID successfully", async () => { - await service.deleteCollections( - [collectionId], - ) + await service.deleteCollections([collectionId]) const collections = await service.listCollections({ - id: collectionId + id: collectionId, }) expect(collections).toHaveLength(0) }) it("should emit events through event bus", async () => { - const eventBusSpy = jest.spyOn(EventBusService.prototype, 'emit') - await service.deleteCollections( - [collectionId], - ) + const eventBusSpy = jest.spyOn(EventBusService.prototype, "emit") + await service.deleteCollections([collectionId]) expect(eventBusSpy).toHaveBeenCalledTimes(1) - expect(eventBusSpy).toHaveBeenCalledWith([{ - eventName: "product-collection.deleted", - data: { id: collectionId } - }]) + expect(eventBusSpy).toHaveBeenCalledWith([ + { + eventName: "product-collection.deleted", + data: { id: collectionId }, + }, + ]) }) }) @@ -286,35 +295,64 @@ describe("ProductModuleService product collections", () => { const collectionId = "test-1" it("should emit events through event bus", async () => { - const eventBusSpy = jest.spyOn(EventBusService.prototype, 'emit') + const eventBusSpy = jest.spyOn(EventBusService.prototype, "emit") - await service.updateCollections( - [{ + await service.updateCollections([ + { id: collectionId, - title: "New Collection" - }] - ) + title: "New Collection", + }, + ]) expect(eventBusSpy).toHaveBeenCalledTimes(1) - expect(eventBusSpy).toHaveBeenCalledWith([{ - eventName: "product-collection.updated", - data: { id: collectionId } - }]) + expect(eventBusSpy).toHaveBeenCalledWith([ + { + eventName: "product-collection.updated", + data: { id: collectionId }, + }, + ]) }) it("should update the value of the collection successfully", async () => { - await service.updateCollections( - [{ + await service.updateCollections([ + { id: collectionId, - title: "New Collection" - }] - ) + title: "New Collection", + }, + ]) const productCollection = await service.retrieveCollection(collectionId) expect(productCollection.title).toEqual("New Collection") }) + it("should add products to a collection successfully", async () => { + await service.updateCollections([ + { + id: collectionId, + product_ids: [productOne.id, productTwo.id], + }, + ]) + + const productCollection = await service.retrieveCollection(collectionId, { + select: ["products.id"], + relations: ["products"], + }) + + expect(productCollection).toEqual( + expect.objectContaining({ + products: [ + expect.objectContaining({ + id: productOne.id, + }), + expect.objectContaining({ + id: productTwo.id, + }), + ], + }) + ) + }) + it("should throw an error when an id does not exist", async () => { let error @@ -322,47 +360,85 @@ describe("ProductModuleService product collections", () => { await service.updateCollections([ { id: "does-not-exist", - title: "New Collection" - } + title: "New Collection", + }, ]) } catch (e) { error = e } - expect(error.message).toEqual('ProductCollection with id "does-not-exist" not found') + expect(error.message).toEqual( + 'ProductCollection with id "does-not-exist" not found' + ) }) }) describe("createCollections", () => { it("should create a collection successfully", async () => { - const res = await service.createCollections( - [{ - title: "New Collection" - }] - ) + const res = await service.createCollections([ + { + title: "New Collection", + }, + ]) const [productCollection] = await service.listCollections({ - title: "New Collection" + title: "New Collection", }) expect(productCollection.title).toEqual("New Collection") }) - it("should emit events through event bus", async () => { - const eventBusSpy = jest.spyOn(EventBusService.prototype, 'emit') + it("should create collection with products successfully", async () => { + await service.createCollections([ + { + title: "New Collection with products", + handle: "new-collection-with-products", + product_ids: [productOne.id, productTwo.id], + }, + ]) - const collections = await service.createCollections( - [{ - title: "New Collection" - }] + const [productCollection] = await service.listCollections( + { + handle: "new-collection-with-products", + }, + { + select: ["title", "handle", "products.id"], + relations: ["products"], + } ) + expect(productCollection).toEqual( + expect.objectContaining({ + title: "New Collection with products", + handle: "new-collection-with-products", + products: [ + expect.objectContaining({ + id: productOne.id, + }), + expect.objectContaining({ + id: productTwo.id, + }), + ], + }) + ) + }) + + it("should emit events through event bus", async () => { + const eventBusSpy = jest.spyOn(EventBusService.prototype, "emit") + + const collections = await service.createCollections([ + { + title: "New Collection", + }, + ]) + expect(eventBusSpy).toHaveBeenCalledTimes(1) - expect(eventBusSpy).toHaveBeenCalledWith([{ - eventName: "product-collection.created", - data: { id: collections[0].id } - }]) + expect(eventBusSpy).toHaveBeenCalledWith([ + { + eventName: "product-collection.created", + data: { id: collections[0].id }, + }, + ]) }) }) }) - diff --git a/packages/product/integration-tests/__tests__/services/product-module-service/product-options.spec.ts b/packages/product/integration-tests/__tests__/services/product-module-service/product-options.spec.ts index 88000d26ee..fb8ffbcf21 100644 --- a/packages/product/integration-tests/__tests__/services/product-module-service/product-options.spec.ts +++ b/packages/product/integration-tests/__tests__/services/product-module-service/product-options.spec.ts @@ -1,9 +1,8 @@ +import { IProductModuleService, ProductTypes } from "@medusajs/types" +import { SqlEntityManager } from "@mikro-orm/postgresql" +import { Product, ProductOption } from "@models" import { initialize } from "../../../../src" import { DB_URL, TestDatabase } from "../../../utils" -import { IProductModuleService } from "@medusajs/types" -import { Product, ProductOption } from "@models" -import { SqlEntityManager } from "@mikro-orm/postgresql" -import { ProductTypes } from "@medusajs/types" describe("ProductModuleService product options", () => { let service: IProductModuleService @@ -103,7 +102,7 @@ describe("ProductModuleService product options", () => { { select: ["title", "product.id"], relations: ["product"], - take: 1 + take: 1, } ) @@ -150,12 +149,13 @@ describe("ProductModuleService product options", () => { id: optionOne.id, }), ]) - ;[options, count] = await service.listAndCountOptions({}, { take: 1 }) expect(count).toEqual(2) - - ;[options, count] = await service.listAndCountOptions({}, { take: 1, skip: 1 }) + ;[options, count] = await service.listAndCountOptions( + {}, + { take: 1, skip: 1 } + ) expect(count).toEqual(2) expect(options).toEqual([ @@ -173,19 +173,21 @@ describe("ProductModuleService product options", () => { { select: ["title", "product.id"], relations: ["product"], - take: 1 + take: 1, } ) expect(count).toEqual(1) - expect(options).toEqual([{ - id: optionOne.id, - title: optionOne.title, - product_id: productOne.id, - product: { - id: productOne.id, + expect(options).toEqual([ + { + id: optionOne.id, + title: optionOne.title, + product_id: productOne.id, + product: { + id: productOne.id, + }, }, - }]) + ]) }) }) @@ -196,18 +198,15 @@ describe("ProductModuleService product options", () => { expect(option).toEqual( expect.objectContaining({ id: optionOne.id, - }), + }) ) }) it("should return requested attributes when requested through config", async () => { - const option = await service.retrieveOption( - optionOne.id, - { - select: ["id", "product.title"], - relations: ["product"], - } - ) + const option = await service.retrieveOption(optionOne.id, { + select: ["id", "product.title"], + relations: ["product"], + }) expect(option).toEqual( expect.objectContaining({ @@ -216,7 +215,7 @@ describe("ProductModuleService product options", () => { id: "product-1", title: "product 1", }, - }), + }) ) }) @@ -229,7 +228,9 @@ describe("ProductModuleService product options", () => { error = e } - expect(error.message).toEqual("ProductOption with id: does-not-exist was not found") + expect(error.message).toEqual( + "ProductOption with id: does-not-exist was not found" + ) }) }) @@ -237,12 +238,10 @@ describe("ProductModuleService product options", () => { const optionId = "option-1" it("should delete the product option given an ID successfully", async () => { - await service.deleteOptions( - [optionId], - ) + await service.deleteOptions([optionId]) const options = await service.listOptions({ - id: optionId + id: optionId, }) expect(options).toHaveLength(0) @@ -253,12 +252,12 @@ describe("ProductModuleService product options", () => { const optionId = "option-1" it("should update the title of the option successfully", async () => { - await service.updateOptions( - [{ + await service.updateOptions([ + { id: optionId, - title: "new test" - }] - ) + title: "new test", + }, + ]) const productOption = await service.retrieveOption(optionId) @@ -272,29 +271,45 @@ describe("ProductModuleService product options", () => { await service.updateOptions([ { id: "does-not-exist", - } + }, ]) } catch (e) { error = e } - expect(error.message).toEqual('ProductOption with id "does-not-exist" not found') + expect(error.message).toEqual( + 'ProductOption with id "does-not-exist" not found' + ) }) }) describe("createOptions", () => { it("should create a option successfully", async () => { - const res = await service.createOptions([{ - title: "test", - product_id: productOne.id - }]) + const res = await service.createOptions([ + { + title: "test", + product_id: productOne.id, + }, + ]) - const productOption = await service.listOptions({ - title: "test" - }) + const [productOption] = await service.listOptions( + { + title: "test", + }, + { + select: ["id", "title", "product.id"], + relations: ["product"], + } + ) - expect(productOption[0]?.title).toEqual("test") + expect(productOption).toEqual( + expect.objectContaining({ + title: "test", + product: expect.objectContaining({ + id: productOne.id, + }), + }) + ) }) }) }) - diff --git a/packages/product/src/models/product-option.ts b/packages/product/src/models/product-option.ts index dab5fbc180..a3a556573d 100644 --- a/packages/product/src/models/product-option.ts +++ b/packages/product/src/models/product-option.ts @@ -1,3 +1,4 @@ +import { DAL } from "@medusajs/types" import { DALUtils, generateEntityId } from "@medusajs/utils" import { BeforeCreate, @@ -14,7 +15,6 @@ import { } from "@mikro-orm/core" import { Product } from "./index" import ProductOptionValue from "./product-option-value" -import { DAL } from "@medusajs/types" type OptionalRelations = | "values" @@ -39,8 +39,9 @@ class ProductOption { @ManyToOne(() => Product, { index: "IDX_product_option_product_id", fieldName: "product_id", + nullable: true, }) - product: Product + product!: Product @OneToMany(() => ProductOptionValue, (value) => value.option, { cascade: [Cascade.REMOVE, "soft-remove" as any], diff --git a/packages/product/src/repositories/product-collection.ts b/packages/product/src/repositories/product-collection.ts index fe97e1c1af..a672d6f6a3 100644 --- a/packages/product/src/repositories/product-collection.ts +++ b/packages/product/src/repositories/product-collection.ts @@ -1,12 +1,20 @@ -import { ProductCollection } from "@models" +import { Context, DAL, ProductTypes } from "@medusajs/types" +import { DALUtils, MedusaError } from "@medusajs/utils" import { + LoadStrategy, FilterQuery as MikroFilterQuery, FindOptions as MikroOptions, - LoadStrategy, } from "@mikro-orm/core" -import { Context, DAL, ProductTypes } from "@medusajs/types" import { SqlEntityManager } from "@mikro-orm/postgresql" -import { DALUtils, MedusaError } from "@medusajs/utils" +import { ProductCollection } from "@models" + +type UpdateProductCollection = ProductTypes.UpdateProductCollectionDTO & { + products?: string[] +} + +type CreateProductCollection = ProductTypes.CreateProductCollectionDTO & { + products?: string[] +} // eslint-disable-next-line max-len export class ProductCollectionRepository extends DALUtils.MikroOrmBaseRepository { @@ -71,12 +79,18 @@ export class ProductCollectionRepository extends DALUtils.MikroOrmBaseRepository } async create( - data: ProductTypes.CreateProductCollectionDTO[], + data: CreateProductCollection[], context: Context = {} ): Promise { const manager = this.getActiveManager(context) const productCollections = data.map((collectionData) => { + if (collectionData.product_ids) { + collectionData.products = collectionData.product_ids + + delete collectionData.product_ids + } + return manager.create(ProductCollection, collectionData) }) @@ -86,7 +100,7 @@ export class ProductCollectionRepository extends DALUtils.MikroOrmBaseRepository } async update( - data: ProductTypes.UpdateProductCollectionDTO[], + data: UpdateProductCollection[], context: Context = {} ): Promise { const manager = this.getActiveManager(context) @@ -119,6 +133,12 @@ export class ProductCollectionRepository extends DALUtils.MikroOrmBaseRepository ) } + if (collectionData.product_ids) { + collectionData.products = collectionData.product_ids + + delete collectionData.product_ids + } + return manager.assign(existingCollection, collectionData) }) diff --git a/packages/product/src/repositories/product-option.ts b/packages/product/src/repositories/product-option.ts index 956d65f377..f06ae8b538 100644 --- a/packages/product/src/repositories/product-option.ts +++ b/packages/product/src/repositories/product-option.ts @@ -1,12 +1,12 @@ +import { Context, DAL, ProductTypes } from "@medusajs/types" +import { DALUtils, MedusaError } from "@medusajs/utils" import { + LoadStrategy, FilterQuery as MikroFilterQuery, FindOptions as MikroOptions, - LoadStrategy, } from "@mikro-orm/core" -import { Product, ProductOption } from "@models" -import { Context, DAL, ProductTypes } from "@medusajs/types" import { SqlEntityManager } from "@mikro-orm/postgresql" -import { DALUtils, MedusaError } from "@medusajs/utils" +import { Product, ProductOption } from "@models" // eslint-disable-next-line max-len export class ProductOptionRepository extends DALUtils.MikroOrmAbstractBaseRepository { @@ -97,7 +97,7 @@ export class ProductOptionRepository extends DALUtils.MikroOrmAbstractBaseReposi if (productId) { const product = existingProductsMap.get(productId) - optionData.product = product + optionData.product_id = product?.id } return manager.create(ProductOption, optionData) diff --git a/packages/product/src/services/product-module-service.ts b/packages/product/src/services/product-module-service.ts index 93b3521129..f53effa1ee 100644 --- a/packages/product/src/services/product-module-service.ts +++ b/packages/product/src/services/product-module-service.ts @@ -446,7 +446,11 @@ export default class ProductModuleService< sharedContext ) - return JSON.parse(JSON.stringify(productOptions)) + return await this.baseRepository_.serialize< + ProductTypes.ProductOptionDTO[] + >(productOptions, { + populate: true, + }) } @InjectTransactionManager(shouldForceTransaction, "baseRepository_") @@ -459,7 +463,11 @@ export default class ProductModuleService< sharedContext ) - return JSON.parse(JSON.stringify(productOptions)) + return await this.baseRepository_.serialize< + ProductTypes.ProductOptionDTO[] + >(productOptions, { + populate: true, + }) } @InjectTransactionManager(shouldForceTransaction, "baseRepository_") diff --git a/packages/types/src/product/common.ts b/packages/types/src/product/common.ts index 98a572d056..aa9a02875b 100644 --- a/packages/types/src/product/common.ts +++ b/packages/types/src/product/common.ts @@ -22,6 +22,7 @@ export interface ProductDTO { is_giftcard: boolean status: ProductStatus thumbnail?: string | null + width?: number | null weight?: number | null length?: number | null height?: number | null @@ -41,6 +42,7 @@ export interface ProductDTO { created_at?: string | Date updated_at?: string | Date deleted_at?: string | Date + metadata?: Record } export interface ProductVariantDTO { @@ -193,6 +195,7 @@ export interface FilterableProductOptionProps export interface FilterableProductCollectionProps extends BaseFilterable { id?: string | string[] + handle?: string | string[] title?: string } @@ -222,7 +225,7 @@ export interface FilterableProductCategoryProps export interface CreateProductCollectionDTO { title: string handle?: string - products?: ProductDTO[] + product_ids?: string[] metadata?: Record } @@ -231,7 +234,7 @@ export interface UpdateProductCollectionDTO { value?: string title?: string handle?: string - products?: ProductDTO[] + product_ids?: string[] metadata?: Record } @@ -269,7 +272,6 @@ export interface UpdateProductTagDTO { export interface CreateProductOptionDTO { title: string product_id?: string - product?: Record } export interface UpdateProductOptionDTO {