diff --git a/.changeset/three-pans-knock.md b/.changeset/three-pans-knock.md new file mode 100644 index 0000000000..0fe9eafdc0 --- /dev/null +++ b/.changeset/three-pans-knock.md @@ -0,0 +1,8 @@ +--- +"@medusajs/medusa": minor +"medusa-react": patch +"@medusajs/event-bus-local": minor +"@medusajs/event-bus-redis": minor +--- + +feat(medusa,event-bus-local,event-bus-redis): Event Bus module (Redis + Local) diff --git a/.eslintignore b/.eslintignore index 7c26663d15..431cffc5ce 100644 --- a/.eslintignore +++ b/.eslintignore @@ -10,6 +10,8 @@ packages/* !packages/admin-ui !packages/admin !packages/medusa-payment-stripe +!packages/event-bus-redis +!packages/event-bus-local diff --git a/.eslintrc.js b/.eslintrc.js index 49f7bc6f00..3d2b7e4b71 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -83,6 +83,8 @@ module.exports = { project: [ "./packages/medusa/tsconfig.json", "./packages/medusa-payment-stripe/tsconfig.spec.json", + "./packages/event-bus-local/tsconfig.spec.json", + "./packages/event-bus-redis/tsconfig.spec.json", "./packages/admin-ui/tsconfig.json", ], }, diff --git a/integration-tests/api/__tests__/batch-jobs/__snapshots__/api.js.snap b/integration-tests/api/__tests__/batch-jobs/__snapshots__/api.js.snap deleted file mode 100644 index cbbfedf02c..0000000000 --- a/integration-tests/api/__tests__/batch-jobs/__snapshots__/api.js.snap +++ /dev/null @@ -1,83 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`/admin/batch-jobs GET /admin/batch-jobs lists batch jobs created by the user 1`] = ` -Object { - "batch_jobs": Array [ - Object { - "canceled_at": null, - "completed_at": "2022-06-27T22:00:00.000Z", - "confirmed_at": null, - "context": Object {}, - "created_at": Any, - "created_by": "admin_user", - "deleted_at": null, - "dry_run": false, - "failed_at": null, - "id": "job_5", - "pre_processed_at": null, - "processing_at": null, - "result": null, - "status": "completed", - "type": "product-export", - "updated_at": Any, - }, - Object { - "canceled_at": null, - "completed_at": null, - "confirmed_at": null, - "context": Object {}, - "created_at": Any, - "created_by": "admin_user", - "deleted_at": null, - "dry_run": false, - "failed_at": null, - "id": "job_3", - "pre_processed_at": null, - "processing_at": null, - "result": null, - "status": "created", - "type": "product-export", - "updated_at": Any, - }, - Object { - "canceled_at": null, - "completed_at": null, - "confirmed_at": null, - "context": Object {}, - "created_at": Any, - "created_by": "admin_user", - "deleted_at": null, - "dry_run": false, - "failed_at": null, - "id": "job_2", - "pre_processed_at": null, - "processing_at": null, - "result": null, - "status": "created", - "type": "product-export", - "updated_at": Any, - }, - Object { - "canceled_at": null, - "completed_at": null, - "confirmed_at": null, - "context": Object {}, - "created_at": Any, - "created_by": "admin_user", - "deleted_at": null, - "dry_run": false, - "failed_at": null, - "id": "job_1", - "pre_processed_at": null, - "processing_at": null, - "result": null, - "status": "created", - "type": "product-export", - "updated_at": Any, - }, - ], - "count": 4, - "limit": 10, - "offset": 0, -} -`; diff --git a/integration-tests/api/__tests__/batch-jobs/api.js b/integration-tests/api/__tests__/batch-jobs/api.js index a8eca5dc16..c1ff1e7518 100644 --- a/integration-tests/api/__tests__/batch-jobs/api.js +++ b/integration-tests/api/__tests__/batch-jobs/api.js @@ -84,34 +84,36 @@ describe("/admin/batch-jobs", () => { expect(response.status).toEqual(200) expect(response.data.batch_jobs.length).toEqual(4) - expect(response.data).toMatchSnapshot({ - batch_jobs: [ - { - id: "job_5", - created_at: expect.any(String), - updated_at: expect.any(String), - created_by: "admin_user", - }, - { - id: "job_3", - created_at: expect.any(String), - updated_at: expect.any(String), - created_by: "admin_user", - }, - { - id: "job_2", - created_at: expect.any(String), - updated_at: expect.any(String), - created_by: "admin_user", - }, - { - id: "job_1", - created_at: expect.any(String), - updated_at: expect.any(String), - created_by: "admin_user", - }, - ], - }) + expect(response.data).toEqual( + expect.objectContaining({ + batch_jobs: expect.arrayContaining([ + expect.objectContaining({ + id: "job_5", + created_at: expect.any(String), + updated_at: expect.any(String), + created_by: "admin_user", + }), + expect.objectContaining({ + id: "job_3", + created_at: expect.any(String), + updated_at: expect.any(String), + created_by: "admin_user", + }), + expect.objectContaining({ + id: "job_2", + created_at: expect.any(String), + updated_at: expect.any(String), + created_by: "admin_user", + }), + expect.objectContaining({ + id: "job_1", + created_at: expect.any(String), + updated_at: expect.any(String), + created_by: "admin_user", + }), + ]), + }) + ) }) it("lists batch jobs created by the user and where completed_at is null ", async () => { @@ -214,8 +216,6 @@ describe("/admin/batch-jobs", () => { created_by: "admin_user", status: "created", id: expect.any(String), - created_at: expect.any(String), - updated_at: expect.any(String), }) ) }) @@ -264,28 +264,6 @@ describe("/admin/batch-jobs", () => { await db.teardown() }) - it("Cancels batch job created by the user", async () => { - const api = useApi() - - const jobId = "job_1" - - const response = await api.post( - `/admin/batch-jobs/${jobId}/cancel`, - {}, - adminReqConfig - ) - - expect(response.status).toEqual(200) - expect(response.data.batch_job).toEqual( - expect.objectContaining({ - created_at: expect.any(String), - updated_at: expect.any(String), - canceled_at: expect.any(String), - status: "canceled", - }) - ) - }) - it("Fails to cancel a batch job created by a different user", async () => { expect.assertions(3) const api = useApi() @@ -319,5 +297,27 @@ describe("/admin/batch-jobs", () => { ) }) }) + + it("Cancels batch job created by the user", async () => { + const api = useApi() + + const jobId = "job_1" + + const response = await api.post( + `/admin/batch-jobs/${jobId}/cancel`, + {}, + adminReqConfig + ) + + expect(response.status).toEqual(200) + expect(response.data.batch_job).toEqual( + expect.objectContaining({ + created_at: expect.any(String), + updated_at: expect.any(String), + canceled_at: expect.any(String), + status: "canceled", + }) + ) + }) }) }) diff --git a/integration-tests/api/__tests__/batch-jobs/order/export.js b/integration-tests/api/__tests__/batch-jobs/order/export.js index 5a818c4dad..c52282ba71 100644 --- a/integration-tests/api/__tests__/batch-jobs/order/export.js +++ b/integration-tests/api/__tests__/batch-jobs/order/export.js @@ -29,7 +29,6 @@ describe("Batchjob with type order-export", () => { dbConnection = await initDb({ cwd }) medusaProcess = await setupServer({ cwd, - redisUrl: "redis://127.0.0.1:6379", uploadDir: __dirname, }) }) diff --git a/integration-tests/api/__tests__/batch-jobs/price-list/import.js b/integration-tests/api/__tests__/batch-jobs/price-list/import.js index d209931668..02605049c5 100644 --- a/integration-tests/api/__tests__/batch-jobs/price-list/import.js +++ b/integration-tests/api/__tests__/batch-jobs/price-list/import.js @@ -64,7 +64,6 @@ describe("Price list import batch job", () => { medusaProcess = await setupServer({ cwd, - redisUrl: "redis://127.0.0.1:6379", uploadDir: __dirname, }) }) diff --git a/integration-tests/api/__tests__/batch-jobs/product/export.js b/integration-tests/api/__tests__/batch-jobs/product/export.js index e4bf6d34a1..defbed9b1f 100644 --- a/integration-tests/api/__tests__/batch-jobs/product/export.js +++ b/integration-tests/api/__tests__/batch-jobs/product/export.js @@ -30,7 +30,6 @@ describe("Batch job of product-export type", () => { dbConnection = await initDb({ cwd }) medusaProcess = await setupServer({ cwd, - redisUrl: "redis://127.0.0.1:6379", uploadDir: __dirname, }) }) diff --git a/integration-tests/api/__tests__/batch-jobs/product/ff-sales-channel.js b/integration-tests/api/__tests__/batch-jobs/product/ff-sales-channel.js index 5ee12a3bc9..920331249f 100644 --- a/integration-tests/api/__tests__/batch-jobs/product/ff-sales-channel.js +++ b/integration-tests/api/__tests__/batch-jobs/product/ff-sales-channel.js @@ -8,7 +8,9 @@ const adminSeeder = require("../../../helpers/admin-seeder") const userSeeder = require("../../../helpers/user-seeder") const { simpleSalesChannelFactory } = require("../../../factories") const batchJobSeeder = require("../../../helpers/batch-job-seeder") -const { simpleProductCollectionFactory } = require("../../../factories/simple-product-collection-factory"); +const { + simpleProductCollectionFactory, +} = require("../../../factories/simple-product-collection-factory") const startServerWithEnvironment = require("../../../../helpers/start-server-with-environment").default @@ -52,7 +54,7 @@ describe("Product import - Sales Channel", () => { let dbConnection let medusaProcess - let collectionHandle1 = "test-collection1" + const collectionHandle1 = "test-collection1" beforeAll(async () => { const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) @@ -62,7 +64,6 @@ describe("Product import - Sales Channel", () => { const [process, connection] = await startServerWithEnvironment({ cwd, env: { MEDUSA_FF_SALES_CHANNELS: true }, - redisUrl: "redis://127.0.0.1:6379", uploadDir: __dirname, }) dbConnection = connection @@ -90,7 +91,7 @@ describe("Product import - Sales Channel", () => { name: "Import Sales Channel 2", }) await simpleProductCollectionFactory(dbConnection, { - handle: collectionHandle1 + handle: collectionHandle1, }) } catch (e) { console.log(e) @@ -169,8 +170,8 @@ describe("Product import - Sales Channel", () => { }), ], collection: expect.objectContaining({ - handle: collectionHandle1 - }) + handle: collectionHandle1, + }), }), ]) }) diff --git a/integration-tests/api/__tests__/batch-jobs/product/import.js b/integration-tests/api/__tests__/batch-jobs/product/import.js index 2acb346675..351aeeed7f 100644 --- a/integration-tests/api/__tests__/batch-jobs/product/import.js +++ b/integration-tests/api/__tests__/batch-jobs/product/import.js @@ -63,7 +63,6 @@ describe("Product import batch job", () => { medusaProcess = await setupServer({ cwd, - redisUrl: "redis://127.0.0.1:6379", uploadDir: __dirname, }) }) @@ -81,7 +80,7 @@ describe("Product import batch job", () => { await batchJobSeeder(dbConnection) await adminSeeder(dbConnection) await userSeeder(dbConnection) - await simpleProductCollectionFactory(dbConnection, [ + await simpleProductCollectionFactory(dbConnection, [ { handle: collectionHandle1, }, diff --git a/integration-tests/api/package.json b/integration-tests/api/package.json index ac6b86543d..75ea9555fd 100644 --- a/integration-tests/api/package.json +++ b/integration-tests/api/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@medusajs/cache-inmemory": "*", + "@medusajs/event-bus-local": "*", "@medusajs/medusa": "*", "faker": "^5.5.3", "medusa-interfaces": "*", diff --git a/integration-tests/api/src/services/local-file-service.js b/integration-tests/api/src/services/local-file-service.js index 1bc25a2013..dc4fbfbe33 100644 --- a/integration-tests/api/src/services/local-file-service.js +++ b/integration-tests/api/src/services/local-file-service.js @@ -1,8 +1,8 @@ import { AbstractFileService } from "@medusajs/medusa" -import stream from "stream" -import { resolve } from "path" import * as fs from "fs" import mkdirp from "mkdirp" +import { resolve } from "path" +import stream from "stream" export default class LocalFileService extends AbstractFileService { constructor({}, options) { diff --git a/integration-tests/plugins/package.json b/integration-tests/plugins/package.json index 047169b8b3..00b3addbd3 100644 --- a/integration-tests/plugins/package.json +++ b/integration-tests/plugins/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@medusajs/cache-inmemory": "*", + "@medusajs/event-bus-local": "*", "@medusajs/medusa": "*", "faker": "^5.5.3", "medusa-fulfillment-webshipper": "*", diff --git a/packages/event-bus-local/.gitignore b/packages/event-bus-local/.gitignore new file mode 100644 index 0000000000..874c6c69d3 --- /dev/null +++ b/packages/event-bus-local/.gitignore @@ -0,0 +1,6 @@ +/dist +node_modules +.DS_store +.env* +.env +*.sql diff --git a/packages/event-bus-local/README.md b/packages/event-bus-local/README.md new file mode 100644 index 0000000000..1c625dcf8d --- /dev/null +++ b/packages/event-bus-local/README.md @@ -0,0 +1,63 @@ +

+ + Medusa + +

+

+ @medusajs/event-bus-local +

+ +

+ Documentation | + Website +

+ +

+An open source composable commerce engine built for developers. +

+

+ + Medusa is released under the MIT license. + + + Current CircleCI build status. + + + PRs welcome! + + Product Hunt + + Discord Chat + + + Follow @medusajs + +

+ +## Overview + +Local Event Bus module for Medusa. When installed, the events system of Medusa is powered by the Node EventEmitter. This module installed by default in new (> v1.8.0) Medusa projects. + +The Node EventEmitter is limited to a single process environment. We generally recommend using the `@medusajs/event-bus-redis` module in a production environment. + +## Getting started + +Install the module: + +```bash +yarn add @medusajs/event-bus-local +``` + +You don't need to add the module to your project configuration as it is the default one. Medusa will try to use it, if no other event buses are installed. + +```js +module.exports = { + // ... + modules: [ ... ], + // ... +} +``` + +## Configuration + +The module comes with no configuration options. \ No newline at end of file diff --git a/packages/event-bus-local/jest.config.js b/packages/event-bus-local/jest.config.js new file mode 100644 index 0000000000..14cd7ed6e7 --- /dev/null +++ b/packages/event-bus-local/jest.config.js @@ -0,0 +1,13 @@ +module.exports = { + globals: { + "ts-jest": { + tsConfig: "tsconfig.spec.json", + isolatedModules: false, + }, + }, + transform: { + "^.+\\.[jt]s?$": "ts-jest", + }, + testEnvironment: `node`, + moduleFileExtensions: [`js`, `jsx`, `ts`, `tsx`, `json`], +} diff --git a/packages/event-bus-local/package.json b/packages/event-bus-local/package.json new file mode 100644 index 0000000000..d4316e96ad --- /dev/null +++ b/packages/event-bus-local/package.json @@ -0,0 +1,37 @@ +{ + "name": "@medusajs/event-bus-local", + "version": "0.1.0", + "description": "Local Event Bus Module for Medusa", + "main": "dist/index.js", + "files": [ + "dist" + ], + "repository": { + "type": "git", + "url": "https://github.com/medusajs/medusa", + "directory": "packages/event-bus-local" + }, + "publishConfig": { + "access": "public" + }, + "author": "Medusa", + "license": "MIT", + "devDependencies": { + "@medusajs/types": "^0.0.1", + "cross-env": "^5.2.1", + "jest": "^25.5.2", + "ts-jest": "^25.5.1", + "typescript": "^4.4.4" + }, + "scripts": { + "watch": "tsc --build --watch", + "prepare": "cross-env NODE_ENV=production yarn run build", + "build": "tsc --build", + "test": "jest --passWithNoTests", + "test:unit": "jest --passWithNoTests" + }, + "dependencies": { + "@medusajs/modules-sdk": "*", + "@medusajs/utils": "^0.0.1" + } +} diff --git a/packages/event-bus-local/src/index.ts b/packages/event-bus-local/src/index.ts new file mode 100644 index 0000000000..394dee7a76 --- /dev/null +++ b/packages/event-bus-local/src/index.ts @@ -0,0 +1,13 @@ +import { ModuleExports } from "@medusajs/modules-sdk" +import Loader from "./loaders" +import LocalEventBus from "./services/event-bus-local" + +export const service = LocalEventBus +export const loaders = [Loader] + +const moduleDefinition: ModuleExports = { + service, + loaders, +} + +export default moduleDefinition diff --git a/packages/event-bus-local/src/loaders/index.ts b/packages/event-bus-local/src/loaders/index.ts new file mode 100644 index 0000000000..82751cc97b --- /dev/null +++ b/packages/event-bus-local/src/loaders/index.ts @@ -0,0 +1,8 @@ +import { LoaderOptions } from "@medusajs/modules-sdk" + + +export default async ({ logger }: LoaderOptions): Promise => { + logger?.warn( + "Local Event Bus installed. This is not recommended for production." + ) +} diff --git a/packages/event-bus-local/src/services/__tests__/event-bus-local.js b/packages/event-bus-local/src/services/__tests__/event-bus-local.js new file mode 100644 index 0000000000..ccc695e6fc --- /dev/null +++ b/packages/event-bus-local/src/services/__tests__/event-bus-local.js @@ -0,0 +1,70 @@ +import LocalEventBusService from "../event-bus-local" + +jest.genMockFromModule("events") +jest.mock("events") + +const loggerMock = { + info: jest.fn().mockReturnValue(console.log), + warn: jest.fn().mockReturnValue(console.log), + error: jest.fn().mockReturnValue(console.log), +} + +const moduleDeps = { + logger: loggerMock, +} + +describe("LocalEventBusService", () => { + let eventBus + + describe("emit", () => { + describe("Successfully emits events", () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + it("Emits an event", () => { + eventBus = new LocalEventBusService( + moduleDeps, + {}, + { + resources: "shared", + } + ) + + eventBus.eventEmitter_.emit.mockImplementationOnce((data) => data) + + eventBus.emit("eventName", { hi: "1234" }) + + expect(eventBus.eventEmitter_.emit).toHaveBeenCalledTimes(1) + expect(eventBus.eventEmitter_.emit).toHaveBeenCalledWith("eventName", { + hi: "1234", + }) + }) + + it("Emits multiple events", () => { + eventBus = new LocalEventBusService( + moduleDeps, + {}, + { + resources: "shared", + } + ) + + eventBus.eventEmitter_.emit.mockImplementationOnce((data) => data) + + eventBus.emit([ + { eventName: "event-1", data: { hi: "1234" } }, + { eventName: "event-2", data: { hi: "5678" } }, + ]) + + expect(eventBus.eventEmitter_.emit).toHaveBeenCalledTimes(2) + expect(eventBus.eventEmitter_.emit).toHaveBeenCalledWith("event-1", { + hi: "1234", + }) + expect(eventBus.eventEmitter_.emit).toHaveBeenCalledWith("event-2", { + hi: "5678", + }) + }) + }) + }) +}) diff --git a/packages/event-bus-local/src/services/event-bus-local.ts b/packages/event-bus-local/src/services/event-bus-local.ts new file mode 100644 index 0000000000..e4a66b055a --- /dev/null +++ b/packages/event-bus-local/src/services/event-bus-local.ts @@ -0,0 +1,80 @@ +import { Logger, MedusaContainer } from "@medusajs/modules-sdk" +import { EmitData, Subscriber } from "@medusajs/types" +import { AbstractEventBusModuleService } from "@medusajs/utils" +import { EventEmitter } from "events" + +type InjectedDependencies = { + logger: Logger +} + +const eventEmitter = new EventEmitter() + +export default class LocalEventBusService extends AbstractEventBusModuleService { + protected readonly logger_: Logger + protected readonly eventEmitter_: EventEmitter + + constructor({ logger }: MedusaContainer & InjectedDependencies) { + // @ts-ignore + super(...arguments) + + this.logger_ = logger + this.eventEmitter_ = eventEmitter + } + + async emit( + eventName: string, + data: T, + options: Record + ): Promise + + /** + * Emit a number of events + * @param {EmitData} data - the data to send to the subscriber. + */ + async emit(data: EmitData[]): Promise + + async emit[] = string>( + eventOrData: TInput, + data?: T, + options: Record = {} + ): Promise { + const isBulkEmit = Array.isArray(eventOrData) + + const events: EmitData[] = isBulkEmit + ? eventOrData + : [{ eventName: eventOrData, data }] + + for (const event of events) { + const eventListenersCount = this.eventEmitter_.listenerCount( + event.eventName + ) + + this.logger_.info( + `Processing ${event.eventName} which has ${eventListenersCount} subscribers` + ) + + if (eventListenersCount === 0) { + continue + } + + + try { + this.eventEmitter_.emit(event.eventName, event.data) + } catch (error) { + this.logger_.error( + `An error occurred while processing ${event.eventName}: ${error}` + ) + } + } + } + + subscribe(event: string | symbol, subscriber: Subscriber): this { + this.eventEmitter_.on(event, subscriber) + return this + } + + unsubscribe(event: string | symbol, subscriber: Subscriber): this { + this.eventEmitter_.off(event, subscriber) + return this + } +} diff --git a/packages/event-bus-local/tsconfig.json b/packages/event-bus-local/tsconfig.json new file mode 100644 index 0000000000..07b7ad9286 --- /dev/null +++ b/packages/event-bus-local/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "lib": ["es5", "es6", "es2019"], + "target": "es6", + "outDir": "./dist", + "esModuleInterop": true, + "declaration": true, + "module": "commonjs", + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "sourceMap": true, + "noImplicitReturns": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "noImplicitThis": true, + "allowJs": true, + "skipLibCheck": true, + "downlevelIteration": true // to use ES5 specific tooling + }, + "include": ["src"], + "exclude": [ + "dist", + "./src/**/__tests__", + "./src/**/__mocks__", + "./src/**/__fixtures__", + "node_modules" + ] +} diff --git a/packages/event-bus-local/tsconfig.spec.json b/packages/event-bus-local/tsconfig.spec.json new file mode 100644 index 0000000000..9b62409191 --- /dev/null +++ b/packages/event-bus-local/tsconfig.spec.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": ["src"], + "exclude": ["node_modules"] +} diff --git a/packages/event-bus-redis/.gitignore b/packages/event-bus-redis/.gitignore new file mode 100644 index 0000000000..874c6c69d3 --- /dev/null +++ b/packages/event-bus-redis/.gitignore @@ -0,0 +1,6 @@ +/dist +node_modules +.DS_store +.env* +.env +*.sql diff --git a/packages/event-bus-redis/README.md b/packages/event-bus-redis/README.md new file mode 100644 index 0000000000..2e4406683a --- /dev/null +++ b/packages/event-bus-redis/README.md @@ -0,0 +1,79 @@ +

+ + Medusa + +

+

+ @medusajs/event-bus-redis +

+ +

+ Documentation | + Website +

+ +

+An open source composable commerce engine built for developers. +

+

+ + Medusa is released under the MIT license. + + + Current CircleCI build status. + + + PRs welcome! + + Product Hunt + + Discord Chat + + + Follow @medusajs + +

+ +## Overview + +Redis Event Bus module for Medusa. When installed, the events system of Medusa is powered by BullMQ and `io-redis`. BullMQ is responsible for the message queue and worker. `io-redis` is the underlying Redis client, that BullMQ connects to for events storage. + +## Getting started + +Install the module: + +```bash +yarn add @medusajs/event-bus-redis +``` + +Add the module to your `medusa-config.js`: + +```js +module.exports = { + // ... + modules: [ + { + resolve: "@medusajs/event-bus-redis", + options: { + redisUrl: "redis:.." + }, + }, + ], + // ... +} +``` + +## Configuration + +The module can be configured with the following options: + +| Option | Type | Description | Default | +| --------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | +| `redisUrl` | `string` | URL of the Redis instance to connect to. | `events-worker` | +| `queueName` | `string?` | Name of the BullMQ queue. | `events-queue` | +| `queueOptions` | `object?` | Options for the BullMQ queue. See BullMQ's [documentation](https://api.docs.bullmq.io/interfaces/QueueOptions.html). | `{}` | +| `redisOptions` | `object?` | Options for the Redis instance. See `io-redis`'s [documentation](https://luin.github.io/ioredis/index.html#RedisOptions) | `{}` | + +**Info**: See how the options are applied in the [RedisEventBusService](https://github.com/medusajs/medusa/blob/0c1d1d590463fa30b083c4312293348bdf6596be/packages/event-bus-redis/src/services/event-bus-redis.ts#L52) and [loader](https://github.com/medusajs/medusa/blob/0c1d1d590463fa30b083c4312293348bdf6596be/packages/event-bus-redis/src/loaders/index.ts). + +If you do not provide a `redisUrl` in the module options, the server will fail to start. \ No newline at end of file diff --git a/packages/event-bus-redis/jest.config.js b/packages/event-bus-redis/jest.config.js new file mode 100644 index 0000000000..14cd7ed6e7 --- /dev/null +++ b/packages/event-bus-redis/jest.config.js @@ -0,0 +1,13 @@ +module.exports = { + globals: { + "ts-jest": { + tsConfig: "tsconfig.spec.json", + isolatedModules: false, + }, + }, + transform: { + "^.+\\.[jt]s?$": "ts-jest", + }, + testEnvironment: `node`, + moduleFileExtensions: [`js`, `jsx`, `ts`, `tsx`, `json`], +} diff --git a/packages/event-bus-redis/package.json b/packages/event-bus-redis/package.json new file mode 100644 index 0000000000..c870db9208 --- /dev/null +++ b/packages/event-bus-redis/package.json @@ -0,0 +1,40 @@ +{ + "name": "@medusajs/event-bus-redis", + "version": "0.1.0", + "description": "Redis Event Bus Module for Medusa", + "main": "dist/index.js", + "files": [ + "dist" + ], + "repository": { + "type": "git", + "url": "https://github.com/medusajs/medusa", + "directory": "packages/event-bus-redis" + }, + "publishConfig": { + "access": "public" + }, + "author": "Medusa", + "license": "MIT", + "devDependencies": { + "@medusajs/types": "^0.0.1", + "cross-env": "^5.2.1", + "jest": "^25.5.2", + "medusa-test-utils": "^1.1.39", + "ts-jest": "^25.5.1", + "typescript": "^4.4.4" + }, + "scripts": { + "watch": "tsc --build --watch", + "prepare": "cross-env NODE_ENV=production yarn run build", + "build": "tsc --build", + "test": "jest --passWithNoTests", + "test:unit": "jest --passWithNoTests" + }, + "dependencies": { + "@medusajs/modules-sdk": "*", + "@medusajs/utils": "^0.0.1", + "bullmq": "^3.5.6", + "ioredis": "^5.2.5" + } +} diff --git a/packages/event-bus-redis/src/index.ts b/packages/event-bus-redis/src/index.ts new file mode 100644 index 0000000000..54388999b0 --- /dev/null +++ b/packages/event-bus-redis/src/index.ts @@ -0,0 +1,13 @@ +import { ModuleExports } from "@medusajs/modules-sdk" +import Loader from "./loaders" +import RedisEventBusService from "./services/event-bus-redis" + +const service = RedisEventBusService +const loaders = [Loader] + +const moduleDefinition: ModuleExports = { + service, + loaders, +} + +export default moduleDefinition diff --git a/packages/event-bus-redis/src/loaders/index.ts b/packages/event-bus-redis/src/loaders/index.ts new file mode 100644 index 0000000000..a405013078 --- /dev/null +++ b/packages/event-bus-redis/src/loaders/index.ts @@ -0,0 +1,41 @@ +import { LoaderOptions } from "@medusajs/modules-sdk" +import { asValue } from "awilix" +import Redis from "ioredis" +import { EOL } from "os" +import { EventBusRedisModuleOptions } from "../types" + +export default async ({ + container, + logger, + options, +}: LoaderOptions): Promise => { + const { redisUrl, redisOptions } = options as EventBusRedisModuleOptions + + if (!redisUrl) { + throw Error( + "No `redis_url` provided in project config. It is required for the Redis Event Bus." + ) + } + + const connection = new Redis(redisUrl, { + // Required config. See: https://github.com/OptimalBits/bull/blob/develop/CHANGELOG.md#breaking-changes + maxRetriesPerRequest: null, + enableReadyCheck: false, + // Lazy connect to properly handle connection errors + lazyConnect: true, + ...(redisOptions ?? {}), + }) + + try { + await connection.connect() + logger?.info(`Connection to Redis in module 'event-bus-redis' established`) + } catch (err) { + logger?.error( + `An error occurred while connecting to Redis in module 'event-bus-redis':${EOL} ${err}` + ) + } + + container.register({ + eventBusRedisConnection: asValue(connection), + }) +} diff --git a/packages/event-bus-redis/src/services/__tests__/event-bus.js b/packages/event-bus-redis/src/services/__tests__/event-bus.js new file mode 100644 index 0000000000..633631b17f --- /dev/null +++ b/packages/event-bus-redis/src/services/__tests__/event-bus.js @@ -0,0 +1,319 @@ +import { Queue, Worker } from "bullmq" +import { MockManager } from "medusa-test-utils" +import RedisEventBusService from "../event-bus-redis" + +jest.genMockFromModule("bullmq") +jest.genMockFromModule("ioredis") +jest.mock("bullmq") +jest.mock("ioredis") + +const loggerMock = { + info: jest.fn().mockReturnValue(console.log), + warn: jest.fn().mockReturnValue(console.log), + error: jest.fn().mockReturnValue(console.log), +} + +const simpleModuleOptions = { redisUrl: "test-url" } +const moduleDeps = { + manager: MockManager, + logger: loggerMock, + eventBusRedisConnection: {}, +} + +describe("RedisEventBusService", () => { + let eventBus + + describe("constructor", () => { + beforeAll(() => { + jest.clearAllMocks() + }) + + it("Creates a queue + worker", () => { + eventBus = new RedisEventBusService(moduleDeps, simpleModuleOptions, { + resources: "shared", + }) + + expect(Queue).toHaveBeenCalledTimes(1) + expect(Queue).toHaveBeenCalledWith("events-queue", { + connection: expect.any(Object), + prefix: "RedisEventBusService", + }) + + expect(Worker).toHaveBeenCalledTimes(1) + expect(Worker).toHaveBeenCalledWith( + "events-queue", + expect.any(Function), + { + connection: expect.any(Object), + prefix: "RedisEventBusService", + } + ) + }) + + it("Throws on isolated module declaration", () => { + try { + eventBus = new RedisEventBusService(moduleDeps, simpleModuleOptions, { + resources: "isolated", + }) + } catch (error) { + expect(error.message).toEqual( + "At the moment this module can only be used with shared resources" + ) + } + }) + }) + + describe("emit", () => { + describe("Successfully emits events", () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + it("Adds job to queue with default options", () => { + eventBus = new RedisEventBusService(moduleDeps, simpleModuleOptions, { + resources: "shared", + }) + + eventBus.queue_.addBulk.mockImplementationOnce(() => "hi") + eventBus.emit("eventName", { hi: "1234" }) + + expect(eventBus.queue_.addBulk).toHaveBeenCalledTimes(1) + expect(eventBus.queue_.addBulk).toHaveBeenCalledWith([ + { + name: "eventName", + data: { eventName: "eventName", data: { hi: "1234" } }, + opts: { + attempts: 1, + removeOnComplete: true, + }, + }, + ]) + }) + + it("Adds job to queue with custom options passed directly upon emitting", () => { + eventBus = new RedisEventBusService(moduleDeps, simpleModuleOptions, { + resources: "shared", + }) + + eventBus.queue_.addBulk.mockImplementationOnce(() => "hi") + eventBus.emit( + "eventName", + { hi: "1234" }, + { attempts: 3, backoff: 5000, delay: 1000 } + ) + + expect(eventBus.queue_.addBulk).toHaveBeenCalledTimes(1) + expect(eventBus.queue_.addBulk).toHaveBeenCalledWith([ + { + name: "eventName", + data: { eventName: "eventName", data: { hi: "1234" } }, + opts: { + attempts: 3, + backoff: 5000, + delay: 1000, + removeOnComplete: true, + }, + }, + ]) + }) + + it("Adds job to queue with module job options", () => { + eventBus = new RedisEventBusService( + moduleDeps, + { + ...simpleModuleOptions, + jobOptions: { + removeOnComplete: { + age: 5, + }, + attempts: 7, + }, + }, + { + resources: "shared", + } + ) + + eventBus.queue_.addBulk.mockImplementationOnce(() => "hi") + eventBus.emit("eventName", { hi: "1234" }) + + expect(eventBus.queue_.addBulk).toHaveBeenCalledTimes(1) + expect(eventBus.queue_.addBulk).toHaveBeenCalledWith([ + { + name: "eventName", + data: { eventName: "eventName", data: { hi: "1234" } }, + opts: { + attempts: 7, + removeOnComplete: { + age: 5, + }, + }, + }, + ]) + }) + + it("Adds job to queue with default, local, and global options merged", () => { + eventBus = new RedisEventBusService( + moduleDeps, + { + ...simpleModuleOptions, + jobOptions: { + removeOnComplete: 5, + }, + }, + { + resources: "shared", + } + ) + + eventBus.queue_.addBulk.mockImplementationOnce(() => "hi") + eventBus.emit("eventName", { hi: "1234" }, { delay: 1000 }) + + expect(eventBus.queue_.addBulk).toHaveBeenCalledTimes(1) + expect(eventBus.queue_.addBulk).toHaveBeenCalledWith([ + { + name: "eventName", + data: { eventName: "eventName", data: { hi: "1234" } }, + opts: { + attempts: 1, + removeOnComplete: 5, + delay: 1000, + }, + }, + ]) + }) + }) + }) + + describe("worker_", () => { + let result + + describe("Successfully processes the jobs", () => { + beforeEach(async () => { + jest.clearAllMocks() + + eventBus = new RedisEventBusService(moduleDeps, simpleModuleOptions, { + resources: "shared", + }) + }) + + it("Processes a simple event with no options", async () => { + eventBus.subscribe("eventName", () => Promise.resolve("hi")) + + result = await eventBus.worker_({ + data: { eventName: "eventName", data: {} }, + opts: { attempts: 1 }, + }) + + expect(loggerMock.info).toHaveBeenCalledTimes(1) + expect(loggerMock.info).toHaveBeenCalledWith( + "Processing eventName which has 1 subscribers" + ) + + expect(result).toEqual(["hi"]) + }) + + it("Processes event with failing subscribers", async () => { + eventBus.subscribe("eventName", () => Promise.resolve("hi")) + eventBus.subscribe("eventName", () => Promise.reject("fail1")) + eventBus.subscribe("eventName", () => Promise.resolve("hi2")) + eventBus.subscribe("eventName", () => Promise.reject("fail2")) + + result = await eventBus.worker_({ + data: { eventName: "eventName", data: {} }, + update: (data) => data, + opts: { attempts: 1 }, + }) + + expect(loggerMock.info).toHaveBeenCalledTimes(1) + expect(loggerMock.info).toHaveBeenCalledWith( + "Processing eventName which has 4 subscribers" + ) + + expect(loggerMock.warn).toHaveBeenCalledTimes(3) + expect(loggerMock.warn).toHaveBeenCalledWith( + "An error occurred while processing eventName: fail1" + ) + expect(loggerMock.warn).toHaveBeenCalledWith( + "An error occurred while processing eventName: fail2" + ) + + expect(loggerMock.warn).toHaveBeenCalledWith( + "One or more subscribers of eventName failed. Retrying is not configured. Use 'attempts' option when emitting events." + ) + + expect(result).toEqual(["hi", "fail1", "hi2", "fail2"]) + }) + + it("Retries processing when subcribers fail, if configured - final attempt", async () => { + eventBus.subscribe("eventName", async () => Promise.resolve("hi"), { + subscriberId: "1", + }) + eventBus.subscribe("eventName", async () => Promise.reject("fail1"), { + subscriberId: "2", + }) + + result = await eventBus + .worker_({ + data: { + eventName: "eventName", + data: {}, + completedSubscriberIds: ["1"], + }, + attemptsMade: 2, + update: (data) => data, + opts: { attempts: 2 }, + }) + .catch((error) => void 0) + + expect(loggerMock.warn).toHaveBeenCalledTimes(1) + expect(loggerMock.warn).toHaveBeenCalledWith( + "An error occurred while processing eventName: fail1" + ) + + expect(loggerMock.info).toHaveBeenCalledTimes(2) + expect(loggerMock.info).toHaveBeenCalledWith( + "Final retry attempt for eventName" + ) + expect(loggerMock.info).toHaveBeenCalledWith( + "Retrying eventName which has 2 subscribers (1 of them failed)" + ) + }) + + it("Retries processing when subcribers fail, if configured", async () => { + eventBus.subscribe("eventName", async () => Promise.resolve("hi"), { + subscriberId: "1", + }) + eventBus.subscribe("eventName", async () => Promise.reject("fail1"), { + subscriberId: "2", + }) + + result = await eventBus + .worker_({ + data: { + eventName: "eventName", + data: {}, + completedSubscriberIds: ["1"], + }, + attemptsMade: 2, + update: (data) => data, + opts: { attempts: 3 }, + }) + .catch((err) => void 0) + + expect(loggerMock.warn).toHaveBeenCalledTimes(2) + expect(loggerMock.warn).toHaveBeenCalledWith( + "An error occurred while processing eventName: fail1" + ) + expect(loggerMock.warn).toHaveBeenCalledWith( + "One or more subscribers of eventName failed. Retrying..." + ) + + expect(loggerMock.info).toHaveBeenCalledTimes(1) + expect(loggerMock.info).toHaveBeenCalledWith( + "Retrying eventName which has 2 subscribers (1 of them failed)" + ) + }) + }) + }) +}) diff --git a/packages/event-bus-redis/src/services/event-bus-redis.ts b/packages/event-bus-redis/src/services/event-bus-redis.ts new file mode 100644 index 0000000000..340b4e5705 --- /dev/null +++ b/packages/event-bus-redis/src/services/event-bus-redis.ts @@ -0,0 +1,209 @@ +import { InternalModuleDeclaration, Logger } from "@medusajs/modules-sdk" +import { ConfigModule, EmitData } from "@medusajs/types" +import { AbstractEventBusModuleService } from "@medusajs/utils" +import { BulkJobOptions, JobsOptions, Queue, Worker } from "bullmq" +import { Redis } from "ioredis" +import { BullJob, EmitOptions, EventBusRedisModuleOptions } from "../types" + +type InjectedDependencies = { + logger: Logger + configModule: ConfigModule + eventBusRedisConnection: Redis +} + +/** + * Can keep track of multiple subscribers to different events and run the + * subscribers when events happen. Events will run asynchronously. + */ +export default class RedisEventBusService extends AbstractEventBusModuleService { + protected readonly config_: ConfigModule + protected readonly logger_: Logger + protected readonly moduleOptions_: EventBusRedisModuleOptions + protected readonly moduleDeclaration_: InternalModuleDeclaration + + protected queue_: Queue + + constructor( + { configModule, logger, eventBusRedisConnection }: InjectedDependencies, + moduleOptions: EventBusRedisModuleOptions = {}, + moduleDeclaration: InternalModuleDeclaration + ) { + // @ts-ignore + super(...arguments) + + this.moduleOptions_ = moduleOptions + this.config_ = configModule + this.logger_ = logger + + this.queue_ = new Queue(moduleOptions.queueName ?? `events-queue`, { + prefix: `${this.constructor.name}`, + ...(moduleOptions.queueOptions ?? {}), + connection: eventBusRedisConnection, + }) + + // Register our worker to handle emit calls + new Worker(moduleOptions.queueName ?? "events-queue", this.worker_, { + prefix: `${this.constructor.name}`, + ...(moduleOptions.workerOptions ?? {}), + connection: eventBusRedisConnection, + }) + } + + /** + * Emit a single event + * @param {string} eventName - the name of the event to be process. + * @param data - the data to send to the subscriber. + * @param options - options to add the job with + */ + async emit( + eventName: string, + data: T, + options: Record + ): Promise + + /** + * Emit a number of events + * @param {EmitData} data - the data to send to the subscriber. + */ + async emit(data: EmitData[]): Promise + + async emit[] = string>( + eventNameOrData: TInput, + data?: T, + options: BulkJobOptions | JobsOptions = {} + ): Promise { + const globalJobOptions = this.moduleOptions_.jobOptions ?? {} + + const isBulkEmit = Array.isArray(eventNameOrData) + + const opts = { + // default options + removeOnComplete: true, + attempts: 1, + // global options + ...globalJobOptions, + } as EmitOptions + + const events = isBulkEmit + ? eventNameOrData.map((event) => ({ + name: event.eventName, + data: { eventName: event.eventName, data: event.data }, + opts: { + ...opts, + // local options + ...event.options, + }, + })) + : [ + { + name: eventNameOrData as string, + data: { eventName: eventNameOrData, data }, + opts: { + ...opts, + // local options + ...options, + }, + }, + ] + + await this.queue_.addBulk(events) + } + + /** + * Handles incoming jobs. + * @param job The job object + * @return resolves to the results of the subscriber calls. + */ + worker_ = async (job: BullJob): Promise => { + const { eventName, data } = job.data + const eventSubscribers = this.eventToSubscribersMap.get(eventName) || [] + const wildcardSubscribers = this.eventToSubscribersMap.get("*") || [] + + const allSubscribers = eventSubscribers.concat(wildcardSubscribers) + + // Pull already completed subscribers from the job data + const completedSubscribers = job.data.completedSubscriberIds || [] + + // Filter out already completed subscribers from the all subscribers + const subscribersInCurrentAttempt = allSubscribers.filter( + (subscriber) => + subscriber.id && !completedSubscribers.includes(subscriber.id) + ) + + const currentAttempt = job.attemptsMade + const isRetry = currentAttempt > 1 + const configuredAttempts = job.opts.attempts + + const isFinalAttempt = currentAttempt === configuredAttempts + + if (isRetry) { + if (isFinalAttempt) { + this.logger_.info(`Final retry attempt for ${eventName}`) + } + + this.logger_.info( + `Retrying ${eventName} which has ${eventSubscribers.length} subscribers (${subscribersInCurrentAttempt.length} of them failed)` + ) + } else { + this.logger_.info( + `Processing ${eventName} which has ${eventSubscribers.length} subscribers` + ) + } + + const completedSubscribersInCurrentAttempt: string[] = [] + + const subscribersResult = await Promise.all( + subscribersInCurrentAttempt.map(async ({ id, subscriber }) => { + return await subscriber(data, eventName) + .then(async (data) => { + // For every subscriber that completes successfully, add their id to the list of completed subscribers + completedSubscribersInCurrentAttempt.push(id) + return data + }) + .catch((err) => { + this.logger_.warn( + `An error occurred while processing ${eventName}: ${err}` + ) + return err + }) + }) + ) + + // If the number of completed subscribers is different from the number of subcribers to process in current attempt, some of them failed + const didSubscribersFail = + completedSubscribersInCurrentAttempt.length !== + subscribersInCurrentAttempt.length + + const isRetriesConfigured = configuredAttempts! > 1 + + // Therefore, if retrying is configured, we try again + const shouldRetry = + didSubscribersFail && isRetriesConfigured && !isFinalAttempt + + if (shouldRetry) { + const updatedCompletedSubscribers = [ + ...completedSubscribers, + ...completedSubscribersInCurrentAttempt, + ] + + job.data.completedSubscriberIds = updatedCompletedSubscribers + + await job.update(job.data) + + const errorMessage = `One or more subscribers of ${eventName} failed. Retrying...` + + this.logger_.warn(errorMessage) + + return Promise.reject(Error(errorMessage)) + } + + if (didSubscribersFail && !isFinalAttempt) { + // If retrying is not configured, we log a warning to allow server admins to recover manually + this.logger_.warn( + `One or more subscribers of ${eventName} failed. Retrying is not configured. Use 'attempts' option when emitting events.` + ) + } + + return Promise.resolve(subscribersResult) + } +} diff --git a/packages/event-bus-redis/src/types/index.ts b/packages/event-bus-redis/src/types/index.ts new file mode 100644 index 0000000000..9f7bdc8e5e --- /dev/null +++ b/packages/event-bus-redis/src/types/index.ts @@ -0,0 +1,40 @@ +import { Job, JobsOptions, QueueOptions, WorkerOptions } from "bullmq" +import { RedisOptions } from "ioredis" + +export type JobData = { + eventName: string + data: T + completedSubscriberIds?: string[] | undefined +} + +export type BullJob = { + data: JobData +} & Job + +export type EmitOptions = JobsOptions + +export type EventBusRedisModuleOptions = { + queueName?: string + queueOptions?: QueueOptions + + workerOptions?: WorkerOptions + + redisUrl?: string + redisOptions?: RedisOptions + + /** + * Global options passed to all `EventBusService.emit` in the core as well as your own emitters. The options are forwarded to Bull's `Queue.add` method. + * + * The global options can be overridden by passing options to `EventBusService.emit` directly. + * + * Example + * ```js + * { + * removeOnComplete: { age: 10 }, + * } + * ``` + * + * @see https://api.docs.bullmq.io/interfaces/BaseJobOptions.html + */ + jobOptions?: EmitOptions +} diff --git a/packages/event-bus-redis/tsconfig.json b/packages/event-bus-redis/tsconfig.json new file mode 100644 index 0000000000..731ef8a6e0 --- /dev/null +++ b/packages/event-bus-redis/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "lib": ["es2020"], + "target": "es2020", + "outDir": "./dist", + "esModuleInterop": true, + "declaration": true, + "module": "commonjs", + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "sourceMap": true, + "noImplicitReturns": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "noImplicitThis": true, + "allowJs": true, + "skipLibCheck": true, + "downlevelIteration": true // to use ES5 specific tooling + }, + "include": ["src"], + "exclude": [ + "dist", + "./src/**/__tests__", + "./src/**/__mocks__", + "./src/**/__fixtures__", + "node_modules" + ] +} diff --git a/packages/event-bus-redis/tsconfig.spec.json b/packages/event-bus-redis/tsconfig.spec.json new file mode 100644 index 0000000000..9b62409191 --- /dev/null +++ b/packages/event-bus-redis/tsconfig.spec.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": ["src"], + "exclude": ["node_modules"] +} diff --git a/packages/inventory/src/initialize/index.ts b/packages/inventory/src/initialize/index.ts index 48102e3a9e..6c2140bbcc 100644 --- a/packages/inventory/src/initialize/index.ts +++ b/packages/inventory/src/initialize/index.ts @@ -1,15 +1,16 @@ -import { IEventBusService, IInventoryService } from "@medusajs/medusa" +import { IInventoryService } from "@medusajs/medusa" import { ExternalModuleDeclaration, InternalModuleDeclaration, - MedusaModule, + MedusaModule } from "@medusajs/modules-sdk" +import { EventBusTypes } from "@medusajs/types" import { InventoryServiceInitializeOptions } from "../types" export const initialize = async ( options?: InventoryServiceInitializeOptions | ExternalModuleDeclaration, injectedDependencies?: { - eventBusService: IEventBusService + eventBusService: EventBusTypes.IEventBusService } ): Promise => { const serviceKey = "inventoryService" diff --git a/packages/inventory/src/services/inventory-item.ts b/packages/inventory/src/services/inventory-item.ts index 8523ebdde9..ad7e2ad9b1 100644 --- a/packages/inventory/src/services/inventory-item.ts +++ b/packages/inventory/src/services/inventory-item.ts @@ -3,10 +3,9 @@ import { CreateInventoryItemInput, FilterableInventoryItemProps, FindConfig, - IEventBusService, - InventoryItemDTO, + InventoryItemDTO } from "@medusajs/medusa" -import { SharedContext } from "@medusajs/types" +import { EventBusTypes, SharedContext } from "@medusajs/types" import { InjectEntityManager, MedusaContext } from "@medusajs/utils" import { isDefined, MedusaError } from "medusa-core-utils" import { DeepPartial, EntityManager, FindManyOptions } from "typeorm" @@ -14,7 +13,7 @@ import { InventoryItem } from "../models" import { getListQuery } from "../utils/query" type InjectedDependencies = { - eventBusService: IEventBusService + eventBusService: EventBusTypes.IEventBusService manager: EntityManager } @@ -26,7 +25,7 @@ export default class InventoryItemService { } protected readonly manager_: EntityManager - protected readonly eventBusService_: IEventBusService | undefined + protected readonly eventBusService_: EventBusTypes.IEventBusService | undefined constructor({ eventBusService, manager }: InjectedDependencies) { this.manager_ = manager diff --git a/packages/inventory/src/services/inventory-level.ts b/packages/inventory/src/services/inventory-level.ts index c9fa49d0f3..c061f8c90a 100644 --- a/packages/inventory/src/services/inventory-level.ts +++ b/packages/inventory/src/services/inventory-level.ts @@ -2,17 +2,16 @@ import { buildQuery, CreateInventoryLevelInput, FilterableInventoryLevelProps, - FindConfig, - IEventBusService, + FindConfig } from "@medusajs/medusa" -import { SharedContext } from "@medusajs/types" +import { EventBusTypes, SharedContext } from "@medusajs/types" import { InjectEntityManager, MedusaContext } from "@medusajs/utils" import { isDefined, MedusaError } from "medusa-core-utils" import { DeepPartial, EntityManager, FindManyOptions, In } from "typeorm" import { InventoryLevel } from "../models" type InjectedDependencies = { - eventBusService: IEventBusService + eventBusService: EventBusTypes.IEventBusService manager: EntityManager } @@ -24,7 +23,7 @@ export default class InventoryLevelService { } protected readonly manager_: EntityManager - protected readonly eventBusService_: IEventBusService | undefined + protected readonly eventBusService_: EventBusTypes.IEventBusService | undefined constructor({ eventBusService, manager }: InjectedDependencies) { this.manager_ = manager diff --git a/packages/inventory/src/services/inventory.ts b/packages/inventory/src/services/inventory.ts index b366d898fc..2bd5549ed8 100644 --- a/packages/inventory/src/services/inventory.ts +++ b/packages/inventory/src/services/inventory.ts @@ -8,34 +8,33 @@ import { FilterableInventoryLevelProps, FilterableReservationItemProps, FindConfig, - IEventBusService, IInventoryService, InventoryItemDTO, InventoryLevelDTO, ReservationItemDTO, UpdateInventoryLevelInput, - UpdateReservationItemInput, + UpdateReservationItemInput } from "@medusajs/medusa" -import { SharedContext } from "@medusajs/types" +import { EventBusTypes, SharedContext } from "@medusajs/types" import { InjectEntityManager, MedusaContext } from "@medusajs/utils" import { MedusaError } from "medusa-core-utils" import { EntityManager } from "typeorm" import { InventoryItemService, InventoryLevelService, - ReservationItemService, + ReservationItemService } from "./" type InjectedDependencies = { manager: EntityManager - eventBusService: IEventBusService + eventBusService: EventBusTypes.IEventBusService inventoryItemService: InventoryItemService inventoryLevelService: InventoryLevelService reservationItemService: ReservationItemService } export default class InventoryService implements IInventoryService { protected readonly manager_: EntityManager - protected readonly eventBusService_: IEventBusService | undefined + protected readonly eventBusService_: EventBusTypes.IEventBusService | undefined protected readonly inventoryItemService_: InventoryItemService protected readonly reservationItemService_: ReservationItemService protected readonly inventoryLevelService_: InventoryLevelService diff --git a/packages/inventory/src/services/reservation-item.ts b/packages/inventory/src/services/reservation-item.ts index 406c911af0..a9a920248d 100644 --- a/packages/inventory/src/services/reservation-item.ts +++ b/packages/inventory/src/services/reservation-item.ts @@ -2,11 +2,9 @@ import { buildQuery, CreateReservationItemInput, FilterableReservationItemProps, - FindConfig, - IEventBusService, - UpdateReservationItemInput, + FindConfig, UpdateReservationItemInput } from "@medusajs/medusa" -import { SharedContext } from "@medusajs/types" +import { EventBusTypes, SharedContext } from "@medusajs/types" import { InjectEntityManager, MedusaContext } from "@medusajs/utils" import { isDefined, MedusaError } from "medusa-core-utils" import { EntityManager, FindManyOptions } from "typeorm" @@ -14,7 +12,7 @@ import { InventoryLevelService } from "." import { ReservationItem } from "../models" type InjectedDependencies = { - eventBusService: IEventBusService + eventBusService: EventBusTypes.IEventBusService manager: EntityManager inventoryLevelService: InventoryLevelService } @@ -27,7 +25,7 @@ export default class ReservationItemService { } protected readonly manager_: EntityManager - protected readonly eventBusService_: IEventBusService | undefined + protected readonly eventBusService_: EventBusTypes.IEventBusService | undefined protected readonly inventoryLevelService_: InventoryLevelService constructor({ diff --git a/packages/medusa/package.json b/packages/medusa/package.json index e56791f4db..8f8dc1a53f 100644 --- a/packages/medusa/package.json +++ b/packages/medusa/package.json @@ -22,6 +22,7 @@ "@babel/cli": "^7.14.3", "@babel/core": "^7.14.3", "@babel/preset-typescript": "^7.13.0", + "@medusajs/types": "*", "@types/express": "^4.17.17", "@types/jest": "^27.5.2", "@types/jsonwebtoken": "^8.5.9", @@ -51,11 +52,12 @@ "dependencies": { "@medusajs/medusa-cli": "^1.3.8", "@medusajs/modules-sdk": "*", + "@medusajs/utils": "*", "@types/ioredis": "^4.28.10", "@types/lodash": "^4.14.191", "awilix": "^8.0.0", "body-parser": "^1.19.0", - "bull": "^3.12.1", + "bullmq": "^3.5.6", "chokidar": "^3.4.2", "class-transformer": "^0.5.1", "class-validator": "^0.13.2", @@ -68,7 +70,7 @@ "express-session": "^1.17.3", "fs-exists-cached": "^1.0.0", "glob": "^7.1.6", - "ioredis": "^4.17.3", + "ioredis": "^5.2.5", "ioredis-mock": "^5.6.0", "iso8601-duration": "^1.3.0", "jsonwebtoken": "^8.5.1", diff --git a/packages/medusa/src/api/routes/admin/order-edits/__tests__/get-order.ts b/packages/medusa/src/api/routes/admin/order-edits/__tests__/get-order.ts index 6efa6306a6..f0eee17bd7 100644 --- a/packages/medusa/src/api/routes/admin/order-edits/__tests__/get-order.ts +++ b/packages/medusa/src/api/routes/admin/order-edits/__tests__/get-order.ts @@ -3,7 +3,7 @@ import { request } from "../../../../../helpers/test-request" import { orderEditServiceMock } from "../../../../../services/__mocks__/order-edit" import { defaultOrderEditFields, - defaultOrderEditRelations + defaultOrderEditRelations, } from "../../../../../types/order-edit" describe("GET /admin/order-edits/:id", () => { diff --git a/packages/medusa/src/api/routes/admin/orders/request-return.ts b/packages/medusa/src/api/routes/admin/orders/request-return.ts index f46884bfcd..ef69a37b13 100644 --- a/packages/medusa/src/api/routes/admin/orders/request-return.ts +++ b/packages/medusa/src/api/routes/admin/orders/request-return.ts @@ -4,22 +4,20 @@ import { IsInt, IsOptional, IsString, - ValidateNested, + ValidateNested } from "class-validator" import { EventBusService, OrderService, - ReturnService, + ReturnService } from "../../../../services" import { Type } from "class-transformer" import { isDefined, MedusaError } from "medusa-core-utils" import { EntityManager } from "typeorm" import { Order, Return } from "../../../../models" -import { OrdersReturnItem } from "../../../../types/orders" import { FindParams } from "../../../../types/common" -import { FlagRouter } from "../../../../utils/flag-router" -import { IInventoryService } from "../../../../interfaces" +import { OrdersReturnItem } from "../../../../types/orders" /** * @oas [post] /admin/orders/{id}/return diff --git a/packages/medusa/src/api/routes/store/orders/request-order.ts b/packages/medusa/src/api/routes/store/orders/request-order.ts index 7d3c8ed4d4..00d0fc1f5f 100644 --- a/packages/medusa/src/api/routes/store/orders/request-order.ts +++ b/packages/medusa/src/api/routes/store/orders/request-order.ts @@ -1,10 +1,7 @@ import { IsNotEmpty, IsString } from "class-validator" import { MedusaError } from "medusa-core-utils" -import { - CustomerService, - EventBusService, - OrderService, -} from "../../../../services" +import { CustomerService, OrderService } from "../../../../services" +import EventBusService from "../../../../services/event-bus" import TokenService from "../../../../services/token" import { TokenEvents } from "../../../../types/token" diff --git a/packages/medusa/src/helpers/test-request.js b/packages/medusa/src/helpers/test-request.js index 3437cccc34..bc17b23d6c 100644 --- a/packages/medusa/src/helpers/test-request.js +++ b/packages/medusa/src/helpers/test-request.js @@ -1,3 +1,8 @@ +import { + moduleHelper, + moduleLoader, + registerModules, +} from "@medusajs/modules-sdk" import { asValue, createContainer } from "awilix" import express from "express" import jwt from "jsonwebtoken" @@ -7,11 +12,6 @@ import "reflect-metadata" import supertest from "supertest" import apiLoader from "../loaders/api" import featureFlagLoader, { featureFlagRouter } from "../loaders/feature-flags" -import { - moduleLoader, - moduleHelper, - registerModules, -} from "@medusajs/modules-sdk" import passportLoader from "../loaders/passport" import servicesLoader from "../loaders/services" import strategiesLoader from "../loaders/strategies" diff --git a/packages/medusa/src/interfaces/__tests__/event-bus-service.spec.ts b/packages/medusa/src/interfaces/__tests__/event-bus-service.spec.ts new file mode 100644 index 0000000000..9ef023bb88 --- /dev/null +++ b/packages/medusa/src/interfaces/__tests__/event-bus-service.spec.ts @@ -0,0 +1,132 @@ +import { EventBusTypes } from "@medusajs/types" +import { EventBusUtils } from "@medusajs/utils" +import { EntityManager } from "typeorm" + +class EventBus extends EventBusUtils.AbstractEventBusModuleService { + protected manager_!: EntityManager + + constructor(protected readonly container) { + super() + this.container = container + } + + 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 { + const isBulkEmit = Array.isArray(eventOrData) + const event = isBulkEmit ? eventOrData[0].eventName : eventOrData + + console.log( + `[${event}] Local Event Bus installed. Emitting events has no effect.` + ) + } +} + +describe("AbstractEventBusService", () => { + let eventBus + + describe("subscribe", () => { + beforeAll(() => { + jest.clearAllMocks() + }) + + beforeEach(() => { + eventBus = new EventBus({}) + }) + + it("successfully adds subscriber", () => { + eventBus.subscribe("eventName", () => "test", { + subscriberId: "my-subscriber", + }) + + expect(eventBus.eventToSubscribersMap_.get("eventName").length).toEqual(1) + }) + + it("successfully adds multiple subscribers with explicit ids", () => { + eventBus.subscribe("eventName", () => "test", { + subscriberId: "my-subscriber-1", + }) + + eventBus.subscribe("eventName", () => "test", { + subscriberId: "my-subscriber-2", + }) + + expect(eventBus.eventToSubscribersMap_.get("eventName").length).toEqual(2) + }) + + it("successfully adds multiple subscribers with generates ids", () => { + eventBus.subscribe("eventName", () => "test") + + eventBus.subscribe("eventName", () => "test") + + expect(eventBus.eventToSubscribersMap_.get("eventName").length).toEqual(2) + }) + + it("throws when subscriber already exists", async () => { + expect.assertions(1) + + eventBus.subscribe("eventName", () => "test", { + subscriberId: "my-subscriber", + }) + + try { + eventBus.subscribe("eventName", () => "new", { + subscriberId: "my-subscriber", + }) + } catch (error) { + expect(error.message).toBe( + "Subscriber with id my-subscriber already exists" + ) + } + }) + + it("throws when subscriber is not a function", async () => { + expect.assertions(1) + + try { + eventBus.subscribe("eventName", "definitely-not-a-function") + } catch (error) { + expect(error.message).toBe("Subscriber must be a function") + } + }) + }) + + describe("unsubscribe", () => { + beforeAll(() => { + jest.clearAllMocks() + }) + + beforeEach(() => { + eventBus = new EventBus({}) + + eventBus.subscribe("eventName", () => "test", { subscriberId: "test" }) + }) + + it("successfully removes subscriber", () => { + eventBus.unsubscribe("eventName", () => "test", { subscriberId: "test" }) + + expect(eventBus.eventToSubscribersMap_.get("eventName").length).toEqual(0) + }) + + it("does nothing if subscriber does not exist", () => { + eventBus.unsubscribe("eventName", () => "non-existing") + + expect(eventBus.eventToSubscribersMap_.get("eventName").length).toEqual(1) + }) + + it("does nothing if event has no subcribers", () => { + eventBus.unsubscribe("non-existing", () => "test") + + expect(eventBus.eventToSubscribersMap_.get("eventName").length).toEqual(1) + }) + }) +}) diff --git a/packages/medusa/src/interfaces/batch-job-strategy.ts b/packages/medusa/src/interfaces/batch-job-strategy.ts index 924bcbcec2..386a601900 100644 --- a/packages/medusa/src/interfaces/batch-job-strategy.ts +++ b/packages/medusa/src/interfaces/batch-job-strategy.ts @@ -1,8 +1,8 @@ -import { TransactionBaseService } from "./transaction-base-service" -import { BatchJobResultError, CreateBatchJobInput } from "../types/batch-job" -import { ProductExportBatchJob } from "../strategies/batch-jobs/product/types" -import { BatchJobService } from "../services" import { BatchJob } from "../models" +import { BatchJobService } from "../services" +import { ProductExportBatchJob } from "../strategies/batch-jobs/product/types" +import { BatchJobResultError, CreateBatchJobInput } from "../types/batch-job" +import { TransactionBaseService } from "./transaction-base-service" export interface IBatchJobStrategy extends TransactionBaseService { /** diff --git a/packages/medusa/src/interfaces/index.ts b/packages/medusa/src/interfaces/index.ts index 13ebe68d05..7e13c79a08 100644 --- a/packages/medusa/src/interfaces/index.ts +++ b/packages/medusa/src/interfaces/index.ts @@ -1,14 +1,14 @@ -export * from "./tax-calculation-strategy" -export * from "./cart-completion-strategy" -export * from "./tax-service" -export * from "./transaction-base-service" export * from "./batch-job-strategy" +export * from "./cart-completion-strategy" export * from "./file-service" -export * from "./notification-service" -export * from "./price-selection-strategy" export * from "./models/base-entity" export * from "./models/soft-deletable-entity" -export * from "./search-service" -export * from "./payment-service" +export * from "./notification-service" export * from "./payment-processor" +export * from "./payment-service" +export * from "./price-selection-strategy" +export * from "./search-service" export * from "./services" +export * from "./tax-calculation-strategy" +export * from "./tax-service" +export * from "./transaction-base-service" diff --git a/packages/medusa/src/interfaces/services/index.ts b/packages/medusa/src/interfaces/services/index.ts index facfa6d934..56f14db3b1 100644 --- a/packages/medusa/src/interfaces/services/index.ts +++ b/packages/medusa/src/interfaces/services/index.ts @@ -1,4 +1,3 @@ export * from "./cache" -export * from "./event-bus" -export * from "./stock-location" export * from "./inventory" +export * from "./stock-location" diff --git a/packages/medusa/src/loaders/redis.ts b/packages/medusa/src/loaders/redis.ts index 7519eb4ce0..eda7a7909f 100644 --- a/packages/medusa/src/loaders/redis.ts +++ b/packages/medusa/src/loaders/redis.ts @@ -1,8 +1,8 @@ import { asValue } from "awilix" -import RealRedis from "ioredis" +import Redis from "ioredis" import FakeRedis from "ioredis-mock" -import { ConfigModule, MedusaContainer } from "../types/global" -import { Logger } from "../types/global" +import { EOL } from "os" +import { ConfigModule, Logger, MedusaContainer } from "../types/global" type Options = { container: MedusaContainer @@ -10,19 +10,27 @@ type Options = { logger: Logger } +// TODO: Will be removed when the strict dependency on Redis in the core is removed async function redisLoader({ container, configModule, logger, }: Options): Promise { if (configModule.projectConfig.redis_url) { - // Economical way of dealing with redis clients - const client = new RealRedis(configModule.projectConfig.redis_url) - const subscriber = new RealRedis(configModule.projectConfig.redis_url) + const redisClient = new Redis(configModule.projectConfig.redis_url, { + // Lazy connect to properly handle connection errors + lazyConnect: true, + }) + + try { + await redisClient.connect() + logger?.info(`Connection to Redis established`) + } catch (err) { + logger?.error(`An error occurred while connecting to Redis:${EOL} ${err}`) + } container.register({ - redisClient: asValue(client), - redisSubscriber: asValue(subscriber), + redisClient: asValue(redisClient), }) } else { if (process.env.NODE_ENV === "production") { @@ -38,7 +46,6 @@ async function redisLoader({ container.register({ redisClient: asValue(client), - redisSubscriber: asValue(client), }) } } diff --git a/packages/medusa/src/loaders/search-index.ts b/packages/medusa/src/loaders/search-index.ts index 134c0a6dd0..5b4fa50688 100644 --- a/packages/medusa/src/loaders/search-index.ts +++ b/packages/medusa/src/loaders/search-index.ts @@ -1,7 +1,6 @@ -import { MedusaContainer } from "../types/global" -import { Logger } from "../types/global" -import { EventBusService } from "../services" import { AbstractSearchService } from "../interfaces" +import { EventBusService } from "../services" +import { Logger, MedusaContainer } from "../types/global" export const SEARCH_INDEX_EVENT = "SEARCH_INDEX_EVENT" diff --git a/packages/medusa/src/loaders/services.ts b/packages/medusa/src/loaders/services.ts index 90a0d12792..857953b660 100644 --- a/packages/medusa/src/loaders/services.ts +++ b/packages/medusa/src/loaders/services.ts @@ -1,8 +1,8 @@ import { asFunction } from "awilix" import glob from "glob" +import { isDefined } from "medusa-core-utils" import path from "path" import { ConfigModule, MedusaContainer } from "../types/global" -import { isDefined } from "medusa-core-utils" import formatRegistrationName from "../utils/format-registration-name" type Options = { diff --git a/packages/medusa/src/models/swap.ts b/packages/medusa/src/models/swap.ts index e60fd65ac2..8cab93bfd0 100644 --- a/packages/medusa/src/models/swap.ts +++ b/packages/medusa/src/models/swap.ts @@ -6,10 +6,12 @@ import { JoinColumn, ManyToOne, OneToMany, - OneToOne, + OneToOne } from "typeorm" import { DbAwareColumn, resolveDbType } from "../utils/db-aware-column" +import { SoftDeletableEntity } from "../interfaces/models/soft-deletable-entity" +import { generateEntityId } from "../utils/generate-entity-id" import { Address } from "./address" import { Cart } from "./cart" import { Fulfillment } from "./fulfillment" @@ -18,8 +20,6 @@ import { Order } from "./order" import { Payment } from "./payment" import { Return } from "./return" import { ShippingMethod } from "./shipping-method" -import { SoftDeletableEntity } from "../interfaces/models/soft-deletable-entity" -import { generateEntityId } from "../utils/generate-entity-id" export enum SwapFulfillmentStatus { NOT_FULFILLED = "not_fulfilled", diff --git a/packages/medusa/src/repositories/staged-job.ts b/packages/medusa/src/repositories/staged-job.ts index 01b6f9867c..982ced8083 100644 --- a/packages/medusa/src/repositories/staged-job.ts +++ b/packages/medusa/src/repositories/staged-job.ts @@ -1,7 +1,6 @@ import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity" import { dataSource } from "../loaders/database" import { StagedJob } from "../models" -import { rowSqlResultsToEntityTransformer } from "../utils/row-sql-results-to-entity-transformer" export const StagedJobRepository = dataSource.getRepository(StagedJob).extend({ async insertBulk(jobToCreates: QueryDeepPartialEntity[]) { @@ -13,16 +12,13 @@ export const StagedJobRepository = dataSource.getRepository(StagedJob).extend({ // TODO: remove if statement once this issue is resolved https://github.com/typeorm/typeorm/issues/9850 if (!queryBuilder.connection.driver.isReturningSqlSupported("insert")) { const rawStagedJobs = await queryBuilder.execute() - return rawStagedJobs.generatedMaps + return rawStagedJobs.generatedMaps.map((d) => + this.create(d) + ) as StagedJob[] } const rawStagedJobs = await queryBuilder.returning("*").execute() - - return rowSqlResultsToEntityTransformer( - rawStagedJobs.raw, - queryBuilder, - this.queryRunner! - ) + return rawStagedJobs.generatedMaps.map((d) => this.create(d)) }, }) export default StagedJobRepository diff --git a/packages/medusa/src/services/__mocks__/staged-job.js b/packages/medusa/src/services/__mocks__/staged-job.js new file mode 100644 index 0000000000..e97d9b7206 --- /dev/null +++ b/packages/medusa/src/services/__mocks__/staged-job.js @@ -0,0 +1,25 @@ +const job1 = { + event_name: "test", + data: { + id: "test", + }, +} + +const StagedJobServiceMock = { + withTransaction: function () { + return this + }, + create: jest.fn().mockImplementation((data) => { + return Promise.resolve(data) + }), + + list: jest.fn().mockImplementation((config) => { + return Promise.resolve([job1]) + }), +} + +const mock = jest.fn().mockImplementation(() => { + return StagedJobServiceMock +}) + +export default mock diff --git a/packages/medusa/src/services/__tests__/batch-job.ts b/packages/medusa/src/services/__tests__/batch-job.ts index 90c0491f57..1ff420cd54 100644 --- a/packages/medusa/src/services/__tests__/batch-job.ts +++ b/packages/medusa/src/services/__tests__/batch-job.ts @@ -1,168 +1,188 @@ import { IdMap, MockManager, MockRepository } from "medusa-test-utils" -import BatchJobService from "../batch-job" -import { EventBusService } from "../index" -import { BatchJobStatus } from "../../types/batch-job" import { BatchJob } from "../../models" +import { BatchJobStatus } from "../../types/batch-job" +import BatchJobService from "../batch-job" +import EventBusService from "../event-bus" const eventBusServiceMock = { emit: jest.fn(), - withTransaction: function() { + withTransaction: function () { return this }, } as unknown as EventBusService const batchJobRepositoryMock = MockRepository({ create: jest.fn().mockImplementation((data) => { return Object.assign(new BatchJob(), data) - }) + }), }) -describe('BatchJobService', () => { +describe("BatchJobService", () => { const batchJobId_1 = IdMap.getId("batchJob_1") const batchJobService = new BatchJobService({ manager: MockManager, eventBusService: eventBusServiceMock, - batchJobRepository: batchJobRepositoryMock + batchJobRepository: batchJobRepositoryMock, } as any) afterEach(() => { jest.clearAllMocks() }) - describe('update status', () => { + describe("update status", () => { describe("confirm", () => { - it('should be able to confirm_processing a batch job to emit the processing event', async () => { + it("should be able to confirm_processing a batch job to emit the processing event", async () => { const batchJob = batchJobRepositoryMock.create({ id: batchJobId_1, dry_run: true, - status: BatchJobStatus.PRE_PROCESSED + status: BatchJobStatus.PRE_PROCESSED, }) const updatedBatchJob = await batchJobService.confirm(batchJob) expect(updatedBatchJob.processing_at).not.toBeTruthy() - expect(eventBusServiceMock.emit) - .toHaveBeenCalledWith(BatchJobService.Events.CONFIRMED, { id: batchJobId_1 }) + expect(eventBusServiceMock.emit).toHaveBeenCalledWith( + BatchJobService.Events.CONFIRMED, + { id: batchJobId_1 } + ) }) - it('should not be able to confirm a batch job with the wrong status', async () => { + it("should not be able to confirm a batch job with the wrong status", async () => { const batchJob = batchJobRepositoryMock.create({ id: batchJobId_1, dry_run: true, - status: BatchJobStatus.CREATED + status: BatchJobStatus.CREATED, }) - const err = await batchJobService.confirm(batchJob) - .catch(e => e) + const err = await batchJobService.confirm(batchJob).catch((e) => e) expect(err).toBeTruthy() - expect(err.message).toBe("Cannot confirm processing for a batch job that is not pre processed") + expect(err.message).toBe( + "Cannot confirm processing for a batch job that is not pre processed" + ) expect(eventBusServiceMock.emit).toHaveBeenCalledTimes(0) }) }) describe("complete", () => { - it('should be able to complete a batch job', async () => { + it("should be able to complete a batch job", async () => { const batchJob = batchJobRepositoryMock.create({ id: batchJobId_1, dry_run: true, - status: BatchJobStatus.PROCESSING + status: BatchJobStatus.PROCESSING, }) const updatedBatchJob = await batchJobService.complete(batchJob) expect(updatedBatchJob.completed_at).toBeTruthy() - expect(eventBusServiceMock.emit) - .toHaveBeenCalledWith(BatchJobService.Events.COMPLETED, { id: batchJobId_1 }) + expect(eventBusServiceMock.emit).toHaveBeenCalledWith( + BatchJobService.Events.COMPLETED, + { id: batchJobId_1 } + ) const batchJob2 = batchJobRepositoryMock.create({ id: batchJobId_1, dry_run: false, - status: BatchJobStatus.PROCESSING + status: BatchJobStatus.PROCESSING, }) const updatedBatchJob2 = await batchJobService.complete(batchJob2) expect(updatedBatchJob2.completed_at).toBeTruthy() - expect(eventBusServiceMock.emit) - .toHaveBeenCalledWith(BatchJobService.Events.COMPLETED, { id: batchJobId_1 }) + expect(eventBusServiceMock.emit).toHaveBeenCalledWith( + BatchJobService.Events.COMPLETED, + { id: batchJobId_1 } + ) }) - it('should not be able to complete a batch job with the wrong status', async () => { + it("should not be able to complete a batch job with the wrong status", async () => { const batchJob = batchJobRepositoryMock.create({ id: batchJobId_1, dry_run: true, - status: BatchJobStatus.CREATED + status: BatchJobStatus.CREATED, }) - const err = await batchJobService.complete(batchJob) - .catch(e => e) + const err = await batchJobService.complete(batchJob).catch((e) => e) expect(err).toBeTruthy() - expect(err.message).toBe( `Cannot complete a batch job with status "${batchJob.status}". The batch job must be processing`) + expect(err.message).toBe( + `Cannot complete a batch job with status "${batchJob.status}". The batch job must be processing` + ) expect(eventBusServiceMock.emit).toHaveBeenCalledTimes(0) const batchJob2 = batchJobRepositoryMock.create({ id: batchJobId_1, dry_run: false, - status: BatchJobStatus.PRE_PROCESSED + status: BatchJobStatus.PRE_PROCESSED, }) - const err2 = await batchJobService.complete(batchJob2) - .catch(e => e) + const err2 = await batchJobService.complete(batchJob2).catch((e) => e) expect(err2).toBeTruthy() - expect(err2.message).toBe( `Cannot complete a batch job with status "${batchJob2.status}". The batch job must be processing`) + expect(err2.message).toBe( + `Cannot complete a batch job with status "${batchJob2.status}". The batch job must be processing` + ) expect(eventBusServiceMock.emit).toHaveBeenCalledTimes(0) }) }) describe("pre processed", () => { - it('should be able to mark as pre processed a batch job in dry_run', async () => { + it("should be able to mark as pre processed a batch job in dry_run", async () => { const batchJob = batchJobRepositoryMock.create({ id: batchJobId_1, dry_run: true, - status: BatchJobStatus.CREATED + status: BatchJobStatus.CREATED, }) - const updatedBatchJob = await batchJobService.setPreProcessingDone(batchJob) + const updatedBatchJob = await batchJobService.setPreProcessingDone( + batchJob + ) expect(updatedBatchJob.pre_processed_at).toBeTruthy() - expect(eventBusServiceMock.emit) - .toHaveBeenCalledWith(BatchJobService.Events.PRE_PROCESSED, { id: batchJobId_1 }) + expect(eventBusServiceMock.emit).toHaveBeenCalledWith( + BatchJobService.Events.PRE_PROCESSED, + { id: batchJobId_1 } + ) }) - it('should be able to mark as completed a batch job that has been pre processed but not in dry_run', async () => { + it("should be able to mark as completed a batch job that has been pre processed but not in dry_run", async () => { const batchJob = batchJobRepositoryMock.create({ id: batchJobId_1, dry_run: false, - status: BatchJobStatus.CREATED + status: BatchJobStatus.CREATED, }) - const updatedBatchJob = await batchJobService.setPreProcessingDone(batchJob) + const updatedBatchJob = await batchJobService.setPreProcessingDone( + batchJob + ) expect(updatedBatchJob.pre_processed_at).toBeTruthy() expect(updatedBatchJob.confirmed_at).toBeTruthy() expect(eventBusServiceMock.emit).toHaveBeenCalledTimes(2) - expect(eventBusServiceMock.emit) - .toHaveBeenCalledWith(BatchJobService.Events.PRE_PROCESSED, { id: batchJobId_1 }) - expect(eventBusServiceMock.emit) - .toHaveBeenLastCalledWith(BatchJobService.Events.CONFIRMED, { id: batchJobId_1 }) + expect(eventBusServiceMock.emit).toHaveBeenCalledWith( + BatchJobService.Events.PRE_PROCESSED, + { id: batchJobId_1 } + ) + expect(eventBusServiceMock.emit).toHaveBeenLastCalledWith( + BatchJobService.Events.CONFIRMED, + { id: batchJobId_1 } + ) }) }) describe("cancel", () => { - it('should be able to cancel a batch job', async () => { + it("should be able to cancel a batch job", async () => { const batchJob = batchJobRepositoryMock.create({ id: batchJobId_1, - status: BatchJobStatus.CREATED + status: BatchJobStatus.CREATED, }) const updatedBatchJob = await batchJobService.cancel(batchJob) expect(updatedBatchJob.canceled_at).toBeTruthy() - expect(eventBusServiceMock.emit) - .toHaveBeenCalledWith(BatchJobService.Events.CANCELED, { id: batchJobId_1 }) + expect(eventBusServiceMock.emit).toHaveBeenCalledWith( + BatchJobService.Events.CANCELED, + { id: batchJobId_1 } + ) }) - it('should not be able to cancel a batch job with the wrong status', async () => { + it("should not be able to cancel a batch job with the wrong status", async () => { const batchJob = batchJobRepositoryMock.create({ id: batchJobId_1, - status: BatchJobStatus.COMPLETED + status: BatchJobStatus.COMPLETED, }) - const err = await batchJobService.cancel(batchJob) - .catch(e => e) + const err = await batchJobService.cancel(batchJob).catch((e) => e) expect(err).toBeTruthy() expect(err.message).toBe("Cannot cancel completed batch job") expect(eventBusServiceMock.emit).toHaveBeenCalledTimes(0) @@ -170,30 +190,35 @@ describe('BatchJobService', () => { }) describe("processing", () => { - it('should be able to mark as processing a batch job', async () => { + it("should be able to mark as processing a batch job", async () => { const batchJob = batchJobRepositoryMock.create({ id: batchJobId_1, - status: BatchJobStatus.CONFIRMED + status: BatchJobStatus.CONFIRMED, }) const updatedBatchJob = await batchJobService.setProcessing(batchJob) expect(updatedBatchJob.processing_at).toBeTruthy() - expect(eventBusServiceMock.emit) - .toHaveBeenCalledWith(BatchJobService.Events.PROCESSING, { id: batchJobId_1 }) + expect(eventBusServiceMock.emit).toHaveBeenCalledWith( + BatchJobService.Events.PROCESSING, + { id: batchJobId_1 } + ) }) - it('should not be able to mark as processing a batch job with the wrong status', async () => { + it("should not be able to mark as processing a batch job with the wrong status", async () => { const batchJob = batchJobRepositoryMock.create({ id: batchJobId_1, - status: BatchJobStatus.COMPLETED + status: BatchJobStatus.COMPLETED, }) - const err = await batchJobService.setProcessing(batchJob) - .catch(e => e) + const err = await batchJobService + .setProcessing(batchJob) + .catch((e) => e) expect(err).toBeTruthy() - expect(err.message).toBe("Cannot mark a batch job as processing if the status is different that confirmed") + expect(err.message).toBe( + "Cannot mark a batch job as processing if the status is different that confirmed" + ) expect(eventBusServiceMock.emit).toHaveBeenCalledTimes(0) }) }) }) -}) \ No newline at end of file +}) diff --git a/packages/medusa/src/services/__tests__/currency.ts b/packages/medusa/src/services/__tests__/currency.ts index 270df58308..087a26e766 100644 --- a/packages/medusa/src/services/__tests__/currency.ts +++ b/packages/medusa/src/services/__tests__/currency.ts @@ -1,36 +1,35 @@ import { IdMap, MockManager, MockRepository } from "medusa-test-utils" -import { EventBusService } from "../index" -import { Currency } from "../../models" -import CurrencyService from "../currency" -import { FlagRouter } from "../../utils/flag-router" import TaxInclusivePricingFeatureFlag from "../../loaders/feature-flags/tax-inclusive-pricing" +import { Currency } from "../../models" +import { FlagRouter } from "../../utils/flag-router" +import CurrencyService from "../currency" +import EventBusService from "../event-bus" const currencyCode = IdMap.getId("currency-1") const eventBusServiceMock = { emit: jest.fn(), - withTransaction: function() { + withTransaction: function () { return this }, } as unknown as EventBusService const currencyRepositoryMock = MockRepository({ findOne: jest.fn().mockImplementation(() => { return { - code: currencyCode + code: currencyCode, } }), save: jest.fn().mockImplementation((data) => { return Object.assign(new Currency(), data) - }) + }), }) - -describe('CurrencyService', () => { +describe("CurrencyService", () => { const currencyService = new CurrencyService({ manager: MockManager, currencyRepository: currencyRepositoryMock, eventBusService: eventBusServiceMock, featureFlagRouter: new FlagRouter({ - [TaxInclusivePricingFeatureFlag.key]: true + [TaxInclusivePricingFeatureFlag.key]: true, }), }) diff --git a/packages/medusa/src/services/__tests__/event-bus.js b/packages/medusa/src/services/__tests__/event-bus.js deleted file mode 100644 index ad7a0a4295..0000000000 --- a/packages/medusa/src/services/__tests__/event-bus.js +++ /dev/null @@ -1,536 +0,0 @@ -import Bull from "bull" -import { MockManager, MockRepository } from "medusa-test-utils" -import config from "../../loaders/config" -import EventBusService from "../event-bus" - -jest.genMockFromModule("bull") -jest.mock("bull") -jest.mock("../../loaders/config") - -config.redisURI = "testhost" - -const loggerMock = { - info: jest.fn().mockReturnValue(console.log), - warn: jest.fn().mockReturnValue(console.log), - error: jest.fn().mockReturnValue(console.log), -} - -describe("EventBusService", () => { - describe("constructor", () => { - let eventBus - beforeAll(() => { - jest.resetAllMocks() - const stagedJobRepository = MockRepository({ - find: () => Promise.resolve([]), - }) - - eventBus = new EventBusService( - { - manager: MockManager, - stagedJobRepository, - logger: loggerMock, - }, - { - projectConfig: { - redis_url: "localhost", - }, - } - ) - }) - - afterAll(async () => { - await eventBus.stopEnqueuer() - }) - - it("creates bull queue", () => { - expect(Bull).toHaveBeenCalledTimes(1) - expect(Bull).toHaveBeenCalledWith("EventBusService:queue", { - createClient: expect.any(Function), - }) - }) - }) - - describe("subscribe", () => { - let eventBus - - beforeEach(() => { - jest.resetAllMocks() - - eventBus = new EventBusService( - { - manager: MockManager, - logger: loggerMock, - }, - { - projectConfig: { - redis_url: "localhost", - }, - } - ) - }) - - afterAll(async () => { - await eventBus.stopEnqueuer() - }) - - it("throws when subscriber already exists", async () => { - expect.assertions(1) - - eventBus.subscribe("eventName", () => "test", { - subscriberId: "my-subscriber", - }) - - try { - eventBus.subscribe("eventName", () => "new", { - subscriberId: "my-subscriber", - }) - } catch (error) { - expect(error.message).toBe( - "Subscriber with id my-subscriber already exists" - ) - } - }) - - it("successfully adds subscriber", () => { - eventBus.subscribe("eventName", () => "test", { - subscriberId: "my-subscriber", - }) - - expect(eventBus.eventToSubscribersMap_.get("eventName").length).toEqual(1) - }) - - describe("fails when adding non-function subscriber", () => { - let eventBus - beforeAll(() => { - jest.resetAllMocks() - const stagedJobRepository = MockRepository({ - find: () => Promise.resolve([]), - }) - - eventBus = new EventBusService({ - manager: MockManager, - stagedJobRepository, - logger: loggerMock, - }) - }) - afterAll(async () => { - await eventBus.stopEnqueuer() - }) - - it("rejects subscriber with error", () => { - try { - eventBus.subscribe("eventName", 1234) - } catch (err) { - expect(err.message).toEqual("Subscriber must be a function") - } - }) - }) - }) - - describe("emit", () => { - const eventName = "eventName" - const defaultOptions = { - attempts: 1, - removeOnComplete: true, - } - - const data = { hi: "1234" } - const bulkData = [{ hi: "1234" }, { hi: "12345" }] - - const mockManager = MockManager - - describe("successfully adds job to queue", () => { - let eventBus - let stagedJobRepository - - beforeEach(() => { - stagedJobRepository = MockRepository({ - insertBulk: async (data) => data, - create: (data) => data, - }) - - eventBus = new EventBusService( - { - logger: loggerMock, - manager: mockManager, - stagedJobRepository, - }, - { - projectConfig: { - redis_url: "localhost", - }, - } - ) - - eventBus.queue_.addBulk.mockImplementationOnce(() => "hi") - }) - - afterEach(async () => { - await eventBus.stopEnqueuer() - jest.clearAllMocks() - }) - - it("calls queue.addBulk", async () => { - await eventBus.emit(eventName, data) - - expect(eventBus.queue_.addBulk).toHaveBeenCalled() - expect(eventBus.queue_.addBulk).toHaveBeenCalledWith([ - { - data: { - data, - eventName, - }, - opts: defaultOptions, - }, - ]) - }) - - it("calls stagedJob repository insertBulk", async () => { - await eventBus.withTransaction(mockManager).emit(eventName, data) - - expect(stagedJobRepository.create).toHaveBeenCalled() - expect(stagedJobRepository.create).toHaveBeenCalledWith({ - event_name: eventName, - data: data, - options: defaultOptions, - }) - - expect(stagedJobRepository.insertBulk).toHaveBeenCalled() - expect(stagedJobRepository.insertBulk).toHaveBeenCalledWith([ - { - event_name: eventName, - data, - options: defaultOptions, - }, - ]) - }) - }) - - describe("successfully adds jobs in bulk to queue", () => { - let eventBus - let stagedJobRepository - - beforeEach(() => { - stagedJobRepository = MockRepository({ - insertBulk: async (data) => data, - create: (data) => data, - }) - - eventBus = new EventBusService( - { - logger: loggerMock, - manager: mockManager, - stagedJobRepository, - }, - { - projectConfig: { - redis_url: "localhost", - }, - } - ) - - eventBus.queue_.addBulk.mockImplementationOnce(() => "hi") - }) - - afterEach(async () => { - jest.clearAllMocks() - await eventBus.stopEnqueuer() - }) - - it("calls queue.addBulk", async () => { - await eventBus.emit([ - { eventName, data: bulkData[0] }, - { eventName, data: bulkData[1] }, - ]) - - expect(eventBus.queue_.addBulk).toHaveBeenCalledTimes(1) - expect(eventBus.queue_.addBulk).toHaveBeenCalledWith([ - { - data: { - data: bulkData[0], - eventName, - }, - opts: defaultOptions, - }, - { - data: { - data: bulkData[1], - eventName, - }, - opts: defaultOptions, - }, - ]) - }) - - it("calls stagedJob repository insertBulk", async () => { - await eventBus.withTransaction(mockManager).emit([ - { eventName, data: bulkData[0] }, - { eventName, data: bulkData[1] }, - ]) - - expect(stagedJobRepository.create).toHaveBeenCalledTimes(2) - expect(stagedJobRepository.create).toHaveBeenNthCalledWith(1, { - data: bulkData[0], - event_name: eventName, - options: defaultOptions, - }) - expect(stagedJobRepository.create).toHaveBeenNthCalledWith(2, { - data: bulkData[1], - event_name: eventName, - options: defaultOptions, - }) - - expect(stagedJobRepository.insertBulk).toHaveBeenCalledTimes(1) - expect(stagedJobRepository.insertBulk).toHaveBeenCalledWith([ - { - data: bulkData[0], - event_name: eventName, - options: defaultOptions, - }, - { - data: bulkData[1], - event_name: eventName, - options: defaultOptions, - }, - ]) - }) - }) - - describe("successfully adds job to queue with global options", () => { - let eventBus - let stagedJobRepository - - beforeEach(() => { - stagedJobRepository = MockRepository({ - insertBulk: async (data) => data, - create: (data) => data, - }) - - eventBus = new EventBusService( - { - logger: loggerMock, - manager: mockManager, - stagedJobRepository, - }, - { - projectConfig: { - event_options: { removeOnComplete: 10 }, - redis_url: "localhost", - }, - } - ) - - eventBus.queue_.addBulk.mockImplementationOnce(() => "hi") - - eventBus.emit(eventName, data) - }) - - afterEach(async () => { - jest.clearAllMocks() - await eventBus.stopEnqueuer() - }) - - it("calls queue.addBulk", () => { - expect(eventBus.queue_.addBulk).toHaveBeenCalled() - expect(eventBus.queue_.addBulk).toHaveBeenCalledWith([ - { - data: { - data, - eventName, - }, - opts: { removeOnComplete: 10, attempts: 1 }, - }, - ]) - }) - }) - - describe("successfully adds job to queue with default options", () => { - let eventBus - let stagedJobRepository - - beforeEach(() => { - stagedJobRepository = MockRepository({ - insertBulk: async (data) => data, - create: (data) => data, - }) - - eventBus = new EventBusService( - { - logger: loggerMock, - manager: mockManager, - stagedJobRepository, - }, - { - projectConfig: { - redis_url: "localhost", - }, - } - ) - - eventBus.queue_.addBulk.mockImplementationOnce(() => "hi") - - eventBus.emit(eventName, data) - }) - - afterEach(async () => { - jest.clearAllMocks() - await eventBus.stopEnqueuer() - }) - - it("calls queue.addBulk", () => { - expect(eventBus.queue_.addBulk).toHaveBeenCalled() - expect(eventBus.queue_.addBulk).toHaveBeenCalledWith([ - { - data: { - data, - eventName, - }, - opts: { removeOnComplete: true, attempts: 1 }, - }, - ]) - }) - }) - - describe("successfully adds job to queue with local options and global options merged", () => { - let eventBus - let stagedJobRepository - - beforeEach(() => { - stagedJobRepository = MockRepository({ - insertBulk: async (data) => data, - create: (data) => data, - }) - - eventBus = new EventBusService( - { - logger: loggerMock, - manager: MockManager, - stagedJobRepository, - }, - { - projectConfig: { - event_options: { removeOnComplete: 10 }, - redis_url: "localhost", - }, - } - ) - - eventBus.queue_.addBulk.mockImplementationOnce(() => "hi") - - eventBus.emit(eventName, data, { - attempts: 10, - delay: 1000, - backoff: { type: "exponential" }, - }) - }) - - afterEach(async () => { - jest.clearAllMocks() - await eventBus.stopEnqueuer() - }) - - it("calls queue.add", () => { - expect(eventBus.queue_.addBulk).toHaveBeenCalled() - expect(eventBus.queue_.addBulk).toHaveBeenCalledWith([ - { - data: { - data, - eventName, - }, - opts: { - removeOnComplete: 10, // global option - attempts: 10, // local option - delay: 1000, // local option - backoff: { type: "exponential" }, // local option - }, - }, - ]) - }) - }) - }) - - describe("worker", () => { - let eventBus - let result - describe("successfully runs the worker", () => { - beforeAll(async () => { - jest.resetAllMocks() - const stagedJobRepository = MockRepository({ - find: () => Promise.resolve([]), - }) - - eventBus = new EventBusService({ - manager: MockManager, - stagedJobRepository, - logger: loggerMock, - }) - eventBus.subscribe("eventName", () => Promise.resolve("hi")) - result = await eventBus.worker_({ - data: { eventName: "eventName", data: {} }, - }) - }) - - afterAll(async () => { - await eventBus.stopEnqueuer() - }) - it("calls logger", () => { - expect(loggerMock.info).toHaveBeenCalled() - expect(loggerMock.info).toHaveBeenCalledWith( - "Processing eventName which has 1 subscribers" - ) - }) - - it("returns array with hi", async () => { - expect(result).toEqual(["hi"]) - }) - }) - - describe("continue if errors occur", () => { - let eventBus - beforeAll(async () => { - jest.resetAllMocks() - - eventBus = new EventBusService({ - manager: MockManager, - logger: loggerMock, - }) - - eventBus.subscribe("eventName", () => Promise.resolve("hi")) - eventBus.subscribe("eventName", () => Promise.resolve("hi2")) - eventBus.subscribe("eventName", () => Promise.resolve("hi3")) - eventBus.subscribe("eventName", () => Promise.reject("fail1")) - eventBus.subscribe("eventName", () => Promise.reject("fail2")) - eventBus.subscribe("eventName", () => Promise.reject("fail3")) - - result = await eventBus.worker_({ - data: { eventName: "eventName", data: {} }, - update: (data) => data, - opts: { attempts: 1 }, - }) - }) - - afterAll(async () => { - await eventBus.stopEnqueuer() - }) - - it("calls logger warn on rejections", () => { - expect(loggerMock.warn).toHaveBeenCalledTimes(4) - expect(loggerMock.warn).toHaveBeenCalledWith( - "An error occurred while processing eventName: fail1" - ) - expect(loggerMock.warn).toHaveBeenCalledWith( - "An error occurred while processing eventName: fail2" - ) - expect(loggerMock.warn).toHaveBeenCalledWith( - "An error occurred while processing eventName: fail3" - ) - }) - - it("calls logger warn from retry not kicking in", () => { - expect(loggerMock.warn).toHaveBeenCalledWith( - "One or more subscribers of eventName failed. Retrying is not configured. Use 'attempts' option when emitting events." - ) - }) - }) - }) -}) diff --git a/packages/medusa/src/services/__tests__/job-scheduler.js b/packages/medusa/src/services/__tests__/job-scheduler.js index a4d89ee5a6..af7d6f1936 100644 --- a/packages/medusa/src/services/__tests__/job-scheduler.js +++ b/packages/medusa/src/services/__tests__/job-scheduler.js @@ -1,12 +1,9 @@ -import Bull from "bull" -import config from "../../loaders/config" +import { Queue } from "bullmq" import JobSchedulerService from "../job-scheduler" -jest.genMockFromModule("bull") -jest.mock("bull") -jest.mock("../../loaders/config") - -config.redisURI = "testhost" +jest.genMockFromModule("bullmq") +jest.mock("bullmq") +jest.mock("ioredis") const loggerMock = { info: jest.fn().mockReturnValue(console.log), @@ -15,90 +12,112 @@ const loggerMock = { } describe("JobSchedulerService", () => { - describe("constructor", () => { - let jobScheduler - beforeAll(() => { - jest.resetAllMocks() + let scheduler - jobScheduler = new JobSchedulerService({ - logger: loggerMock, - }) + describe("constructor", () => { + beforeAll(() => { + jest.clearAllMocks() + + scheduler = new JobSchedulerService( + { + logger: loggerMock, + }, + { + projectConfig: { + redis_url: "testhost", + }, + } + ) }) it("creates bull queue", () => { - expect(Bull).toHaveBeenCalledTimes(1) - expect(Bull).toHaveBeenCalledWith("scheduled-jobs:queue", { - createClient: expect.any(Function), + expect(Queue).toHaveBeenCalledTimes(1) + expect(Queue).toHaveBeenCalledWith("scheduled-jobs:queue", { + connection: expect.any(Object), + prefix: "JobSchedulerService", }) }) }) describe("create", () => { let jobScheduler - describe("successfully creates scheduled job and add handler", () => { - beforeAll(() => { - jest.resetAllMocks() - jobScheduler = new JobSchedulerService({ + beforeAll(async () => { + jest.resetAllMocks() + + jobScheduler = new JobSchedulerService( + { logger: loggerMock, - }) - - jobScheduler.create( - "eventName", - { data: "test" }, - "* * * * *", - () => "test" - ) - }) - - it("added the handler to the job queue", () => { - expect(jobScheduler.handlers_.get("eventName").length).toEqual(1) - expect(jobScheduler.queue_.add).toHaveBeenCalledWith( - { - eventName: "eventName", - data: { data: "test" }, + }, + { + projectConfig: { + redis_url: "testhost", }, - { - repeat: { cron: "* * * * *" }, - } - ) + } + ) + + await jobScheduler.create( + "eventName", + { data: "test" }, + "* * * * *", + () => "test" + ) + }) + + it("added the handler to the job queue", () => { + expect(jobScheduler.handlers_.get("eventName").length).toEqual(1) + expect(jobScheduler.queue_.add).toHaveBeenCalledWith( + "eventName", + { + eventName: "eventName", + data: { data: "test" }, + }, + { + repeat: { pattern: "* * * * *" }, + } + ) + }) + }) + + describe("scheduledJobWorker", () => { + let jobScheduler + let result + + beforeAll(async () => { + jest.resetAllMocks() + + jobScheduler = new JobSchedulerService( + { + logger: loggerMock, + }, + { + projectConfig: { + redis_url: "testhost", + }, + } + ) + + await jobScheduler.create( + "eventName", + { data: "test" }, + "* * * * *", + () => Promise.resolve("hi") + ) + + result = await jobScheduler.scheduledJobsWorker({ + data: { eventName: "eventName", data: {} }, }) }) - describe("scheduledJobWorker", () => { - let jobScheduler - let result - describe("successfully runs the worker", () => { - beforeAll(async () => { - jest.resetAllMocks() + it("calls logger", () => { + expect(loggerMock.info).toHaveBeenCalled() + expect(loggerMock.info).toHaveBeenCalledWith( + "Processing scheduled job: eventName" + ) + }) - jobScheduler = new JobSchedulerService( - { - logger: loggerMock, - }, - {} - ) - - jobScheduler.create("eventName", { data: "test" }, "* * * * *", () => - Promise.resolve("hi") - ) - - result = await jobScheduler.scheduledJobsWorker({ - data: { eventName: "eventName", data: {} }, - }) - }) - - it("calls logger", () => { - expect(loggerMock.info).toHaveBeenCalled() - expect(loggerMock.info).toHaveBeenCalledWith( - "Processing scheduled job: eventName" - ) - }) - - it("returns array with hi", async () => { - expect(result).toEqual(["hi"]) - }) - }) + it("returns array with hi", async () => { + expect(result).toEqual(["hi"]) }) }) }) diff --git a/packages/medusa/src/services/__tests__/order-edit-item-change.ts b/packages/medusa/src/services/__tests__/order-edit-item-change.ts index 7f491ed7b9..bcffc5d092 100644 --- a/packages/medusa/src/services/__tests__/order-edit-item-change.ts +++ b/packages/medusa/src/services/__tests__/order-edit-item-change.ts @@ -1,7 +1,12 @@ import { IdMap, MockManager, MockRepository } from "medusa-test-utils" -import { EventBusService, LineItemService, OrderEditItemChangeService, TaxProviderService, } from "../index" -import { EventBusServiceMock } from "../__mocks__/event-bus" import { In } from "typeorm" +import EventBusService from "../event-bus" +import { + LineItemService, + OrderEditItemChangeService, + TaxProviderService +} from "../index" +import { EventBusServiceMock } from "../__mocks__/event-bus" import { LineItemServiceMock } from "../__mocks__/line-item" const taxProviderServiceMock = { diff --git a/packages/medusa/src/services/__tests__/order-edit.ts b/packages/medusa/src/services/__tests__/order-edit.ts index 8e3fcf0c64..d26e9b6150 100644 --- a/packages/medusa/src/services/__tests__/order-edit.ts +++ b/packages/medusa/src/services/__tests__/order-edit.ts @@ -1,7 +1,7 @@ import { IdMap, MockManager, MockRepository } from "medusa-test-utils" import { OrderEditItemChangeType, OrderEditStatus } from "../../models" +import EventBusService from "../event-bus" import { - EventBusService, LineItemService, NewTotalsService, OrderEditItemChangeService, diff --git a/packages/medusa/src/services/__tests__/payment-collection.ts b/packages/medusa/src/services/__tests__/payment-collection.ts index 09cf300de8..38bfd816bc 100644 --- a/packages/medusa/src/services/__tests__/payment-collection.ts +++ b/packages/medusa/src/services/__tests__/payment-collection.ts @@ -1,10 +1,22 @@ import { IdMap, MockManager, MockRepository } from "medusa-test-utils" -import { CustomerService, EventBusService, PaymentCollectionService, PaymentProviderService, } from "../index" -import { PaymentCollection, PaymentCollectionStatus, PaymentCollectionType, } from "../../models" -import { EventBusServiceMock } from "../__mocks__/event-bus" -import { DefaultProviderMock, PaymentProviderServiceMock, } from "../__mocks__/payment-provider" -import { CustomerServiceMock } from "../__mocks__/customer" +import { + PaymentCollection, + PaymentCollectionStatus, + PaymentCollectionType +} from "../../models" import { PaymentCollectionsSessionsBatchInput } from "../../types/payment-collection" +import EventBusService from "../event-bus" +import { + CustomerService, + PaymentCollectionService, + PaymentProviderService +} from "../index" +import { CustomerServiceMock } from "../__mocks__/customer" +import { EventBusServiceMock } from "../__mocks__/event-bus" +import { + DefaultProviderMock, + PaymentProviderServiceMock +} from "../__mocks__/payment-provider" describe("PaymentCollectionService", () => { afterEach(() => { @@ -414,9 +426,7 @@ describe("PaymentCollectionService", () => { "lebron" ) - expect(PaymentProviderServiceMock.createSession).toHaveBeenCalledTimes( - 1 - ) + expect(PaymentProviderServiceMock.createSession).toHaveBeenCalledTimes(1) expect(CustomerServiceMock.retrieve).toHaveBeenCalledTimes(1) expect(paymentCollectionRepository.save).toHaveBeenCalledTimes(1) }) @@ -430,12 +440,8 @@ describe("PaymentCollectionService", () => { "lebron" ) - expect(PaymentProviderServiceMock.createSession).toHaveBeenCalledTimes( - 0 - ) - expect(PaymentProviderServiceMock.updateSession).toHaveBeenCalledTimes( - 1 - ) + expect(PaymentProviderServiceMock.createSession).toHaveBeenCalledTimes(0) + expect(PaymentProviderServiceMock.updateSession).toHaveBeenCalledTimes(1) expect(CustomerServiceMock.retrieve).toHaveBeenCalledTimes(1) expect(paymentCollectionRepository.save).toHaveBeenCalledTimes(1) }) @@ -453,15 +459,9 @@ describe("PaymentCollectionService", () => { IdMap.getId("lebron") ) - expect(PaymentProviderServiceMock.createSession).toHaveBeenCalledTimes( - 1 - ) - expect(PaymentProviderServiceMock.updateSession).toHaveBeenCalledTimes( - 0 - ) - expect(paymentCollectionRepository.delete).toHaveBeenCalledTimes( - 1 - ) + expect(PaymentProviderServiceMock.createSession).toHaveBeenCalledTimes(1) + expect(PaymentProviderServiceMock.updateSession).toHaveBeenCalledTimes(0) + expect(paymentCollectionRepository.delete).toHaveBeenCalledTimes(1) expect(paymentCollectionRepository.save).toHaveBeenCalledTimes(1) }) @@ -508,9 +508,7 @@ describe("PaymentCollectionService", () => { "customer1" ) - expect(PaymentProviderServiceMock.createSession).toHaveBeenCalledTimes( - 0 - ) + expect(PaymentProviderServiceMock.createSession).toHaveBeenCalledTimes(0) expect(ret).rejects.toThrow( `The sum of sessions is not equal to 100 on Payment Collection` ) @@ -531,9 +529,7 @@ describe("PaymentCollectionService", () => { "customer1" ) - expect(PaymentProviderServiceMock.createSession).toHaveBeenCalledTimes( - 0 - ) + expect(PaymentProviderServiceMock.createSession).toHaveBeenCalledTimes(0) expect(multiRet).rejects.toThrow( `The sum of sessions is not equal to 100 on Payment Collection` ) @@ -580,12 +576,8 @@ describe("PaymentCollectionService", () => { "lebron" ) - expect(PaymentProviderServiceMock.createSession).toHaveBeenCalledTimes( - 1 - ) - expect(PaymentProviderServiceMock.updateSession).toHaveBeenCalledTimes( - 1 - ) + expect(PaymentProviderServiceMock.createSession).toHaveBeenCalledTimes(1) + expect(PaymentProviderServiceMock.updateSession).toHaveBeenCalledTimes(1) expect(CustomerServiceMock.retrieve).toHaveBeenCalledTimes(1) expect(paymentCollectionRepository.save).toHaveBeenCalledTimes(1) }) @@ -603,15 +595,9 @@ describe("PaymentCollectionService", () => { IdMap.getId("lebron") ) - expect(PaymentProviderServiceMock.createSession).toHaveBeenCalledTimes( - 1 - ) - expect(PaymentProviderServiceMock.updateSession).toHaveBeenCalledTimes( - 0 - ) - expect(paymentCollectionRepository.delete).toHaveBeenCalledTimes( - 1 - ) + expect(PaymentProviderServiceMock.createSession).toHaveBeenCalledTimes(1) + expect(PaymentProviderServiceMock.updateSession).toHaveBeenCalledTimes(0) + expect(paymentCollectionRepository.delete).toHaveBeenCalledTimes(1) expect(paymentCollectionRepository.save).toHaveBeenCalledTimes(1) }) @@ -623,13 +609,9 @@ describe("PaymentCollectionService", () => { "customer1" ) - expect( - PaymentProviderServiceMock.refreshSession - ).toHaveBeenCalledTimes(1) + expect(PaymentProviderServiceMock.refreshSession).toHaveBeenCalledTimes(1) expect(DefaultProviderMock.deletePayment).toHaveBeenCalledTimes(1) - expect(PaymentProviderServiceMock.createSession).toHaveBeenCalledTimes( - 1 - ) + expect(PaymentProviderServiceMock.createSession).toHaveBeenCalledTimes(1) }) it("should throw to refresh a payment session that doesn't exist", async () => { @@ -689,9 +671,7 @@ describe("PaymentCollectionService", () => { expect(PaymentProviderServiceMock.authorizePayment).toHaveBeenCalledTimes( 2 ) - expect(PaymentProviderServiceMock.createPayment).toHaveBeenCalledTimes( - 2 - ) + expect(PaymentProviderServiceMock.createPayment).toHaveBeenCalledTimes(2) expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(1) }) @@ -704,9 +684,7 @@ describe("PaymentCollectionService", () => { expect(PaymentProviderServiceMock.authorizePayment).toHaveBeenCalledTimes( 1 ) - expect(PaymentProviderServiceMock.createPayment).toHaveBeenCalledTimes( - 1 - ) + expect(PaymentProviderServiceMock.createPayment).toHaveBeenCalledTimes(1) expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(1) }) @@ -719,9 +697,7 @@ describe("PaymentCollectionService", () => { expect(PaymentProviderServiceMock.authorizePayment).toHaveBeenCalledTimes( 0 ) - expect(PaymentProviderServiceMock.createPayment).toHaveBeenCalledTimes( - 0 - ) + expect(PaymentProviderServiceMock.createPayment).toHaveBeenCalledTimes(0) expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(0) }) }) diff --git a/packages/medusa/src/services/__tests__/product-category.ts b/packages/medusa/src/services/__tests__/product-category.ts index 7b99c28fe3..7468652700 100644 --- a/packages/medusa/src/services/__tests__/product-category.ts +++ b/packages/medusa/src/services/__tests__/product-category.ts @@ -1,13 +1,10 @@ import { IdMap, MockManager as manager } from "medusa-test-utils" -import ProductCategoryService from "../product-category" -import { EventBusService } from "../" +import { EventBusService, ProductCategoryService } from "../" import { invalidProdCategoryId, productCategoryRepositoryMock as productCategoryRepository, validProdCategoryId, - validProdCategoryIdWithChildren, - validProdCategoryWithSiblings, - validProdCategoryRankChange + validProdCategoryIdWithChildren, validProdCategoryRankChange, validProdCategoryWithSiblings } from "../../repositories/__mocks__/product-category" import { tempReorderRank } from "../../types/product-category" import { EventBusServiceMock as eventBusService } from "../__mocks__/event-bus" @@ -74,7 +71,9 @@ describe("ProductCategoryService", () => { const [result, count] = await productCategoryService .listAndCount({ q: IdMap.getId(invalidProdCategoryId) }) - expect(productCategoryRepository.getFreeTextSearchResultsAndCount).toHaveBeenCalledTimes(1) + expect( + productCategoryRepository.getFreeTextSearchResultsAndCount + ).toHaveBeenCalledTimes(1) expect(result).toEqual([]) expect(count).toEqual(0) }) @@ -115,8 +114,9 @@ describe("ProductCategoryService", () => { expect(eventBusService.emit).toHaveBeenCalledTimes(1) expect(eventBusService.emit).toHaveBeenCalledWith( - "product-category.created", { - "id": IdMap.getId(validProdCategoryId) + "product-category.created", + { + id: IdMap.getId(validProdCategoryId), } ) }) @@ -129,7 +129,9 @@ describe("ProductCategoryService", () => { ) expect(productCategoryRepository.delete).toBeCalledTimes(1) - expect(productCategoryRepository.delete).toBeCalledWith(IdMap.getId(validProdCategoryId)) + expect(productCategoryRepository.delete).toBeCalledWith( + IdMap.getId(validProdCategoryId) + ) }) it("returns without failure on not-found product category id", async () => { @@ -156,8 +158,9 @@ describe("ProductCategoryService", () => { expect(eventBusService.emit).toHaveBeenCalledTimes(1) expect(eventBusService.emit).toHaveBeenCalledWith( - "product-category.deleted", { - "id": IdMap.getId(validProdCategoryId) + "product-category.deleted", + { + id: IdMap.getId(validProdCategoryId), } ) }) @@ -182,11 +185,9 @@ describe("ProductCategoryService", () => { describe("update", () => { it("successfully updates a product category", async () => { - await productCategoryService.update( - IdMap.getId(validProdCategoryId), { - name: "bathrobes", - } - ) + await productCategoryService.update(IdMap.getId(validProdCategoryId), { + name: "bathrobes", + }) expect(productCategoryRepository.save).toHaveBeenCalledTimes(1) expect(productCategoryRepository.save).toHaveBeenCalledWith( @@ -221,28 +222,32 @@ describe("ProductCategoryService", () => { }) it("fails on not-found Id product category", async () => { - const error = await productCategoryService.update( - IdMap.getId(invalidProdCategoryId), { + const error = await productCategoryService + .update(IdMap.getId(invalidProdCategoryId), { name: "bathrobes", - } - ).catch(e => e) + }) + .catch((e) => e) expect(error.message).toBe( - `ProductCategory with id: ${IdMap.getId(invalidProdCategoryId)} was not found` + `ProductCategory with id: ${IdMap.getId( + invalidProdCategoryId + )} was not found` ) }) it("emits a message on successful update", async () => { const result = await productCategoryService.update( - IdMap.getId(validProdCategoryId), { + IdMap.getId(validProdCategoryId), + { name: "bathrobes", } ) expect(eventBusService.emit).toHaveBeenCalledTimes(1) expect(eventBusService.emit).toHaveBeenCalledWith( - "product-category.updated", { - "id": IdMap.getId(validProdCategoryId) + "product-category.updated", + { + id: IdMap.getId(validProdCategoryId), } ) }) diff --git a/packages/medusa/src/services/__tests__/publishable-api-key.ts b/packages/medusa/src/services/__tests__/publishable-api-key.ts index 1cc3313451..8c0e093932 100644 --- a/packages/medusa/src/services/__tests__/publishable-api-key.ts +++ b/packages/medusa/src/services/__tests__/publishable-api-key.ts @@ -1,8 +1,8 @@ import { IdMap, MockManager, MockRepository } from "medusa-test-utils" +import EventBusService from "../event-bus" -import { EventBusService } from "../index" -import { EventBusServiceMock } from "../__mocks__/event-bus" import PublishableApiKeyService from "../publishable-api-key" +import { EventBusServiceMock } from "../__mocks__/event-bus" const pubKeyToRetrieve = { id: IdMap.getId("pub-key-to-retrieve"), diff --git a/packages/medusa/src/services/__tests__/region.ts b/packages/medusa/src/services/__tests__/region.ts index b456a4fc33..f53edf5ea6 100644 --- a/packages/medusa/src/services/__tests__/region.ts +++ b/packages/medusa/src/services/__tests__/region.ts @@ -1,8 +1,8 @@ import { IdMap, MockManager, MockRepository } from "medusa-test-utils" import { CreateRegionInput } from "../../types/region" import { FlagRouter } from "../../utils/flag-router" +import EventBusService from "../event-bus" import { - EventBusService, FulfillmentProviderService, PaymentProviderService, StoreService, diff --git a/packages/medusa/src/services/__tests__/sales-channel.ts b/packages/medusa/src/services/__tests__/sales-channel.ts index b8cda0ac2a..41527ce27d 100644 --- a/packages/medusa/src/services/__tests__/sales-channel.ts +++ b/packages/medusa/src/services/__tests__/sales-channel.ts @@ -1,9 +1,7 @@ import { IdMap, MockManager, MockRepository } from "medusa-test-utils" +import { EventBusService, StoreService } from "../index" import SalesChannelService from "../sales-channel" import { EventBusServiceMock } from "../__mocks__/event-bus" -import { EventBusService, StoreService } from "../index" -import { FindManyOptions, FindOneOptions } from "typeorm" -import { SalesChannel } from "../../models" import { store, StoreServiceMock } from "../__mocks__/store" describe("SalesChannelService", () => { diff --git a/packages/medusa/src/services/__tests__/swap.ts b/packages/medusa/src/services/__tests__/swap.ts index 769b2aefe7..9a2419590f 100644 --- a/packages/medusa/src/services/__tests__/swap.ts +++ b/packages/medusa/src/services/__tests__/swap.ts @@ -1,14 +1,10 @@ import { IdMap, MockManager, MockRepository } from "medusa-test-utils" -import SwapService from "../swap" -import { - ProductVariantInventoryServiceMock -} from "../__mocks__/product-variant-inventory" -import { - LineItemAdjustmentServiceMock -} from "../__mocks__/line-item-adjustment" +import { Order, Swap } from "../../models" +import { SwapRepository } from "../../repositories/swap" +import CartService from "../cart" +import EventBusService from "../event-bus" import { CustomShippingOptionService, - EventBusService, FulfillmentService, LineItemService, OrderService, @@ -16,12 +12,16 @@ import { ProductVariantInventoryService, ReturnService, ShippingOptionService, - TotalsService, + TotalsService } from "../index" -import CartService from "../cart" -import { Order, Swap } from "../../models" -import { SwapRepository } from "../../repositories/swap" import LineItemAdjustmentService from "../line-item-adjustment" +import SwapService from "../swap" +import { + LineItemAdjustmentServiceMock +} from "../__mocks__/line-item-adjustment" +import { + ProductVariantInventoryServiceMock +} from "../__mocks__/product-variant-inventory" /* ******************** DEFAULT REPOSITORY MOCKS ******************** */ diff --git a/packages/medusa/src/services/batch-job.ts b/packages/medusa/src/services/batch-job.ts index e36b3de043..b87ccfd9e1 100644 --- a/packages/medusa/src/services/batch-job.ts +++ b/packages/medusa/src/services/batch-job.ts @@ -1,4 +1,7 @@ +import { Request } from "express" +import { isDefined, MedusaError } from "medusa-core-utils" import { EntityManager } from "typeorm" +import { TransactionBaseService } from "../interfaces" import { BatchJob } from "../models" import { BatchJobRepository } from "../repositories/batch-job" import { @@ -10,11 +13,9 @@ import { FilterableBatchJobProps, } from "../types/batch-job" import { FindConfig } from "../types/common" -import { TransactionBaseService } from "../interfaces" import { buildQuery } from "../utils" -import { isDefined, MedusaError } from "medusa-core-utils" -import { EventBusService, StrategyResolverService } from "./index" -import { Request } from "express" +import EventBusService from "./event-bus" +import { StrategyResolverService } from "./index" type InjectedDependencies = { manager: EntityManager diff --git a/packages/medusa/src/services/discount-condition.ts b/packages/medusa/src/services/discount-condition.ts index b6607f7e72..f94b072de7 100644 --- a/packages/medusa/src/services/discount-condition.ts +++ b/packages/medusa/src/services/discount-condition.ts @@ -1,6 +1,6 @@ import { isDefined, MedusaError } from "medusa-core-utils" import { EntityManager } from "typeorm" -import { EventBusService } from "." +import { TransactionBaseService } from "../interfaces" import { DiscountCondition, DiscountConditionCustomerGroup, @@ -13,8 +13,8 @@ import { import { DiscountConditionRepository } from "../repositories/discount-condition" import { FindConfig } from "../types/common" import { DiscountConditionInput } from "../types/discount" -import { TransactionBaseService } from "../interfaces" import { buildQuery, PostgresError } from "../utils" +import EventBusService from "./event-bus" type InjectedDependencies = { manager: EntityManager diff --git a/packages/medusa/src/services/discount.ts b/packages/medusa/src/services/discount.ts index 58a311b20a..7e69e07e61 100644 --- a/packages/medusa/src/services/discount.ts +++ b/packages/medusa/src/services/discount.ts @@ -9,7 +9,6 @@ import { In, } from "typeorm" import { - EventBusService, NewTotalsService, ProductService, RegionService, @@ -36,12 +35,13 @@ import { UpdateDiscountInput, UpdateDiscountRuleInput, } from "../types/discount" +import { CalculationContextData } from "../types/totals" import { buildQuery, setMetadata } from "../utils" import { isFuture, isPast } from "../utils/date-helpers" import { FlagRouter } from "../utils/flag-router" import CustomerService from "./customer" import DiscountConditionService from "./discount-condition" -import { CalculationContextData } from "../types/totals" +import EventBusService from "./event-bus" /** * Provides layer to manipulate discounts. diff --git a/packages/medusa/src/services/event-bus.ts b/packages/medusa/src/services/event-bus.ts index a32fc269a9..cf4a371336 100644 --- a/packages/medusa/src/services/event-bus.ts +++ b/packages/medusa/src/services/event-bus.ts @@ -1,150 +1,68 @@ -import Bull, { JobOptions } from "bull" -import Redis from "ioredis" -import { DeepPartial, EntityManager, In } from "typeorm" -import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity" -import { ulid } from "ulid" +import { EventBusTypes } from "@medusajs/types" +import { EventBusUtils } from "@medusajs/utils" +import { EntityManager } from "typeorm" +import { TransactionBaseService } from "../interfaces" import { StagedJob } from "../models" -import { StagedJobRepository } from "../repositories/staged-job" -import { ConfigModule, Logger } from "../types/global" +import { ConfigModule } from "../types/global" import { isString } from "../utils" import { sleep } from "../utils/sleep" -import JobSchedulerService, { CreateJobOptions } from "./job-scheduler" +import StagedJobService from "./staged-job" type InjectedDependencies = { - manager: EntityManager - logger: Logger - stagedJobRepository: typeof StagedJobRepository - jobSchedulerService: JobSchedulerService - redisClient: Redis.Redis - redisSubscriber: Redis.Redis -} - -type Subscriber = (data: T, eventName: string) => Promise - -type SubscriberContext = { - subscriberId: string -} - -type BullJob = { - update: (data: unknown) => void - attemptsMade: number - opts: EmitOptions - data: { - eventName: string - data: T - completedSubscriberIds: string[] | undefined - } -} - -type SubscriberDescriptor = { - id: string - subscriber: Subscriber -} - -export type EmitOptions = { - delay?: number - attempts: number - backoff?: { - type: "fixed" | "exponential" - delay: number - } -} & JobOptions - -export type EmitData = { - eventName: string - data: T - opts?: Record & EmitOptions + stagedJobService: StagedJobService + eventBusModuleService: EventBusUtils.AbstractEventBusModuleService } /** * Can keep track of multiple subscribers to different events and run the * subscribers when events happen. Events will run asynchronously. */ -export default class EventBusService { +export default class EventBusService + extends TransactionBaseService + implements EventBusTypes.IEventBusService +{ protected readonly config_: ConfigModule - protected readonly manager_: EntityManager - protected readonly logger_: Logger - protected readonly stagedJobRepository_: typeof StagedJobRepository - protected readonly jobSchedulerService_: JobSchedulerService - protected readonly eventToSubscribersMap_: Map< - string | symbol, - SubscriberDescriptor[] - > - protected readonly redisClient_: Redis.Redis - protected readonly redisSubscriber_: Redis.Redis - protected queue_: Bull + protected readonly stagedJobService_: StagedJobService + // eslint-disable-next-line max-len + protected readonly eventBusModuleService_: EventBusUtils.AbstractEventBusModuleService + protected shouldEnqueuerRun: boolean - protected transactionManager_: EntityManager | undefined protected enqueue_: Promise constructor( - { - manager, - logger, - stagedJobRepository, - redisClient, - redisSubscriber, - jobSchedulerService, - }: InjectedDependencies, - config: ConfigModule, - singleton = true + { stagedJobService, eventBusModuleService }: InjectedDependencies, + config, + isSingleton = true ) { + // eslint-disable-next-line prefer-rest-params + super(arguments[0]) + this.config_ = config - this.manager_ = manager - this.logger_ = logger - this.jobSchedulerService_ = jobSchedulerService - this.stagedJobRepository_ = stagedJobRepository + this.eventBusModuleService_ = eventBusModuleService + this.stagedJobService_ = stagedJobService - if (singleton) { - const opts = { - createClient: (type: string): Redis.Redis => { - switch (type) { - case "client": - return redisClient - case "subscriber": - return redisSubscriber - default: - if (config.projectConfig.redis_url) { - return new Redis(config.projectConfig.redis_url) - } - return redisClient - } - }, - } - - this.eventToSubscribersMap_ = new Map() - this.queue_ = new Bull(`${this.constructor.name}:queue`, opts) - this.redisClient_ = redisClient - this.redisSubscriber_ = redisSubscriber - // Register our worker to handle emit calls - this.queue_.process(this.worker_) - - if (process.env.NODE_ENV !== "test") { - this.startEnqueuer() - } + if (process.env.NODE_ENV !== "test" && isSingleton) { + this.startEnqueuer() } } - withTransaction(transactionManager): this | EventBusService { + withTransaction(transactionManager?: EntityManager): this { if (!transactionManager) { return this } - const cloned = new EventBusService( + const cloned = new (this.constructor as any)( { manager: transactionManager, - stagedJobRepository: this.stagedJobRepository_, - jobSchedulerService: this.jobSchedulerService_, - logger: this.logger_, - redisClient: this.redisClient_, - redisSubscriber: this.redisSubscriber_, + stagedJobService: this.stagedJobService_, + eventBusModuleService: this.eventBusModuleService_, }, this.config_, false ) + cloned.manager_ = transactionManager cloned.transactionManager_ = transactionManager - cloned.queue_ = this.queue_ return cloned } @@ -153,70 +71,36 @@ export default class EventBusService { * Adds a function to a list of event subscribers. * @param event - the event that the subscriber will listen for. * @param subscriber - the function to be called when a certain event - * @param context - context to use when attaching subscriber * happens. Subscribers must return a Promise. + * @param context - subscriber context * @return this */ subscribe( event: string | symbol, - subscriber: Subscriber, - context?: SubscriberContext + subscriber: EventBusTypes.Subscriber, + context?: EventBusTypes.SubscriberContext ): this { if (typeof subscriber !== "function") { throw new Error("Subscriber must be a function") } - /** - * If context is provided, we use the subscriberId from it - * otherwise we generate a random using a ulid - */ - const subscriberId = - context?.subscriberId ?? `${event.toString()}-${ulid()}` - - const newSubscriberDescriptor = { subscriber, id: subscriberId } - - const existingSubscribers = this.eventToSubscribersMap_.get(event) ?? [] - - const subscriberAlreadyExists = existingSubscribers.find( - (sub) => sub.id === subscriberId - ) - - if (subscriberAlreadyExists) { - throw Error(`Subscriber with id ${subscriberId} already exists`) - } - - this.eventToSubscribersMap_.set(event, [ - ...existingSubscribers, - newSubscriberDescriptor, - ]) - + this.eventBusModuleService_.subscribe(event, subscriber, context) return this } /** - * Adds a function to a list of event subscribers. - * @param event - the event that the subscriber will listen for. - * @param subscriber - the function to be called when a certain event - * happens. Subscribers must return a Promise. + * Removes function from the list of event subscribers. + * @param event - the event of the subcriber. + * @param subscriber - the function to be removed + * @param context - subscriber context * @return this */ - unsubscribe(event: string | symbol, subscriber: Subscriber): this { - if (typeof subscriber !== "function") { - throw new Error("Subscriber must be a function") - } - - const existingSubscribers = this.eventToSubscribersMap_.get(event) - - if (existingSubscribers?.length) { - const subIndex = existingSubscribers?.findIndex( - (sub) => sub.subscriber === subscriber - ) - - if (subIndex !== -1) { - this.eventToSubscribersMap_.get(event)?.splice(subIndex as number, 1) - } - } - + unsubscribe( + event: string | symbol, + subscriber: EventBusTypes.Subscriber, + context: EventBusTypes.SubscriberContext + ): this { + this.eventBusModuleService_.unsubscribe(event, subscriber, context) return this } @@ -225,7 +109,7 @@ export default class EventBusService { * @param data - The data to use to process the events * @return the jobs from our queue */ - async emit(data: EmitData[]): Promise + async emit(data: EventBusTypes.EmitData[]): Promise /** * Calls all subscribers when an event occurs. @@ -237,80 +121,54 @@ export default class EventBusService { async emit( eventName: string, data: T, - options?: Record & EmitOptions + options?: Record ): Promise async emit< T, - TInput extends string | EmitData[] = string, - TResult = TInput extends EmitData[] ? StagedJob[] : StagedJob + TInput extends string | EventBusTypes.EmitData[] = string, + TResult = TInput extends EventBusTypes.EmitData[] + ? StagedJob[] + : StagedJob >( eventNameOrData: TInput, data?: T, - options: Record & EmitOptions = {} + options: Record = {} ): Promise { - const globalEventOptions = this.config_?.projectConfig?.event_options ?? {} - + const manager = this.activeManager_ const isBulkEmit = !isString(eventNameOrData) const events = isBulkEmit ? eventNameOrData.map((event) => ({ - data: { eventName: event.eventName, data: event.data }, - opts: event.opts, + eventName: event.eventName, + data: event.data, + options: event.options, })) : [ { - data: { eventName: eventNameOrData, data }, - opts: options, + eventName: eventNameOrData, + data: data, + options: options, }, ] - // The order of precedence for job options is: - // 1. local options - // 2. global options - // 3. default options - const defaultOptions: EmitOptions = { - attempts: 1, // default - removeOnComplete: true, // default - ...globalEventOptions, // global - } - - for (const event of events) { - event.opts = { - ...defaultOptions, - ...(event.opts ?? {}), // local - } - } - /** - * If we are in an ongoing transaction, we store the jobs in the database - * instead of processing them immediately. We only want to process those - * events, if the transaction successfully commits. This is to avoid jobs - * being processed if the transaction fails. + * We store events in the database when in an ongoing transaction. * - * In case of a failing transaction, kobs stored in the database are removed + * If we are in a long-running transaction, the ACID properties of a + * transaction ensure, that events are kept invisible to the enqueuer + * until the trasaction has commited. + * + * This patterns also gives us at-least-once delivery of events, as events + * are only removed from the database, if they are successfully delivered. + * + * In case of a failing transaction, jobs stored in the database are removed * as part of the rollback. */ - if (this.transactionManager_) { - const stagedJobRepository = this.transactionManager_.withRepository( - this.stagedJobRepository_ - ) + const stagedJobs = await this.stagedJobService_ + .withTransaction(manager) + .create(events) - const jobsToCreate = events.map((event) => { - return stagedJobRepository.create({ - event_name: event.data.eventName, - data: event.data.data, - options: event.opts, - } as DeepPartial) as QueryDeepPartialEntity - }) - - const stagedJobs = await stagedJobRepository.insertBulk(jobsToCreate) - - return (!isBulkEmit ? stagedJobs[0] : stagedJobs) as unknown as TResult - } - - if (this.config_?.projectConfig?.redis_url) { - await this.queue_.addBulk(events) - } + return (!isBulkEmit ? stagedJobs[0] : stagedJobs) as unknown as TResult } startEnqueuer(): void { @@ -324,17 +182,14 @@ export default class EventBusService { } async enqueuer_(): Promise { - while (this.shouldEnqueuerRun) { - const listConfig = { - relations: [], - skip: 0, - take: 1000, - } + const listConfig = { + relations: [], + skip: 0, + take: 1000, + } - const stagedJobRepo = this.manager_.withRepository( - this.stagedJobRepository_ - ) - const jobs = await stagedJobRepo.find(listConfig) + while (this.shouldEnqueuerRun) { + const jobs = await this.stagedJobService_.list(listConfig) if (!jobs.length) { await sleep(3000) @@ -343,138 +198,17 @@ export default class EventBusService { const eventsData = jobs.map((job) => { return { - data: { eventName: job.event_name, data: job.data }, - opts: { jobId: job.id, ...job.options }, + eventName: job.event_name, + data: job.data, + options: { jobId: job.id, ...job.options }, } }) - await this.queue_.addBulk(eventsData).then(async () => { - return await stagedJobRepo.delete({ id: In(jobs.map((j) => j.id)) }) + await this.eventBusModuleService_.emit(eventsData).then(async () => { + return await this.stagedJobService_.delete(jobs.map((j) => j.id)) }) await sleep(3000) } } - - /** - * Handles incoming jobs. - * @param job The job object - * @return resolves to the results of the subscriber calls. - */ - worker_ = async (job: BullJob): Promise => { - const { eventName, data } = job.data - const eventSubscribers = this.eventToSubscribersMap_.get(eventName) || [] - const wildcardSubscribers = this.eventToSubscribersMap_.get("*") || [] - - const allSubscribers = eventSubscribers.concat(wildcardSubscribers) - - // Pull already completed subscribers from the job data - const completedSubscribers = job.data.completedSubscriberIds || [] - - // Filter out already completed subscribers from the all subscribers - const subscribersInCurrentAttempt = allSubscribers.filter( - (subscriber) => - subscriber.id && !completedSubscribers.includes(subscriber.id) - ) - - const isRetry = job.attemptsMade > 0 - const currentAttempt = job.attemptsMade + 1 - - const isFinalAttempt = job?.opts?.attempts === currentAttempt - - if (isRetry) { - if (isFinalAttempt) { - this.logger_.info(`Final retry attempt for ${eventName}`) - } - - this.logger_.info( - `Retrying ${eventName} which has ${eventSubscribers.length} subscribers (${subscribersInCurrentAttempt.length} of them failed)` - ) - } else { - this.logger_.info( - `Processing ${eventName} which has ${eventSubscribers.length} subscribers` - ) - } - - const completedSubscribersInCurrentAttempt: string[] = [] - - const subscribersResult = await Promise.all( - subscribersInCurrentAttempt.map(async ({ id, subscriber }) => { - return subscriber(data, eventName) - .then((data) => { - // For every subscriber that completes successfully, add their id to the list of completed subscribers - completedSubscribersInCurrentAttempt.push(id) - return data - }) - .catch((err) => { - this.logger_.warn( - `An error occurred while processing ${eventName}: ${err}` - ) - return err - }) - }) - ) - - // If the number of completed subscribers is different from the number of subcribers to process in current attempt, some of them failed - const didSubscribersFail = - completedSubscribersInCurrentAttempt.length !== - subscribersInCurrentAttempt.length - - const isRetriesConfigured = job?.opts?.attempts > 1 - - // Therefore, if retrying is configured, we try again - const shouldRetry = - didSubscribersFail && isRetriesConfigured && !isFinalAttempt - - if (shouldRetry) { - const updatedCompletedSubscribers = [ - ...completedSubscribers, - ...completedSubscribersInCurrentAttempt, - ] - - job.data.completedSubscriberIds = updatedCompletedSubscribers - - job.update(job.data) - - const errorMessage = `One or more subscribers of ${eventName} failed. Retrying...` - - this.logger_.warn(errorMessage) - - return Promise.reject(Error(errorMessage)) - } - - if (didSubscribersFail && !isFinalAttempt) { - // If retrying is not configured, we log a warning to allow server admins to recover manually - this.logger_.warn( - `One or more subscribers of ${eventName} failed. Retrying is not configured. Use 'attempts' option when emitting events.` - ) - } - - return Promise.resolve(subscribersResult) - } - - /** - * Registers a cron job. - * @deprecated All cron job logic has been refactored to the `JobSchedulerService`. This method will be removed in a future release. - * @param eventName - the name of the event - * @param data - the data to be sent with the event - * @param cron - the cron pattern - * @param handler - the handler to call on each cron job - * @return void - */ - async createCronJob( - eventName: string, - data: T, - cron: string, - handler: Subscriber, - options?: CreateJobOptions - ): Promise { - await this.jobSchedulerService_.create( - eventName, - data, - cron, - handler, - options ?? {} - ) - } } diff --git a/packages/medusa/src/services/gift-card.ts b/packages/medusa/src/services/gift-card.ts index 25094ccc82..0ba1146c3c 100644 --- a/packages/medusa/src/services/gift-card.ts +++ b/packages/medusa/src/services/gift-card.ts @@ -1,7 +1,6 @@ import { isDefined, MedusaError } from "medusa-core-utils" import randomize from "randomatic" import { EntityManager } from "typeorm" -import { EventBusService } from "." import { TransactionBaseService } from "../interfaces" import { GiftCard, Region } from "../models" import { GiftCardRepository } from "../repositories/gift-card" @@ -13,6 +12,7 @@ import { UpdateGiftCardInput, } from "../types/gift-card" import { buildQuery, setMetadata } from "../utils" +import EventBusService from "./event-bus" import RegionService from "./region" type InjectedDependencies = { diff --git a/packages/medusa/src/services/index.ts b/packages/medusa/src/services/index.ts index 192d151ca4..b9430eaf76 100644 --- a/packages/medusa/src/services/index.ts +++ b/packages/medusa/src/services/index.ts @@ -19,33 +19,34 @@ export { default as IdempotencyKeyService } from "./idempotency-key" export { default as LineItemService } from "./line-item" export { default as LineItemAdjustmentService } from "./line-item-adjustment" export { default as MiddlewareService } from "./middleware" +export { default as NewTotalsService } from "./new-totals" export { default as NoteService } from "./note" export { default as NotificationService } from "./notification" export { default as OauthService } from "./oauth" export { default as OrderService } from "./order" export { default as OrderEditService } from "./order-edit" export { default as OrderEditItemChangeService } from "./order-edit-item-change" +export { default as PaymentService } from "./payment" export { default as PaymentCollectionService } from "./payment-collection" export { default as PaymentProviderService } from "./payment-provider" -export { default as PaymentService } from "./payment" export { default as PriceListService } from "./price-list" export { default as PricingService } from "./pricing" export { default as ProductService } from "./product" export { default as ProductCategoryService } from "./product-category" export { default as ProductCollectionService } from "./product-collection" export { default as ProductTypeService } from "./product-type" -export { default as ProductVariantInventoryService } from "./product-variant-inventory" export { default as ProductVariantService } from "./product-variant" - +export { default as ProductVariantInventoryService } from "./product-variant-inventory" export { default as RegionService } from "./region" export { default as ReturnService } from "./return" export { default as ReturnReasonService } from "./return-reason" +export { default as SalesChannelService } from "./sales-channel" export { default as SalesChannelInventoryService } from "./sales-channel-inventory" export { default as SalesChannelLocationService } from "./sales-channel-location" -export { default as SalesChannelService } from "./sales-channel" export { default as SearchService } from "./search" export { default as ShippingOptionService } from "./shipping-option" export { default as ShippingProfileService } from "./shipping-profile" +export { default as StagedJobService } from "./staged-job" export { default as StoreService } from "./store" export { default as StrategyResolverService } from "./strategy-resolver" export { default as SwapService } from "./swap" @@ -54,5 +55,4 @@ export { default as TaxProviderService } from "./tax-provider" export { default as TaxRateService } from "./tax-rate" export { default as TokenService } from "./token" export { default as TotalsService } from "./totals" -export { default as NewTotalsService } from "./new-totals" export { default as UserService } from "./user" diff --git a/packages/medusa/src/services/invite.ts b/packages/medusa/src/services/invite.ts index 0e1d0a23df..b53d7964e8 100644 --- a/packages/medusa/src/services/invite.ts +++ b/packages/medusa/src/services/invite.ts @@ -1,15 +1,16 @@ import jwt, { JwtPayload } from "jsonwebtoken" import { MedusaError } from "medusa-core-utils" import { EntityManager } from "typeorm" -import { EventBusService, UserService } from "." +import { UserService } from "." import { User } from ".." +import { TransactionBaseService } from "../interfaces" import { UserRoles } from "../models/user" import { InviteRepository } from "../repositories/invite" import { UserRepository } from "../repositories/user" -import { ListInvite } from "../types/invites" import { ConfigModule } from "../types/global" -import { TransactionBaseService } from "../interfaces" +import { ListInvite } from "../types/invites" import { buildQuery } from "../utils" +import EventBusService from "./event-bus" // 7 days const DEFAULT_VALID_DURATION = 1000 * 60 * 60 * 24 * 7 diff --git a/packages/medusa/src/services/job-scheduler.ts b/packages/medusa/src/services/job-scheduler.ts index 4acfa01bdb..63c477eff1 100644 --- a/packages/medusa/src/services/job-scheduler.ts +++ b/packages/medusa/src/services/job-scheduler.ts @@ -1,11 +1,9 @@ -import Bull from "bull" +import { Job, Queue, Worker } from "bullmq" import Redis from "ioredis" import { ConfigModule, Logger } from "../types/global" type InjectedDependencies = { logger: Logger - redisClient: Redis.Redis - redisSubscriber: Redis.Redis } type ScheduledJobHandler = ( @@ -22,36 +20,34 @@ export default class JobSchedulerService { protected readonly logger_: Logger protected readonly handlers_: Map = new Map() - protected readonly queue_: Bull + protected readonly queue_: Queue constructor( - { logger, redisClient, redisSubscriber }: InjectedDependencies, + { logger }: InjectedDependencies, config: ConfigModule, singleton = true ) { this.config_ = config this.logger_ = logger - if (singleton) { - const opts = { - createClient: (type: string): Redis.Redis => { - switch (type) { - case "client": - return redisClient - case "subscriber": - return redisSubscriber - default: - if (config.projectConfig.redis_url) { - return new Redis(config.projectConfig.redis_url) - } - return redisClient - } - }, - } + if (singleton && config?.projectConfig?.redis_url) { + // Required config + // See: https://github.com/OptimalBits/bull/blob/develop/CHANGELOG.md#breaking-changes + const connection = new Redis(config.projectConfig.redis_url, { + maxRetriesPerRequest: null, + enableReadyCheck: false, + }) + + this.queue_ = new Queue(`scheduled-jobs:queue`, { + connection, + prefix: `${this.constructor.name}`, + }) - this.queue_ = new Bull(`scheduled-jobs:queue`, opts) // Register scheduled job worker - this.queue_.process(this.scheduledJobsWorker) + new Worker("scheduled-jobs:queue", this.scheduledJobsWorker, { + connection, + prefix: `${this.constructor.name}`, + }) } } @@ -112,7 +108,7 @@ export default class JobSchedulerService { schedule: string, handler: ScheduledJobHandler, options: CreateJobOptions - ): Promise { + ): Promise { this.logger_.info(`Registering ${eventName}`) this.registerHandler(eventName, handler) @@ -120,10 +116,10 @@ export default class JobSchedulerService { eventName, data, } - const repeatOpts = { repeat: { cron: schedule } } + const repeatOpts = { repeat: { pattern: schedule } } if (options?.keepExisting) { - return await this.queue_.add(jobToCreate, repeatOpts) + return await this.queue_.add(eventName, jobToCreate, repeatOpts) } const existingJobs = (await this.queue_.getRepeatableJobs()) ?? [] @@ -134,6 +130,6 @@ export default class JobSchedulerService { await this.queue_.removeRepeatableByKey(existingJob.key) } - return await this.queue_.add(jobToCreate, repeatOpts) + return await this.queue_.add(eventName, jobToCreate, repeatOpts) } } diff --git a/packages/medusa/src/services/note.ts b/packages/medusa/src/services/note.ts index 65f6412970..910c013ef7 100644 --- a/packages/medusa/src/services/note.ts +++ b/packages/medusa/src/services/note.ts @@ -1,12 +1,12 @@ import { isDefined, MedusaError } from "medusa-core-utils" import { EntityManager } from "typeorm" import { TransactionBaseService } from "../interfaces" -import { NoteRepository } from "../repositories/note" -import EventBusService from "./event-bus" -import { FindConfig, Selector } from "../types/common" import { Note } from "../models" -import { buildQuery } from "../utils" +import { NoteRepository } from "../repositories/note" +import { FindConfig, Selector } from "../types/common" import { CreateNoteInput } from "../types/note" +import { buildQuery } from "../utils" +import EventBusService from "./event-bus" type InjectedDependencies = { manager: EntityManager diff --git a/packages/medusa/src/services/order-edit-item-change.ts b/packages/medusa/src/services/order-edit-item-change.ts index 0e1542fa41..d6d3a65822 100644 --- a/packages/medusa/src/services/order-edit-item-change.ts +++ b/packages/medusa/src/services/order-edit-item-change.ts @@ -1,18 +1,19 @@ -import { TransactionBaseService } from "../interfaces" -import { OrderItemChangeRepository } from "../repositories/order-item-change" -import { EntityManager, In } from "typeorm" -import { EventBusService, LineItemService } from "./index" -import { FindConfig, Selector } from "../types/common" -import { OrderItemChange } from "../models" -import { buildQuery } from "../utils" +import { EventBusTypes } from "@medusajs/types" import { MedusaError } from "medusa-core-utils" -import TaxProviderService from "./tax-provider" +import { EntityManager, In } from "typeorm" +import { TransactionBaseService } from "../interfaces" +import { OrderItemChange } from "../models" +import { OrderItemChangeRepository } from "../repositories/order-item-change" +import { FindConfig, Selector } from "../types/common" import { CreateOrderEditItemChangeInput } from "../types/order-edit" +import { buildQuery } from "../utils" +import { LineItemService } from "./index" +import TaxProviderService from "./tax-provider" type InjectedDependencies = { manager: EntityManager orderItemChangeRepository: typeof OrderItemChangeRepository - eventBusService: EventBusService + eventBusService: EventBusTypes.IEventBusService lineItemService: LineItemService taxProviderService: TaxProviderService } @@ -25,7 +26,7 @@ export default class OrderEditItemChangeService extends TransactionBaseService { // eslint-disable-next-line max-len protected readonly orderItemChangeRepository_: typeof OrderItemChangeRepository - protected readonly eventBus_: EventBusService + protected readonly eventBus_: EventBusTypes.IEventBusService protected readonly lineItemService_: LineItemService protected readonly taxProviderService_: TaxProviderService diff --git a/packages/medusa/src/services/order-edit.ts b/packages/medusa/src/services/order-edit.ts index cabf262432..8f9d426fe3 100644 --- a/packages/medusa/src/services/order-edit.ts +++ b/packages/medusa/src/services/order-edit.ts @@ -22,8 +22,8 @@ import { CreateOrderEditInput, } from "../types/order-edit" import { buildQuery, isString } from "../utils" +import EventBusService from "./event-bus" import { - EventBusService, LineItemAdjustmentService, LineItemService, NewTotalsService, diff --git a/packages/medusa/src/services/order.ts b/packages/medusa/src/services/order.ts index 80fb8b5237..90c7f50416 100644 --- a/packages/medusa/src/services/order.ts +++ b/packages/medusa/src/services/order.ts @@ -8,7 +8,7 @@ import { Not, Raw, } from "typeorm" -import { TransactionBaseService } from "../interfaces" +import { IInventoryService, TransactionBaseService } from "../interfaces" import SalesChannelFeatureFlag from "../loaders/feature-flags/sales-channels" import { Address, @@ -50,7 +50,6 @@ import { CustomerService, DiscountService, DraftOrderService, - EventBusService, FulfillmentProviderService, FulfillmentService, GiftCardService, @@ -64,6 +63,7 @@ import { TaxProviderService, TotalsService } from "." +import EventBusService from "./event-bus" export const ORDER_CART_ALREADY_EXISTS_ERROR = "Order from cart already exists" @@ -86,6 +86,7 @@ type InjectedDependencies = { addressRepository: typeof AddressRepository giftCardService: GiftCardService draftOrderService: DraftOrderService + inventoryService: IInventoryService eventBusService: EventBusService featureFlagRouter: FlagRouter productVariantInventoryService: ProductVariantInventoryService @@ -128,6 +129,7 @@ class OrderService extends TransactionBaseService { protected readonly addressRepository_: typeof AddressRepository protected readonly giftCardService_: GiftCardService protected readonly draftOrderService_: DraftOrderService + protected readonly inventoryService_: IInventoryService protected readonly eventBus_: EventBusService protected readonly featureFlagRouter_: FlagRouter // eslint-disable-next-line max-len diff --git a/packages/medusa/src/services/payment-collection.ts b/packages/medusa/src/services/payment-collection.ts index b08e6919e5..8021c071c5 100644 --- a/packages/medusa/src/services/payment-collection.ts +++ b/packages/medusa/src/services/payment-collection.ts @@ -1,28 +1,25 @@ -import { DeepPartial, EntityManager } from "typeorm" import { isDefined, MedusaError } from "medusa-core-utils" +import { DeepPartial, EntityManager } from "typeorm" -import { FindConfig } from "../types/common" -import { buildQuery, isString, setMetadata } from "../utils" -import { PaymentCollectionRepository } from "../repositories/payment-collection" import { PaymentCollection, PaymentCollectionStatus, PaymentSession, PaymentSessionStatus, } from "../models" -import { TransactionBaseService } from "../interfaces" -import { - CustomerService, - EventBusService, - PaymentProviderService, -} from "./index" +import { PaymentCollectionRepository } from "../repositories/payment-collection" +import { FindConfig } from "../types/common" +import { buildQuery, isString, setMetadata } from "../utils" +import { CustomerService, PaymentProviderService } from "./index" +import { TransactionBaseService } from "../interfaces" +import { CreatePaymentInput, PaymentSessionInput } from "../types/payment" import { CreatePaymentCollectionInput, PaymentCollectionsSessionsBatchInput, PaymentCollectionsSessionsInput, } from "../types/payment-collection" -import { CreatePaymentInput, PaymentSessionInput } from "../types/payment" +import EventBusService from "./event-bus" type InjectedDependencies = { manager: EntityManager diff --git a/packages/medusa/src/services/payment.ts b/packages/medusa/src/services/payment.ts index 2889853bf0..0272e7fbe3 100644 --- a/packages/medusa/src/services/payment.ts +++ b/packages/medusa/src/services/payment.ts @@ -1,12 +1,13 @@ -import { PaymentRepository } from "./../repositories/payment" -import { EntityManager } from "typeorm" import { isDefined, MedusaError } from "medusa-core-utils" +import { EntityManager } from "typeorm" +import { PaymentRepository } from "./../repositories/payment" -import { Payment, Refund } from "../models" import { TransactionBaseService } from "../interfaces" -import { EventBusService, PaymentProviderService } from "./index" -import { buildQuery } from "../utils" +import { Payment, Refund } from "../models" import { FindConfig } from "../types/common" +import { buildQuery } from "../utils" +import EventBusService from "./event-bus" +import { PaymentProviderService } from "./index" type InjectedDependencies = { manager: EntityManager diff --git a/packages/medusa/src/services/product-category.ts b/packages/medusa/src/services/product-category.ts index a17234bc9c..2558c3d91d 100644 --- a/packages/medusa/src/services/product-category.ts +++ b/packages/medusa/src/services/product-category.ts @@ -1,23 +1,22 @@ import { isDefined, MedusaError } from "medusa-core-utils" -import { EntityManager, IsNull, MoreThanOrEqual, Between, Not } from "typeorm" +import { Between, EntityManager, MoreThanOrEqual, Not } from "typeorm" +import { EventBusService } from "." import { TransactionBaseService } from "../interfaces" import { ProductCategory } from "../models" import { ProductCategoryRepository } from "../repositories/product-category" import { FindConfig, QuerySelector, - TreeQuerySelector, Selector, + TreeQuerySelector, } from "../types/common" -import { buildQuery, nullableValue } from "../utils" -import { EventBusService } from "." import { CreateProductCategoryInput, - UpdateProductCategoryInput, ReorderConditions, tempReorderRank, + UpdateProductCategoryInput, } from "../types/product-category" -import { isNumber } from "lodash" +import { buildQuery, nullableValue } from "../utils" type InjectedDependencies = { manager: EntityManager diff --git a/packages/medusa/src/services/product.ts b/packages/medusa/src/services/product.ts index 87faaf8357..61fbf764cb 100644 --- a/packages/medusa/src/services/product.ts +++ b/packages/medusa/src/services/product.ts @@ -1,5 +1,3 @@ -import { FlagRouter } from "../utils/flag-router" - import { isDefined, MedusaError } from "medusa-core-utils" import { EntityManager } from "typeorm" import { ProductVariantService, SearchService } from "." @@ -31,6 +29,7 @@ import { UpdateProductInput, } from "../types/product" import { buildQuery, isString, setMetadata } from "../utils" +import { FlagRouter } from "../utils/flag-router" import EventBusService from "./event-bus" type InjectedDependencies = { diff --git a/packages/medusa/src/services/publishable-api-key.ts b/packages/medusa/src/services/publishable-api-key.ts index c48d1e1613..e8ffa007f1 100644 --- a/packages/medusa/src/services/publishable-api-key.ts +++ b/packages/medusa/src/services/publishable-api-key.ts @@ -1,17 +1,17 @@ -import { EntityManager, FindOptionsWhere, ILike } from "typeorm" import { isDefined, MedusaError } from "medusa-core-utils" +import { EntityManager, FindOptionsWhere, ILike } from "typeorm" -import { PublishableApiKeyRepository } from "../repositories/publishable-api-key" -import { FindConfig, Selector } from "../types/common" -import { PublishableApiKey, SalesChannel } from "../models" import { TransactionBaseService } from "../interfaces" -import EventBusService from "./event-bus" -import { buildQuery, isString } from "../utils" +import { PublishableApiKey, SalesChannel } from "../models" +import { PublishableApiKeyRepository } from "../repositories/publishable-api-key" +import { PublishableApiKeySalesChannelRepository } from "../repositories/publishable-api-key-sales-channel" +import { FindConfig, Selector } from "../types/common" import { CreatePublishableApiKeyInput, UpdatePublishableApiKeyInput, } from "../types/publishable-api-key" -import { PublishableApiKeySalesChannelRepository } from "../repositories/publishable-api-key-sales-channel" +import { buildQuery, isString } from "../utils" +import EventBusService from "./event-bus" type InjectedDependencies = { manager: EntityManager diff --git a/packages/medusa/src/services/sales-channel-inventory.ts b/packages/medusa/src/services/sales-channel-inventory.ts index 91f9526771..1f833bcfec 100644 --- a/packages/medusa/src/services/sales-channel-inventory.ts +++ b/packages/medusa/src/services/sales-channel-inventory.ts @@ -1,18 +1,20 @@ import { EntityManager } from "typeorm" -import { EventBusService, SalesChannelLocationService } from "./" import { IInventoryService, TransactionBaseService } from "../interfaces" +import { EventBusTypes } from "@medusajs/types" +import { SalesChannelLocationService } from "./" + type InjectedDependencies = { inventoryService: IInventoryService salesChannelLocationService: SalesChannelLocationService - eventBusService: EventBusService + eventBusService: EventBusTypes.IEventBusService manager: EntityManager } class SalesChannelInventoryService extends TransactionBaseService { protected readonly salesChannelLocationService_: SalesChannelLocationService - protected readonly eventBusService_: EventBusService + protected readonly eventBusService_: EventBusTypes.IEventBusService protected readonly inventoryService_: IInventoryService constructor({ diff --git a/packages/medusa/src/services/sales-channel-location.ts b/packages/medusa/src/services/sales-channel-location.ts index 756a269813..2302d72dcb 100644 --- a/packages/medusa/src/services/sales-channel-location.ts +++ b/packages/medusa/src/services/sales-channel-location.ts @@ -1,14 +1,16 @@ import { EntityManager, In } from "typeorm" import { IStockLocationService, TransactionBaseService } from "../interfaces" -import { EventBusService, SalesChannelService } from "./" +import { SalesChannelService } from "./" + +import { EventBusTypes } from "@medusajs/types" -import { SalesChannelLocation } from "../models/sales-channel-location" import { MedusaError } from "medusa-core-utils" +import { SalesChannelLocation } from "../models/sales-channel-location" type InjectedDependencies = { stockLocationService: IStockLocationService salesChannelService: SalesChannelService - eventBusService: EventBusService + eventBusService: EventBusTypes.IEventBusService manager: EntityManager } @@ -18,7 +20,7 @@ type InjectedDependencies = { class SalesChannelLocationService extends TransactionBaseService { protected readonly salesChannelService_: SalesChannelService - protected readonly eventBusService_: EventBusService + protected readonly eventBusService_: EventBusTypes.IEventBusService protected readonly stockLocationService_: IStockLocationService constructor({ diff --git a/packages/medusa/src/services/sales-channel.ts b/packages/medusa/src/services/sales-channel.ts index 6c15d8b66e..32282e7804 100644 --- a/packages/medusa/src/services/sales-channel.ts +++ b/packages/medusa/src/services/sales-channel.ts @@ -1,17 +1,17 @@ +import { FindConfig, QuerySelector, Selector } from "../types/common" import { CreateSalesChannelInput, UpdateSalesChannelInput, } from "../types/sales-channels" -import { FindConfig, QuerySelector, Selector } from "../types/common" -import { EntityManager } from "typeorm" -import EventBusService from "./event-bus" import { isDefined, MedusaError } from "medusa-core-utils" +import { EntityManager } from "typeorm" +import { TransactionBaseService } from "../interfaces" import { SalesChannel } from "../models" import { SalesChannelRepository } from "../repositories/sales-channel" -import StoreService from "./store" -import { TransactionBaseService } from "../interfaces" import { buildQuery } from "../utils" +import EventBusService from "./event-bus" +import StoreService from "./store" type InjectedDependencies = { salesChannelRepository: typeof SalesChannelRepository diff --git a/packages/medusa/src/services/staged-job.ts b/packages/medusa/src/services/staged-job.ts new file mode 100644 index 0000000000..5a4ae32adb --- /dev/null +++ b/packages/medusa/src/services/staged-job.ts @@ -0,0 +1,63 @@ +import { EventBusTypes } from "@medusajs/types" +import { DeepPartial, EntityManager, In } from "typeorm" +import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity" +import { TransactionBaseService } from "../interfaces" +import { StagedJob } from "../models" +import { StagedJobRepository } from "../repositories/staged-job" +import { FindConfig } from "../types/common" +import { isString } from "../utils" + +type StagedJobServiceProps = { + manager: EntityManager + stagedJobRepository: typeof StagedJobRepository +} + +/** + * Provides layer to manipulate users. + */ +class StagedJobService extends TransactionBaseService { + protected stagedJobRepository_: typeof StagedJobRepository + + constructor({ stagedJobRepository }: StagedJobServiceProps) { + // eslint-disable-next-line prefer-rest-params + super(arguments[0]) + + this.stagedJobRepository_ = stagedJobRepository + } + + async list(config: FindConfig) { + const stagedJobRepo = this.activeManager_.withRepository( + this.stagedJobRepository_ + ) + + return await stagedJobRepo.find(config) + } + + async delete(stagedJobIds: string | string[]): Promise { + const manager = this.activeManager_ + const stagedJobRepo = manager.withRepository(this.stagedJobRepository_) + const sjIds = isString(stagedJobIds) ? [stagedJobIds] : stagedJobIds + + await stagedJobRepo.delete({ id: In(sjIds) }) + } + + async create(data: EventBusTypes.EmitData[] | EventBusTypes.EmitData) { + return await this.atomicPhase_(async (manager) => { + const stagedJobRepo = manager.withRepository(this.stagedJobRepository_) + + const data_ = Array.isArray(data) ? data : [data] + + const stagedJobs = data_.map((job) => + stagedJobRepo.create({ + event_name: job.eventName, + data: job.data, + options: job.options, + } as DeepPartial) + ) as QueryDeepPartialEntity[] + + return await stagedJobRepo.insertBulk(stagedJobs) + }) + } +} + +export default StagedJobService diff --git a/packages/medusa/src/services/swap.ts b/packages/medusa/src/services/swap.ts index 4bcc1f490d..44b4d73dfc 100644 --- a/packages/medusa/src/services/swap.ts +++ b/packages/medusa/src/services/swap.ts @@ -23,11 +23,12 @@ import { SwapRepository } from "../repositories/swap" import { FindConfig, Selector, WithRequiredProperty } from "../types/common" import { CreateShipmentConfig } from "../types/fulfillment" import { OrdersReturnItem } from "../types/orders" -import CartService from "./cart" import { + CartService, CustomShippingOptionService, EventBusService, FulfillmentService, + LineItemAdjustmentService, LineItemService, OrderService, PaymentProviderService, @@ -36,7 +37,6 @@ import { ShippingOptionService, TotalsService, } from "./index" -import LineItemAdjustmentService from "./line-item-adjustment" type InjectedProps = { manager: EntityManager @@ -44,7 +44,6 @@ type InjectedProps = { swapRepository: typeof SwapRepository cartService: CartService - eventBus: EventBusService orderService: OrderService returnService: ReturnService totalsService: TotalsService diff --git a/packages/medusa/src/services/tax-provider.ts b/packages/medusa/src/services/tax-provider.ts index f335462781..00ec5d48f1 100644 --- a/packages/medusa/src/services/tax-provider.ts +++ b/packages/medusa/src/services/tax-provider.ts @@ -1,10 +1,14 @@ -import { MedusaError } from "medusa-core-utils" import { AwilixContainer } from "awilix" +import { MedusaError } from "medusa-core-utils" import { In } from "typeorm" -import { LineItemTaxLineRepository } from "../repositories/line-item-tax-line" -import { ShippingMethodTaxLineRepository } from "../repositories/shipping-method-tax-line" -import { TaxProviderRepository } from "../repositories/tax-provider" +import { + ICacheService, + ITaxService, + ItemTaxCalculationLine, + TaxCalculationContext, + TransactionBaseService, +} from "../interfaces" import { Cart, LineItem, @@ -14,19 +18,15 @@ import { ShippingMethodTaxLine, TaxProvider, } from "../models" +import { LineItemTaxLineRepository } from "../repositories/line-item-tax-line" +import { ShippingMethodTaxLineRepository } from "../repositories/shipping-method-tax-line" +import { TaxProviderRepository } from "../repositories/tax-provider" import { isCart } from "../types/cart" -import { - ICacheService, - ITaxService, - ItemTaxCalculationLine, - TaxCalculationContext, - TransactionBaseService, -} from "../interfaces" import { TaxLinesMaps, TaxServiceRate } from "../types/tax-service" +import EventBusService from "./event-bus" import TaxRateService from "./tax-rate" -import EventBusService from "./event-bus" type RegionDetails = { id: string diff --git a/packages/medusa/src/strategies/batch-jobs/order/export.ts b/packages/medusa/src/strategies/batch-jobs/order/export.ts index 516f7023b3..ee93ea2d36 100644 --- a/packages/medusa/src/strategies/batch-jobs/order/export.ts +++ b/packages/medusa/src/strategies/batch-jobs/order/export.ts @@ -6,16 +6,15 @@ import { orderExportPropertiesDescriptors, } from "." import { AdminPostBatchesReq } from "../../../api" -import { IFileService } from "../../../interfaces" -import { AbstractBatchJobStrategy } from "../../../interfaces" +import { AbstractBatchJobStrategy, IFileService } from "../../../interfaces" +import SalesChannelFeatureFlag from "../../../loaders/feature-flags/sales-channels" import { Order } from "../../../models" import { OrderService } from "../../../services" import BatchJobService from "../../../services/batch-job" import { BatchJobStatus } from "../../../types/batch-job" -import { prepareListQuery } from "../../../utils/get-query-config" -import { FlagRouter } from "../../../utils/flag-router" -import SalesChannelFeatureFlag from "../../../loaders/feature-flags/sales-channels" import { FindConfig } from "../../../types/common" +import { FlagRouter } from "../../../utils/flag-router" +import { prepareListQuery } from "../../../utils/get-query-config" type InjectedDependencies = { fileService: IFileService diff --git a/packages/medusa/src/subscribers/batch-job.ts b/packages/medusa/src/subscribers/batch-job.ts index 90f374474a..e61c41b202 100644 --- a/packages/medusa/src/subscribers/batch-job.ts +++ b/packages/medusa/src/subscribers/batch-job.ts @@ -1,7 +1,9 @@ -import BatchJobService from "../services/batch-job" -import EventBusService from "../services/event-bus" -import { StrategyResolverService } from "../services" import { EntityManager } from "typeorm" +import { + BatchJobService, + EventBusService, + StrategyResolverService, +} from "../services" type InjectedDependencies = { eventBusService: EventBusService @@ -27,9 +29,15 @@ class BatchJobSubscriber { this.strategyResolver_ = strategyResolverService this.manager_ = manager - this.eventBusService_ - .subscribe(BatchJobService.Events.CREATED, this.preProcessBatchJob) - .subscribe(BatchJobService.Events.CONFIRMED, this.processBatchJob) + this.eventBusService_.subscribe( + BatchJobService.Events.CREATED, + this.preProcessBatchJob + ) as EventBusService + + this.eventBusService_.subscribe( + BatchJobService.Events.CONFIRMED, + this.processBatchJob + ) as EventBusService } preProcessBatchJob = async (data): Promise => { diff --git a/packages/medusa/src/subscribers/cart.ts b/packages/medusa/src/subscribers/cart.ts index 89f828f88b..0fb8a12900 100644 --- a/packages/medusa/src/subscribers/cart.ts +++ b/packages/medusa/src/subscribers/cart.ts @@ -1,6 +1,5 @@ -import EventBusService from "../services/event-bus" -import { CartService } from "../services" import { EntityManager } from "typeorm" +import { CartService, EventBusService } from "../services" type InjectedDependencies = { eventBusService: EventBusService diff --git a/packages/medusa/src/subscribers/search-indexing.ts b/packages/medusa/src/subscribers/search-indexing.ts index 6047b92155..d16b97ece8 100644 --- a/packages/medusa/src/subscribers/search-indexing.ts +++ b/packages/medusa/src/subscribers/search-indexing.ts @@ -1,21 +1,21 @@ +import { EventBusTypes } from "@medusajs/types" import { indexTypes } from "medusa-core-utils" import { ISearchService } from "../interfaces" import ProductCategoryFeatureFlag from "../loaders/feature-flags/product-categories" import { SEARCH_INDEX_EVENT } from "../loaders/search-index" import { Product } from "../models" -import EventBusService from "../services/event-bus" import ProductService from "../services/product" import { FlagRouter } from "../utils/flag-router" type InjectedDependencies = { - eventBusService: EventBusService + eventBusService: EventBusTypes.IEventBusService searchService: ISearchService productService: ProductService featureFlagRouter: FlagRouter } class SearchIndexingSubscriber { - private readonly eventBusService_: EventBusService + private readonly eventBusService_: EventBusTypes.IEventBusService private readonly searchService_: ISearchService private readonly productService_: ProductService private readonly featureFlagRouter_: FlagRouter diff --git a/packages/medusa/src/types/global.ts b/packages/medusa/src/types/global.ts index 375536c8ec..9218ae2234 100644 --- a/packages/medusa/src/types/global.ts +++ b/packages/medusa/src/types/global.ts @@ -1,13 +1,8 @@ -import { - ExternalModuleDeclaration, - InternalModuleDeclaration, -} from "@medusajs/modules-sdk" +import { CommonTypes } from "@medusajs/types" import { Request } from "express" import { MedusaContainer as coreMedusaContainer } from "medusa-core-utils" -import { LoggerOptions } from "typeorm" import { Logger as _Logger } from "winston" import { Customer, User } from "../models" -import { EmitOptions } from "../services/event-bus" import { FindConfig, RequestQueryFields } from "./common" declare global { @@ -44,67 +39,4 @@ export type Logger = _Logger & { export type Constructor = new (...args: any[]) => T -type SessionOptions = { - name?: string - resave?: boolean - rolling?: boolean - saveUninitialized?: boolean - secret?: string - ttl?: number -} - -export type ConfigModule = { - projectConfig: { - redis_url?: string - - /** - * Global options passed to all `EventBusService.emit` in the core as well as your own emitters. The options are forwarded to Bull's `Queue.add` method. - * - * The global options can be overridden by passing options to `EventBusService.emit` directly. - * - * Note: This will be deprecated as we move to Event Bus module in 1.8 - * - * - * Example - * ```js - * { - * removeOnComplete: { age: 10 }, - * } - * ``` - * - * @see https://github.com/OptimalBits/bull/blob/develop/REFERENCE.md#queueadd - */ - event_options?: Record & EmitOptions - - session_options?: SessionOptions - - jwt_secret?: string - cookie_secret?: string - - database_url?: string - database_type: string - database_database?: string - database_schema?: string - database_logging: LoggerOptions - - database_extra?: Record & { - ssl: { rejectUnauthorized: false } - } - store_cors?: string - admin_cors?: string - } - featureFlags: Record - modules?: Record< - string, - | false - | string - | Partial - > - plugins: ( - | { - resolve: string - options: Record - } - | string - )[] -} +export type ConfigModule = CommonTypes.ConfigModule diff --git a/packages/medusa/src/utils/sleep.ts b/packages/medusa/src/utils/sleep.ts index f294976b85..a30eb4b386 100644 --- a/packages/medusa/src/utils/sleep.ts +++ b/packages/medusa/src/utils/sleep.ts @@ -1,5 +1,3 @@ -export async function sleep(ms: number) { - return new Promise((resolve) => { - setTimeout(resolve, ms) - }) -} +import { promisify } from "util" + +export const sleep = promisify(setTimeout) diff --git a/packages/modules-sdk/src/definitions.ts b/packages/modules-sdk/src/definitions.ts index 0a89d65107..5e0c41afa0 100644 --- a/packages/modules-sdk/src/definitions.ts +++ b/packages/modules-sdk/src/definitions.ts @@ -1,6 +1,18 @@ import { ModuleDefinition, MODULE_RESOURCE_TYPE, MODULE_SCOPE } from "./types" export const MODULE_DEFINITIONS: ModuleDefinition[] = [ + { + key: "eventBus", + registrationName: "eventBusModuleService", + defaultPackage: "@medusajs/event-bus-local", + label: "EventBusModuleService", + canOverride: true, + isRequired: true, + defaultModuleDeclaration: { + scope: MODULE_SCOPE.INTERNAL, + resources: MODULE_RESOURCE_TYPE.SHARED, + }, + }, { key: "stockLocationService", registrationName: "stockLocationService", diff --git a/packages/stock-location/src/initialize/index.ts b/packages/stock-location/src/initialize/index.ts index 64a72a451e..74a55ff955 100644 --- a/packages/stock-location/src/initialize/index.ts +++ b/packages/stock-location/src/initialize/index.ts @@ -1,15 +1,16 @@ -import { IEventBusService, IStockLocationService } from "@medusajs/medusa" +import { IStockLocationService } from "@medusajs/medusa" import { ExternalModuleDeclaration, InternalModuleDeclaration, - MedusaModule, + MedusaModule } from "@medusajs/modules-sdk" +import { EventBusTypes } from "@medusajs/types" import { StockLocationServiceInitializeOptions } from "../types" export const initialize = async ( options?: StockLocationServiceInitializeOptions | ExternalModuleDeclaration, injectedDependencies?: { - eventBusService: IEventBusService + eventBusService: EventBusTypes.IEventBusService } ): Promise => { const serviceKey = "stockLocationService" diff --git a/packages/stock-location/src/services/stock-location.ts b/packages/stock-location/src/services/stock-location.ts index 4732cf73fe..8f28e2da66 100644 --- a/packages/stock-location/src/services/stock-location.ts +++ b/packages/stock-location/src/services/stock-location.ts @@ -3,13 +3,12 @@ import { CreateStockLocationInput, FilterableStockLocationProps, FindConfig, - IEventBusService, setMetadata, StockLocationAddressInput, - UpdateStockLocationInput, + UpdateStockLocationInput } from "@medusajs/medusa" import { InternalModuleDeclaration } from "@medusajs/modules-sdk" -import { SharedContext } from "@medusajs/types" +import { EventBusTypes, SharedContext } from "@medusajs/types" import { InjectEntityManager, MedusaContext } from "@medusajs/utils" import { isDefined, MedusaError } from "medusa-core-utils" import { EntityManager } from "typeorm" @@ -17,7 +16,7 @@ import { StockLocation, StockLocationAddress } from "../models" type InjectedDependencies = { manager: EntityManager - eventBusService: IEventBusService + eventBusService: EventBusTypes.IEventBusService } /** @@ -32,7 +31,7 @@ export default class StockLocationService { } protected readonly manager_: EntityManager - protected readonly eventBusService_: IEventBusService + protected readonly eventBusService_: EventBusTypes.IEventBusService constructor( { eventBusService, manager }: InjectedDependencies, diff --git a/packages/types/package.json b/packages/types/package.json index 14e48670c3..de6a034185 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -16,6 +16,9 @@ ], "author": "Medusa", "license": "MIT", + "dependencies": { + "@medusajs/modules-sdk": "^0.0.1" + }, "devDependencies": { "cross-env": "^5.2.1", "typeorm": "^0.3.11", @@ -24,6 +27,7 @@ "scripts": { "prepare": "cross-env NODE_ENV=production yarn run build", "build": "tsc --build", + "watch": "tsc --build --watch", "test": "exit 0" } } diff --git a/packages/types/src/bundles.ts b/packages/types/src/bundles.ts new file mode 100644 index 0000000000..ea38e52654 --- /dev/null +++ b/packages/types/src/bundles.ts @@ -0,0 +1,4 @@ +export * as CommonTypes from "./common" +export * as EventBusTypes from "./event-bus" +export * as TransactionBaseTypes from "./transaction-base" + diff --git a/packages/types/src/common/config-module.ts b/packages/types/src/common/config-module.ts new file mode 100644 index 0000000000..f4e9f6eb01 --- /dev/null +++ b/packages/types/src/common/config-module.ts @@ -0,0 +1,51 @@ +import { + ExternalModuleDeclaration, + InternalModuleDeclaration +} from "@medusajs/modules-sdk" +import { LoggerOptions } from "typeorm" + +type SessionOptions = { + name?: string + resave?: boolean + rolling?: boolean + saveUninitialized?: boolean + secret?: string + ttl?: number +} + +export type ConfigModule = { + projectConfig: { + redis_url?: string + + session_options?: SessionOptions + + jwt_secret?: string + cookie_secret?: string + + database_url?: string + database_type: string + database_database?: string + database_schema?: string + database_logging: LoggerOptions + + database_extra?: Record & { + ssl: { rejectUnauthorized: false } + } + store_cors?: string + admin_cors?: string + } + featureFlags: Record + modules?: Record< + string, + | false + | string + | Partial + > + plugins: ( + | { + resolve: string + options: Record + } + | string + )[] +} diff --git a/packages/types/src/common/index.ts b/packages/types/src/common/index.ts new file mode 100644 index 0000000000..c21e29bd25 --- /dev/null +++ b/packages/types/src/common/index.ts @@ -0,0 +1 @@ +export * from "./config-module"; diff --git a/packages/types/src/event-bus/common.ts b/packages/types/src/event-bus/common.ts new file mode 100644 index 0000000000..0032328f18 --- /dev/null +++ b/packages/types/src/event-bus/common.ts @@ -0,0 +1,25 @@ +export type Subscriber = ( + data: T, + eventName: string + ) => Promise + + export type SubscriberContext = { + subscriberId: string + } + + export type SubscriberDescriptor = { + id: string + subscriber: Subscriber + } + + export type EventHandler = ( + data: T, + eventName: string + ) => Promise + + export type EmitData = { + eventName: string + data: T + options?: Record + } + \ No newline at end of file diff --git a/packages/types/src/event-bus/event-bus-module.ts b/packages/types/src/event-bus/event-bus-module.ts new file mode 100644 index 0000000000..c6a70a258f --- /dev/null +++ b/packages/types/src/event-bus/event-bus-module.ts @@ -0,0 +1,22 @@ +import { EmitData, Subscriber, SubscriberContext } from "./common" + +export interface IEventBusModuleService { + emit( + eventName: string, + data: T, + options: Record + ): Promise + emit(data: EmitData[]): Promise + + subscribe( + eventName: string | symbol, + subscriber: Subscriber, + context?: SubscriberContext + ): this + + unsubscribe( + eventName: string | symbol, + subscriber: Subscriber, + context?: SubscriberContext + ): this +} diff --git a/packages/types/src/event-bus/event-bus.ts b/packages/types/src/event-bus/event-bus.ts new file mode 100644 index 0000000000..3c1ac674fc --- /dev/null +++ b/packages/types/src/event-bus/event-bus.ts @@ -0,0 +1,17 @@ +import { Subscriber, SubscriberContext } from "." +import { ITransactionBaseService } from "../transaction-base/transaction-base" + +export interface IEventBusService extends ITransactionBaseService { + subscribe( + eventName: string | symbol, + subscriber: Subscriber, + context?: SubscriberContext + ): this + + unsubscribe( + eventName: string | symbol, + subscriber: Subscriber, + context?: SubscriberContext + ): this + emit(event: string, data: T, options?: unknown): Promise +} \ No newline at end of file diff --git a/packages/types/src/event-bus/index.ts b/packages/types/src/event-bus/index.ts new file mode 100644 index 0000000000..1dc5f033d2 --- /dev/null +++ b/packages/types/src/event-bus/index.ts @@ -0,0 +1,4 @@ +export * from "./common" +export * from "./event-bus" +export * from "./event-bus-module" + diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index c901199825..07956c6722 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -1 +1,5 @@ +export * from "./bundles" +export * from "./common" +export * from "./event-bus" export * from "./shared-context" +export * from "./transaction-base" diff --git a/packages/types/src/transaction-base/index.ts b/packages/types/src/transaction-base/index.ts new file mode 100644 index 0000000000..0017eb8241 --- /dev/null +++ b/packages/types/src/transaction-base/index.ts @@ -0,0 +1,2 @@ +export * from "./transaction-base"; + diff --git a/packages/medusa/src/interfaces/services/event-bus.ts b/packages/types/src/transaction-base/transaction-base.ts similarity index 55% rename from packages/medusa/src/interfaces/services/event-bus.ts rename to packages/types/src/transaction-base/transaction-base.ts index 480d91a003..86c6aaea66 100644 --- a/packages/medusa/src/interfaces/services/event-bus.ts +++ b/packages/types/src/transaction-base/transaction-base.ts @@ -1,6 +1,5 @@ import { EntityManager } from "typeorm" -export interface IEventBusService { - emit(event: string, data: any): Promise +export interface ITransactionBaseService { withTransaction(transactionManager?: EntityManager): this } diff --git a/packages/utils/package.json b/packages/utils/package.json index 2542314b5d..1497e65c0a 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -30,6 +30,7 @@ "scripts": { "prepare": "cross-env NODE_ENV=production yarn run build", "build": "tsc --build", + "watch": "tsc --build --watch", "test": "jest --passWithNoTests src" } } diff --git a/packages/utils/src/bundles.ts b/packages/utils/src/bundles.ts new file mode 100644 index 0000000000..fd0b93c08a --- /dev/null +++ b/packages/utils/src/bundles.ts @@ -0,0 +1,3 @@ +export * as DecoratorUtils from "./decorators"; +export * as EventBusUtils from "./event-bus"; + diff --git a/packages/utils/src/event-bus/index.ts b/packages/utils/src/event-bus/index.ts new file mode 100644 index 0000000000..c9920b55ee --- /dev/null +++ b/packages/utils/src/event-bus/index.ts @@ -0,0 +1,89 @@ +import { EventBusTypes } from "@medusajs/types" +import { ulid } from "ulid" + +export abstract class AbstractEventBusModuleService + implements EventBusTypes.IEventBusModuleService +{ + protected eventToSubscribersMap_: Map< + string | symbol, + EventBusTypes.SubscriberDescriptor[] + > = new Map() + + public get eventToSubscribersMap(): Map< + string | symbol, + EventBusTypes.SubscriberDescriptor[] + > { + return this.eventToSubscribersMap_ + } + + abstract emit( + eventName: string, + data: T, + options: Record + ): Promise + abstract emit(data: EventBusTypes.EmitData[]): Promise + + public subscribe( + eventName: string | symbol, + subscriber: EventBusTypes.Subscriber, + context?: EventBusTypes.SubscriberContext + ): this { + if (typeof subscriber !== `function`) { + throw new Error("Subscriber must be a function") + } + /** + * If context is provided, we use the subscriberId from it + * otherwise we generate a random using a ulid + */ + + const randId = ulid() + const event = eventName.toString() + + const subscriberId = context?.subscriberId ?? `${event}-${randId}` + + const newSubscriberDescriptor = { subscriber, id: subscriberId } + + const existingSubscribers = this.eventToSubscribersMap_.get(event) ?? [] + + const subscriberAlreadyExists = existingSubscribers.find( + (sub) => sub.id === subscriberId + ) + + if (subscriberAlreadyExists) { + throw Error(`Subscriber with id ${subscriberId} already exists`) + } + + this.eventToSubscribersMap_.set(event, [ + ...existingSubscribers, + newSubscriberDescriptor, + ]) + + return this + } + + unsubscribe( + eventName: string | symbol, + subscriber: EventBusTypes.Subscriber, + context: EventBusTypes.SubscriberContext + ): this { + if (typeof subscriber !== `function`) { + throw new Error("Subscriber must be a function") + } + + const existingSubscribers = this.eventToSubscribersMap_.get(eventName) + + if (existingSubscribers?.length) { + const subIndex = existingSubscribers?.findIndex( + (sub) => sub.id === context?.subscriberId + ) + + if (subIndex !== -1) { + this.eventToSubscribersMap_ + .get(eventName) + ?.splice(subIndex as number, 1) + } + } + + return this + } +} diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index e117d66ea0..0be17901c2 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1 +1,4 @@ +export * from "./bundles" export * from "./decorators" +export * from "./event-bus" + diff --git a/yarn.lock b/yarn.lock index c103530e70..77b975e9dd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4053,6 +4053,13 @@ __metadata: languageName: node linkType: hard +"@ioredis/commands@npm:^1.1.1": + version: 1.2.0 + resolution: "@ioredis/commands@npm:1.2.0" + checksum: a5d3c29dd84d8a28b7c67a441ac1715cbd7337a7b88649c0f17c345d89aa218578d2b360760017c48149ef8a70f44b051af9ac0921a0622c2b479614c4f65b36 + languageName: node + linkType: hard + "@istanbuljs/load-nyc-config@npm:^1.0.0": version: 1.1.0 resolution: "@istanbuljs/load-nyc-config@npm:1.1.0" @@ -5739,6 +5746,37 @@ __metadata: languageName: unknown linkType: soft +"@medusajs/event-bus-local@*, @medusajs/event-bus-local@workspace:packages/event-bus-local": + version: 0.0.0-use.local + resolution: "@medusajs/event-bus-local@workspace:packages/event-bus-local" + dependencies: + "@medusajs/modules-sdk": "*" + "@medusajs/types": ^0.0.1 + "@medusajs/utils": ^0.0.1 + cross-env: ^5.2.1 + jest: ^25.5.2 + ts-jest: ^25.5.1 + typescript: ^4.4.4 + languageName: unknown + linkType: soft + +"@medusajs/event-bus-redis@workspace:packages/event-bus-redis": + version: 0.0.0-use.local + resolution: "@medusajs/event-bus-redis@workspace:packages/event-bus-redis" + dependencies: + "@medusajs/modules-sdk": "*" + "@medusajs/types": ^0.0.1 + "@medusajs/utils": ^0.0.1 + bullmq: ^3.5.6 + cross-env: ^5.2.1 + ioredis: ^5.2.5 + jest: ^25.5.2 + medusa-test-utils: ^1.1.39 + ts-jest: ^25.5.1 + typescript: ^4.4.4 + languageName: unknown + linkType: soft + "@medusajs/inventory@workspace:packages/inventory": version: 0.0.0-use.local resolution: "@medusajs/inventory@workspace:packages/inventory" @@ -5855,6 +5893,8 @@ __metadata: "@babel/preset-typescript": ^7.13.0 "@medusajs/medusa-cli": ^1.3.8 "@medusajs/modules-sdk": "*" + "@medusajs/types": "*" + "@medusajs/utils": "*" "@types/express": ^4.17.17 "@types/ioredis": ^4.28.10 "@types/jest": ^27.5.2 @@ -5864,7 +5904,7 @@ __metadata: awilix: ^8.0.0 babel-preset-medusa-package: ^1.1.19 body-parser: ^1.19.0 - bull: ^3.12.1 + bullmq: ^3.5.6 chokidar: ^3.4.2 class-transformer: ^0.5.1 class-validator: ^0.13.2 @@ -5878,7 +5918,7 @@ __metadata: express-session: ^1.17.3 fs-exists-cached: ^1.0.0 glob: ^7.1.6 - ioredis: ^4.17.3 + ioredis: ^5.2.5 ioredis-mock: ^5.6.0 iso8601-duration: ^1.3.0 jest: ^25.5.4 @@ -5918,7 +5958,7 @@ __metadata: languageName: unknown linkType: soft -"@medusajs/modules-sdk@*, @medusajs/modules-sdk@workspace:packages/modules-sdk": +"@medusajs/modules-sdk@*, @medusajs/modules-sdk@^0.0.1, @medusajs/modules-sdk@workspace:packages/modules-sdk": version: 0.0.0-use.local resolution: "@medusajs/modules-sdk@workspace:packages/modules-sdk" dependencies: @@ -6003,17 +6043,18 @@ __metadata: languageName: unknown linkType: soft -"@medusajs/types@*, @medusajs/types@workspace:packages/types": +"@medusajs/types@*, @medusajs/types@^0.0.1, @medusajs/types@workspace:packages/types": version: 0.0.0-use.local resolution: "@medusajs/types@workspace:packages/types" dependencies: + "@medusajs/modules-sdk": ^0.0.1 cross-env: ^5.2.1 typeorm: ^0.3.11 typescript: ^4.4.4 languageName: unknown linkType: soft -"@medusajs/utils@^0.0.1, @medusajs/utils@workspace:packages/utils": +"@medusajs/utils@*, @medusajs/utils@^0.0.1, @medusajs/utils@workspace:packages/utils": version: 0.0.0-use.local resolution: "@medusajs/utils@workspace:packages/utils" dependencies: @@ -6063,6 +6104,13 @@ __metadata: languageName: node linkType: hard +"@msgpackr-extract/msgpackr-extract-darwin-arm64@npm:2.2.0": + version: 2.2.0 + resolution: "@msgpackr-extract/msgpackr-extract-darwin-arm64@npm:2.2.0" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@msgpackr-extract/msgpackr-extract-darwin-x64@npm:2.0.2": version: 2.0.2 resolution: "@msgpackr-extract/msgpackr-extract-darwin-x64@npm:2.0.2" @@ -6070,6 +6118,13 @@ __metadata: languageName: node linkType: hard +"@msgpackr-extract/msgpackr-extract-darwin-x64@npm:2.2.0": + version: 2.2.0 + resolution: "@msgpackr-extract/msgpackr-extract-darwin-x64@npm:2.2.0" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@msgpackr-extract/msgpackr-extract-linux-arm64@npm:2.0.2": version: 2.0.2 resolution: "@msgpackr-extract/msgpackr-extract-linux-arm64@npm:2.0.2" @@ -6077,6 +6132,13 @@ __metadata: languageName: node linkType: hard +"@msgpackr-extract/msgpackr-extract-linux-arm64@npm:2.2.0": + version: 2.2.0 + resolution: "@msgpackr-extract/msgpackr-extract-linux-arm64@npm:2.2.0" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + "@msgpackr-extract/msgpackr-extract-linux-arm@npm:2.0.2": version: 2.0.2 resolution: "@msgpackr-extract/msgpackr-extract-linux-arm@npm:2.0.2" @@ -6084,6 +6146,13 @@ __metadata: languageName: node linkType: hard +"@msgpackr-extract/msgpackr-extract-linux-arm@npm:2.2.0": + version: 2.2.0 + resolution: "@msgpackr-extract/msgpackr-extract-linux-arm@npm:2.2.0" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + "@msgpackr-extract/msgpackr-extract-linux-x64@npm:2.0.2": version: 2.0.2 resolution: "@msgpackr-extract/msgpackr-extract-linux-x64@npm:2.0.2" @@ -6091,6 +6160,13 @@ __metadata: languageName: node linkType: hard +"@msgpackr-extract/msgpackr-extract-linux-x64@npm:2.2.0": + version: 2.2.0 + resolution: "@msgpackr-extract/msgpackr-extract-linux-x64@npm:2.2.0" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + "@msgpackr-extract/msgpackr-extract-win32-x64@npm:2.0.2": version: 2.0.2 resolution: "@msgpackr-extract/msgpackr-extract-win32-x64@npm:2.0.2" @@ -6098,6 +6174,13 @@ __metadata: languageName: node linkType: hard +"@msgpackr-extract/msgpackr-extract-win32-x64@npm:2.2.0": + version: 2.2.0 + resolution: "@msgpackr-extract/msgpackr-extract-win32-x64@npm:2.2.0" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@mswjs/cookies@npm:^0.1.6": version: 0.1.7 resolution: "@mswjs/cookies@npm:0.1.7" @@ -14923,21 +15006,19 @@ __metadata: languageName: node linkType: hard -"bull@npm:^3.12.1": - version: 3.29.3 - resolution: "bull@npm:3.29.3" +"bullmq@npm:^3.5.6": + version: 3.5.6 + resolution: "bullmq@npm:3.5.6" dependencies: - cron-parser: ^2.13.0 - debuglog: ^1.0.0 - get-port: ^5.1.1 - ioredis: ^4.27.0 + cron-parser: ^4.6.0 + glob: ^8.0.3 + ioredis: ^5.2.2 lodash: ^4.17.21 - p-timeout: ^3.2.0 - promise.prototype.finally: ^3.1.2 - semver: ^7.3.2 - util.promisify: ^1.0.1 - uuid: ^8.3.0 - checksum: 6860a17a2abc45bfa887c8fed0f666da2291d382dea3b5be581b82ba992e6edc487d55b5cf2e668f29e7123930958d2bdefae5a2bb003414767d4f209ebc2b4f + msgpackr: ^1.6.2 + semver: ^7.3.7 + tslib: ^2.0.0 + uuid: ^9.0.0 + checksum: a65af3fe64cfc21803e5379bfb1ac01411a1a7c3e5329369373ceff67d4ef90bc8efba9245d00384599d8f55a86ef2d6a8cc6c5447210ae3a3598bb6d4a851e2 languageName: node linkType: hard @@ -16734,17 +16815,7 @@ __metadata: languageName: node linkType: hard -"cron-parser@npm:^2.13.0": - version: 2.18.0 - resolution: "cron-parser@npm:2.18.0" - dependencies: - is-nan: ^1.3.0 - moment-timezone: ^0.5.31 - checksum: 1361cf0af8a749a355c7dd45ec85d94f8b1b768e6ce19955fea38d72b22874c0c63add070a1fe543a4ee4fa5229b6ae43760934df0d4aed6499581108657df25 - languageName: node - linkType: hard - -"cron-parser@npm:^4.2.0": +"cron-parser@npm:^4.2.0, cron-parser@npm:^4.6.0": version: 4.7.1 resolution: "cron-parser@npm:4.7.1" dependencies: @@ -17320,13 +17391,6 @@ __metadata: languageName: node linkType: hard -"debuglog@npm:^1.0.0": - version: 1.0.1 - resolution: "debuglog@npm:1.0.1" - checksum: d98ac9abe6a528fcbb4d843b1caf5a9116998c76e1263d8ff4db2c086aa96fa7ea4c752a81050fa2e4304129ef330e6e4dc9dd4d47141afd7db80bf699f08219 - languageName: node - linkType: hard - "decamelize-keys@npm:^1.1.0": version: 1.1.0 resolution: "decamelize-keys@npm:1.1.0" @@ -17569,6 +17633,13 @@ __metadata: languageName: node linkType: hard +"denque@npm:^2.0.1": + version: 2.1.0 + resolution: "denque@npm:2.1.0" + checksum: f9ef81aa0af9c6c614a727cb3bd13c5d7db2af1abf9e6352045b86e85873e629690f6222f4edd49d10e4ccf8f078bbeec0794fafaf61b659c0589d0c511ec363 + languageName: node + linkType: hard + "depd@npm:2.0.0, depd@npm:~2.0.0": version: 2.0.0 resolution: "depd@npm:2.0.0" @@ -20690,15 +20761,6 @@ __metadata: languageName: node linkType: hard -"for-each@npm:^0.3.3": - version: 0.3.3 - resolution: "for-each@npm:0.3.3" - dependencies: - is-callable: ^1.1.3 - checksum: 22330d8a2db728dbf003ec9182c2d421fbcd2969b02b4f97ec288721cda63eb28f2c08585ddccd0f77cb2930af8d958005c9e72f47141dc51816127a118f39aa - languageName: node - linkType: hard - "for-in@npm:^1.0.2": version: 1.0.2 resolution: "for-in@npm:1.0.2" @@ -23298,6 +23360,7 @@ __metadata: "@babel/core": ^7.12.10 "@babel/node": ^7.12.10 "@medusajs/cache-inmemory": "*" + "@medusajs/event-bus-local": "*" "@medusajs/medusa": "*" babel-preset-medusa-package: "*" faker: ^5.5.3 @@ -23316,6 +23379,7 @@ __metadata: "@babel/core": ^7.12.10 "@babel/node": ^7.12.10 "@medusajs/cache-inmemory": "*" + "@medusajs/event-bus-local": "*" "@medusajs/medusa": "*" babel-preset-medusa-package: "*" faker: ^5.5.3 @@ -23386,7 +23450,7 @@ __metadata: languageName: node linkType: hard -"ioredis@npm:^4.17.3, ioredis@npm:^4.27.0, ioredis@npm:^4.27.9": +"ioredis@npm:^4.27.9": version: 4.28.5 resolution: "ioredis@npm:4.28.5" dependencies: @@ -23405,6 +23469,23 @@ __metadata: languageName: node linkType: hard +"ioredis@npm:^5.2.2, ioredis@npm:^5.2.5": + version: 5.2.5 + resolution: "ioredis@npm:5.2.5" + dependencies: + "@ioredis/commands": ^1.1.1 + cluster-key-slot: ^1.1.0 + debug: ^4.3.4 + denque: ^2.0.1 + lodash.defaults: ^4.2.0 + lodash.isarguments: ^3.1.0 + redis-errors: ^1.2.0 + redis-parser: ^3.0.0 + standard-as-callback: ^2.1.0 + checksum: fe41af91cc5e3bf17d296ba32b521f6b1dae80854c3b70953ff2858751b106abebe8a8b6c1cb7a00d5534942a929ce835ef2ba78ac3db06ef5d6a336dd872268 + languageName: node + linkType: hard + "ip-regex@npm:^2.1.0": version: 2.1.0 resolution: "ip-regex@npm:2.1.0" @@ -23569,7 +23650,7 @@ __metadata: languageName: node linkType: hard -"is-callable@npm:^1.1.3, is-callable@npm:^1.1.4, is-callable@npm:^1.2.4": +"is-callable@npm:^1.1.4, is-callable@npm:^1.2.4": version: 1.2.4 resolution: "is-callable@npm:1.2.4" checksum: bda3c67128741129d61e1cb7ca89025ca56b39bf3564657989567c9f6d1e20d6f5579750d3c1fa8887903c6dc669fbc695e33a1363e7c5ec944077e39d24f73d @@ -23860,16 +23941,6 @@ __metadata: languageName: node linkType: hard -"is-nan@npm:^1.3.0": - version: 1.3.2 - resolution: "is-nan@npm:1.3.2" - dependencies: - call-bind: ^1.0.0 - define-properties: ^1.1.3 - checksum: 8bfb286f85763f9c2e28ea32e9127702fe980ffd15fa5d63ade3be7786559e6e21355d3625dd364c769c033c5aedf0a2ed3d4025d336abf1b9241e3d9eddc5b0 - languageName: node - linkType: hard - "is-negative-zero@npm:^2.0.2": version: 2.0.2 resolution: "is-negative-zero@npm:2.0.2" @@ -26248,7 +26319,7 @@ __metadata: languageName: node linkType: hard -"jest@npm:^25.5.4": +"jest@npm:^25.5.2, jest@npm:^25.5.4": version: 25.5.4 resolution: "jest@npm:25.5.4" dependencies: @@ -29711,16 +29782,7 @@ __metadata: languageName: node linkType: hard -"moment-timezone@npm:^0.5.31": - version: 0.5.34 - resolution: "moment-timezone@npm:0.5.34" - dependencies: - moment: ">= 2.9.0" - checksum: 26b2dc80648004ee29fdcf819d4aa153fadf71eb061a90386cf738214e388009ec189b9da39e7320ff81dd0d213c0b1958c15d69cdb660982246daf7d225a1d5 - languageName: node - linkType: hard - -"moment@npm:>= 2.9.0, moment@npm:^2.19.3, moment@npm:^2.27.0, moment@npm:^2.29.1": +"moment@npm:^2.19.3, moment@npm:^2.27.0, moment@npm:^2.29.1": version: 2.29.4 resolution: "moment@npm:2.29.4" checksum: 844c6f3ce42862ac9467c8ca4f5e48a00750078682cc5bda1bc0e50cc7ca88e2115a0f932d65a06e4a90e26cb78892be9b3ca3dd6546ca2c4d994cebb787fc2b @@ -29818,6 +29880,37 @@ __metadata: languageName: node linkType: hard +"msgpackr-extract@npm:^2.2.0": + version: 2.2.0 + resolution: "msgpackr-extract@npm:2.2.0" + dependencies: + "@msgpackr-extract/msgpackr-extract-darwin-arm64": 2.2.0 + "@msgpackr-extract/msgpackr-extract-darwin-x64": 2.2.0 + "@msgpackr-extract/msgpackr-extract-linux-arm": 2.2.0 + "@msgpackr-extract/msgpackr-extract-linux-arm64": 2.2.0 + "@msgpackr-extract/msgpackr-extract-linux-x64": 2.2.0 + "@msgpackr-extract/msgpackr-extract-win32-x64": 2.2.0 + node-gyp: latest + node-gyp-build-optional-packages: 5.0.3 + dependenciesMeta: + "@msgpackr-extract/msgpackr-extract-darwin-arm64": + optional: true + "@msgpackr-extract/msgpackr-extract-darwin-x64": + optional: true + "@msgpackr-extract/msgpackr-extract-linux-arm": + optional: true + "@msgpackr-extract/msgpackr-extract-linux-arm64": + optional: true + "@msgpackr-extract/msgpackr-extract-linux-x64": + optional: true + "@msgpackr-extract/msgpackr-extract-win32-x64": + optional: true + bin: + download-msgpackr-prebuilds: bin/download-prebuilds.js + checksum: 9882805b27a55855cf4420d271f5e256979d07421d75e0db434125d2ccd2f0f6a6bf602d444ea8b49b0b2ff42a13c77424f92a2b5d48adfbd3fda7723e17aeb8 + languageName: node + linkType: hard + "msgpackr@npm:^1.5.4": version: 1.6.1 resolution: "msgpackr@npm:1.6.1" @@ -29830,6 +29923,18 @@ __metadata: languageName: node linkType: hard +"msgpackr@npm:^1.6.2": + version: 1.8.1 + resolution: "msgpackr@npm:1.8.1" + dependencies: + msgpackr-extract: ^2.2.0 + dependenciesMeta: + msgpackr-extract: + optional: true + checksum: 64cd58a51ff417f6332b46f941bd612aab53a1982e31b17be628f44e04807a40bc59b24662f7d40f95d0646d02de7f64a85f96052ac4f778aef8ed9c7bf2d601 + languageName: node + linkType: hard + "msw-storybook-addon@npm:^1.5.0": version: 1.6.3 resolution: "msw-storybook-addon@npm:1.6.3" @@ -30791,7 +30896,7 @@ __metadata: languageName: node linkType: hard -"object.getownpropertydescriptors@npm:^2.0.3, object.getownpropertydescriptors@npm:^2.1.1, object.getownpropertydescriptors@npm:^2.1.2": +"object.getownpropertydescriptors@npm:^2.0.3, object.getownpropertydescriptors@npm:^2.1.2": version: 2.1.4 resolution: "object.getownpropertydescriptors@npm:2.1.4" dependencies: @@ -33046,7 +33151,7 @@ __metadata: languageName: node linkType: hard -"promise.prototype.finally@npm:^3.1.0, promise.prototype.finally@npm:^3.1.2": +"promise.prototype.finally@npm:^3.1.0": version: 3.1.3 resolution: "promise.prototype.finally@npm:3.1.3" dependencies: @@ -39612,19 +39717,6 @@ __metadata: languageName: node linkType: hard -"util.promisify@npm:^1.0.1": - version: 1.1.1 - resolution: "util.promisify@npm:1.1.1" - dependencies: - call-bind: ^1.0.0 - define-properties: ^1.1.3 - for-each: ^0.3.3 - has-symbols: ^1.0.1 - object.getownpropertydescriptors: ^2.1.1 - checksum: aacccbf770c667430ca3b7fce9a2a04a80fcd1f9f4de5507ea54cc3bbbcdcd33cbd2501ac23d1c477c5c40817234f6068b89cf7792f0610fe6e7df7ac0fe83ce - languageName: node - linkType: hard - "util@npm:0.10.3": version: 0.10.3 resolution: "util@npm:0.10.3"