chore: fix interfaces for product module (#5387)

Some changes based on @shahednasser's PR - https://github.com/medusajs/medusa/pull/5341
This commit is contained in:
Riqwan Thamir authored and GitHub committed 2023-10-18 13:38:12 +00:00
1 parent a0963f0edf
commit 453297f525
8 files changed
+280 -152

No files matched your search

+6
View File
@@ -0,0 +1,6 @@
---
"@medusajs/product": patch
"@medusajs/types": patch
---
chore(product,types): fix interfaces for product module
@@ -1,12 +1,11 @@
import { IProductModuleService } from "@medusajs/types" import { IProductModuleService, ProductTypes } from "@medusajs/types"
import { Product, ProductCollection } from "@models"
import { SqlEntityManager } from "@mikro-orm/postgresql" import { SqlEntityManager } from "@mikro-orm/postgresql"
import { ProductTypes } from "@medusajs/types" import { Product, ProductCollection } from "@models"
import { initialize } from "../../../../src" import { initialize } from "../../../../src"
import { DB_URL, TestDatabase } from "../../../utils"
import { createCollections } from "../../../__fixtures__/product"
import { EventBusService } from "../../../__fixtures__/event-bus" import { EventBusService } from "../../../__fixtures__/event-bus"
import { createCollections } from "../../../__fixtures__/product"
import { DB_URL, TestDatabase } from "../../../utils"
describe("ProductModuleService product collections", () => { describe("ProductModuleService product collections", () => {
let service: IProductModuleService let service: IProductModuleService
@@ -24,14 +23,17 @@ describe("ProductModuleService product collections", () => {
repositoryManager = await TestDatabase.forkManager() repositoryManager = await TestDatabase.forkManager()
eventBus = new EventBusService() eventBus = new EventBusService()
service = await initialize({ service = await initialize(
database: { {
clientUrl: DB_URL, database: {
schema: process.env.MEDUSA_PRODUCT_DB_SCHEMA, clientUrl: DB_URL,
schema: process.env.MEDUSA_PRODUCT_DB_SCHEMA,
},
}, },
}, { {
eventBusModuleService: eventBus eventBusModuleService: eventBus,
}) }
)
testManager = await TestDatabase.forkManager() testManager = await TestDatabase.forkManager()
@@ -47,15 +49,18 @@ describe("ProductModuleService product collections", () => {
status: ProductTypes.ProductStatus.PUBLISHED, status: ProductTypes.ProductStatus.PUBLISHED,
}) })
const productCollectionsData = [{ const productCollectionsData = [
id: "test-1", {
title: "collection 1", id: "test-1",
products: [productOne], title: "collection 1",
},{ products: [productOne],
id: "test-2", },
title: "collection", {
products: [productTwo], id: "test-2",
}] title: "collection",
products: [productTwo],
},
]
productCollections = await createCollections( productCollections = await createCollections(
testManager, testManager,
@@ -65,7 +70,10 @@ describe("ProductModuleService product collections", () => {
productCollectionOne = productCollections[0] productCollectionOne = productCollections[0]
productCollectionTwo = productCollections[1] productCollectionTwo = productCollections[1]
await testManager.persistAndFlush([productCollectionOne, productCollectionTwo]) await testManager.persistAndFlush([
productCollectionOne,
productCollectionTwo,
])
}) })
afterEach(async () => { afterEach(async () => {
@@ -130,7 +138,7 @@ describe("ProductModuleService product collections", () => {
expect.objectContaining({ expect.objectContaining({
id: "product-1", id: "product-1",
title: "product 1", title: "product 1",
}) }),
], ],
}), }),
]) ])
@@ -198,10 +206,12 @@ describe("ProductModuleService product collections", () => {
expect.objectContaining({ expect.objectContaining({
id: "test-1", id: "test-1",
title: "collection 1", title: "collection 1",
products: [expect.objectContaining({ products: [
id: "product-1", expect.objectContaining({
title: "product 1", id: "product-1",
})], title: "product 1",
}),
],
}), }),
]) ])
}) })
@@ -215,28 +225,27 @@ describe("ProductModuleService product collections", () => {
expect.objectContaining({ expect.objectContaining({
id: "test-1", id: "test-1",
title: "collection 1", title: "collection 1",
}), })
) )
}) })
it("should return requested attributes when requested through config", async () => { it("should return requested attributes when requested through config", async () => {
const result = await service.retrieveCollection( const result = await service.retrieveCollection(productCollectionOne.id, {
productCollectionOne.id, select: ["id", "title", "products.title"],
{ relations: ["products"],
select: ["id", "title", "products.title"], })
relations: ["products"],
}
)
expect(result).toEqual( expect(result).toEqual(
expect.objectContaining({ expect.objectContaining({
id: "test-1", id: "test-1",
title: "collection 1", title: "collection 1",
products: [expect.objectContaining({ products: [
id: "product-1", expect.objectContaining({
title: "product 1", id: "product-1",
})], title: "product 1",
}), }),
],
})
) )
}) })
@@ -249,7 +258,9 @@ describe("ProductModuleService product collections", () => {
error = e 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" const collectionId = "test-1"
it("should delete the product collection given an ID successfully", async () => { it("should delete the product collection given an ID successfully", async () => {
await service.deleteCollections( await service.deleteCollections([collectionId])
[collectionId],
)
const collections = await service.listCollections({ const collections = await service.listCollections({
id: collectionId id: collectionId,
}) })
expect(collections).toHaveLength(0) expect(collections).toHaveLength(0)
}) })
it("should emit events through event bus", async () => { it("should emit events through event bus", async () => {
const eventBusSpy = jest.spyOn(EventBusService.prototype, 'emit') const eventBusSpy = jest.spyOn(EventBusService.prototype, "emit")
await service.deleteCollections( await service.deleteCollections([collectionId])
[collectionId],
)
expect(eventBusSpy).toHaveBeenCalledTimes(1) expect(eventBusSpy).toHaveBeenCalledTimes(1)
expect(eventBusSpy).toHaveBeenCalledWith([{ expect(eventBusSpy).toHaveBeenCalledWith([
eventName: "product-collection.deleted", {
data: { id: collectionId } eventName: "product-collection.deleted",
}]) data: { id: collectionId },
},
])
}) })
}) })
@@ -286,35 +295,64 @@ describe("ProductModuleService product collections", () => {
const collectionId = "test-1" const collectionId = "test-1"
it("should emit events through event bus", async () => { 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, id: collectionId,
title: "New Collection" title: "New Collection",
}] },
) ])
expect(eventBusSpy).toHaveBeenCalledTimes(1) expect(eventBusSpy).toHaveBeenCalledTimes(1)
expect(eventBusSpy).toHaveBeenCalledWith([{ expect(eventBusSpy).toHaveBeenCalledWith([
eventName: "product-collection.updated", {
data: { id: collectionId } eventName: "product-collection.updated",
}]) data: { id: collectionId },
},
])
}) })
it("should update the value of the collection successfully", async () => { it("should update the value of the collection successfully", async () => {
await service.updateCollections( await service.updateCollections([
[{ {
id: collectionId, id: collectionId,
title: "New Collection" title: "New Collection",
}] },
) ])
const productCollection = await service.retrieveCollection(collectionId) const productCollection = await service.retrieveCollection(collectionId)
expect(productCollection.title).toEqual("New Collection") 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 () => { it("should throw an error when an id does not exist", async () => {
let error let error
@@ -322,47 +360,85 @@ describe("ProductModuleService product collections", () => {
await service.updateCollections([ await service.updateCollections([
{ {
id: "does-not-exist", id: "does-not-exist",
title: "New Collection" title: "New Collection",
} },
]) ])
} catch (e) { } catch (e) {
error = 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", () => { describe("createCollections", () => {
it("should create a collection successfully", async () => { it("should create a collection successfully", async () => {
const res = await service.createCollections( const res = await service.createCollections([
[{ {
title: "New Collection" title: "New Collection",
}] },
) ])
const [productCollection] = await service.listCollections({ const [productCollection] = await service.listCollections({
title: "New Collection" title: "New Collection",
}) })
expect(productCollection.title).toEqual("New Collection") expect(productCollection.title).toEqual("New Collection")
}) })
it("should emit events through event bus", async () => { it("should create collection with products successfully", async () => {
const eventBusSpy = jest.spyOn(EventBusService.prototype, 'emit') await service.createCollections([
{
title: "New Collection with products",
handle: "new-collection-with-products",
product_ids: [productOne.id, productTwo.id],
},
])
const collections = await service.createCollections( const [productCollection] = await service.listCollections(
[{ {
title: "New Collection" 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).toHaveBeenCalledTimes(1)
expect(eventBusSpy).toHaveBeenCalledWith([{ expect(eventBusSpy).toHaveBeenCalledWith([
eventName: "product-collection.created", {
data: { id: collections[0].id } eventName: "product-collection.created",
}]) data: { id: collections[0].id },
},
])
}) })
}) })
}) })
@@ -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 { initialize } from "../../../../src"
import { DB_URL, TestDatabase } from "../../../utils" 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", () => { describe("ProductModuleService product options", () => {
let service: IProductModuleService let service: IProductModuleService
@@ -103,7 +102,7 @@ describe("ProductModuleService product options", () => {
{ {
select: ["title", "product.id"], select: ["title", "product.id"],
relations: ["product"], relations: ["product"],
take: 1 take: 1,
} }
) )
@@ -150,12 +149,13 @@ describe("ProductModuleService product options", () => {
id: optionOne.id, id: optionOne.id,
}), }),
]) ])
;[options, count] = await service.listAndCountOptions({}, { take: 1 }) ;[options, count] = await service.listAndCountOptions({}, { take: 1 })
expect(count).toEqual(2) expect(count).toEqual(2)
;[options, count] = await service.listAndCountOptions(
;[options, count] = await service.listAndCountOptions({}, { take: 1, skip: 1 }) {},
{ take: 1, skip: 1 }
)
expect(count).toEqual(2) expect(count).toEqual(2)
expect(options).toEqual([ expect(options).toEqual([
@@ -173,19 +173,21 @@ describe("ProductModuleService product options", () => {
{ {
select: ["title", "product.id"], select: ["title", "product.id"],
relations: ["product"], relations: ["product"],
take: 1 take: 1,
} }
) )
expect(count).toEqual(1) expect(count).toEqual(1)
expect(options).toEqual([{ expect(options).toEqual([
id: optionOne.id, {
title: optionOne.title, id: optionOne.id,
product_id: productOne.id, title: optionOne.title,
product: { product_id: productOne.id,
id: productOne.id, product: {
id: productOne.id,
},
}, },
}]) ])
}) })
}) })
@@ -196,18 +198,15 @@ describe("ProductModuleService product options", () => {
expect(option).toEqual( expect(option).toEqual(
expect.objectContaining({ expect.objectContaining({
id: optionOne.id, id: optionOne.id,
}), })
) )
}) })
it("should return requested attributes when requested through config", async () => { it("should return requested attributes when requested through config", async () => {
const option = await service.retrieveOption( const option = await service.retrieveOption(optionOne.id, {
optionOne.id, select: ["id", "product.title"],
{ relations: ["product"],
select: ["id", "product.title"], })
relations: ["product"],
}
)
expect(option).toEqual( expect(option).toEqual(
expect.objectContaining({ expect.objectContaining({
@@ -216,7 +215,7 @@ describe("ProductModuleService product options", () => {
id: "product-1", id: "product-1",
title: "product 1", title: "product 1",
}, },
}), })
) )
}) })
@@ -229,7 +228,9 @@ describe("ProductModuleService product options", () => {
error = e 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" const optionId = "option-1"
it("should delete the product option given an ID successfully", async () => { it("should delete the product option given an ID successfully", async () => {
await service.deleteOptions( await service.deleteOptions([optionId])
[optionId],
)
const options = await service.listOptions({ const options = await service.listOptions({
id: optionId id: optionId,
}) })
expect(options).toHaveLength(0) expect(options).toHaveLength(0)
@@ -253,12 +252,12 @@ describe("ProductModuleService product options", () => {
const optionId = "option-1" const optionId = "option-1"
it("should update the title of the option successfully", async () => { it("should update the title of the option successfully", async () => {
await service.updateOptions( await service.updateOptions([
[{ {
id: optionId, id: optionId,
title: "new test" title: "new test",
}] },
) ])
const productOption = await service.retrieveOption(optionId) const productOption = await service.retrieveOption(optionId)
@@ -272,29 +271,45 @@ describe("ProductModuleService product options", () => {
await service.updateOptions([ await service.updateOptions([
{ {
id: "does-not-exist", id: "does-not-exist",
} },
]) ])
} catch (e) { } catch (e) {
error = 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", () => { describe("createOptions", () => {
it("should create a option successfully", async () => { it("should create a option successfully", async () => {
const res = await service.createOptions([{ const res = await service.createOptions([
title: "test", {
product_id: productOne.id title: "test",
}]) product_id: productOne.id,
},
])
const productOption = await service.listOptions({ const [productOption] = await service.listOptions(
title: "test" {
}) 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,
}),
})
)
}) })
}) })
}) })
@@ -1,3 +1,4 @@
import { DAL } from "@medusajs/types"
import { DALUtils, generateEntityId } from "@medusajs/utils" import { DALUtils, generateEntityId } from "@medusajs/utils"
import { import {
BeforeCreate, BeforeCreate,
@@ -14,7 +15,6 @@ import {
} from "@mikro-orm/core" } from "@mikro-orm/core"
import { Product } from "./index" import { Product } from "./index"
import ProductOptionValue from "./product-option-value" import ProductOptionValue from "./product-option-value"
import { DAL } from "@medusajs/types"
type OptionalRelations = type OptionalRelations =
| "values" | "values"
@@ -39,8 +39,9 @@ class ProductOption {
@ManyToOne(() => Product, { @ManyToOne(() => Product, {
index: "IDX_product_option_product_id", index: "IDX_product_option_product_id",
fieldName: "product_id", fieldName: "product_id",
nullable: true,
}) })
product: Product product!: Product
@OneToMany(() => ProductOptionValue, (value) => value.option, { @OneToMany(() => ProductOptionValue, (value) => value.option, {
cascade: [Cascade.REMOVE, "soft-remove" as any], cascade: [Cascade.REMOVE, "soft-remove" as any],
@@ -1,12 +1,20 @@
import { ProductCollection } from "@models" import { Context, DAL, ProductTypes } from "@medusajs/types"
import { DALUtils, MedusaError } from "@medusajs/utils"
import { import {
LoadStrategy,
FilterQuery as MikroFilterQuery, FilterQuery as MikroFilterQuery,
FindOptions as MikroOptions, FindOptions as MikroOptions,
LoadStrategy,
} from "@mikro-orm/core" } from "@mikro-orm/core"
import { Context, DAL, ProductTypes } from "@medusajs/types"
import { SqlEntityManager } from "@mikro-orm/postgresql" 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 // eslint-disable-next-line max-len
export class ProductCollectionRepository extends DALUtils.MikroOrmBaseRepository { export class ProductCollectionRepository extends DALUtils.MikroOrmBaseRepository {
@@ -71,12 +79,18 @@ export class ProductCollectionRepository extends DALUtils.MikroOrmBaseRepository
} }
async create( async create(
data: ProductTypes.CreateProductCollectionDTO[], data: CreateProductCollection[],
context: Context = {} context: Context = {}
): Promise<ProductCollection[]> { ): Promise<ProductCollection[]> {
const manager = this.getActiveManager<SqlEntityManager>(context) const manager = this.getActiveManager<SqlEntityManager>(context)
const productCollections = data.map((collectionData) => { const productCollections = data.map((collectionData) => {
if (collectionData.product_ids) {
collectionData.products = collectionData.product_ids
delete collectionData.product_ids
}
return manager.create(ProductCollection, collectionData) return manager.create(ProductCollection, collectionData)
}) })
@@ -86,7 +100,7 @@ export class ProductCollectionRepository extends DALUtils.MikroOrmBaseRepository
} }
async update( async update(
data: ProductTypes.UpdateProductCollectionDTO[], data: UpdateProductCollection[],
context: Context = {} context: Context = {}
): Promise<ProductCollection[]> { ): Promise<ProductCollection[]> {
const manager = this.getActiveManager<SqlEntityManager>(context) const manager = this.getActiveManager<SqlEntityManager>(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) return manager.assign(existingCollection, collectionData)
}) })
@@ -1,12 +1,12 @@
import { Context, DAL, ProductTypes } from "@medusajs/types"
import { DALUtils, MedusaError } from "@medusajs/utils"
import { import {
LoadStrategy,
FilterQuery as MikroFilterQuery, FilterQuery as MikroFilterQuery,
FindOptions as MikroOptions, FindOptions as MikroOptions,
LoadStrategy,
} from "@mikro-orm/core" } from "@mikro-orm/core"
import { Product, ProductOption } from "@models"
import { Context, DAL, ProductTypes } from "@medusajs/types"
import { SqlEntityManager } from "@mikro-orm/postgresql" import { SqlEntityManager } from "@mikro-orm/postgresql"
import { DALUtils, MedusaError } from "@medusajs/utils" import { Product, ProductOption } from "@models"
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
export class ProductOptionRepository extends DALUtils.MikroOrmAbstractBaseRepository<ProductOption> { export class ProductOptionRepository extends DALUtils.MikroOrmAbstractBaseRepository<ProductOption> {
@@ -97,7 +97,7 @@ export class ProductOptionRepository extends DALUtils.MikroOrmAbstractBaseReposi
if (productId) { if (productId) {
const product = existingProductsMap.get(productId) const product = existingProductsMap.get(productId)
optionData.product = product optionData.product_id = product?.id
} }
return manager.create(ProductOption, optionData) return manager.create(ProductOption, optionData)
@@ -446,7 +446,11 @@ export default class ProductModuleService<
sharedContext sharedContext
) )
return JSON.parse(JSON.stringify(productOptions)) return await this.baseRepository_.serialize<
ProductTypes.ProductOptionDTO[]
>(productOptions, {
populate: true,
})
} }
@InjectTransactionManager(shouldForceTransaction, "baseRepository_") @InjectTransactionManager(shouldForceTransaction, "baseRepository_")
@@ -459,7 +463,11 @@ export default class ProductModuleService<
sharedContext sharedContext
) )
return JSON.parse(JSON.stringify(productOptions)) return await this.baseRepository_.serialize<
ProductTypes.ProductOptionDTO[]
>(productOptions, {
populate: true,
})
} }
@InjectTransactionManager(shouldForceTransaction, "baseRepository_") @InjectTransactionManager(shouldForceTransaction, "baseRepository_")
+5 -3
View File
@@ -22,6 +22,7 @@ export interface ProductDTO {
is_giftcard: boolean is_giftcard: boolean
status: ProductStatus status: ProductStatus
thumbnail?: string | null thumbnail?: string | null
width?: number | null
weight?: number | null weight?: number | null
length?: number | null length?: number | null
height?: number | null height?: number | null
@@ -41,6 +42,7 @@ export interface ProductDTO {
created_at?: string | Date created_at?: string | Date
updated_at?: string | Date updated_at?: string | Date
deleted_at?: string | Date deleted_at?: string | Date
metadata?: Record<string, unknown>
} }
export interface ProductVariantDTO { export interface ProductVariantDTO {
@@ -193,6 +195,7 @@ export interface FilterableProductOptionProps
export interface FilterableProductCollectionProps export interface FilterableProductCollectionProps
extends BaseFilterable<FilterableProductCollectionProps> { extends BaseFilterable<FilterableProductCollectionProps> {
id?: string | string[] id?: string | string[]
handle?: string | string[]
title?: string title?: string
} }
@@ -222,7 +225,7 @@ export interface FilterableProductCategoryProps
export interface CreateProductCollectionDTO { export interface CreateProductCollectionDTO {
title: string title: string
handle?: string handle?: string
products?: ProductDTO[] product_ids?: string[]
metadata?: Record<string, unknown> metadata?: Record<string, unknown>
} }
@@ -231,7 +234,7 @@ export interface UpdateProductCollectionDTO {
value?: string value?: string
title?: string title?: string
handle?: string handle?: string
products?: ProductDTO[] product_ids?: string[]
metadata?: Record<string, unknown> metadata?: Record<string, unknown>
} }
@@ -269,7 +272,6 @@ export interface UpdateProductTagDTO {
export interface CreateProductOptionDTO { export interface CreateProductOptionDTO {
title: string title: string
product_id?: string product_id?: string
product?: Record<any, any>
} }
export interface UpdateProductOptionDTO { export interface UpdateProductOptionDTO {