From 8af55aed87da7252c7c261175bc98331466a0da8 Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Fri, 4 Aug 2023 11:26:02 +0200 Subject: [PATCH] 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 --- .changeset/fuzzy-tables-build.md | 7 + .eslintignore | 1 + .eslintrc.js | 1 + packages/modules-sdk/src/definitions.ts | 8 +- .../__fixtures__/event-bus/index.ts | 36 ++ .../integration-tests/__tests__/module.ts | 2 - .../product-categories.spec.ts | 50 +++ .../product-collections.spec.ts | 52 +++ .../product-module-service/products.spec.ts | 309 ++++++++++++++++++ .../src/services/product-module-service.ts | 137 +++++++- packages/product/src/types/index.ts | 4 +- packages/product/src/types/services/index.ts | 1 + .../src/types/services/product-category.ts | 10 + .../src/types/services/product-collection.ts | 9 + .../product/src/types/services/product.ts | 10 + .../types/src/event-bus/event-bus-module.ts | 2 +- 16 files changed, 616 insertions(+), 23 deletions(-) create mode 100644 .changeset/fuzzy-tables-build.md create mode 100644 packages/product/integration-tests/__fixtures__/event-bus/index.ts create mode 100644 packages/product/src/types/services/product-collection.ts diff --git a/.changeset/fuzzy-tables-build.md b/.changeset/fuzzy-tables-build.md new file mode 100644 index 0000000000..8709ac8305 --- /dev/null +++ b/.changeset/fuzzy-tables-build.md @@ -0,0 +1,7 @@ +--- +"@medusajs/product": patch +"@medusajs/types": patch +"@medusajs/modules-sdk": patch +--- + +feat(product,types): added event bus events for products diff --git a/.eslintignore b/.eslintignore index e5bffebae1..0469b3263f 100644 --- a/.eslintignore +++ b/.eslintignore @@ -20,6 +20,7 @@ packages/* !packages/cache-redis !packages/cache-inmemory !packages/create-medusa-app +!packages/product **/models/* diff --git a/.eslintrc.js b/.eslintrc.js index e392021ce7..896d7bf212 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -95,6 +95,7 @@ module.exports = { "./packages/cache-redis/tsconfig.spec.json", "./packages/cache-inmemory/tsconfig.spec.json", "./packages/create-medusa-app/tsconfig.json", + "./packages/product/tsconfig.json", ], }, rules: { diff --git a/packages/modules-sdk/src/definitions.ts b/packages/modules-sdk/src/definitions.ts index 742c17d0da..771976246e 100644 --- a/packages/modules-sdk/src/definitions.ts +++ b/packages/modules-sdk/src/definitions.ts @@ -12,11 +12,15 @@ export enum Modules { PRODUCT = "productService", } +export enum ModuleRegistrationName { + EVENT_BUS = "eventBusModuleService" +} + export const ModulesDefinition: { [key: string | Modules]: ModuleDefinition } = { [Modules.EVENT_BUS]: { key: Modules.EVENT_BUS, - registrationName: "eventBusModuleService", + registrationName: ModuleRegistrationName.EVENT_BUS, defaultPackage: "@medusajs/event-bus-local", label: "EventBusModuleService", canOverride: true, @@ -75,7 +79,7 @@ export const ModulesDefinition: { [key: string | Modules]: ModuleDefinition } = isRequired: false, canOverride: true, isQueryable: true, - dependencies: [], + dependencies: [ModuleRegistrationName.EVENT_BUS], defaultModuleDeclaration: { scope: MODULE_SCOPE.EXTERNAL, }, diff --git a/packages/product/integration-tests/__fixtures__/event-bus/index.ts b/packages/product/integration-tests/__fixtures__/event-bus/index.ts new file mode 100644 index 0000000000..ce67d6c00e --- /dev/null +++ b/packages/product/integration-tests/__fixtures__/event-bus/index.ts @@ -0,0 +1,36 @@ +import { + EmitData, + EventBusTypes, + Subscriber, + IEventBusModuleService +} from "@medusajs/types" + +export class EventBusService implements IEventBusModuleService { + async emit( + eventName: string, + data: T, + options: Record + ): Promise + async emit(data: EventBusTypes.EmitData[]): Promise + async emit[] = string>( + eventOrData: TInput, + data?: T, + options: Record = {} + ): Promise {} + + subscribe(event: string | symbol, subscriber: Subscriber): this { + return this + } + + unsubscribe( + event: string | symbol, + subscriber: Subscriber, + context?: EventBusTypes.SubscriberContext + ): this { + return this + } + + withTransaction() { + + } +} diff --git a/packages/product/integration-tests/__tests__/module.ts b/packages/product/integration-tests/__tests__/module.ts index 3b4b1ae8f0..6331ea7095 100644 --- a/packages/product/integration-tests/__tests__/module.ts +++ b/packages/product/integration-tests/__tests__/module.ts @@ -5,8 +5,6 @@ import { ProductRepository } from "../__fixtures__/module" import { createProductAndTags } from "../__fixtures__/product" import { productsData } from "../__fixtures__/product/data" import { DB_URL, TestDatabase } from "../utils" -import { buildProductAndRelationsData } from "../__fixtures__/product/data/create-product" -import { kebabCase } from "@medusajs/utils" import { IProductModuleService } from "@medusajs/types" const beforeEach_ = async () => { diff --git a/packages/product/integration-tests/__tests__/services/product-module-service/product-categories.spec.ts b/packages/product/integration-tests/__tests__/services/product-module-service/product-categories.spec.ts index 1f5d4a0aec..11013c715d 100644 --- a/packages/product/integration-tests/__tests__/services/product-module-service/product-categories.spec.ts +++ b/packages/product/integration-tests/__tests__/services/product-module-service/product-categories.spec.ts @@ -6,6 +6,7 @@ import { initialize } from "../../../../src" import { DB_URL, TestDatabase } from "../../../utils" import { createProductCategories } from "../../../__fixtures__/product-category" import { productCategoriesRankData } from "../../../__fixtures__/product-category/data" +import { EventBusService } from "../../../__fixtures__/event-bus" describe("ProductModuleService product categories", () => { let service: IProductModuleService @@ -16,16 +17,20 @@ describe("ProductModuleService product categories", () => { let productCategoryOne: ProductCategory let productCategoryTwo: ProductCategory let productCategories: ProductCategory[] + let eventBus beforeEach(async () => { await TestDatabase.setupDatabase() repositoryManager = await TestDatabase.forkManager() + eventBus = new EventBusService() service = await initialize({ database: { clientUrl: DB_URL, schema: process.env.MEDUSA_PRODUCT_DB_SCHEMA, }, + }, { + eventBusModuleService: eventBus }) testManager = await TestDatabase.forkManager() @@ -68,6 +73,7 @@ describe("ProductModuleService product categories", () => { afterEach(async () => { await TestDatabase.clearDatabase() + jest.clearAllMocks() }) 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 () => { await service.createCategory({ name: "New Category", @@ -356,6 +378,21 @@ describe("ProductModuleService product categories", () => { 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 () => { await service.updateCategory(productCategoryZero.id, { name: "New Category", @@ -513,6 +550,19 @@ describe("ProductModuleService product categories", () => { 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 () => { let error 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 aeed5958e3..a521cee195 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 @@ -6,6 +6,7 @@ import { ProductTypes } from "@medusajs/types" import { initialize } from "../../../../src" import { DB_URL, TestDatabase } from "../../../utils" import { createCollections } from "../../../__fixtures__/product" +import { EventBusService } from "../../../__fixtures__/event-bus" describe("ProductModuleService product collections", () => { let service: IProductModuleService @@ -16,16 +17,20 @@ describe("ProductModuleService product collections", () => { let productCollectionOne: ProductCollection let productCollectionTwo: ProductCollection let productCollections: ProductCollection[] + let eventBus beforeEach(async () => { await TestDatabase.setupDatabase() repositoryManager = await TestDatabase.forkManager() + eventBus = new EventBusService() service = await initialize({ database: { clientUrl: DB_URL, schema: process.env.MEDUSA_PRODUCT_DB_SCHEMA, }, + }, { + eventBusModuleService: eventBus }) testManager = await TestDatabase.forkManager() @@ -65,6 +70,7 @@ describe("ProductModuleService product collections", () => { afterEach(async () => { await TestDatabase.clearDatabase() + jest.clearAllMocks() }) describe("listCollections", () => { @@ -261,11 +267,41 @@ describe("ProductModuleService product collections", () => { 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", () => { 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 () => { await service.updateCollections( [{ @@ -311,6 +347,22 @@ describe("ProductModuleService product collections", () => { 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 } + }]) + }) }) }) diff --git a/packages/product/integration-tests/__tests__/services/product-module-service/products.spec.ts b/packages/product/integration-tests/__tests__/services/product-module-service/products.spec.ts index 4c80bab349..056257e1f9 100644 --- a/packages/product/integration-tests/__tests__/services/product-module-service/products.spec.ts +++ b/packages/product/integration-tests/__tests__/services/product-module-service/products.spec.ts @@ -1,12 +1,14 @@ import { MedusaModule } from "@medusajs/modules-sdk" import { Product, ProductCategory, ProductCollection, ProductType, ProductVariant } from "@models" import { IProductModuleService, ProductTypes } from "@medusajs/types" +import { kebabCase } from "@medusajs/utils" import { initialize } from "../../../../src" import { DB_URL, TestDatabase } from "../../../utils" import { buildProductAndRelationsData } from "../../../__fixtures__/product/data/create-product" import { createProductCategories } from "../../../__fixtures__/product-category" import { createCollections, createTypes } from "../../../__fixtures__/product" +import { EventBusService } from "../../../__fixtures__/event-bus" const beforeEach_ = async () => { await TestDatabase.setupDatabase() @@ -15,6 +17,7 @@ const beforeEach_ = async () => { const afterEach_ = async () => { await TestDatabase.clearDatabase() + jest.clearAllMocks() } describe("ProductModuleService products", function () { @@ -32,6 +35,7 @@ describe("ProductModuleService products", function () { let productTypeOne: ProductType let productTypeTwo: ProductType let images = ["image-1"] + let eventBus const productCategoriesData = [{ id: "test-1", @@ -135,11 +139,14 @@ describe("ProductModuleService products", function () { MedusaModule.clearInstances() + eventBus = new EventBusService() module = await initialize({ database: { clientUrl: DB_URL, 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 () => { const updateData = { id: productOne.id, @@ -461,4 +490,284 @@ describe("ProductModuleService products", function () { 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() + } + }) + }) }) diff --git a/packages/product/src/services/product-module-service.ts b/packages/product/src/services/product-module-service.ts index 7596f2c777..a9fb343fa5 100644 --- a/packages/product/src/services/product-module-service.ts +++ b/packages/product/src/services/product-module-service.ts @@ -25,13 +25,31 @@ import { InternalModuleDeclaration, JoinerServiceConfig, ProductTypes, + IEventBusModuleService, } from "@medusajs/types" import ProductImageService from "./product-image" + import { - ProductServiceTypes, - ProductVariantServiceTypes, -} from "../types/services" + CreateProductCategoryDTO, + ProductCategoryEventData, + 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 { InjectManager, InjectTransactionManager, @@ -49,7 +67,6 @@ import { joinerConfig, LinkableKeys, } from "./../joiner-config" -import { ProductCategoryServiceTypes } from "../types" type InjectedDependencies = { baseRepository: DAL.RepositoryService @@ -61,6 +78,7 @@ type InjectedDependencies = { productImageService: ProductImageService productTypeService: ProductTypeService productOptionService: ProductOptionService + eventBusModuleService?: IEventBusModuleService } export default class ProductModuleService< @@ -80,12 +98,16 @@ export default class ProductModuleService< TProductVariant, TProduct > + + // eslint-disable-next-line max-len protected readonly productCategoryService_: ProductCategoryService protected readonly productTagService_: ProductTagService + // eslint-disable-next-line max-len protected readonly productCollectionService_: ProductCollectionService protected readonly productImageService_: ProductImageService protected readonly productTypeService_: ProductTypeService protected readonly productOptionService_: ProductOptionService + protected readonly eventBusModuleService_?: IEventBusModuleService constructor( { @@ -98,6 +120,7 @@ export default class ProductModuleService< productImageService, productTypeService, productOptionService, + eventBusModuleService, }: InjectedDependencies, protected readonly moduleDeclaration: InternalModuleDeclaration ) { @@ -110,6 +133,7 @@ export default class ProductModuleService< this.productImageService_ = productImageService this.productTypeService_ = productTypeService this.productOptionService_ = productOptionService + this.eventBusModuleService_ = eventBusModuleService } __joinerConfig(): JoinerServiceConfig { @@ -499,6 +523,13 @@ export default class ProductModuleService< sharedContext ) + await this.eventBusModuleService_?.emit( + productCollections.map(({ id }) => ({ + eventName: ProductCollectionEvents.COLLECTION_CREATED, + data: { id }, + })) + ) + return JSON.parse(JSON.stringify(productCollections)) } @@ -512,6 +543,13 @@ export default class ProductModuleService< sharedContext ) + await this.eventBusModuleService_?.emit( + productCollections.map(({ id }) => ({ + eventName: ProductCollectionEvents.COLLECTION_UPDATED, + data: { id }, + })) + ) + return JSON.parse(JSON.stringify(productCollections)) } @@ -524,6 +562,13 @@ export default class ProductModuleService< productCollectionIds, sharedContext ) + + await this.eventBusModuleService_?.emit( + productCollectionIds.map((id) => ({ + eventName: ProductCollectionEvents.COLLECTION_DELETED, + data: { id }, + })) + ) } @InjectManager("baseRepository_") @@ -558,7 +603,7 @@ export default class ProductModuleService< @InjectTransactionManager(shouldForceTransaction, "baseRepository_") async createCategory( - data: ProductCategoryServiceTypes.CreateProductCategoryDTO, + data: CreateProductCategoryDTO, @MedusaContext() sharedContext: Context = {} ) { const productCategory = await this.productCategoryService_.create( @@ -566,13 +611,18 @@ export default class ProductModuleService< sharedContext ) + await this.eventBusModuleService_?.emit( + ProductCategoryEvents.CATEGORY_CREATED, + { id: productCategory.id } + ) + return JSON.parse(JSON.stringify(productCategory)) } @InjectTransactionManager(shouldForceTransaction, "baseRepository_") async updateCategory( categoryId: string, - data: ProductCategoryServiceTypes.UpdateProductCategoryDTO, + data: UpdateProductCategoryDTO, @MedusaContext() sharedContext: Context = {} ) { const productCategory = await this.productCategoryService_.update( @@ -581,6 +631,11 @@ export default class ProductModuleService< sharedContext ) + await this.eventBusModuleService_?.emit( + ProductCategoryEvents.CATEGORY_UPDATED, + { id: productCategory.id } + ) + return JSON.parse(JSON.stringify(productCategory)) } @@ -590,6 +645,11 @@ export default class ProductModuleService< @MedusaContext() sharedContext: Context = {} ): Promise { await this.productCategoryService_.delete(categoryId, sharedContext) + + await this.eventBusModuleService_?.emit( + ProductCategoryEvents.CATEGORY_DELETED, + { id: categoryId } + ) } @InjectManager("baseRepository_") @@ -612,10 +672,20 @@ export default class ProductModuleService< sharedContext?: Context ): Promise { const products = await this.create_(data, sharedContext) - - return this.baseRepository_.serialize(products, { + const createdProducts = await this.baseRepository_.serialize< + ProductTypes.ProductDTO[] + >(products, { populate: true, }) + + await this.eventBusModuleService_?.emit( + createdProducts.map(({ id }) => ({ + eventName: ProductEvents.PRODUCT_CREATED, + data: { id }, + })) + ) + + return createdProducts } async update( @@ -624,9 +694,20 @@ export default class ProductModuleService< ): Promise { const products = await this.update_(data, sharedContext) - return this.baseRepository_.serialize(products, { + const updatedProducts = await this.baseRepository_.serialize< + ProductTypes.ProductDTO[] + >(products, { populate: true, }) + + await this.eventBusModuleService_?.emit( + updatedProducts.map(({ id }) => ({ + eventName: ProductEvents.PRODUCT_UPDATED, + data: { id }, + })) + ) + + return updatedProducts } @InjectTransactionManager(shouldForceTransaction, "baseRepository_") @@ -701,7 +782,7 @@ export default class ProductModuleService< ...option, } const product = productByHandleMap.get(handle) - const productId = product?.id! + const productId = product?.id if (productId) { productOptionsData.product_id = productId @@ -810,7 +891,7 @@ export default class ProductModuleService< sharedContext ) - return productData as ProductServiceTypes.UpdateProductDTO + return productData as UpdateProductDTO }) ) @@ -905,7 +986,7 @@ export default class ProductModuleService< promises.push( this.productVariantService_.update( productByIdMap.get(productId)!, - variants as unknown as ProductVariantServiceTypes.UpdateProductVariantDTO[], + variants as unknown as UpdateProductVariantDTO[], sharedContext ) ) @@ -937,9 +1018,13 @@ export default class ProductModuleService< if (productData.images?.length) { productData.images = await this.productImageService_.upsert( - productData.images.map((image) => - isString(image) ? image : image.url - ), + productData.images.map((image) => { + if (isString(image)) { + return image + } else { + return image.url + } + }), sharedContext ) } @@ -977,6 +1062,13 @@ export default class ProductModuleService< @MedusaContext() sharedContext: Context = {} ): Promise { await this.productService_.delete(productIds, sharedContext) + + await this.eventBusModuleService_?.emit( + productIds.map((id) => ({ + eventName: ProductEvents.PRODUCT_DELETED, + data: { id }, + })) + ) } async softDelete< @@ -992,11 +1084,24 @@ export default class ProductModuleService< }, sharedContext: Context = {} ): Promise, string[]> | void> { - let [, cascadedEntitiesMap] = await this.softDelete_( + let [products, cascadedEntitiesMap] = await this.softDelete_( productIds, sharedContext ) + const softDeletedProducts = await this.baseRepository_.serialize< + ProductTypes.ProductDTO[] + >(products, { + populate: true, + }) + + await this.eventBusModuleService_?.emit( + softDeletedProducts.map(({ id }) => ({ + eventName: ProductEvents.PRODUCT_DELETED, + data: { id }, + })) + ) + let mappedCascadedEntitiesMap if (returnLinkableKeys) { mappedCascadedEntitiesMap = mapObjectTo< diff --git a/packages/product/src/types/index.ts b/packages/product/src/types/index.ts index 0d290a8666..d7228f8329 100644 --- a/packages/product/src/types/index.ts +++ b/packages/product/src/types/index.ts @@ -1,9 +1,9 @@ export * from "./services" -import { IEventBusService } from "@medusajs/types" +import { IEventBusModuleService } from "@medusajs/types" export type InitializeModuleInjectableDependencies = { - eventBusService?: IEventBusService + eventBusModuleService?: IEventBusModuleService } export * from "./services" diff --git a/packages/product/src/types/services/index.ts b/packages/product/src/types/services/index.ts index d6e12795aa..40c769ce36 100644 --- a/packages/product/src/types/services/index.ts +++ b/packages/product/src/types/services/index.ts @@ -1,3 +1,4 @@ export * as ProductCategoryServiceTypes from "./product-category" export * as ProductServiceTypes from "./product" export * as ProductVariantServiceTypes from "./product-variant" +export * as ProductCollectionServiceTypes from "./product-collection" diff --git a/packages/product/src/types/services/product-category.ts b/packages/product/src/types/services/product-category.ts index 85decef2a5..81021e628e 100644 --- a/packages/product/src/types/services/product-category.ts +++ b/packages/product/src/types/services/product-category.ts @@ -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 { name: string handle?: string diff --git a/packages/product/src/types/services/product-collection.ts b/packages/product/src/types/services/product-collection.ts new file mode 100644 index 0000000000..0d337c15a4 --- /dev/null +++ b/packages/product/src/types/services/product-collection.ts @@ -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", +} \ No newline at end of file diff --git a/packages/product/src/types/services/product.ts b/packages/product/src/types/services/product.ts index e2405ddb16..9a48927ac0 100644 --- a/packages/product/src/types/services/product.ts +++ b/packages/product/src/types/services/product.ts @@ -1,5 +1,15 @@ 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 { id: string title?: string diff --git a/packages/types/src/event-bus/event-bus-module.ts b/packages/types/src/event-bus/event-bus-module.ts index c6a70a258f..b7e139b2cf 100644 --- a/packages/types/src/event-bus/event-bus-module.ts +++ b/packages/types/src/event-bus/event-bus-module.ts @@ -4,7 +4,7 @@ export interface IEventBusModuleService { emit( eventName: string, data: T, - options: Record + options?: Record ): Promise emit(data: EmitData[]): Promise