feat(medusa,event-bus-local,event-bus-redis): Event Bus modules (#2599)

This commit is contained in:
Oliver Windall Juhl
2023-03-22 10:26:21 +01:00
committed by GitHub
parent 7408111d11
commit ef5ef9f5a2
114 changed files with 2423 additions and 1669 deletions

View File

@@ -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)

View File

@@ -10,6 +10,8 @@ packages/*
!packages/admin-ui
!packages/admin
!packages/medusa-payment-stripe
!packages/event-bus-redis
!packages/event-bus-local

View File

@@ -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",
],
},

View File

@@ -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<String>,
"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<String>,
},
Object {
"canceled_at": null,
"completed_at": null,
"confirmed_at": null,
"context": Object {},
"created_at": Any<String>,
"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<String>,
},
Object {
"canceled_at": null,
"completed_at": null,
"confirmed_at": null,
"context": Object {},
"created_at": Any<String>,
"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<String>,
},
Object {
"canceled_at": null,
"completed_at": null,
"confirmed_at": null,
"context": Object {},
"created_at": Any<String>,
"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<String>,
},
],
"count": 4,
"limit": 10,
"offset": 0,
}
`;

View File

@@ -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",
})
)
})
})
})

View File

@@ -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,
})
})

View File

@@ -64,7 +64,6 @@ describe("Price list import batch job", () => {
medusaProcess = await setupServer({
cwd,
redisUrl: "redis://127.0.0.1:6379",
uploadDir: __dirname,
})
})

View File

@@ -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,
})
})

View File

@@ -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,
}),
}),
])
})

View File

@@ -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,
},

View File

@@ -10,6 +10,7 @@
},
"dependencies": {
"@medusajs/cache-inmemory": "*",
"@medusajs/event-bus-local": "*",
"@medusajs/medusa": "*",
"faker": "^5.5.3",
"medusa-interfaces": "*",

View File

@@ -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) {

View File

@@ -10,6 +10,7 @@
},
"dependencies": {
"@medusajs/cache-inmemory": "*",
"@medusajs/event-bus-local": "*",
"@medusajs/medusa": "*",
"faker": "^5.5.3",
"medusa-fulfillment-webshipper": "*",

6
packages/event-bus-local/.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
/dist
node_modules
.DS_store
.env*
.env
*.sql

View File

@@ -0,0 +1,63 @@
<p align="center">
<a href="https://www.medusajs.com">
<img alt="Medusa" src="https://user-images.githubusercontent.com/7554214/153162406-bf8fd16f-aa98-4604-b87b-e13ab4baf604.png" width="100" />
</a>
</p>
<h1 align="center">
@medusajs/event-bus-local
</h1>
<h4 align="center">
<a href="https://docs.medusajs.com">Documentation</a> |
<a href="https://www.medusajs.com">Website</a>
</h4>
<p align="center">
An open source composable commerce engine built for developers.
</p>
<p align="center">
<a href="https://github.com/medusajs/medusa/blob/master/LICENSE">
<img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="Medusa is released under the MIT license." />
</a>
<a href="https://circleci.com/gh/medusajs/medusa">
<img src="https://circleci.com/gh/medusajs/medusa.svg?style=shield" alt="Current CircleCI build status." />
</a>
<a href="https://github.com/medusajs/medusa/blob/master/CONTRIBUTING.md">
<img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat" alt="PRs welcome!" />
</a>
<a href="https://www.producthunt.com/posts/medusa"><img src="https://img.shields.io/badge/Product%20Hunt-%231%20Product%20of%20the%20Day-%23DA552E" alt="Product Hunt"></a>
<a href="https://discord.gg/xpCwq3Kfn8">
<img src="https://img.shields.io/badge/chat-on%20discord-7289DA.svg" alt="Discord Chat" />
</a>
<a href="https://twitter.com/intent/follow?screen_name=medusajs">
<img src="https://img.shields.io/twitter/follow/medusajs.svg?label=Follow%20@medusajs" alt="Follow @medusajs" />
</a>
</p>
## 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.

View File

@@ -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`],
}

View File

@@ -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"
}
}

View File

@@ -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

View File

@@ -0,0 +1,8 @@
import { LoaderOptions } from "@medusajs/modules-sdk"
export default async ({ logger }: LoaderOptions): Promise<void> => {
logger?.warn(
"Local Event Bus installed. This is not recommended for production."
)
}

View File

@@ -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",
})
})
})
})
})

View File

@@ -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<T>(
eventName: string,
data: T,
options: Record<string, unknown>
): Promise<void>
/**
* Emit a number of events
* @param {EmitData} data - the data to send to the subscriber.
*/
async emit<T>(data: EmitData<T>[]): Promise<void>
async emit<T, TInput extends string | EmitData<T>[] = string>(
eventOrData: TInput,
data?: T,
options: Record<string, unknown> = {}
): Promise<void> {
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
}
}

View File

@@ -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"
]
}

View File

@@ -0,0 +1,5 @@
{
"extends": "./tsconfig.json",
"include": ["src"],
"exclude": ["node_modules"]
}

6
packages/event-bus-redis/.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
/dist
node_modules
.DS_store
.env*
.env
*.sql

View File

@@ -0,0 +1,79 @@
<p align="center">
<a href="https://www.medusajs.com">
<img alt="Medusa" src="https://user-images.githubusercontent.com/7554214/153162406-bf8fd16f-aa98-4604-b87b-e13ab4baf604.png" width="100" />
</a>
</p>
<h1 align="center">
@medusajs/event-bus-redis
</h1>
<h4 align="center">
<a href="https://docs.medusajs.com">Documentation</a> |
<a href="https://www.medusajs.com">Website</a>
</h4>
<p align="center">
An open source composable commerce engine built for developers.
</p>
<p align="center">
<a href="https://github.com/medusajs/medusa/blob/master/LICENSE">
<img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="Medusa is released under the MIT license." />
</a>
<a href="https://circleci.com/gh/medusajs/medusa">
<img src="https://circleci.com/gh/medusajs/medusa.svg?style=shield" alt="Current CircleCI build status." />
</a>
<a href="https://github.com/medusajs/medusa/blob/master/CONTRIBUTING.md">
<img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat" alt="PRs welcome!" />
</a>
<a href="https://www.producthunt.com/posts/medusa"><img src="https://img.shields.io/badge/Product%20Hunt-%231%20Product%20of%20the%20Day-%23DA552E" alt="Product Hunt"></a>
<a href="https://discord.gg/xpCwq3Kfn8">
<img src="https://img.shields.io/badge/chat-on%20discord-7289DA.svg" alt="Discord Chat" />
</a>
<a href="https://twitter.com/intent/follow?screen_name=medusajs">
<img src="https://img.shields.io/twitter/follow/medusajs.svg?label=Follow%20@medusajs" alt="Follow @medusajs" />
</a>
</p>
## 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.

View File

@@ -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`],
}

View File

@@ -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"
}
}

View File

@@ -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

View File

@@ -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<void> => {
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),
})
}

View File

@@ -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)"
)
})
})
})
})

View File

@@ -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<T>(
eventName: string,
data: T,
options: Record<string, unknown>
): Promise<void>
/**
* Emit a number of events
* @param {EmitData} data - the data to send to the subscriber.
*/
async emit<T>(data: EmitData<T>[]): Promise<void>
async emit<T, TInput extends string | EmitData<T>[] = string>(
eventNameOrData: TInput,
data?: T,
options: BulkJobOptions | JobsOptions = {}
): Promise<void> {
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 <T>(job: BullJob<T>): Promise<unknown> => {
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)
}
}

View File

@@ -0,0 +1,40 @@
import { Job, JobsOptions, QueueOptions, WorkerOptions } from "bullmq"
import { RedisOptions } from "ioredis"
export type JobData<T> = {
eventName: string
data: T
completedSubscriberIds?: string[] | undefined
}
export type BullJob<T> = {
data: JobData<T>
} & 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
}

View File

@@ -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"
]
}

View File

@@ -0,0 +1,5 @@
{
"extends": "./tsconfig.json",
"include": ["src"],
"exclude": ["node_modules"]
}

View File

@@ -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<IInventoryService> => {
const serviceKey = "inventoryService"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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({

View File

@@ -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",

View File

@@ -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", () => {

View File

@@ -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

View File

@@ -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"

View File

@@ -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"

View File

@@ -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<T>(
eventName: string,
data: T,
options: Record<string, unknown>
): Promise<void>
async emit<T>(data: EventBusTypes.EmitData<T>[]): Promise<void>
async emit<T, TInput extends string | EventBusTypes.EmitData<T>[] = string>(
eventOrData: TInput,
data?: T,
options: Record<string, unknown> = {}
): Promise<void> {
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)
})
})
})

View File

@@ -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 {
/**

View File

@@ -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"

View File

@@ -1,4 +1,3 @@
export * from "./cache"
export * from "./event-bus"
export * from "./stock-location"
export * from "./inventory"
export * from "./stock-location"

View File

@@ -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<void> {
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),
})
}
}

View File

@@ -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"

View File

@@ -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 = {

View File

@@ -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",

View File

@@ -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<StagedJob>[]) {
@@ -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

View File

@@ -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

View File

@@ -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,28 +190,33 @@ 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)
})
})

View File

@@ -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,
}),
})

View File

@@ -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."
)
})
})
})
})

View File

@@ -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"])
})
})
})

View File

@@ -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 = {

View File

@@ -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,

View File

@@ -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)
})
})

View File

@@ -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),
}
)
})

View File

@@ -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"),

View File

@@ -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,

View File

@@ -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", () => {

View File

@@ -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 ******************** */

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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<T = unknown> = (data: T, eventName: string) => Promise<void>
type SubscriberContext = {
subscriberId: string
}
type BullJob<T> = {
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<T = unknown> = {
eventName: string
data: T
opts?: Record<string, unknown> & 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<void>
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<T>(data: EmitData<T>[]): Promise<StagedJob[] | void>
async emit<T>(data: EventBusTypes.EmitData<T>[]): Promise<StagedJob[] | void>
/**
* Calls all subscribers when an event occurs.
@@ -237,80 +121,54 @@ export default class EventBusService {
async emit<T>(
eventName: string,
data: T,
options?: Record<string, unknown> & EmitOptions
options?: Record<string, unknown>
): Promise<StagedJob | void>
async emit<
T,
TInput extends string | EmitData<T>[] = string,
TResult = TInput extends EmitData<T>[] ? StagedJob[] : StagedJob
TInput extends string | EventBusTypes.EmitData<T>[] = string,
TResult = TInput extends EventBusTypes.EmitData<T>[]
? StagedJob[]
: StagedJob
>(
eventNameOrData: TInput,
data?: T,
options: Record<string, unknown> & EmitOptions = {}
options: Record<string, unknown> = {}
): Promise<TResult | void> {
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<StagedJob>) as QueryDeepPartialEntity<StagedJob>
})
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<void> {
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 <T>(job: BullJob<T>): Promise<unknown> => {
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<T>(
eventName: string,
data: T,
cron: string,
handler: Subscriber,
options?: CreateJobOptions
): Promise<void> {
await this.jobSchedulerService_.create(
eventName,
data,
cron,
handler,
options ?? {}
)
}
}

View File

@@ -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 = {

View File

@@ -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"

View File

@@ -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

View File

@@ -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<T = unknown> = (
@@ -22,36 +20,34 @@ export default class JobSchedulerService {
protected readonly logger_: Logger
protected readonly handlers_: Map<string | symbol, ScheduledJobHandler[]> =
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<void> {
): Promise<Job> {
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)
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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,

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 = {

View File

@@ -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

View File

@@ -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({

View File

@@ -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({

View File

@@ -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

View File

@@ -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<StagedJob>) {
const stagedJobRepo = this.activeManager_.withRepository(
this.stagedJobRepository_
)
return await stagedJobRepo.find(config)
}
async delete(stagedJobIds: string | string[]): Promise<void> {
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<StagedJob>)
) as QueryDeepPartialEntity<StagedJob>[]
return await stagedJobRepo.insertBulk(stagedJobs)
})
}
}
export default StagedJobService

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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<void> => {

View File

@@ -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

View File

@@ -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

View File

@@ -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<T> = 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<string, unknown> & 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<string, unknown> & {
ssl: { rejectUnauthorized: false }
}
store_cors?: string
admin_cors?: string
}
featureFlags: Record<string, boolean | string>
modules?: Record<
string,
| false
| string
| Partial<InternalModuleDeclaration | ExternalModuleDeclaration>
>
plugins: (
| {
resolve: string
options: Record<string, unknown>
}
| string
)[]
}
export type ConfigModule = CommonTypes.ConfigModule

View File

@@ -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)

View File

@@ -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",

View File

@@ -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<IStockLocationService> => {
const serviceKey = "stockLocationService"

View File

@@ -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,

View File

@@ -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"
}
}

View File

@@ -0,0 +1,4 @@
export * as CommonTypes from "./common"
export * as EventBusTypes from "./event-bus"
export * as TransactionBaseTypes from "./transaction-base"

Some files were not shown because too many files have changed in this diff Show More