feat(product, types, modules-sdk): added event bus events for products (#4654)

what:

- adds an eventbus dependency to product module.
- emits events on product, category and collection CUD

RESOLVES CORE-1450
This commit is contained in:
Riqwan Thamir
2023-08-04 11:26:02 +02:00
committed by GitHub
parent 43f34866c8
commit 8af55aed87
16 changed files with 616 additions and 23 deletions

View File

@@ -0,0 +1,7 @@
---
"@medusajs/product": patch
"@medusajs/types": patch
"@medusajs/modules-sdk": patch
---
feat(product,types): added event bus events for products

View File

@@ -20,6 +20,7 @@ packages/*
!packages/cache-redis !packages/cache-redis
!packages/cache-inmemory !packages/cache-inmemory
!packages/create-medusa-app !packages/create-medusa-app
!packages/product
**/models/* **/models/*

View File

@@ -95,6 +95,7 @@ module.exports = {
"./packages/cache-redis/tsconfig.spec.json", "./packages/cache-redis/tsconfig.spec.json",
"./packages/cache-inmemory/tsconfig.spec.json", "./packages/cache-inmemory/tsconfig.spec.json",
"./packages/create-medusa-app/tsconfig.json", "./packages/create-medusa-app/tsconfig.json",
"./packages/product/tsconfig.json",
], ],
}, },
rules: { rules: {

View File

@@ -12,11 +12,15 @@ export enum Modules {
PRODUCT = "productService", PRODUCT = "productService",
} }
export enum ModuleRegistrationName {
EVENT_BUS = "eventBusModuleService"
}
export const ModulesDefinition: { [key: string | Modules]: ModuleDefinition } = export const ModulesDefinition: { [key: string | Modules]: ModuleDefinition } =
{ {
[Modules.EVENT_BUS]: { [Modules.EVENT_BUS]: {
key: Modules.EVENT_BUS, key: Modules.EVENT_BUS,
registrationName: "eventBusModuleService", registrationName: ModuleRegistrationName.EVENT_BUS,
defaultPackage: "@medusajs/event-bus-local", defaultPackage: "@medusajs/event-bus-local",
label: "EventBusModuleService", label: "EventBusModuleService",
canOverride: true, canOverride: true,
@@ -75,7 +79,7 @@ export const ModulesDefinition: { [key: string | Modules]: ModuleDefinition } =
isRequired: false, isRequired: false,
canOverride: true, canOverride: true,
isQueryable: true, isQueryable: true,
dependencies: [], dependencies: [ModuleRegistrationName.EVENT_BUS],
defaultModuleDeclaration: { defaultModuleDeclaration: {
scope: MODULE_SCOPE.EXTERNAL, scope: MODULE_SCOPE.EXTERNAL,
}, },

View File

@@ -0,0 +1,36 @@
import {
EmitData,
EventBusTypes,
Subscriber,
IEventBusModuleService
} from "@medusajs/types"
export class EventBusService implements IEventBusModuleService {
async emit<T>(
eventName: string,
data: T,
options: Record<string, unknown>
): Promise<void>
async emit<T>(data: EventBusTypes.EmitData<T>[]): Promise<void>
async emit<T, TInput extends string | EventBusTypes.EmitData<T>[] = string>(
eventOrData: TInput,
data?: T,
options: Record<string, unknown> = {}
): Promise<void> {}
subscribe(event: string | symbol, subscriber: Subscriber): this {
return this
}
unsubscribe(
event: string | symbol,
subscriber: Subscriber,
context?: EventBusTypes.SubscriberContext
): this {
return this
}
withTransaction() {
}
}

View File

@@ -5,8 +5,6 @@ import { ProductRepository } from "../__fixtures__/module"
import { createProductAndTags } from "../__fixtures__/product" import { createProductAndTags } from "../__fixtures__/product"
import { productsData } from "../__fixtures__/product/data" import { productsData } from "../__fixtures__/product/data"
import { DB_URL, TestDatabase } from "../utils" import { DB_URL, TestDatabase } from "../utils"
import { buildProductAndRelationsData } from "../__fixtures__/product/data/create-product"
import { kebabCase } from "@medusajs/utils"
import { IProductModuleService } from "@medusajs/types" import { IProductModuleService } from "@medusajs/types"
const beforeEach_ = async () => { const beforeEach_ = async () => {

View File

@@ -6,6 +6,7 @@ import { initialize } from "../../../../src"
import { DB_URL, TestDatabase } from "../../../utils" import { DB_URL, TestDatabase } from "../../../utils"
import { createProductCategories } from "../../../__fixtures__/product-category" import { createProductCategories } from "../../../__fixtures__/product-category"
import { productCategoriesRankData } from "../../../__fixtures__/product-category/data" import { productCategoriesRankData } from "../../../__fixtures__/product-category/data"
import { EventBusService } from "../../../__fixtures__/event-bus"
describe("ProductModuleService product categories", () => { describe("ProductModuleService product categories", () => {
let service: IProductModuleService let service: IProductModuleService
@@ -16,16 +17,20 @@ describe("ProductModuleService product categories", () => {
let productCategoryOne: ProductCategory let productCategoryOne: ProductCategory
let productCategoryTwo: ProductCategory let productCategoryTwo: ProductCategory
let productCategories: ProductCategory[] let productCategories: ProductCategory[]
let eventBus
beforeEach(async () => { beforeEach(async () => {
await TestDatabase.setupDatabase() await TestDatabase.setupDatabase()
repositoryManager = await TestDatabase.forkManager() repositoryManager = await TestDatabase.forkManager()
eventBus = new EventBusService()
service = await initialize({ service = await initialize({
database: { database: {
clientUrl: DB_URL, clientUrl: DB_URL,
schema: process.env.MEDUSA_PRODUCT_DB_SCHEMA, schema: process.env.MEDUSA_PRODUCT_DB_SCHEMA,
}, },
}, {
eventBusModuleService: eventBus
}) })
testManager = await TestDatabase.forkManager() testManager = await TestDatabase.forkManager()
@@ -68,6 +73,7 @@ describe("ProductModuleService product categories", () => {
afterEach(async () => { afterEach(async () => {
await TestDatabase.clearDatabase() await TestDatabase.clearDatabase()
jest.clearAllMocks()
}) })
describe("listCategories", () => { describe("listCategories", () => {
@@ -279,6 +285,22 @@ describe("ProductModuleService product categories", () => {
) )
}) })
it("should emit events through event bus", async () => {
const eventBusSpy = jest.spyOn(EventBusService.prototype, 'emit')
const category = await service.createCategory({
name: "New Category",
parent_category_id: productCategoryOne.id,
})
expect(eventBusSpy).toHaveBeenCalledTimes(1)
expect(eventBusSpy).toHaveBeenCalledWith(
"product-category.created",
{
id: category.id
}
)
})
it("should append rank from an existing category depending on parent", async () => { it("should append rank from an existing category depending on parent", async () => {
await service.createCategory({ await service.createCategory({
name: "New Category", name: "New Category",
@@ -356,6 +378,21 @@ describe("ProductModuleService product categories", () => {
productCategoryZeroTwo = categories[5] productCategoryZeroTwo = categories[5]
}) })
it("should emit events through event bus", async () => {
const eventBusSpy = jest.spyOn(EventBusService.prototype, 'emit')
await service.updateCategory(productCategoryZero.id, {
name: "New Category",
})
expect(eventBusSpy).toHaveBeenCalledTimes(1)
expect(eventBusSpy).toHaveBeenCalledWith(
"product-category.updated",
{
id: productCategoryZero.id
}
)
})
it("should update the name of the category successfully", async () => { it("should update the name of the category successfully", async () => {
await service.updateCategory(productCategoryZero.id, { await service.updateCategory(productCategoryZero.id, {
name: "New Category", name: "New Category",
@@ -513,6 +550,19 @@ describe("ProductModuleService product categories", () => {
productCategoryTwo = categories[2] productCategoryTwo = categories[2]
}) })
it("should emit events through event bus", async () => {
const eventBusSpy = jest.spyOn(EventBusService.prototype, 'emit')
await service.deleteCategory(productCategoryOne.id)
expect(eventBusSpy).toHaveBeenCalledTimes(1)
expect(eventBusSpy).toHaveBeenCalledWith(
"product-category.deleted",
{
id: productCategoryOne.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

View File

@@ -6,6 +6,7 @@ import { ProductTypes } from "@medusajs/types"
import { initialize } from "../../../../src" import { initialize } from "../../../../src"
import { DB_URL, TestDatabase } from "../../../utils" import { DB_URL, TestDatabase } from "../../../utils"
import { createCollections } from "../../../__fixtures__/product" import { createCollections } from "../../../__fixtures__/product"
import { EventBusService } from "../../../__fixtures__/event-bus"
describe("ProductModuleService product collections", () => { describe("ProductModuleService product collections", () => {
let service: IProductModuleService let service: IProductModuleService
@@ -16,16 +17,20 @@ describe("ProductModuleService product collections", () => {
let productCollectionOne: ProductCollection let productCollectionOne: ProductCollection
let productCollectionTwo: ProductCollection let productCollectionTwo: ProductCollection
let productCollections: ProductCollection[] let productCollections: ProductCollection[]
let eventBus
beforeEach(async () => { beforeEach(async () => {
await TestDatabase.setupDatabase() await TestDatabase.setupDatabase()
repositoryManager = await TestDatabase.forkManager() repositoryManager = await TestDatabase.forkManager()
eventBus = new EventBusService()
service = await initialize({ service = await initialize({
database: { database: {
clientUrl: DB_URL, clientUrl: DB_URL,
schema: process.env.MEDUSA_PRODUCT_DB_SCHEMA, schema: process.env.MEDUSA_PRODUCT_DB_SCHEMA,
}, },
}, {
eventBusModuleService: eventBus
}) })
testManager = await TestDatabase.forkManager() testManager = await TestDatabase.forkManager()
@@ -65,6 +70,7 @@ describe("ProductModuleService product collections", () => {
afterEach(async () => { afterEach(async () => {
await TestDatabase.clearDatabase() await TestDatabase.clearDatabase()
jest.clearAllMocks()
}) })
describe("listCollections", () => { describe("listCollections", () => {
@@ -261,11 +267,41 @@ describe("ProductModuleService product collections", () => {
expect(collections).toHaveLength(0) expect(collections).toHaveLength(0)
}) })
it("should emit events through event bus", async () => {
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 }
}])
})
}) })
describe("updateCollections", () => { describe("updateCollections", () => {
const collectionId = "test-1" const collectionId = "test-1"
it("should emit events through event bus", async () => {
const eventBusSpy = jest.spyOn(EventBusService.prototype, 'emit')
await service.updateCollections(
[{
id: collectionId,
title: "New Collection"
}]
)
expect(eventBusSpy).toHaveBeenCalledTimes(1)
expect(eventBusSpy).toHaveBeenCalledWith([{
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(
[{ [{
@@ -311,6 +347,22 @@ describe("ProductModuleService product collections", () => {
expect(productCollection.title).toEqual("New Collection") expect(productCollection.title).toEqual("New Collection")
}) })
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 }
}])
})
}) })
}) })

View File

@@ -1,12 +1,14 @@
import { MedusaModule } from "@medusajs/modules-sdk" import { MedusaModule } from "@medusajs/modules-sdk"
import { Product, ProductCategory, ProductCollection, ProductType, ProductVariant } from "@models" import { Product, ProductCategory, ProductCollection, ProductType, ProductVariant } from "@models"
import { IProductModuleService, ProductTypes } from "@medusajs/types" import { IProductModuleService, ProductTypes } from "@medusajs/types"
import { kebabCase } from "@medusajs/utils"
import { initialize } from "../../../../src" import { initialize } from "../../../../src"
import { DB_URL, TestDatabase } from "../../../utils" import { DB_URL, TestDatabase } from "../../../utils"
import { buildProductAndRelationsData } from "../../../__fixtures__/product/data/create-product" import { buildProductAndRelationsData } from "../../../__fixtures__/product/data/create-product"
import { createProductCategories } from "../../../__fixtures__/product-category" import { createProductCategories } from "../../../__fixtures__/product-category"
import { createCollections, createTypes } from "../../../__fixtures__/product" import { createCollections, createTypes } from "../../../__fixtures__/product"
import { EventBusService } from "../../../__fixtures__/event-bus"
const beforeEach_ = async () => { const beforeEach_ = async () => {
await TestDatabase.setupDatabase() await TestDatabase.setupDatabase()
@@ -15,6 +17,7 @@ const beforeEach_ = async () => {
const afterEach_ = async () => { const afterEach_ = async () => {
await TestDatabase.clearDatabase() await TestDatabase.clearDatabase()
jest.clearAllMocks()
} }
describe("ProductModuleService products", function () { describe("ProductModuleService products", function () {
@@ -32,6 +35,7 @@ describe("ProductModuleService products", function () {
let productTypeOne: ProductType let productTypeOne: ProductType
let productTypeTwo: ProductType let productTypeTwo: ProductType
let images = ["image-1"] let images = ["image-1"]
let eventBus
const productCategoriesData = [{ const productCategoriesData = [{
id: "test-1", id: "test-1",
@@ -135,11 +139,14 @@ describe("ProductModuleService products", function () {
MedusaModule.clearInstances() MedusaModule.clearInstances()
eventBus = new EventBusService()
module = await initialize({ module = await initialize({
database: { database: {
clientUrl: DB_URL, clientUrl: DB_URL,
schema: process.env.MEDUSA_PRODUCT_DB_SCHEMA, schema: process.env.MEDUSA_PRODUCT_DB_SCHEMA,
}, },
}, {
eventBusModuleService: eventBus
}) })
}) })
@@ -228,6 +235,28 @@ describe("ProductModuleService products", function () {
) )
}) })
it("should emit events through event bus", async () => {
const eventBusSpy = jest.spyOn(EventBusService.prototype, 'emit')
const data = buildProductAndRelationsData({
images,
thumbnail: images[0],
})
const updateData = {
...data,
id: productOne.id,
title: "updated title"
}
await module.update([updateData])
expect(eventBusSpy).toHaveBeenCalledTimes(1)
expect(eventBusSpy).toHaveBeenCalledWith([{
eventName: "product.updated",
data: { id: productOne.id }
}])
})
it("should add relationships to a product", async () => { it("should add relationships to a product", async () => {
const updateData = { const updateData = {
id: productOne.id, id: productOne.id,
@@ -461,4 +490,284 @@ describe("ProductModuleService products", function () {
expect(error.message).toEqual(`ProductVariant with id "does-not-exist" not found`) expect(error.message).toEqual(`ProductVariant with id "does-not-exist" not found`)
}) })
}) })
describe("create", function () {
let module: IProductModuleService
let images = ["image-1"]
let eventBus
beforeEach(async () => {
await beforeEach_()
MedusaModule.clearInstances()
eventBus = new EventBusService()
module = await initialize({
database: {
clientUrl: DB_URL,
schema: process.env.MEDUSA_PRODUCT_DB_SCHEMA,
},
}, {
eventBusModuleService: eventBus
})
})
afterEach(afterEach_)
it("should create a product", async () => {
const data = buildProductAndRelationsData({
images,
thumbnail: images[0],
})
const products = await module.create([data])
expect(products).toHaveLength(1)
expect(products[0].images).toHaveLength(1)
expect(products[0].options).toHaveLength(1)
expect(products[0].tags).toHaveLength(1)
expect(products[0].categories).toHaveLength(0)
expect(products[0].variants).toHaveLength(1)
expect(products[0]).toEqual(
expect.objectContaining({
id: expect.any(String),
title: data.title,
handle: kebabCase(data.title),
description: data.description,
subtitle: data.subtitle,
is_giftcard: data.is_giftcard,
discountable: data.discountable,
thumbnail: images[0],
status: data.status,
images: expect.arrayContaining([
expect.objectContaining({
id: expect.any(String),
url: images[0],
}),
]),
options: expect.arrayContaining([
expect.objectContaining({
id: expect.any(String),
title: data.options[0].title,
values: expect.arrayContaining([
expect.objectContaining({
id: expect.any(String),
value: data.variants[0].options?.[0].value,
}),
]),
}),
]),
tags: expect.arrayContaining([
expect.objectContaining({
id: expect.any(String),
value: data.tags[0].value,
}),
]),
type: expect.objectContaining({
id: expect.any(String),
value: data.type.value,
}),
variants: expect.arrayContaining([
expect.objectContaining({
id: expect.any(String),
title: data.variants[0].title,
sku: data.variants[0].sku,
allow_backorder: false,
manage_inventory: true,
inventory_quantity: 100,
variant_rank: 0,
options: expect.arrayContaining([
expect.objectContaining({
id: expect.any(String),
value: data.variants[0].options?.[0].value,
}),
]),
}),
]),
})
)
})
it("should emit events through eventBus", async () => {
const eventBusSpy = jest.spyOn(EventBusService.prototype, 'emit')
const data = buildProductAndRelationsData({
images,
thumbnail: images[0],
})
const products = await module.create([data])
expect(eventBusSpy).toHaveBeenCalledTimes(1)
expect(eventBusSpy).toHaveBeenCalledWith([{
eventName: "product.created",
data: { id: products[0].id }
}])
})
})
describe("softDelete", function () {
let module: IProductModuleService
let images = ["image-1"]
let eventBus
beforeEach(async () => {
await beforeEach_()
MedusaModule.clearInstances()
eventBus = new EventBusService()
module = await initialize({
database: {
clientUrl: DB_URL,
schema: process.env.MEDUSA_PRODUCT_DB_SCHEMA,
},
}, {
eventBusModuleService: eventBus
})
})
afterEach(afterEach_)
it("should soft delete a product and its cascaded relations", async () => {
const data = buildProductAndRelationsData({
images,
thumbnail: images[0],
})
const products = await module.create([data])
await module.softDelete([products[0].id])
const deletedProducts = await module.list(
{ id: products[0].id },
{
relations: [
"variants",
"variants.options",
"options",
"options.values",
],
withDeleted: true,
}
)
expect(deletedProducts).toHaveLength(1)
expect(deletedProducts[0].deleted_at).not.toBeNull()
for (const option of deletedProducts[0].options) {
expect(option.deleted_at).not.toBeNull()
}
const productOptionsValues = deletedProducts[0].options
.map((o) => o.values)
.flat()
for (const optionValue of productOptionsValues) {
expect(optionValue.deleted_at).not.toBeNull()
}
for (const variant of deletedProducts[0].variants) {
expect(variant.deleted_at).not.toBeNull()
}
const variantsOptions = deletedProducts[0].options
.map((o) => o.values)
.flat()
for (const option of variantsOptions) {
expect(option.deleted_at).not.toBeNull()
}
})
it("should emit events through eventBus", async () => {
const eventBusSpy = jest.spyOn(EventBusService.prototype, 'emit')
const data = buildProductAndRelationsData({
images,
thumbnail: images[0],
})
const products = await module.create([data])
await module.softDelete([products[0].id])
expect(eventBusSpy).toHaveBeenCalledWith([{
eventName: "product.created",
data: { id: products[0].id }
}])
})
})
describe("restore", function () {
let module: IProductModuleService
let images = ["image-1"]
beforeEach(async () => {
await beforeEach_()
MedusaModule.clearInstances()
module = await initialize({
database: {
clientUrl: DB_URL,
schema: process.env.MEDUSA_PRODUCT_DB_SCHEMA,
},
})
})
afterEach(afterEach_)
it("should restore a soft deleted product and its cascaded relations", async () => {
const data = buildProductAndRelationsData({
images,
thumbnail: images[0],
})
const products = await module.create([data])
await module.softDelete([products[0].id])
await module.restore([products[0].id])
const deletedProducts = await module.list(
{ id: products[0].id },
{
relations: [
"variants",
"variants.options",
"variants.options",
"options",
"options.values",
],
withDeleted: true,
}
)
expect(deletedProducts).toHaveLength(1)
expect(deletedProducts[0].deleted_at).toBeNull()
for (const option of deletedProducts[0].options) {
expect(option.deleted_at).toBeNull()
}
const productOptionsValues = deletedProducts[0].options
.map((o) => o.values)
.flat()
for (const optionValue of productOptionsValues) {
expect(optionValue.deleted_at).toBeNull()
}
for (const variant of deletedProducts[0].variants) {
expect(variant.deleted_at).toBeNull()
}
const variantsOptions = deletedProducts[0].options
.map((o) => o.values)
.flat()
for (const option of variantsOptions) {
expect(option.deleted_at).toBeNull()
}
})
})
}) })

View File

@@ -25,13 +25,31 @@ import {
InternalModuleDeclaration, InternalModuleDeclaration,
JoinerServiceConfig, JoinerServiceConfig,
ProductTypes, ProductTypes,
IEventBusModuleService,
} from "@medusajs/types" } from "@medusajs/types"
import ProductImageService from "./product-image" import ProductImageService from "./product-image"
import { import {
ProductServiceTypes, CreateProductCategoryDTO,
ProductVariantServiceTypes, ProductCategoryEventData,
} from "../types/services" ProductCategoryEvents,
UpdateProductCategoryDTO,
} from "../types/services/product-category"
import { UpdateProductVariantDTO } from "../types/services/product-variant"
import {
ProductCollectionEventData,
ProductCollectionEvents,
} from "../types/services/product-collection"
import {
ProductEventData,
ProductEvents,
UpdateProductDTO,
} from "../types/services/product"
import { import {
InjectManager, InjectManager,
InjectTransactionManager, InjectTransactionManager,
@@ -49,7 +67,6 @@ import {
joinerConfig, joinerConfig,
LinkableKeys, LinkableKeys,
} from "./../joiner-config" } from "./../joiner-config"
import { ProductCategoryServiceTypes } from "../types"
type InjectedDependencies = { type InjectedDependencies = {
baseRepository: DAL.RepositoryService baseRepository: DAL.RepositoryService
@@ -61,6 +78,7 @@ type InjectedDependencies = {
productImageService: ProductImageService<any> productImageService: ProductImageService<any>
productTypeService: ProductTypeService<any> productTypeService: ProductTypeService<any>
productOptionService: ProductOptionService<any> productOptionService: ProductOptionService<any>
eventBusModuleService?: IEventBusModuleService
} }
export default class ProductModuleService< export default class ProductModuleService<
@@ -80,12 +98,16 @@ export default class ProductModuleService<
TProductVariant, TProductVariant,
TProduct TProduct
> >
// eslint-disable-next-line max-len
protected readonly productCategoryService_: ProductCategoryService<TProductCategory> protected readonly productCategoryService_: ProductCategoryService<TProductCategory>
protected readonly productTagService_: ProductTagService<TProductTag> protected readonly productTagService_: ProductTagService<TProductTag>
// eslint-disable-next-line max-len
protected readonly productCollectionService_: ProductCollectionService<TProductCollection> protected readonly productCollectionService_: ProductCollectionService<TProductCollection>
protected readonly productImageService_: ProductImageService<TProductImage> protected readonly productImageService_: ProductImageService<TProductImage>
protected readonly productTypeService_: ProductTypeService<TProductType> protected readonly productTypeService_: ProductTypeService<TProductType>
protected readonly productOptionService_: ProductOptionService<TProductOption> protected readonly productOptionService_: ProductOptionService<TProductOption>
protected readonly eventBusModuleService_?: IEventBusModuleService
constructor( constructor(
{ {
@@ -98,6 +120,7 @@ export default class ProductModuleService<
productImageService, productImageService,
productTypeService, productTypeService,
productOptionService, productOptionService,
eventBusModuleService,
}: InjectedDependencies, }: InjectedDependencies,
protected readonly moduleDeclaration: InternalModuleDeclaration protected readonly moduleDeclaration: InternalModuleDeclaration
) { ) {
@@ -110,6 +133,7 @@ export default class ProductModuleService<
this.productImageService_ = productImageService this.productImageService_ = productImageService
this.productTypeService_ = productTypeService this.productTypeService_ = productTypeService
this.productOptionService_ = productOptionService this.productOptionService_ = productOptionService
this.eventBusModuleService_ = eventBusModuleService
} }
__joinerConfig(): JoinerServiceConfig { __joinerConfig(): JoinerServiceConfig {
@@ -499,6 +523,13 @@ export default class ProductModuleService<
sharedContext sharedContext
) )
await this.eventBusModuleService_?.emit<ProductCollectionEventData>(
productCollections.map(({ id }) => ({
eventName: ProductCollectionEvents.COLLECTION_CREATED,
data: { id },
}))
)
return JSON.parse(JSON.stringify(productCollections)) return JSON.parse(JSON.stringify(productCollections))
} }
@@ -512,6 +543,13 @@ export default class ProductModuleService<
sharedContext sharedContext
) )
await this.eventBusModuleService_?.emit<ProductCollectionEventData>(
productCollections.map(({ id }) => ({
eventName: ProductCollectionEvents.COLLECTION_UPDATED,
data: { id },
}))
)
return JSON.parse(JSON.stringify(productCollections)) return JSON.parse(JSON.stringify(productCollections))
} }
@@ -524,6 +562,13 @@ export default class ProductModuleService<
productCollectionIds, productCollectionIds,
sharedContext sharedContext
) )
await this.eventBusModuleService_?.emit<ProductCollectionEventData>(
productCollectionIds.map((id) => ({
eventName: ProductCollectionEvents.COLLECTION_DELETED,
data: { id },
}))
)
} }
@InjectManager("baseRepository_") @InjectManager("baseRepository_")
@@ -558,7 +603,7 @@ export default class ProductModuleService<
@InjectTransactionManager(shouldForceTransaction, "baseRepository_") @InjectTransactionManager(shouldForceTransaction, "baseRepository_")
async createCategory( async createCategory(
data: ProductCategoryServiceTypes.CreateProductCategoryDTO, data: CreateProductCategoryDTO,
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
) { ) {
const productCategory = await this.productCategoryService_.create( const productCategory = await this.productCategoryService_.create(
@@ -566,13 +611,18 @@ export default class ProductModuleService<
sharedContext sharedContext
) )
await this.eventBusModuleService_?.emit<ProductCategoryEventData>(
ProductCategoryEvents.CATEGORY_CREATED,
{ id: productCategory.id }
)
return JSON.parse(JSON.stringify(productCategory)) return JSON.parse(JSON.stringify(productCategory))
} }
@InjectTransactionManager(shouldForceTransaction, "baseRepository_") @InjectTransactionManager(shouldForceTransaction, "baseRepository_")
async updateCategory( async updateCategory(
categoryId: string, categoryId: string,
data: ProductCategoryServiceTypes.UpdateProductCategoryDTO, data: UpdateProductCategoryDTO,
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
) { ) {
const productCategory = await this.productCategoryService_.update( const productCategory = await this.productCategoryService_.update(
@@ -581,6 +631,11 @@ export default class ProductModuleService<
sharedContext sharedContext
) )
await this.eventBusModuleService_?.emit<ProductCategoryEventData>(
ProductCategoryEvents.CATEGORY_UPDATED,
{ id: productCategory.id }
)
return JSON.parse(JSON.stringify(productCategory)) return JSON.parse(JSON.stringify(productCategory))
} }
@@ -590,6 +645,11 @@ export default class ProductModuleService<
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<void> { ): Promise<void> {
await this.productCategoryService_.delete(categoryId, sharedContext) await this.productCategoryService_.delete(categoryId, sharedContext)
await this.eventBusModuleService_?.emit<ProductCategoryEventData>(
ProductCategoryEvents.CATEGORY_DELETED,
{ id: categoryId }
)
} }
@InjectManager("baseRepository_") @InjectManager("baseRepository_")
@@ -612,10 +672,20 @@ export default class ProductModuleService<
sharedContext?: Context sharedContext?: Context
): Promise<ProductTypes.ProductDTO[]> { ): Promise<ProductTypes.ProductDTO[]> {
const products = await this.create_(data, sharedContext) const products = await this.create_(data, sharedContext)
const createdProducts = await this.baseRepository_.serialize<
return this.baseRepository_.serialize<ProductTypes.ProductDTO[]>(products, { ProductTypes.ProductDTO[]
>(products, {
populate: true, populate: true,
}) })
await this.eventBusModuleService_?.emit<ProductEventData>(
createdProducts.map(({ id }) => ({
eventName: ProductEvents.PRODUCT_CREATED,
data: { id },
}))
)
return createdProducts
} }
async update( async update(
@@ -624,9 +694,20 @@ export default class ProductModuleService<
): Promise<ProductTypes.ProductDTO[]> { ): Promise<ProductTypes.ProductDTO[]> {
const products = await this.update_(data, sharedContext) const products = await this.update_(data, sharedContext)
return this.baseRepository_.serialize<ProductTypes.ProductDTO[]>(products, { const updatedProducts = await this.baseRepository_.serialize<
ProductTypes.ProductDTO[]
>(products, {
populate: true, populate: true,
}) })
await this.eventBusModuleService_?.emit<ProductEventData>(
updatedProducts.map(({ id }) => ({
eventName: ProductEvents.PRODUCT_UPDATED,
data: { id },
}))
)
return updatedProducts
} }
@InjectTransactionManager(shouldForceTransaction, "baseRepository_") @InjectTransactionManager(shouldForceTransaction, "baseRepository_")
@@ -701,7 +782,7 @@ export default class ProductModuleService<
...option, ...option,
} }
const product = productByHandleMap.get(handle) const product = productByHandleMap.get(handle)
const productId = product?.id! const productId = product?.id
if (productId) { if (productId) {
productOptionsData.product_id = productId productOptionsData.product_id = productId
@@ -810,7 +891,7 @@ export default class ProductModuleService<
sharedContext sharedContext
) )
return productData as ProductServiceTypes.UpdateProductDTO return productData as UpdateProductDTO
}) })
) )
@@ -905,7 +986,7 @@ export default class ProductModuleService<
promises.push( promises.push(
this.productVariantService_.update( this.productVariantService_.update(
productByIdMap.get(productId)!, productByIdMap.get(productId)!,
variants as unknown as ProductVariantServiceTypes.UpdateProductVariantDTO[], variants as unknown as UpdateProductVariantDTO[],
sharedContext sharedContext
) )
) )
@@ -937,9 +1018,13 @@ export default class ProductModuleService<
if (productData.images?.length) { if (productData.images?.length) {
productData.images = await this.productImageService_.upsert( productData.images = await this.productImageService_.upsert(
productData.images.map((image) => productData.images.map((image) => {
isString(image) ? image : image.url if (isString(image)) {
), return image
} else {
return image.url
}
}),
sharedContext sharedContext
) )
} }
@@ -977,6 +1062,13 @@ export default class ProductModuleService<
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<void> { ): Promise<void> {
await this.productService_.delete(productIds, sharedContext) await this.productService_.delete(productIds, sharedContext)
await this.eventBusModuleService_?.emit<ProductEventData>(
productIds.map((id) => ({
eventName: ProductEvents.PRODUCT_DELETED,
data: { id },
}))
)
} }
async softDelete< async softDelete<
@@ -992,11 +1084,24 @@ export default class ProductModuleService<
}, },
sharedContext: Context = {} sharedContext: Context = {}
): Promise<Record<Lowercase<keyof typeof LinkableKeys>, string[]> | void> { ): Promise<Record<Lowercase<keyof typeof LinkableKeys>, string[]> | void> {
let [, cascadedEntitiesMap] = await this.softDelete_( let [products, cascadedEntitiesMap] = await this.softDelete_(
productIds, productIds,
sharedContext sharedContext
) )
const softDeletedProducts = await this.baseRepository_.serialize<
ProductTypes.ProductDTO[]
>(products, {
populate: true,
})
await this.eventBusModuleService_?.emit<ProductEventData>(
softDeletedProducts.map(({ id }) => ({
eventName: ProductEvents.PRODUCT_DELETED,
data: { id },
}))
)
let mappedCascadedEntitiesMap let mappedCascadedEntitiesMap
if (returnLinkableKeys) { if (returnLinkableKeys) {
mappedCascadedEntitiesMap = mapObjectTo< mappedCascadedEntitiesMap = mapObjectTo<

View File

@@ -1,9 +1,9 @@
export * from "./services" export * from "./services"
import { IEventBusService } from "@medusajs/types" import { IEventBusModuleService } from "@medusajs/types"
export type InitializeModuleInjectableDependencies = { export type InitializeModuleInjectableDependencies = {
eventBusService?: IEventBusService eventBusModuleService?: IEventBusModuleService
} }
export * from "./services" export * from "./services"

View File

@@ -1,3 +1,4 @@
export * as ProductCategoryServiceTypes from "./product-category" export * as ProductCategoryServiceTypes from "./product-category"
export * as ProductServiceTypes from "./product" export * as ProductServiceTypes from "./product"
export * as ProductVariantServiceTypes from "./product-variant" export * as ProductVariantServiceTypes from "./product-variant"
export * as ProductCollectionServiceTypes from "./product-collection"

View File

@@ -1,3 +1,13 @@
export type ProductCategoryEventData = {
id: string
}
export enum ProductCategoryEvents {
CATEGORY_UPDATED = "product-category.updated",
CATEGORY_CREATED = "product-category.created",
CATEGORY_DELETED = "product-category.deleted",
}
export interface CreateProductCategoryDTO { export interface CreateProductCategoryDTO {
name: string name: string
handle?: string handle?: string

View File

@@ -0,0 +1,9 @@
export type ProductCollectionEventData = {
id: string
}
export enum ProductCollectionEvents {
COLLECTION_UPDATED = "product-collection.updated",
COLLECTION_CREATED = "product-collection.created",
COLLECTION_DELETED = "product-collection.deleted",
}

View File

@@ -1,5 +1,15 @@
import { ProductStatus, ProductCategoryDTO } from "@medusajs/types" import { ProductStatus, ProductCategoryDTO } from "@medusajs/types"
export type ProductEventData = {
id: string
}
export enum ProductEvents {
PRODUCT_UPDATED = "product.updated",
PRODUCT_CREATED = "product.created",
PRODUCT_DELETED = "product.deleted",
}
export interface UpdateProductDTO { export interface UpdateProductDTO {
id: string id: string
title?: string title?: string

View File

@@ -4,7 +4,7 @@ export interface IEventBusModuleService {
emit<T>( emit<T>(
eventName: string, eventName: string,
data: T, data: T,
options: Record<string, unknown> options?: Record<string, unknown>
): Promise<void> ): Promise<void>
emit<T>(data: EmitData<T>[]): Promise<void> emit<T>(data: EmitData<T>[]): Promise<void>