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-ui
!packages/admin !packages/admin
!packages/medusa-payment-stripe !packages/medusa-payment-stripe
!packages/event-bus-redis
!packages/event-bus-local

View File

@@ -83,6 +83,8 @@ module.exports = {
project: [ project: [
"./packages/medusa/tsconfig.json", "./packages/medusa/tsconfig.json",
"./packages/medusa-payment-stripe/tsconfig.spec.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", "./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.status).toEqual(200)
expect(response.data.batch_jobs.length).toEqual(4) expect(response.data.batch_jobs.length).toEqual(4)
expect(response.data).toMatchSnapshot({ expect(response.data).toEqual(
batch_jobs: [ expect.objectContaining({
{ batch_jobs: expect.arrayContaining([
id: "job_5", expect.objectContaining({
created_at: expect.any(String), id: "job_5",
updated_at: expect.any(String), created_at: expect.any(String),
created_by: "admin_user", updated_at: expect.any(String),
}, created_by: "admin_user",
{ }),
id: "job_3", expect.objectContaining({
created_at: expect.any(String), id: "job_3",
updated_at: expect.any(String), created_at: expect.any(String),
created_by: "admin_user", updated_at: expect.any(String),
}, created_by: "admin_user",
{ }),
id: "job_2", expect.objectContaining({
created_at: expect.any(String), id: "job_2",
updated_at: expect.any(String), created_at: expect.any(String),
created_by: "admin_user", updated_at: expect.any(String),
}, created_by: "admin_user",
{ }),
id: "job_1", expect.objectContaining({
created_at: expect.any(String), id: "job_1",
updated_at: expect.any(String), created_at: expect.any(String),
created_by: "admin_user", updated_at: expect.any(String),
}, created_by: "admin_user",
], }),
}) ]),
})
)
}) })
it("lists batch jobs created by the user and where completed_at is null ", async () => { 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", created_by: "admin_user",
status: "created", status: "created",
id: expect.any(String), 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() 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 () => { it("Fails to cancel a batch job created by a different user", async () => {
expect.assertions(3) expect.assertions(3)
const api = useApi() 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 }) dbConnection = await initDb({ cwd })
medusaProcess = await setupServer({ medusaProcess = await setupServer({
cwd, cwd,
redisUrl: "redis://127.0.0.1:6379",
uploadDir: __dirname, uploadDir: __dirname,
}) })
}) })

View File

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

View File

@@ -30,7 +30,6 @@ describe("Batch job of product-export type", () => {
dbConnection = await initDb({ cwd }) dbConnection = await initDb({ cwd })
medusaProcess = await setupServer({ medusaProcess = await setupServer({
cwd, cwd,
redisUrl: "redis://127.0.0.1:6379",
uploadDir: __dirname, uploadDir: __dirname,
}) })
}) })

View File

@@ -8,7 +8,9 @@ const adminSeeder = require("../../../helpers/admin-seeder")
const userSeeder = require("../../../helpers/user-seeder") const userSeeder = require("../../../helpers/user-seeder")
const { simpleSalesChannelFactory } = require("../../../factories") const { simpleSalesChannelFactory } = require("../../../factories")
const batchJobSeeder = require("../../../helpers/batch-job-seeder") 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 = const startServerWithEnvironment =
require("../../../../helpers/start-server-with-environment").default require("../../../../helpers/start-server-with-environment").default
@@ -52,7 +54,7 @@ describe("Product import - Sales Channel", () => {
let dbConnection let dbConnection
let medusaProcess let medusaProcess
let collectionHandle1 = "test-collection1" const collectionHandle1 = "test-collection1"
beforeAll(async () => { beforeAll(async () => {
const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) const cwd = path.resolve(path.join(__dirname, "..", "..", ".."))
@@ -62,7 +64,6 @@ describe("Product import - Sales Channel", () => {
const [process, connection] = await startServerWithEnvironment({ const [process, connection] = await startServerWithEnvironment({
cwd, cwd,
env: { MEDUSA_FF_SALES_CHANNELS: true }, env: { MEDUSA_FF_SALES_CHANNELS: true },
redisUrl: "redis://127.0.0.1:6379",
uploadDir: __dirname, uploadDir: __dirname,
}) })
dbConnection = connection dbConnection = connection
@@ -90,7 +91,7 @@ describe("Product import - Sales Channel", () => {
name: "Import Sales Channel 2", name: "Import Sales Channel 2",
}) })
await simpleProductCollectionFactory(dbConnection, { await simpleProductCollectionFactory(dbConnection, {
handle: collectionHandle1 handle: collectionHandle1,
}) })
} catch (e) { } catch (e) {
console.log(e) console.log(e)
@@ -169,8 +170,8 @@ describe("Product import - Sales Channel", () => {
}), }),
], ],
collection: expect.objectContaining({ collection: expect.objectContaining({
handle: collectionHandle1 handle: collectionHandle1,
}) }),
}), }),
]) ])
}) })

View File

@@ -63,7 +63,6 @@ describe("Product import batch job", () => {
medusaProcess = await setupServer({ medusaProcess = await setupServer({
cwd, cwd,
redisUrl: "redis://127.0.0.1:6379",
uploadDir: __dirname, uploadDir: __dirname,
}) })
}) })
@@ -81,7 +80,7 @@ describe("Product import batch job", () => {
await batchJobSeeder(dbConnection) await batchJobSeeder(dbConnection)
await adminSeeder(dbConnection) await adminSeeder(dbConnection)
await userSeeder(dbConnection) await userSeeder(dbConnection)
await simpleProductCollectionFactory(dbConnection, [ await simpleProductCollectionFactory(dbConnection, [
{ {
handle: collectionHandle1, handle: collectionHandle1,
}, },

View File

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

View File

@@ -1,8 +1,8 @@
import { AbstractFileService } from "@medusajs/medusa" import { AbstractFileService } from "@medusajs/medusa"
import stream from "stream"
import { resolve } from "path"
import * as fs from "fs" import * as fs from "fs"
import mkdirp from "mkdirp" import mkdirp from "mkdirp"
import { resolve } from "path"
import stream from "stream"
export default class LocalFileService extends AbstractFileService { export default class LocalFileService extends AbstractFileService {
constructor({}, options) { constructor({}, options) {

View File

@@ -10,6 +10,7 @@
}, },
"dependencies": { "dependencies": {
"@medusajs/cache-inmemory": "*", "@medusajs/cache-inmemory": "*",
"@medusajs/event-bus-local": "*",
"@medusajs/medusa": "*", "@medusajs/medusa": "*",
"faker": "^5.5.3", "faker": "^5.5.3",
"medusa-fulfillment-webshipper": "*", "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 { import {
ExternalModuleDeclaration, ExternalModuleDeclaration,
InternalModuleDeclaration, InternalModuleDeclaration,
MedusaModule, MedusaModule
} from "@medusajs/modules-sdk" } from "@medusajs/modules-sdk"
import { EventBusTypes } from "@medusajs/types"
import { InventoryServiceInitializeOptions } from "../types" import { InventoryServiceInitializeOptions } from "../types"
export const initialize = async ( export const initialize = async (
options?: InventoryServiceInitializeOptions | ExternalModuleDeclaration, options?: InventoryServiceInitializeOptions | ExternalModuleDeclaration,
injectedDependencies?: { injectedDependencies?: {
eventBusService: IEventBusService eventBusService: EventBusTypes.IEventBusService
} }
): Promise<IInventoryService> => { ): Promise<IInventoryService> => {
const serviceKey = "inventoryService" const serviceKey = "inventoryService"

View File

@@ -3,10 +3,9 @@ import {
CreateInventoryItemInput, CreateInventoryItemInput,
FilterableInventoryItemProps, FilterableInventoryItemProps,
FindConfig, FindConfig,
IEventBusService, InventoryItemDTO
InventoryItemDTO,
} from "@medusajs/medusa" } from "@medusajs/medusa"
import { SharedContext } from "@medusajs/types" import { EventBusTypes, SharedContext } from "@medusajs/types"
import { InjectEntityManager, MedusaContext } from "@medusajs/utils" import { InjectEntityManager, MedusaContext } from "@medusajs/utils"
import { isDefined, MedusaError } from "medusa-core-utils" import { isDefined, MedusaError } from "medusa-core-utils"
import { DeepPartial, EntityManager, FindManyOptions } from "typeorm" import { DeepPartial, EntityManager, FindManyOptions } from "typeorm"
@@ -14,7 +13,7 @@ import { InventoryItem } from "../models"
import { getListQuery } from "../utils/query" import { getListQuery } from "../utils/query"
type InjectedDependencies = { type InjectedDependencies = {
eventBusService: IEventBusService eventBusService: EventBusTypes.IEventBusService
manager: EntityManager manager: EntityManager
} }
@@ -26,7 +25,7 @@ export default class InventoryItemService {
} }
protected readonly manager_: EntityManager protected readonly manager_: EntityManager
protected readonly eventBusService_: IEventBusService | undefined protected readonly eventBusService_: EventBusTypes.IEventBusService | undefined
constructor({ eventBusService, manager }: InjectedDependencies) { constructor({ eventBusService, manager }: InjectedDependencies) {
this.manager_ = manager this.manager_ = manager

View File

@@ -2,17 +2,16 @@ import {
buildQuery, buildQuery,
CreateInventoryLevelInput, CreateInventoryLevelInput,
FilterableInventoryLevelProps, FilterableInventoryLevelProps,
FindConfig, FindConfig
IEventBusService,
} from "@medusajs/medusa" } from "@medusajs/medusa"
import { SharedContext } from "@medusajs/types" import { EventBusTypes, SharedContext } from "@medusajs/types"
import { InjectEntityManager, MedusaContext } from "@medusajs/utils" import { InjectEntityManager, MedusaContext } from "@medusajs/utils"
import { isDefined, MedusaError } from "medusa-core-utils" import { isDefined, MedusaError } from "medusa-core-utils"
import { DeepPartial, EntityManager, FindManyOptions, In } from "typeorm" import { DeepPartial, EntityManager, FindManyOptions, In } from "typeorm"
import { InventoryLevel } from "../models" import { InventoryLevel } from "../models"
type InjectedDependencies = { type InjectedDependencies = {
eventBusService: IEventBusService eventBusService: EventBusTypes.IEventBusService
manager: EntityManager manager: EntityManager
} }
@@ -24,7 +23,7 @@ export default class InventoryLevelService {
} }
protected readonly manager_: EntityManager protected readonly manager_: EntityManager
protected readonly eventBusService_: IEventBusService | undefined protected readonly eventBusService_: EventBusTypes.IEventBusService | undefined
constructor({ eventBusService, manager }: InjectedDependencies) { constructor({ eventBusService, manager }: InjectedDependencies) {
this.manager_ = manager this.manager_ = manager

View File

@@ -8,34 +8,33 @@ import {
FilterableInventoryLevelProps, FilterableInventoryLevelProps,
FilterableReservationItemProps, FilterableReservationItemProps,
FindConfig, FindConfig,
IEventBusService,
IInventoryService, IInventoryService,
InventoryItemDTO, InventoryItemDTO,
InventoryLevelDTO, InventoryLevelDTO,
ReservationItemDTO, ReservationItemDTO,
UpdateInventoryLevelInput, UpdateInventoryLevelInput,
UpdateReservationItemInput, UpdateReservationItemInput
} from "@medusajs/medusa" } from "@medusajs/medusa"
import { SharedContext } from "@medusajs/types" import { EventBusTypes, SharedContext } from "@medusajs/types"
import { InjectEntityManager, MedusaContext } from "@medusajs/utils" import { InjectEntityManager, MedusaContext } from "@medusajs/utils"
import { MedusaError } from "medusa-core-utils" import { MedusaError } from "medusa-core-utils"
import { EntityManager } from "typeorm" import { EntityManager } from "typeorm"
import { import {
InventoryItemService, InventoryItemService,
InventoryLevelService, InventoryLevelService,
ReservationItemService, ReservationItemService
} from "./" } from "./"
type InjectedDependencies = { type InjectedDependencies = {
manager: EntityManager manager: EntityManager
eventBusService: IEventBusService eventBusService: EventBusTypes.IEventBusService
inventoryItemService: InventoryItemService inventoryItemService: InventoryItemService
inventoryLevelService: InventoryLevelService inventoryLevelService: InventoryLevelService
reservationItemService: ReservationItemService reservationItemService: ReservationItemService
} }
export default class InventoryService implements IInventoryService { export default class InventoryService implements IInventoryService {
protected readonly manager_: EntityManager protected readonly manager_: EntityManager
protected readonly eventBusService_: IEventBusService | undefined protected readonly eventBusService_: EventBusTypes.IEventBusService | undefined
protected readonly inventoryItemService_: InventoryItemService protected readonly inventoryItemService_: InventoryItemService
protected readonly reservationItemService_: ReservationItemService protected readonly reservationItemService_: ReservationItemService
protected readonly inventoryLevelService_: InventoryLevelService protected readonly inventoryLevelService_: InventoryLevelService

View File

@@ -2,11 +2,9 @@ import {
buildQuery, buildQuery,
CreateReservationItemInput, CreateReservationItemInput,
FilterableReservationItemProps, FilterableReservationItemProps,
FindConfig, FindConfig, UpdateReservationItemInput
IEventBusService,
UpdateReservationItemInput,
} from "@medusajs/medusa" } from "@medusajs/medusa"
import { SharedContext } from "@medusajs/types" import { EventBusTypes, SharedContext } from "@medusajs/types"
import { InjectEntityManager, MedusaContext } from "@medusajs/utils" import { InjectEntityManager, MedusaContext } from "@medusajs/utils"
import { isDefined, MedusaError } from "medusa-core-utils" import { isDefined, MedusaError } from "medusa-core-utils"
import { EntityManager, FindManyOptions } from "typeorm" import { EntityManager, FindManyOptions } from "typeorm"
@@ -14,7 +12,7 @@ import { InventoryLevelService } from "."
import { ReservationItem } from "../models" import { ReservationItem } from "../models"
type InjectedDependencies = { type InjectedDependencies = {
eventBusService: IEventBusService eventBusService: EventBusTypes.IEventBusService
manager: EntityManager manager: EntityManager
inventoryLevelService: InventoryLevelService inventoryLevelService: InventoryLevelService
} }
@@ -27,7 +25,7 @@ export default class ReservationItemService {
} }
protected readonly manager_: EntityManager protected readonly manager_: EntityManager
protected readonly eventBusService_: IEventBusService | undefined protected readonly eventBusService_: EventBusTypes.IEventBusService | undefined
protected readonly inventoryLevelService_: InventoryLevelService protected readonly inventoryLevelService_: InventoryLevelService
constructor({ constructor({

View File

@@ -22,6 +22,7 @@
"@babel/cli": "^7.14.3", "@babel/cli": "^7.14.3",
"@babel/core": "^7.14.3", "@babel/core": "^7.14.3",
"@babel/preset-typescript": "^7.13.0", "@babel/preset-typescript": "^7.13.0",
"@medusajs/types": "*",
"@types/express": "^4.17.17", "@types/express": "^4.17.17",
"@types/jest": "^27.5.2", "@types/jest": "^27.5.2",
"@types/jsonwebtoken": "^8.5.9", "@types/jsonwebtoken": "^8.5.9",
@@ -51,11 +52,12 @@
"dependencies": { "dependencies": {
"@medusajs/medusa-cli": "^1.3.8", "@medusajs/medusa-cli": "^1.3.8",
"@medusajs/modules-sdk": "*", "@medusajs/modules-sdk": "*",
"@medusajs/utils": "*",
"@types/ioredis": "^4.28.10", "@types/ioredis": "^4.28.10",
"@types/lodash": "^4.14.191", "@types/lodash": "^4.14.191",
"awilix": "^8.0.0", "awilix": "^8.0.0",
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"bull": "^3.12.1", "bullmq": "^3.5.6",
"chokidar": "^3.4.2", "chokidar": "^3.4.2",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.13.2", "class-validator": "^0.13.2",
@@ -68,7 +70,7 @@
"express-session": "^1.17.3", "express-session": "^1.17.3",
"fs-exists-cached": "^1.0.0", "fs-exists-cached": "^1.0.0",
"glob": "^7.1.6", "glob": "^7.1.6",
"ioredis": "^4.17.3", "ioredis": "^5.2.5",
"ioredis-mock": "^5.6.0", "ioredis-mock": "^5.6.0",
"iso8601-duration": "^1.3.0", "iso8601-duration": "^1.3.0",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",

View File

@@ -3,7 +3,7 @@ import { request } from "../../../../../helpers/test-request"
import { orderEditServiceMock } from "../../../../../services/__mocks__/order-edit" import { orderEditServiceMock } from "../../../../../services/__mocks__/order-edit"
import { import {
defaultOrderEditFields, defaultOrderEditFields,
defaultOrderEditRelations defaultOrderEditRelations,
} from "../../../../../types/order-edit" } from "../../../../../types/order-edit"
describe("GET /admin/order-edits/:id", () => { describe("GET /admin/order-edits/:id", () => {

View File

@@ -4,22 +4,20 @@ import {
IsInt, IsInt,
IsOptional, IsOptional,
IsString, IsString,
ValidateNested, ValidateNested
} from "class-validator" } from "class-validator"
import { import {
EventBusService, EventBusService,
OrderService, OrderService,
ReturnService, ReturnService
} from "../../../../services" } from "../../../../services"
import { Type } from "class-transformer" import { Type } from "class-transformer"
import { isDefined, MedusaError } from "medusa-core-utils" import { isDefined, MedusaError } from "medusa-core-utils"
import { EntityManager } from "typeorm" import { EntityManager } from "typeorm"
import { Order, Return } from "../../../../models" import { Order, Return } from "../../../../models"
import { OrdersReturnItem } from "../../../../types/orders"
import { FindParams } from "../../../../types/common" import { FindParams } from "../../../../types/common"
import { FlagRouter } from "../../../../utils/flag-router" import { OrdersReturnItem } from "../../../../types/orders"
import { IInventoryService } from "../../../../interfaces"
/** /**
* @oas [post] /admin/orders/{id}/return * @oas [post] /admin/orders/{id}/return

View File

@@ -1,10 +1,7 @@
import { IsNotEmpty, IsString } from "class-validator" import { IsNotEmpty, IsString } from "class-validator"
import { MedusaError } from "medusa-core-utils" import { MedusaError } from "medusa-core-utils"
import { import { CustomerService, OrderService } from "../../../../services"
CustomerService, import EventBusService from "../../../../services/event-bus"
EventBusService,
OrderService,
} from "../../../../services"
import TokenService from "../../../../services/token" import TokenService from "../../../../services/token"
import { TokenEvents } from "../../../../types/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 { asValue, createContainer } from "awilix"
import express from "express" import express from "express"
import jwt from "jsonwebtoken" import jwt from "jsonwebtoken"
@@ -7,11 +12,6 @@ import "reflect-metadata"
import supertest from "supertest" import supertest from "supertest"
import apiLoader from "../loaders/api" import apiLoader from "../loaders/api"
import featureFlagLoader, { featureFlagRouter } from "../loaders/feature-flags" import featureFlagLoader, { featureFlagRouter } from "../loaders/feature-flags"
import {
moduleLoader,
moduleHelper,
registerModules,
} from "@medusajs/modules-sdk"
import passportLoader from "../loaders/passport" import passportLoader from "../loaders/passport"
import servicesLoader from "../loaders/services" import servicesLoader from "../loaders/services"
import strategiesLoader from "../loaders/strategies" 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 { 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 { 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 "./batch-job-strategy"
export * from "./cart-completion-strategy"
export * from "./file-service" export * from "./file-service"
export * from "./notification-service"
export * from "./price-selection-strategy"
export * from "./models/base-entity" export * from "./models/base-entity"
export * from "./models/soft-deletable-entity" export * from "./models/soft-deletable-entity"
export * from "./search-service" export * from "./notification-service"
export * from "./payment-service"
export * from "./payment-processor" export * from "./payment-processor"
export * from "./payment-service"
export * from "./price-selection-strategy"
export * from "./search-service"
export * from "./services" 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 "./cache"
export * from "./event-bus"
export * from "./stock-location"
export * from "./inventory" export * from "./inventory"
export * from "./stock-location"

View File

@@ -1,8 +1,8 @@
import { asValue } from "awilix" import { asValue } from "awilix"
import RealRedis from "ioredis" import Redis from "ioredis"
import FakeRedis from "ioredis-mock" import FakeRedis from "ioredis-mock"
import { ConfigModule, MedusaContainer } from "../types/global" import { EOL } from "os"
import { Logger } from "../types/global" import { ConfigModule, Logger, MedusaContainer } from "../types/global"
type Options = { type Options = {
container: MedusaContainer container: MedusaContainer
@@ -10,19 +10,27 @@ type Options = {
logger: Logger logger: Logger
} }
// TODO: Will be removed when the strict dependency on Redis in the core is removed
async function redisLoader({ async function redisLoader({
container, container,
configModule, configModule,
logger, logger,
}: Options): Promise<void> { }: Options): Promise<void> {
if (configModule.projectConfig.redis_url) { if (configModule.projectConfig.redis_url) {
// Economical way of dealing with redis clients const redisClient = new Redis(configModule.projectConfig.redis_url, {
const client = new RealRedis(configModule.projectConfig.redis_url) // Lazy connect to properly handle connection errors
const subscriber = new RealRedis(configModule.projectConfig.redis_url) 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({ container.register({
redisClient: asValue(client), redisClient: asValue(redisClient),
redisSubscriber: asValue(subscriber),
}) })
} else { } else {
if (process.env.NODE_ENV === "production") { if (process.env.NODE_ENV === "production") {
@@ -38,7 +46,6 @@ async function redisLoader({
container.register({ container.register({
redisClient: asValue(client), 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 { AbstractSearchService } from "../interfaces"
import { EventBusService } from "../services"
import { Logger, MedusaContainer } from "../types/global"
export const SEARCH_INDEX_EVENT = "SEARCH_INDEX_EVENT" export const SEARCH_INDEX_EVENT = "SEARCH_INDEX_EVENT"

View File

@@ -1,8 +1,8 @@
import { asFunction } from "awilix" import { asFunction } from "awilix"
import glob from "glob" import glob from "glob"
import { isDefined } from "medusa-core-utils"
import path from "path" import path from "path"
import { ConfigModule, MedusaContainer } from "../types/global" import { ConfigModule, MedusaContainer } from "../types/global"
import { isDefined } from "medusa-core-utils"
import formatRegistrationName from "../utils/format-registration-name" import formatRegistrationName from "../utils/format-registration-name"
type Options = { type Options = {

View File

@@ -6,10 +6,12 @@ import {
JoinColumn, JoinColumn,
ManyToOne, ManyToOne,
OneToMany, OneToMany,
OneToOne, OneToOne
} from "typeorm" } from "typeorm"
import { DbAwareColumn, resolveDbType } from "../utils/db-aware-column" 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 { Address } from "./address"
import { Cart } from "./cart" import { Cart } from "./cart"
import { Fulfillment } from "./fulfillment" import { Fulfillment } from "./fulfillment"
@@ -18,8 +20,6 @@ import { Order } from "./order"
import { Payment } from "./payment" import { Payment } from "./payment"
import { Return } from "./return" import { Return } from "./return"
import { ShippingMethod } from "./shipping-method" import { ShippingMethod } from "./shipping-method"
import { SoftDeletableEntity } from "../interfaces/models/soft-deletable-entity"
import { generateEntityId } from "../utils/generate-entity-id"
export enum SwapFulfillmentStatus { export enum SwapFulfillmentStatus {
NOT_FULFILLED = "not_fulfilled", NOT_FULFILLED = "not_fulfilled",

View File

@@ -1,7 +1,6 @@
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity" import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity"
import { dataSource } from "../loaders/database" import { dataSource } from "../loaders/database"
import { StagedJob } from "../models" import { StagedJob } from "../models"
import { rowSqlResultsToEntityTransformer } from "../utils/row-sql-results-to-entity-transformer"
export const StagedJobRepository = dataSource.getRepository(StagedJob).extend({ export const StagedJobRepository = dataSource.getRepository(StagedJob).extend({
async insertBulk(jobToCreates: QueryDeepPartialEntity<StagedJob>[]) { 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 // TODO: remove if statement once this issue is resolved https://github.com/typeorm/typeorm/issues/9850
if (!queryBuilder.connection.driver.isReturningSqlSupported("insert")) { if (!queryBuilder.connection.driver.isReturningSqlSupported("insert")) {
const rawStagedJobs = await queryBuilder.execute() const rawStagedJobs = await queryBuilder.execute()
return rawStagedJobs.generatedMaps return rawStagedJobs.generatedMaps.map((d) =>
this.create(d)
) as StagedJob[]
} }
const rawStagedJobs = await queryBuilder.returning("*").execute() const rawStagedJobs = await queryBuilder.returning("*").execute()
return rawStagedJobs.generatedMaps.map((d) => this.create(d))
return rowSqlResultsToEntityTransformer(
rawStagedJobs.raw,
queryBuilder,
this.queryRunner!
)
}, },
}) })
export default StagedJobRepository 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 { 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 { BatchJob } from "../../models"
import { BatchJobStatus } from "../../types/batch-job"
import BatchJobService from "../batch-job"
import EventBusService from "../event-bus"
const eventBusServiceMock = { const eventBusServiceMock = {
emit: jest.fn(), emit: jest.fn(),
withTransaction: function() { withTransaction: function () {
return this return this
}, },
} as unknown as EventBusService } as unknown as EventBusService
const batchJobRepositoryMock = MockRepository({ const batchJobRepositoryMock = MockRepository({
create: jest.fn().mockImplementation((data) => { create: jest.fn().mockImplementation((data) => {
return Object.assign(new BatchJob(), data) return Object.assign(new BatchJob(), data)
}) }),
}) })
describe('BatchJobService', () => { describe("BatchJobService", () => {
const batchJobId_1 = IdMap.getId("batchJob_1") const batchJobId_1 = IdMap.getId("batchJob_1")
const batchJobService = new BatchJobService({ const batchJobService = new BatchJobService({
manager: MockManager, manager: MockManager,
eventBusService: eventBusServiceMock, eventBusService: eventBusServiceMock,
batchJobRepository: batchJobRepositoryMock batchJobRepository: batchJobRepositoryMock,
} as any) } as any)
afterEach(() => { afterEach(() => {
jest.clearAllMocks() jest.clearAllMocks()
}) })
describe('update status', () => { describe("update status", () => {
describe("confirm", () => { 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({ const batchJob = batchJobRepositoryMock.create({
id: batchJobId_1, id: batchJobId_1,
dry_run: true, dry_run: true,
status: BatchJobStatus.PRE_PROCESSED status: BatchJobStatus.PRE_PROCESSED,
}) })
const updatedBatchJob = await batchJobService.confirm(batchJob) const updatedBatchJob = await batchJobService.confirm(batchJob)
expect(updatedBatchJob.processing_at).not.toBeTruthy() expect(updatedBatchJob.processing_at).not.toBeTruthy()
expect(eventBusServiceMock.emit) expect(eventBusServiceMock.emit).toHaveBeenCalledWith(
.toHaveBeenCalledWith(BatchJobService.Events.CONFIRMED, { id: batchJobId_1 }) 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({ const batchJob = batchJobRepositoryMock.create({
id: batchJobId_1, id: batchJobId_1,
dry_run: true, dry_run: true,
status: BatchJobStatus.CREATED status: BatchJobStatus.CREATED,
}) })
const err = await batchJobService.confirm(batchJob) const err = await batchJobService.confirm(batchJob).catch((e) => e)
.catch(e => e)
expect(err).toBeTruthy() 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) expect(eventBusServiceMock.emit).toHaveBeenCalledTimes(0)
}) })
}) })
describe("complete", () => { 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({ const batchJob = batchJobRepositoryMock.create({
id: batchJobId_1, id: batchJobId_1,
dry_run: true, dry_run: true,
status: BatchJobStatus.PROCESSING status: BatchJobStatus.PROCESSING,
}) })
const updatedBatchJob = await batchJobService.complete(batchJob) const updatedBatchJob = await batchJobService.complete(batchJob)
expect(updatedBatchJob.completed_at).toBeTruthy() expect(updatedBatchJob.completed_at).toBeTruthy()
expect(eventBusServiceMock.emit) expect(eventBusServiceMock.emit).toHaveBeenCalledWith(
.toHaveBeenCalledWith(BatchJobService.Events.COMPLETED, { id: batchJobId_1 }) BatchJobService.Events.COMPLETED,
{ id: batchJobId_1 }
)
const batchJob2 = batchJobRepositoryMock.create({ const batchJob2 = batchJobRepositoryMock.create({
id: batchJobId_1, id: batchJobId_1,
dry_run: false, dry_run: false,
status: BatchJobStatus.PROCESSING status: BatchJobStatus.PROCESSING,
}) })
const updatedBatchJob2 = await batchJobService.complete(batchJob2) const updatedBatchJob2 = await batchJobService.complete(batchJob2)
expect(updatedBatchJob2.completed_at).toBeTruthy() expect(updatedBatchJob2.completed_at).toBeTruthy()
expect(eventBusServiceMock.emit) expect(eventBusServiceMock.emit).toHaveBeenCalledWith(
.toHaveBeenCalledWith(BatchJobService.Events.COMPLETED, { id: batchJobId_1 }) 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({ const batchJob = batchJobRepositoryMock.create({
id: batchJobId_1, id: batchJobId_1,
dry_run: true, dry_run: true,
status: BatchJobStatus.CREATED status: BatchJobStatus.CREATED,
}) })
const err = await batchJobService.complete(batchJob) const err = await batchJobService.complete(batchJob).catch((e) => e)
.catch(e => e)
expect(err).toBeTruthy() 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) expect(eventBusServiceMock.emit).toHaveBeenCalledTimes(0)
const batchJob2 = batchJobRepositoryMock.create({ const batchJob2 = batchJobRepositoryMock.create({
id: batchJobId_1, id: batchJobId_1,
dry_run: false, dry_run: false,
status: BatchJobStatus.PRE_PROCESSED status: BatchJobStatus.PRE_PROCESSED,
}) })
const err2 = await batchJobService.complete(batchJob2) const err2 = await batchJobService.complete(batchJob2).catch((e) => e)
.catch(e => e)
expect(err2).toBeTruthy() 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) expect(eventBusServiceMock.emit).toHaveBeenCalledTimes(0)
}) })
}) })
describe("pre processed", () => { 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({ const batchJob = batchJobRepositoryMock.create({
id: batchJobId_1, id: batchJobId_1,
dry_run: true, 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(updatedBatchJob.pre_processed_at).toBeTruthy()
expect(eventBusServiceMock.emit) expect(eventBusServiceMock.emit).toHaveBeenCalledWith(
.toHaveBeenCalledWith(BatchJobService.Events.PRE_PROCESSED, { id: batchJobId_1 }) 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({ const batchJob = batchJobRepositoryMock.create({
id: batchJobId_1, id: batchJobId_1,
dry_run: false, 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.pre_processed_at).toBeTruthy()
expect(updatedBatchJob.confirmed_at).toBeTruthy() expect(updatedBatchJob.confirmed_at).toBeTruthy()
expect(eventBusServiceMock.emit).toHaveBeenCalledTimes(2) expect(eventBusServiceMock.emit).toHaveBeenCalledTimes(2)
expect(eventBusServiceMock.emit) expect(eventBusServiceMock.emit).toHaveBeenCalledWith(
.toHaveBeenCalledWith(BatchJobService.Events.PRE_PROCESSED, { id: batchJobId_1 }) BatchJobService.Events.PRE_PROCESSED,
expect(eventBusServiceMock.emit) { id: batchJobId_1 }
.toHaveBeenLastCalledWith(BatchJobService.Events.CONFIRMED, { id: batchJobId_1 }) )
expect(eventBusServiceMock.emit).toHaveBeenLastCalledWith(
BatchJobService.Events.CONFIRMED,
{ id: batchJobId_1 }
)
}) })
}) })
describe("cancel", () => { 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({ const batchJob = batchJobRepositoryMock.create({
id: batchJobId_1, id: batchJobId_1,
status: BatchJobStatus.CREATED status: BatchJobStatus.CREATED,
}) })
const updatedBatchJob = await batchJobService.cancel(batchJob) const updatedBatchJob = await batchJobService.cancel(batchJob)
expect(updatedBatchJob.canceled_at).toBeTruthy() expect(updatedBatchJob.canceled_at).toBeTruthy()
expect(eventBusServiceMock.emit) expect(eventBusServiceMock.emit).toHaveBeenCalledWith(
.toHaveBeenCalledWith(BatchJobService.Events.CANCELED, { id: batchJobId_1 }) 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({ const batchJob = batchJobRepositoryMock.create({
id: batchJobId_1, id: batchJobId_1,
status: BatchJobStatus.COMPLETED status: BatchJobStatus.COMPLETED,
}) })
const err = await batchJobService.cancel(batchJob) const err = await batchJobService.cancel(batchJob).catch((e) => e)
.catch(e => e)
expect(err).toBeTruthy() expect(err).toBeTruthy()
expect(err.message).toBe("Cannot cancel completed batch job") expect(err.message).toBe("Cannot cancel completed batch job")
expect(eventBusServiceMock.emit).toHaveBeenCalledTimes(0) expect(eventBusServiceMock.emit).toHaveBeenCalledTimes(0)
@@ -170,30 +190,35 @@ describe('BatchJobService', () => {
}) })
describe("processing", () => { 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({ const batchJob = batchJobRepositoryMock.create({
id: batchJobId_1, id: batchJobId_1,
status: BatchJobStatus.CONFIRMED status: BatchJobStatus.CONFIRMED,
}) })
const updatedBatchJob = await batchJobService.setProcessing(batchJob) const updatedBatchJob = await batchJobService.setProcessing(batchJob)
expect(updatedBatchJob.processing_at).toBeTruthy() expect(updatedBatchJob.processing_at).toBeTruthy()
expect(eventBusServiceMock.emit) expect(eventBusServiceMock.emit).toHaveBeenCalledWith(
.toHaveBeenCalledWith(BatchJobService.Events.PROCESSING, { id: batchJobId_1 }) 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({ const batchJob = batchJobRepositoryMock.create({
id: batchJobId_1, id: batchJobId_1,
status: BatchJobStatus.COMPLETED status: BatchJobStatus.COMPLETED,
}) })
const err = await batchJobService.setProcessing(batchJob) const err = await batchJobService
.catch(e => e) .setProcessing(batchJob)
.catch((e) => e)
expect(err).toBeTruthy() 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) expect(eventBusServiceMock.emit).toHaveBeenCalledTimes(0)
}) })
}) })
}) })
}) })

View File

@@ -1,36 +1,35 @@
import { IdMap, MockManager, MockRepository } from "medusa-test-utils" 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 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 currencyCode = IdMap.getId("currency-1")
const eventBusServiceMock = { const eventBusServiceMock = {
emit: jest.fn(), emit: jest.fn(),
withTransaction: function() { withTransaction: function () {
return this return this
}, },
} as unknown as EventBusService } as unknown as EventBusService
const currencyRepositoryMock = MockRepository({ const currencyRepositoryMock = MockRepository({
findOne: jest.fn().mockImplementation(() => { findOne: jest.fn().mockImplementation(() => {
return { return {
code: currencyCode code: currencyCode,
} }
}), }),
save: jest.fn().mockImplementation((data) => { save: jest.fn().mockImplementation((data) => {
return Object.assign(new Currency(), data) return Object.assign(new Currency(), data)
}) }),
}) })
describe("CurrencyService", () => {
describe('CurrencyService', () => {
const currencyService = new CurrencyService({ const currencyService = new CurrencyService({
manager: MockManager, manager: MockManager,
currencyRepository: currencyRepositoryMock, currencyRepository: currencyRepositoryMock,
eventBusService: eventBusServiceMock, eventBusService: eventBusServiceMock,
featureFlagRouter: new FlagRouter({ 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 { Queue } from "bullmq"
import config from "../../loaders/config"
import JobSchedulerService from "../job-scheduler" import JobSchedulerService from "../job-scheduler"
jest.genMockFromModule("bull") jest.genMockFromModule("bullmq")
jest.mock("bull") jest.mock("bullmq")
jest.mock("../../loaders/config") jest.mock("ioredis")
config.redisURI = "testhost"
const loggerMock = { const loggerMock = {
info: jest.fn().mockReturnValue(console.log), info: jest.fn().mockReturnValue(console.log),
@@ -15,90 +12,112 @@ const loggerMock = {
} }
describe("JobSchedulerService", () => { describe("JobSchedulerService", () => {
describe("constructor", () => { let scheduler
let jobScheduler
beforeAll(() => {
jest.resetAllMocks()
jobScheduler = new JobSchedulerService({ describe("constructor", () => {
logger: loggerMock, beforeAll(() => {
}) jest.clearAllMocks()
scheduler = new JobSchedulerService(
{
logger: loggerMock,
},
{
projectConfig: {
redis_url: "testhost",
},
}
)
}) })
it("creates bull queue", () => { it("creates bull queue", () => {
expect(Bull).toHaveBeenCalledTimes(1) expect(Queue).toHaveBeenCalledTimes(1)
expect(Bull).toHaveBeenCalledWith("scheduled-jobs:queue", { expect(Queue).toHaveBeenCalledWith("scheduled-jobs:queue", {
createClient: expect.any(Function), connection: expect.any(Object),
prefix: "JobSchedulerService",
}) })
}) })
}) })
describe("create", () => { describe("create", () => {
let jobScheduler 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, logger: loggerMock,
}) },
{
jobScheduler.create( projectConfig: {
"eventName", redis_url: "testhost",
{ 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" },
}, },
{ }
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", () => { it("calls logger", () => {
let jobScheduler expect(loggerMock.info).toHaveBeenCalled()
let result expect(loggerMock.info).toHaveBeenCalledWith(
describe("successfully runs the worker", () => { "Processing scheduled job: eventName"
beforeAll(async () => { )
jest.resetAllMocks() })
jobScheduler = new JobSchedulerService( it("returns array with hi", async () => {
{ expect(result).toEqual(["hi"])
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"])
})
})
}) })
}) })
}) })

View File

@@ -1,7 +1,12 @@
import { IdMap, MockManager, MockRepository } from "medusa-test-utils" 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 { 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" import { LineItemServiceMock } from "../__mocks__/line-item"
const taxProviderServiceMock = { const taxProviderServiceMock = {

View File

@@ -1,7 +1,7 @@
import { IdMap, MockManager, MockRepository } from "medusa-test-utils" import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
import { OrderEditItemChangeType, OrderEditStatus } from "../../models" import { OrderEditItemChangeType, OrderEditStatus } from "../../models"
import EventBusService from "../event-bus"
import { import {
EventBusService,
LineItemService, LineItemService,
NewTotalsService, NewTotalsService,
OrderEditItemChangeService, OrderEditItemChangeService,

View File

@@ -1,10 +1,22 @@
import { IdMap, MockManager, MockRepository } from "medusa-test-utils" import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
import { CustomerService, EventBusService, PaymentCollectionService, PaymentProviderService, } from "../index" import {
import { PaymentCollection, PaymentCollectionStatus, PaymentCollectionType, } from "../../models" PaymentCollection,
import { EventBusServiceMock } from "../__mocks__/event-bus" PaymentCollectionStatus,
import { DefaultProviderMock, PaymentProviderServiceMock, } from "../__mocks__/payment-provider" PaymentCollectionType
import { CustomerServiceMock } from "../__mocks__/customer" } from "../../models"
import { PaymentCollectionsSessionsBatchInput } from "../../types/payment-collection" 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", () => { describe("PaymentCollectionService", () => {
afterEach(() => { afterEach(() => {
@@ -414,9 +426,7 @@ describe("PaymentCollectionService", () => {
"lebron" "lebron"
) )
expect(PaymentProviderServiceMock.createSession).toHaveBeenCalledTimes( expect(PaymentProviderServiceMock.createSession).toHaveBeenCalledTimes(1)
1
)
expect(CustomerServiceMock.retrieve).toHaveBeenCalledTimes(1) expect(CustomerServiceMock.retrieve).toHaveBeenCalledTimes(1)
expect(paymentCollectionRepository.save).toHaveBeenCalledTimes(1) expect(paymentCollectionRepository.save).toHaveBeenCalledTimes(1)
}) })
@@ -430,12 +440,8 @@ describe("PaymentCollectionService", () => {
"lebron" "lebron"
) )
expect(PaymentProviderServiceMock.createSession).toHaveBeenCalledTimes( expect(PaymentProviderServiceMock.createSession).toHaveBeenCalledTimes(0)
0 expect(PaymentProviderServiceMock.updateSession).toHaveBeenCalledTimes(1)
)
expect(PaymentProviderServiceMock.updateSession).toHaveBeenCalledTimes(
1
)
expect(CustomerServiceMock.retrieve).toHaveBeenCalledTimes(1) expect(CustomerServiceMock.retrieve).toHaveBeenCalledTimes(1)
expect(paymentCollectionRepository.save).toHaveBeenCalledTimes(1) expect(paymentCollectionRepository.save).toHaveBeenCalledTimes(1)
}) })
@@ -453,15 +459,9 @@ describe("PaymentCollectionService", () => {
IdMap.getId("lebron") IdMap.getId("lebron")
) )
expect(PaymentProviderServiceMock.createSession).toHaveBeenCalledTimes( expect(PaymentProviderServiceMock.createSession).toHaveBeenCalledTimes(1)
1 expect(PaymentProviderServiceMock.updateSession).toHaveBeenCalledTimes(0)
) expect(paymentCollectionRepository.delete).toHaveBeenCalledTimes(1)
expect(PaymentProviderServiceMock.updateSession).toHaveBeenCalledTimes(
0
)
expect(paymentCollectionRepository.delete).toHaveBeenCalledTimes(
1
)
expect(paymentCollectionRepository.save).toHaveBeenCalledTimes(1) expect(paymentCollectionRepository.save).toHaveBeenCalledTimes(1)
}) })
@@ -508,9 +508,7 @@ describe("PaymentCollectionService", () => {
"customer1" "customer1"
) )
expect(PaymentProviderServiceMock.createSession).toHaveBeenCalledTimes( expect(PaymentProviderServiceMock.createSession).toHaveBeenCalledTimes(0)
0
)
expect(ret).rejects.toThrow( expect(ret).rejects.toThrow(
`The sum of sessions is not equal to 100 on Payment Collection` `The sum of sessions is not equal to 100 on Payment Collection`
) )
@@ -531,9 +529,7 @@ describe("PaymentCollectionService", () => {
"customer1" "customer1"
) )
expect(PaymentProviderServiceMock.createSession).toHaveBeenCalledTimes( expect(PaymentProviderServiceMock.createSession).toHaveBeenCalledTimes(0)
0
)
expect(multiRet).rejects.toThrow( expect(multiRet).rejects.toThrow(
`The sum of sessions is not equal to 100 on Payment Collection` `The sum of sessions is not equal to 100 on Payment Collection`
) )
@@ -580,12 +576,8 @@ describe("PaymentCollectionService", () => {
"lebron" "lebron"
) )
expect(PaymentProviderServiceMock.createSession).toHaveBeenCalledTimes( expect(PaymentProviderServiceMock.createSession).toHaveBeenCalledTimes(1)
1 expect(PaymentProviderServiceMock.updateSession).toHaveBeenCalledTimes(1)
)
expect(PaymentProviderServiceMock.updateSession).toHaveBeenCalledTimes(
1
)
expect(CustomerServiceMock.retrieve).toHaveBeenCalledTimes(1) expect(CustomerServiceMock.retrieve).toHaveBeenCalledTimes(1)
expect(paymentCollectionRepository.save).toHaveBeenCalledTimes(1) expect(paymentCollectionRepository.save).toHaveBeenCalledTimes(1)
}) })
@@ -603,15 +595,9 @@ describe("PaymentCollectionService", () => {
IdMap.getId("lebron") IdMap.getId("lebron")
) )
expect(PaymentProviderServiceMock.createSession).toHaveBeenCalledTimes( expect(PaymentProviderServiceMock.createSession).toHaveBeenCalledTimes(1)
1 expect(PaymentProviderServiceMock.updateSession).toHaveBeenCalledTimes(0)
) expect(paymentCollectionRepository.delete).toHaveBeenCalledTimes(1)
expect(PaymentProviderServiceMock.updateSession).toHaveBeenCalledTimes(
0
)
expect(paymentCollectionRepository.delete).toHaveBeenCalledTimes(
1
)
expect(paymentCollectionRepository.save).toHaveBeenCalledTimes(1) expect(paymentCollectionRepository.save).toHaveBeenCalledTimes(1)
}) })
@@ -623,13 +609,9 @@ describe("PaymentCollectionService", () => {
"customer1" "customer1"
) )
expect( expect(PaymentProviderServiceMock.refreshSession).toHaveBeenCalledTimes(1)
PaymentProviderServiceMock.refreshSession
).toHaveBeenCalledTimes(1)
expect(DefaultProviderMock.deletePayment).toHaveBeenCalledTimes(1) expect(DefaultProviderMock.deletePayment).toHaveBeenCalledTimes(1)
expect(PaymentProviderServiceMock.createSession).toHaveBeenCalledTimes( expect(PaymentProviderServiceMock.createSession).toHaveBeenCalledTimes(1)
1
)
}) })
it("should throw to refresh a payment session that doesn't exist", async () => { it("should throw to refresh a payment session that doesn't exist", async () => {
@@ -689,9 +671,7 @@ describe("PaymentCollectionService", () => {
expect(PaymentProviderServiceMock.authorizePayment).toHaveBeenCalledTimes( expect(PaymentProviderServiceMock.authorizePayment).toHaveBeenCalledTimes(
2 2
) )
expect(PaymentProviderServiceMock.createPayment).toHaveBeenCalledTimes( expect(PaymentProviderServiceMock.createPayment).toHaveBeenCalledTimes(2)
2
)
expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(1) expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(1)
}) })
@@ -704,9 +684,7 @@ describe("PaymentCollectionService", () => {
expect(PaymentProviderServiceMock.authorizePayment).toHaveBeenCalledTimes( expect(PaymentProviderServiceMock.authorizePayment).toHaveBeenCalledTimes(
1 1
) )
expect(PaymentProviderServiceMock.createPayment).toHaveBeenCalledTimes( expect(PaymentProviderServiceMock.createPayment).toHaveBeenCalledTimes(1)
1
)
expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(1) expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(1)
}) })
@@ -719,9 +697,7 @@ describe("PaymentCollectionService", () => {
expect(PaymentProviderServiceMock.authorizePayment).toHaveBeenCalledTimes( expect(PaymentProviderServiceMock.authorizePayment).toHaveBeenCalledTimes(
0 0
) )
expect(PaymentProviderServiceMock.createPayment).toHaveBeenCalledTimes( expect(PaymentProviderServiceMock.createPayment).toHaveBeenCalledTimes(0)
0
)
expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(0) expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(0)
}) })
}) })

View File

@@ -1,13 +1,10 @@
import { IdMap, MockManager as manager } from "medusa-test-utils" import { IdMap, MockManager as manager } from "medusa-test-utils"
import ProductCategoryService from "../product-category" import { EventBusService, ProductCategoryService } from "../"
import { EventBusService } from "../"
import { import {
invalidProdCategoryId, invalidProdCategoryId,
productCategoryRepositoryMock as productCategoryRepository, productCategoryRepositoryMock as productCategoryRepository,
validProdCategoryId, validProdCategoryId,
validProdCategoryIdWithChildren, validProdCategoryIdWithChildren, validProdCategoryRankChange, validProdCategoryWithSiblings
validProdCategoryWithSiblings,
validProdCategoryRankChange
} from "../../repositories/__mocks__/product-category" } from "../../repositories/__mocks__/product-category"
import { tempReorderRank } from "../../types/product-category" import { tempReorderRank } from "../../types/product-category"
import { EventBusServiceMock as eventBusService } from "../__mocks__/event-bus" import { EventBusServiceMock as eventBusService } from "../__mocks__/event-bus"
@@ -74,7 +71,9 @@ describe("ProductCategoryService", () => {
const [result, count] = await productCategoryService const [result, count] = await productCategoryService
.listAndCount({ q: IdMap.getId(invalidProdCategoryId) }) .listAndCount({ q: IdMap.getId(invalidProdCategoryId) })
expect(productCategoryRepository.getFreeTextSearchResultsAndCount).toHaveBeenCalledTimes(1) expect(
productCategoryRepository.getFreeTextSearchResultsAndCount
).toHaveBeenCalledTimes(1)
expect(result).toEqual([]) expect(result).toEqual([])
expect(count).toEqual(0) expect(count).toEqual(0)
}) })
@@ -115,8 +114,9 @@ describe("ProductCategoryService", () => {
expect(eventBusService.emit).toHaveBeenCalledTimes(1) expect(eventBusService.emit).toHaveBeenCalledTimes(1)
expect(eventBusService.emit).toHaveBeenCalledWith( expect(eventBusService.emit).toHaveBeenCalledWith(
"product-category.created", { "product-category.created",
"id": IdMap.getId(validProdCategoryId) {
id: IdMap.getId(validProdCategoryId),
} }
) )
}) })
@@ -129,7 +129,9 @@ describe("ProductCategoryService", () => {
) )
expect(productCategoryRepository.delete).toBeCalledTimes(1) 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 () => { 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).toHaveBeenCalledTimes(1)
expect(eventBusService.emit).toHaveBeenCalledWith( expect(eventBusService.emit).toHaveBeenCalledWith(
"product-category.deleted", { "product-category.deleted",
"id": IdMap.getId(validProdCategoryId) {
id: IdMap.getId(validProdCategoryId),
} }
) )
}) })
@@ -182,11 +185,9 @@ describe("ProductCategoryService", () => {
describe("update", () => { describe("update", () => {
it("successfully updates a product category", async () => { it("successfully updates a product category", async () => {
await productCategoryService.update( await productCategoryService.update(IdMap.getId(validProdCategoryId), {
IdMap.getId(validProdCategoryId), { name: "bathrobes",
name: "bathrobes", })
}
)
expect(productCategoryRepository.save).toHaveBeenCalledTimes(1) expect(productCategoryRepository.save).toHaveBeenCalledTimes(1)
expect(productCategoryRepository.save).toHaveBeenCalledWith( expect(productCategoryRepository.save).toHaveBeenCalledWith(
@@ -221,28 +222,32 @@ describe("ProductCategoryService", () => {
}) })
it("fails on not-found Id product category", async () => { it("fails on not-found Id product category", async () => {
const error = await productCategoryService.update( const error = await productCategoryService
IdMap.getId(invalidProdCategoryId), { .update(IdMap.getId(invalidProdCategoryId), {
name: "bathrobes", name: "bathrobes",
} })
).catch(e => e) .catch((e) => e)
expect(error.message).toBe( 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 () => { it("emits a message on successful update", async () => {
const result = await productCategoryService.update( const result = await productCategoryService.update(
IdMap.getId(validProdCategoryId), { IdMap.getId(validProdCategoryId),
{
name: "bathrobes", name: "bathrobes",
} }
) )
expect(eventBusService.emit).toHaveBeenCalledTimes(1) expect(eventBusService.emit).toHaveBeenCalledTimes(1)
expect(eventBusService.emit).toHaveBeenCalledWith( expect(eventBusService.emit).toHaveBeenCalledWith(
"product-category.updated", { "product-category.updated",
"id": IdMap.getId(validProdCategoryId) {
id: IdMap.getId(validProdCategoryId),
} }
) )
}) })

View File

@@ -1,8 +1,8 @@
import { IdMap, MockManager, MockRepository } from "medusa-test-utils" 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 PublishableApiKeyService from "../publishable-api-key"
import { EventBusServiceMock } from "../__mocks__/event-bus"
const pubKeyToRetrieve = { const pubKeyToRetrieve = {
id: IdMap.getId("pub-key-to-retrieve"), id: IdMap.getId("pub-key-to-retrieve"),

View File

@@ -1,8 +1,8 @@
import { IdMap, MockManager, MockRepository } from "medusa-test-utils" import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
import { CreateRegionInput } from "../../types/region" import { CreateRegionInput } from "../../types/region"
import { FlagRouter } from "../../utils/flag-router" import { FlagRouter } from "../../utils/flag-router"
import EventBusService from "../event-bus"
import { import {
EventBusService,
FulfillmentProviderService, FulfillmentProviderService,
PaymentProviderService, PaymentProviderService,
StoreService, StoreService,

View File

@@ -1,9 +1,7 @@
import { IdMap, MockManager, MockRepository } from "medusa-test-utils" import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
import { EventBusService, StoreService } from "../index"
import SalesChannelService from "../sales-channel" import SalesChannelService from "../sales-channel"
import { EventBusServiceMock } from "../__mocks__/event-bus" 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" import { store, StoreServiceMock } from "../__mocks__/store"
describe("SalesChannelService", () => { describe("SalesChannelService", () => {

View File

@@ -1,14 +1,10 @@
import { IdMap, MockManager, MockRepository } from "medusa-test-utils" import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
import SwapService from "../swap" import { Order, Swap } from "../../models"
import { import { SwapRepository } from "../../repositories/swap"
ProductVariantInventoryServiceMock import CartService from "../cart"
} from "../__mocks__/product-variant-inventory" import EventBusService from "../event-bus"
import {
LineItemAdjustmentServiceMock
} from "../__mocks__/line-item-adjustment"
import { import {
CustomShippingOptionService, CustomShippingOptionService,
EventBusService,
FulfillmentService, FulfillmentService,
LineItemService, LineItemService,
OrderService, OrderService,
@@ -16,12 +12,16 @@ import {
ProductVariantInventoryService, ProductVariantInventoryService,
ReturnService, ReturnService,
ShippingOptionService, ShippingOptionService,
TotalsService, TotalsService
} from "../index" } from "../index"
import CartService from "../cart"
import { Order, Swap } from "../../models"
import { SwapRepository } from "../../repositories/swap"
import LineItemAdjustmentService from "../line-item-adjustment" 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 ******************** */ /* ******************** 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 { EntityManager } from "typeorm"
import { TransactionBaseService } from "../interfaces"
import { BatchJob } from "../models" import { BatchJob } from "../models"
import { BatchJobRepository } from "../repositories/batch-job" import { BatchJobRepository } from "../repositories/batch-job"
import { import {
@@ -10,11 +13,9 @@ import {
FilterableBatchJobProps, FilterableBatchJobProps,
} from "../types/batch-job" } from "../types/batch-job"
import { FindConfig } from "../types/common" import { FindConfig } from "../types/common"
import { TransactionBaseService } from "../interfaces"
import { buildQuery } from "../utils" import { buildQuery } from "../utils"
import { isDefined, MedusaError } from "medusa-core-utils" import EventBusService from "./event-bus"
import { EventBusService, StrategyResolverService } from "./index" import { StrategyResolverService } from "./index"
import { Request } from "express"
type InjectedDependencies = { type InjectedDependencies = {
manager: EntityManager manager: EntityManager

View File

@@ -1,6 +1,6 @@
import { isDefined, MedusaError } from "medusa-core-utils" import { isDefined, MedusaError } from "medusa-core-utils"
import { EntityManager } from "typeorm" import { EntityManager } from "typeorm"
import { EventBusService } from "." import { TransactionBaseService } from "../interfaces"
import { import {
DiscountCondition, DiscountCondition,
DiscountConditionCustomerGroup, DiscountConditionCustomerGroup,
@@ -13,8 +13,8 @@ import {
import { DiscountConditionRepository } from "../repositories/discount-condition" import { DiscountConditionRepository } from "../repositories/discount-condition"
import { FindConfig } from "../types/common" import { FindConfig } from "../types/common"
import { DiscountConditionInput } from "../types/discount" import { DiscountConditionInput } from "../types/discount"
import { TransactionBaseService } from "../interfaces"
import { buildQuery, PostgresError } from "../utils" import { buildQuery, PostgresError } from "../utils"
import EventBusService from "./event-bus"
type InjectedDependencies = { type InjectedDependencies = {
manager: EntityManager manager: EntityManager

View File

@@ -9,7 +9,6 @@ import {
In, In,
} from "typeorm" } from "typeorm"
import { import {
EventBusService,
NewTotalsService, NewTotalsService,
ProductService, ProductService,
RegionService, RegionService,
@@ -36,12 +35,13 @@ import {
UpdateDiscountInput, UpdateDiscountInput,
UpdateDiscountRuleInput, UpdateDiscountRuleInput,
} from "../types/discount" } from "../types/discount"
import { CalculationContextData } from "../types/totals"
import { buildQuery, setMetadata } from "../utils" import { buildQuery, setMetadata } from "../utils"
import { isFuture, isPast } from "../utils/date-helpers" import { isFuture, isPast } from "../utils/date-helpers"
import { FlagRouter } from "../utils/flag-router" import { FlagRouter } from "../utils/flag-router"
import CustomerService from "./customer" import CustomerService from "./customer"
import DiscountConditionService from "./discount-condition" import DiscountConditionService from "./discount-condition"
import { CalculationContextData } from "../types/totals" import EventBusService from "./event-bus"
/** /**
* Provides layer to manipulate discounts. * Provides layer to manipulate discounts.

View File

@@ -1,150 +1,68 @@
import Bull, { JobOptions } from "bull" import { EventBusTypes } from "@medusajs/types"
import Redis from "ioredis" import { EventBusUtils } from "@medusajs/utils"
import { DeepPartial, EntityManager, In } from "typeorm" import { EntityManager } from "typeorm"
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity" import { TransactionBaseService } from "../interfaces"
import { ulid } from "ulid"
import { StagedJob } from "../models" import { StagedJob } from "../models"
import { StagedJobRepository } from "../repositories/staged-job" import { ConfigModule } from "../types/global"
import { ConfigModule, Logger } from "../types/global"
import { isString } from "../utils" import { isString } from "../utils"
import { sleep } from "../utils/sleep" import { sleep } from "../utils/sleep"
import JobSchedulerService, { CreateJobOptions } from "./job-scheduler" import StagedJobService from "./staged-job"
type InjectedDependencies = { type InjectedDependencies = {
manager: EntityManager stagedJobService: StagedJobService
logger: Logger eventBusModuleService: EventBusUtils.AbstractEventBusModuleService
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
} }
/** /**
* Can keep track of multiple subscribers to different events and run the * Can keep track of multiple subscribers to different events and run the
* subscribers when events happen. Events will run asynchronously. * 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 config_: ConfigModule
protected readonly manager_: EntityManager protected readonly stagedJobService_: StagedJobService
protected readonly logger_: Logger // eslint-disable-next-line max-len
protected readonly stagedJobRepository_: typeof StagedJobRepository protected readonly eventBusModuleService_: EventBusUtils.AbstractEventBusModuleService
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 shouldEnqueuerRun: boolean protected shouldEnqueuerRun: boolean
protected transactionManager_: EntityManager | undefined
protected enqueue_: Promise<void> protected enqueue_: Promise<void>
constructor( constructor(
{ { stagedJobService, eventBusModuleService }: InjectedDependencies,
manager, config,
logger, isSingleton = true
stagedJobRepository,
redisClient,
redisSubscriber,
jobSchedulerService,
}: InjectedDependencies,
config: ConfigModule,
singleton = true
) { ) {
// eslint-disable-next-line prefer-rest-params
super(arguments[0])
this.config_ = config this.config_ = config
this.manager_ = manager this.eventBusModuleService_ = eventBusModuleService
this.logger_ = logger this.stagedJobService_ = stagedJobService
this.jobSchedulerService_ = jobSchedulerService
this.stagedJobRepository_ = stagedJobRepository
if (singleton) { if (process.env.NODE_ENV !== "test" && isSingleton) {
const opts = { this.startEnqueuer()
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()
}
} }
} }
withTransaction(transactionManager): this | EventBusService { withTransaction(transactionManager?: EntityManager): this {
if (!transactionManager) { if (!transactionManager) {
return this return this
} }
const cloned = new EventBusService( const cloned = new (this.constructor as any)(
{ {
manager: transactionManager, manager: transactionManager,
stagedJobRepository: this.stagedJobRepository_, stagedJobService: this.stagedJobService_,
jobSchedulerService: this.jobSchedulerService_, eventBusModuleService: this.eventBusModuleService_,
logger: this.logger_,
redisClient: this.redisClient_,
redisSubscriber: this.redisSubscriber_,
}, },
this.config_, this.config_,
false false
) )
cloned.manager_ = transactionManager
cloned.transactionManager_ = transactionManager cloned.transactionManager_ = transactionManager
cloned.queue_ = this.queue_
return cloned return cloned
} }
@@ -153,70 +71,36 @@ export default class EventBusService {
* Adds a function to a list of event subscribers. * Adds a function to a list of event subscribers.
* @param event - the event that the subscriber will listen for. * @param event - the event that the subscriber will listen for.
* @param subscriber - the function to be called when a certain event * @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. * happens. Subscribers must return a Promise.
* @param context - subscriber context
* @return this * @return this
*/ */
subscribe( subscribe(
event: string | symbol, event: string | symbol,
subscriber: Subscriber, subscriber: EventBusTypes.Subscriber,
context?: SubscriberContext context?: EventBusTypes.SubscriberContext
): this { ): this {
if (typeof subscriber !== "function") { if (typeof subscriber !== "function") {
throw new Error("Subscriber must be a function") throw new Error("Subscriber must be a function")
} }
/** this.eventBusModuleService_.subscribe(event, subscriber, context)
* 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,
])
return this return this
} }
/** /**
* Adds a function to a list of event subscribers. * Removes function from the list of event subscribers.
* @param event - the event that the subscriber will listen for. * @param event - the event of the subcriber.
* @param subscriber - the function to be called when a certain event * @param subscriber - the function to be removed
* happens. Subscribers must return a Promise. * @param context - subscriber context
* @return this * @return this
*/ */
unsubscribe(event: string | symbol, subscriber: Subscriber): this { unsubscribe(
if (typeof subscriber !== "function") { event: string | symbol,
throw new Error("Subscriber must be a function") subscriber: EventBusTypes.Subscriber,
} context: EventBusTypes.SubscriberContext
): this {
const existingSubscribers = this.eventToSubscribersMap_.get(event) this.eventBusModuleService_.unsubscribe(event, subscriber, context)
if (existingSubscribers?.length) {
const subIndex = existingSubscribers?.findIndex(
(sub) => sub.subscriber === subscriber
)
if (subIndex !== -1) {
this.eventToSubscribersMap_.get(event)?.splice(subIndex as number, 1)
}
}
return this return this
} }
@@ -225,7 +109,7 @@ export default class EventBusService {
* @param data - The data to use to process the events * @param data - The data to use to process the events
* @return the jobs from our queue * @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. * Calls all subscribers when an event occurs.
@@ -237,80 +121,54 @@ export default class EventBusService {
async emit<T>( async emit<T>(
eventName: string, eventName: string,
data: T, data: T,
options?: Record<string, unknown> & EmitOptions options?: Record<string, unknown>
): Promise<StagedJob | void> ): Promise<StagedJob | void>
async emit< async emit<
T, T,
TInput extends string | EmitData<T>[] = string, TInput extends string | EventBusTypes.EmitData<T>[] = string,
TResult = TInput extends EmitData<T>[] ? StagedJob[] : StagedJob TResult = TInput extends EventBusTypes.EmitData<T>[]
? StagedJob[]
: StagedJob
>( >(
eventNameOrData: TInput, eventNameOrData: TInput,
data?: T, data?: T,
options: Record<string, unknown> & EmitOptions = {} options: Record<string, unknown> = {}
): Promise<TResult | void> { ): Promise<TResult | void> {
const globalEventOptions = this.config_?.projectConfig?.event_options ?? {} const manager = this.activeManager_
const isBulkEmit = !isString(eventNameOrData) const isBulkEmit = !isString(eventNameOrData)
const events = isBulkEmit const events = isBulkEmit
? eventNameOrData.map((event) => ({ ? eventNameOrData.map((event) => ({
data: { eventName: event.eventName, data: event.data }, eventName: event.eventName,
opts: event.opts, data: event.data,
options: event.options,
})) }))
: [ : [
{ {
data: { eventName: eventNameOrData, data }, eventName: eventNameOrData,
opts: options, 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 * We store events in the database when in an ongoing transaction.
* 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.
* *
* 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. * as part of the rollback.
*/ */
if (this.transactionManager_) { const stagedJobs = await this.stagedJobService_
const stagedJobRepository = this.transactionManager_.withRepository( .withTransaction(manager)
this.stagedJobRepository_ .create(events)
)
const jobsToCreate = events.map((event) => { return (!isBulkEmit ? stagedJobs[0] : stagedJobs) as unknown as TResult
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)
}
} }
startEnqueuer(): void { startEnqueuer(): void {
@@ -324,17 +182,14 @@ export default class EventBusService {
} }
async enqueuer_(): Promise<void> { async enqueuer_(): Promise<void> {
while (this.shouldEnqueuerRun) { const listConfig = {
const listConfig = { relations: [],
relations: [], skip: 0,
skip: 0, take: 1000,
take: 1000, }
}
const stagedJobRepo = this.manager_.withRepository( while (this.shouldEnqueuerRun) {
this.stagedJobRepository_ const jobs = await this.stagedJobService_.list(listConfig)
)
const jobs = await stagedJobRepo.find(listConfig)
if (!jobs.length) { if (!jobs.length) {
await sleep(3000) await sleep(3000)
@@ -343,138 +198,17 @@ export default class EventBusService {
const eventsData = jobs.map((job) => { const eventsData = jobs.map((job) => {
return { return {
data: { eventName: job.event_name, data: job.data }, eventName: job.event_name,
opts: { jobId: job.id, ...job.options }, data: job.data,
options: { jobId: job.id, ...job.options },
} }
}) })
await this.queue_.addBulk(eventsData).then(async () => { await this.eventBusModuleService_.emit(eventsData).then(async () => {
return await stagedJobRepo.delete({ id: In(jobs.map((j) => j.id)) }) return await this.stagedJobService_.delete(jobs.map((j) => j.id))
}) })
await sleep(3000) 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 { isDefined, MedusaError } from "medusa-core-utils"
import randomize from "randomatic" import randomize from "randomatic"
import { EntityManager } from "typeorm" import { EntityManager } from "typeorm"
import { EventBusService } from "."
import { TransactionBaseService } from "../interfaces" import { TransactionBaseService } from "../interfaces"
import { GiftCard, Region } from "../models" import { GiftCard, Region } from "../models"
import { GiftCardRepository } from "../repositories/gift-card" import { GiftCardRepository } from "../repositories/gift-card"
@@ -13,6 +12,7 @@ import {
UpdateGiftCardInput, UpdateGiftCardInput,
} from "../types/gift-card" } from "../types/gift-card"
import { buildQuery, setMetadata } from "../utils" import { buildQuery, setMetadata } from "../utils"
import EventBusService from "./event-bus"
import RegionService from "./region" import RegionService from "./region"
type InjectedDependencies = { 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 LineItemService } from "./line-item"
export { default as LineItemAdjustmentService } from "./line-item-adjustment" export { default as LineItemAdjustmentService } from "./line-item-adjustment"
export { default as MiddlewareService } from "./middleware" export { default as MiddlewareService } from "./middleware"
export { default as NewTotalsService } from "./new-totals"
export { default as NoteService } from "./note" export { default as NoteService } from "./note"
export { default as NotificationService } from "./notification" export { default as NotificationService } from "./notification"
export { default as OauthService } from "./oauth" export { default as OauthService } from "./oauth"
export { default as OrderService } from "./order" export { default as OrderService } from "./order"
export { default as OrderEditService } from "./order-edit" export { default as OrderEditService } from "./order-edit"
export { default as OrderEditItemChangeService } from "./order-edit-item-change" 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 PaymentCollectionService } from "./payment-collection"
export { default as PaymentProviderService } from "./payment-provider" export { default as PaymentProviderService } from "./payment-provider"
export { default as PaymentService } from "./payment"
export { default as PriceListService } from "./price-list" export { default as PriceListService } from "./price-list"
export { default as PricingService } from "./pricing" export { default as PricingService } from "./pricing"
export { default as ProductService } from "./product" export { default as ProductService } from "./product"
export { default as ProductCategoryService } from "./product-category" export { default as ProductCategoryService } from "./product-category"
export { default as ProductCollectionService } from "./product-collection" export { default as ProductCollectionService } from "./product-collection"
export { default as ProductTypeService } from "./product-type" 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 ProductVariantService } from "./product-variant"
export { default as ProductVariantInventoryService } from "./product-variant-inventory"
export { default as RegionService } from "./region" export { default as RegionService } from "./region"
export { default as ReturnService } from "./return" export { default as ReturnService } from "./return"
export { default as ReturnReasonService } from "./return-reason" 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 SalesChannelInventoryService } from "./sales-channel-inventory"
export { default as SalesChannelLocationService } from "./sales-channel-location" export { default as SalesChannelLocationService } from "./sales-channel-location"
export { default as SalesChannelService } from "./sales-channel"
export { default as SearchService } from "./search" export { default as SearchService } from "./search"
export { default as ShippingOptionService } from "./shipping-option" export { default as ShippingOptionService } from "./shipping-option"
export { default as ShippingProfileService } from "./shipping-profile" export { default as ShippingProfileService } from "./shipping-profile"
export { default as StagedJobService } from "./staged-job"
export { default as StoreService } from "./store" export { default as StoreService } from "./store"
export { default as StrategyResolverService } from "./strategy-resolver" export { default as StrategyResolverService } from "./strategy-resolver"
export { default as SwapService } from "./swap" 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 TaxRateService } from "./tax-rate"
export { default as TokenService } from "./token" export { default as TokenService } from "./token"
export { default as TotalsService } from "./totals" export { default as TotalsService } from "./totals"
export { default as NewTotalsService } from "./new-totals"
export { default as UserService } from "./user" export { default as UserService } from "./user"

View File

@@ -1,15 +1,16 @@
import jwt, { JwtPayload } from "jsonwebtoken" import jwt, { JwtPayload } from "jsonwebtoken"
import { MedusaError } from "medusa-core-utils" import { MedusaError } from "medusa-core-utils"
import { EntityManager } from "typeorm" import { EntityManager } from "typeorm"
import { EventBusService, UserService } from "." import { UserService } from "."
import { User } from ".." import { User } from ".."
import { TransactionBaseService } from "../interfaces"
import { UserRoles } from "../models/user" import { UserRoles } from "../models/user"
import { InviteRepository } from "../repositories/invite" import { InviteRepository } from "../repositories/invite"
import { UserRepository } from "../repositories/user" import { UserRepository } from "../repositories/user"
import { ListInvite } from "../types/invites"
import { ConfigModule } from "../types/global" import { ConfigModule } from "../types/global"
import { TransactionBaseService } from "../interfaces" import { ListInvite } from "../types/invites"
import { buildQuery } from "../utils" import { buildQuery } from "../utils"
import EventBusService from "./event-bus"
// 7 days // 7 days
const DEFAULT_VALID_DURATION = 1000 * 60 * 60 * 24 * 7 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 Redis from "ioredis"
import { ConfigModule, Logger } from "../types/global" import { ConfigModule, Logger } from "../types/global"
type InjectedDependencies = { type InjectedDependencies = {
logger: Logger logger: Logger
redisClient: Redis.Redis
redisSubscriber: Redis.Redis
} }
type ScheduledJobHandler<T = unknown> = ( type ScheduledJobHandler<T = unknown> = (
@@ -22,36 +20,34 @@ export default class JobSchedulerService {
protected readonly logger_: Logger protected readonly logger_: Logger
protected readonly handlers_: Map<string | symbol, ScheduledJobHandler[]> = protected readonly handlers_: Map<string | symbol, ScheduledJobHandler[]> =
new Map() new Map()
protected readonly queue_: Bull protected readonly queue_: Queue
constructor( constructor(
{ logger, redisClient, redisSubscriber }: InjectedDependencies, { logger }: InjectedDependencies,
config: ConfigModule, config: ConfigModule,
singleton = true singleton = true
) { ) {
this.config_ = config this.config_ = config
this.logger_ = logger this.logger_ = logger
if (singleton) { if (singleton && config?.projectConfig?.redis_url) {
const opts = { // Required config
createClient: (type: string): Redis.Redis => { // See: https://github.com/OptimalBits/bull/blob/develop/CHANGELOG.md#breaking-changes
switch (type) { const connection = new Redis(config.projectConfig.redis_url, {
case "client": maxRetriesPerRequest: null,
return redisClient enableReadyCheck: false,
case "subscriber": })
return redisSubscriber
default: this.queue_ = new Queue(`scheduled-jobs:queue`, {
if (config.projectConfig.redis_url) { connection,
return new Redis(config.projectConfig.redis_url) prefix: `${this.constructor.name}`,
} })
return redisClient
}
},
}
this.queue_ = new Bull(`scheduled-jobs:queue`, opts)
// Register scheduled job worker // 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, schedule: string,
handler: ScheduledJobHandler, handler: ScheduledJobHandler,
options: CreateJobOptions options: CreateJobOptions
): Promise<void> { ): Promise<Job> {
this.logger_.info(`Registering ${eventName}`) this.logger_.info(`Registering ${eventName}`)
this.registerHandler(eventName, handler) this.registerHandler(eventName, handler)
@@ -120,10 +116,10 @@ export default class JobSchedulerService {
eventName, eventName,
data, data,
} }
const repeatOpts = { repeat: { cron: schedule } } const repeatOpts = { repeat: { pattern: schedule } }
if (options?.keepExisting) { if (options?.keepExisting) {
return await this.queue_.add(jobToCreate, repeatOpts) return await this.queue_.add(eventName, jobToCreate, repeatOpts)
} }
const existingJobs = (await this.queue_.getRepeatableJobs()) ?? [] const existingJobs = (await this.queue_.getRepeatableJobs()) ?? []
@@ -134,6 +130,6 @@ export default class JobSchedulerService {
await this.queue_.removeRepeatableByKey(existingJob.key) 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 { isDefined, MedusaError } from "medusa-core-utils"
import { EntityManager } from "typeorm" import { EntityManager } from "typeorm"
import { TransactionBaseService } from "../interfaces" 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 { Note } from "../models"
import { buildQuery } from "../utils" import { NoteRepository } from "../repositories/note"
import { FindConfig, Selector } from "../types/common"
import { CreateNoteInput } from "../types/note" import { CreateNoteInput } from "../types/note"
import { buildQuery } from "../utils"
import EventBusService from "./event-bus"
type InjectedDependencies = { type InjectedDependencies = {
manager: EntityManager manager: EntityManager

View File

@@ -1,18 +1,19 @@
import { TransactionBaseService } from "../interfaces" import { EventBusTypes } from "@medusajs/types"
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 { MedusaError } from "medusa-core-utils" 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 { CreateOrderEditItemChangeInput } from "../types/order-edit"
import { buildQuery } from "../utils"
import { LineItemService } from "./index"
import TaxProviderService from "./tax-provider"
type InjectedDependencies = { type InjectedDependencies = {
manager: EntityManager manager: EntityManager
orderItemChangeRepository: typeof OrderItemChangeRepository orderItemChangeRepository: typeof OrderItemChangeRepository
eventBusService: EventBusService eventBusService: EventBusTypes.IEventBusService
lineItemService: LineItemService lineItemService: LineItemService
taxProviderService: TaxProviderService taxProviderService: TaxProviderService
} }
@@ -25,7 +26,7 @@ export default class OrderEditItemChangeService extends TransactionBaseService {
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
protected readonly orderItemChangeRepository_: typeof OrderItemChangeRepository protected readonly orderItemChangeRepository_: typeof OrderItemChangeRepository
protected readonly eventBus_: EventBusService protected readonly eventBus_: EventBusTypes.IEventBusService
protected readonly lineItemService_: LineItemService protected readonly lineItemService_: LineItemService
protected readonly taxProviderService_: TaxProviderService protected readonly taxProviderService_: TaxProviderService

View File

@@ -22,8 +22,8 @@ import {
CreateOrderEditInput, CreateOrderEditInput,
} from "../types/order-edit" } from "../types/order-edit"
import { buildQuery, isString } from "../utils" import { buildQuery, isString } from "../utils"
import EventBusService from "./event-bus"
import { import {
EventBusService,
LineItemAdjustmentService, LineItemAdjustmentService,
LineItemService, LineItemService,
NewTotalsService, NewTotalsService,

View File

@@ -8,7 +8,7 @@ import {
Not, Not,
Raw, Raw,
} from "typeorm" } from "typeorm"
import { TransactionBaseService } from "../interfaces" import { IInventoryService, TransactionBaseService } from "../interfaces"
import SalesChannelFeatureFlag from "../loaders/feature-flags/sales-channels" import SalesChannelFeatureFlag from "../loaders/feature-flags/sales-channels"
import { import {
Address, Address,
@@ -50,7 +50,6 @@ import {
CustomerService, CustomerService,
DiscountService, DiscountService,
DraftOrderService, DraftOrderService,
EventBusService,
FulfillmentProviderService, FulfillmentProviderService,
FulfillmentService, FulfillmentService,
GiftCardService, GiftCardService,
@@ -64,6 +63,7 @@ import {
TaxProviderService, TaxProviderService,
TotalsService TotalsService
} from "." } from "."
import EventBusService from "./event-bus"
export const ORDER_CART_ALREADY_EXISTS_ERROR = "Order from cart already exists" export const ORDER_CART_ALREADY_EXISTS_ERROR = "Order from cart already exists"
@@ -86,6 +86,7 @@ type InjectedDependencies = {
addressRepository: typeof AddressRepository addressRepository: typeof AddressRepository
giftCardService: GiftCardService giftCardService: GiftCardService
draftOrderService: DraftOrderService draftOrderService: DraftOrderService
inventoryService: IInventoryService
eventBusService: EventBusService eventBusService: EventBusService
featureFlagRouter: FlagRouter featureFlagRouter: FlagRouter
productVariantInventoryService: ProductVariantInventoryService productVariantInventoryService: ProductVariantInventoryService
@@ -128,6 +129,7 @@ class OrderService extends TransactionBaseService {
protected readonly addressRepository_: typeof AddressRepository protected readonly addressRepository_: typeof AddressRepository
protected readonly giftCardService_: GiftCardService protected readonly giftCardService_: GiftCardService
protected readonly draftOrderService_: DraftOrderService protected readonly draftOrderService_: DraftOrderService
protected readonly inventoryService_: IInventoryService
protected readonly eventBus_: EventBusService protected readonly eventBus_: EventBusService
protected readonly featureFlagRouter_: FlagRouter protected readonly featureFlagRouter_: FlagRouter
// eslint-disable-next-line max-len // 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 { 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 { import {
PaymentCollection, PaymentCollection,
PaymentCollectionStatus, PaymentCollectionStatus,
PaymentSession, PaymentSession,
PaymentSessionStatus, PaymentSessionStatus,
} from "../models" } from "../models"
import { TransactionBaseService } from "../interfaces" import { PaymentCollectionRepository } from "../repositories/payment-collection"
import { import { FindConfig } from "../types/common"
CustomerService, import { buildQuery, isString, setMetadata } from "../utils"
EventBusService, import { CustomerService, PaymentProviderService } from "./index"
PaymentProviderService,
} from "./index"
import { TransactionBaseService } from "../interfaces"
import { CreatePaymentInput, PaymentSessionInput } from "../types/payment"
import { import {
CreatePaymentCollectionInput, CreatePaymentCollectionInput,
PaymentCollectionsSessionsBatchInput, PaymentCollectionsSessionsBatchInput,
PaymentCollectionsSessionsInput, PaymentCollectionsSessionsInput,
} from "../types/payment-collection" } from "../types/payment-collection"
import { CreatePaymentInput, PaymentSessionInput } from "../types/payment" import EventBusService from "./event-bus"
type InjectedDependencies = { type InjectedDependencies = {
manager: EntityManager 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 { 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 { TransactionBaseService } from "../interfaces"
import { EventBusService, PaymentProviderService } from "./index" import { Payment, Refund } from "../models"
import { buildQuery } from "../utils"
import { FindConfig } from "../types/common" import { FindConfig } from "../types/common"
import { buildQuery } from "../utils"
import EventBusService from "./event-bus"
import { PaymentProviderService } from "./index"
type InjectedDependencies = { type InjectedDependencies = {
manager: EntityManager manager: EntityManager

View File

@@ -1,23 +1,22 @@
import { isDefined, MedusaError } from "medusa-core-utils" 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 { TransactionBaseService } from "../interfaces"
import { ProductCategory } from "../models" import { ProductCategory } from "../models"
import { ProductCategoryRepository } from "../repositories/product-category" import { ProductCategoryRepository } from "../repositories/product-category"
import { import {
FindConfig, FindConfig,
QuerySelector, QuerySelector,
TreeQuerySelector,
Selector, Selector,
TreeQuerySelector,
} from "../types/common" } from "../types/common"
import { buildQuery, nullableValue } from "../utils"
import { EventBusService } from "."
import { import {
CreateProductCategoryInput, CreateProductCategoryInput,
UpdateProductCategoryInput,
ReorderConditions, ReorderConditions,
tempReorderRank, tempReorderRank,
UpdateProductCategoryInput,
} from "../types/product-category" } from "../types/product-category"
import { isNumber } from "lodash" import { buildQuery, nullableValue } from "../utils"
type InjectedDependencies = { type InjectedDependencies = {
manager: EntityManager manager: EntityManager

View File

@@ -1,5 +1,3 @@
import { FlagRouter } from "../utils/flag-router"
import { isDefined, MedusaError } from "medusa-core-utils" import { isDefined, MedusaError } from "medusa-core-utils"
import { EntityManager } from "typeorm" import { EntityManager } from "typeorm"
import { ProductVariantService, SearchService } from "." import { ProductVariantService, SearchService } from "."
@@ -31,6 +29,7 @@ import {
UpdateProductInput, UpdateProductInput,
} from "../types/product" } from "../types/product"
import { buildQuery, isString, setMetadata } from "../utils" import { buildQuery, isString, setMetadata } from "../utils"
import { FlagRouter } from "../utils/flag-router"
import EventBusService from "./event-bus" import EventBusService from "./event-bus"
type InjectedDependencies = { type InjectedDependencies = {

View File

@@ -1,17 +1,17 @@
import { EntityManager, FindOptionsWhere, ILike } from "typeorm"
import { isDefined, MedusaError } from "medusa-core-utils" 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 { TransactionBaseService } from "../interfaces"
import EventBusService from "./event-bus" import { PublishableApiKey, SalesChannel } from "../models"
import { buildQuery, isString } from "../utils" import { PublishableApiKeyRepository } from "../repositories/publishable-api-key"
import { PublishableApiKeySalesChannelRepository } from "../repositories/publishable-api-key-sales-channel"
import { FindConfig, Selector } from "../types/common"
import { import {
CreatePublishableApiKeyInput, CreatePublishableApiKeyInput,
UpdatePublishableApiKeyInput, UpdatePublishableApiKeyInput,
} from "../types/publishable-api-key" } 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 = { type InjectedDependencies = {
manager: EntityManager manager: EntityManager

View File

@@ -1,18 +1,20 @@
import { EntityManager } from "typeorm" import { EntityManager } from "typeorm"
import { EventBusService, SalesChannelLocationService } from "./"
import { IInventoryService, TransactionBaseService } from "../interfaces" import { IInventoryService, TransactionBaseService } from "../interfaces"
import { EventBusTypes } from "@medusajs/types"
import { SalesChannelLocationService } from "./"
type InjectedDependencies = { type InjectedDependencies = {
inventoryService: IInventoryService inventoryService: IInventoryService
salesChannelLocationService: SalesChannelLocationService salesChannelLocationService: SalesChannelLocationService
eventBusService: EventBusService eventBusService: EventBusTypes.IEventBusService
manager: EntityManager manager: EntityManager
} }
class SalesChannelInventoryService extends TransactionBaseService { class SalesChannelInventoryService extends TransactionBaseService {
protected readonly salesChannelLocationService_: SalesChannelLocationService protected readonly salesChannelLocationService_: SalesChannelLocationService
protected readonly eventBusService_: EventBusService protected readonly eventBusService_: EventBusTypes.IEventBusService
protected readonly inventoryService_: IInventoryService protected readonly inventoryService_: IInventoryService
constructor({ constructor({

View File

@@ -1,14 +1,16 @@
import { EntityManager, In } from "typeorm" import { EntityManager, In } from "typeorm"
import { IStockLocationService, TransactionBaseService } from "../interfaces" 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 { MedusaError } from "medusa-core-utils"
import { SalesChannelLocation } from "../models/sales-channel-location"
type InjectedDependencies = { type InjectedDependencies = {
stockLocationService: IStockLocationService stockLocationService: IStockLocationService
salesChannelService: SalesChannelService salesChannelService: SalesChannelService
eventBusService: EventBusService eventBusService: EventBusTypes.IEventBusService
manager: EntityManager manager: EntityManager
} }
@@ -18,7 +20,7 @@ type InjectedDependencies = {
class SalesChannelLocationService extends TransactionBaseService { class SalesChannelLocationService extends TransactionBaseService {
protected readonly salesChannelService_: SalesChannelService protected readonly salesChannelService_: SalesChannelService
protected readonly eventBusService_: EventBusService protected readonly eventBusService_: EventBusTypes.IEventBusService
protected readonly stockLocationService_: IStockLocationService protected readonly stockLocationService_: IStockLocationService
constructor({ constructor({

View File

@@ -1,17 +1,17 @@
import { FindConfig, QuerySelector, Selector } from "../types/common"
import { import {
CreateSalesChannelInput, CreateSalesChannelInput,
UpdateSalesChannelInput, UpdateSalesChannelInput,
} from "../types/sales-channels" } 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 { isDefined, MedusaError } from "medusa-core-utils"
import { EntityManager } from "typeorm"
import { TransactionBaseService } from "../interfaces"
import { SalesChannel } from "../models" import { SalesChannel } from "../models"
import { SalesChannelRepository } from "../repositories/sales-channel" import { SalesChannelRepository } from "../repositories/sales-channel"
import StoreService from "./store"
import { TransactionBaseService } from "../interfaces"
import { buildQuery } from "../utils" import { buildQuery } from "../utils"
import EventBusService from "./event-bus"
import StoreService from "./store"
type InjectedDependencies = { type InjectedDependencies = {
salesChannelRepository: typeof SalesChannelRepository 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 { FindConfig, Selector, WithRequiredProperty } from "../types/common"
import { CreateShipmentConfig } from "../types/fulfillment" import { CreateShipmentConfig } from "../types/fulfillment"
import { OrdersReturnItem } from "../types/orders" import { OrdersReturnItem } from "../types/orders"
import CartService from "./cart"
import { import {
CartService,
CustomShippingOptionService, CustomShippingOptionService,
EventBusService, EventBusService,
FulfillmentService, FulfillmentService,
LineItemAdjustmentService,
LineItemService, LineItemService,
OrderService, OrderService,
PaymentProviderService, PaymentProviderService,
@@ -36,7 +37,6 @@ import {
ShippingOptionService, ShippingOptionService,
TotalsService, TotalsService,
} from "./index" } from "./index"
import LineItemAdjustmentService from "./line-item-adjustment"
type InjectedProps = { type InjectedProps = {
manager: EntityManager manager: EntityManager
@@ -44,7 +44,6 @@ type InjectedProps = {
swapRepository: typeof SwapRepository swapRepository: typeof SwapRepository
cartService: CartService cartService: CartService
eventBus: EventBusService
orderService: OrderService orderService: OrderService
returnService: ReturnService returnService: ReturnService
totalsService: TotalsService totalsService: TotalsService

View File

@@ -1,10 +1,14 @@
import { MedusaError } from "medusa-core-utils"
import { AwilixContainer } from "awilix" import { AwilixContainer } from "awilix"
import { MedusaError } from "medusa-core-utils"
import { In } from "typeorm" import { In } from "typeorm"
import { LineItemTaxLineRepository } from "../repositories/line-item-tax-line" import {
import { ShippingMethodTaxLineRepository } from "../repositories/shipping-method-tax-line" ICacheService,
import { TaxProviderRepository } from "../repositories/tax-provider" ITaxService,
ItemTaxCalculationLine,
TaxCalculationContext,
TransactionBaseService,
} from "../interfaces"
import { import {
Cart, Cart,
LineItem, LineItem,
@@ -14,19 +18,15 @@ import {
ShippingMethodTaxLine, ShippingMethodTaxLine,
TaxProvider, TaxProvider,
} from "../models" } 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 { isCart } from "../types/cart"
import {
ICacheService,
ITaxService,
ItemTaxCalculationLine,
TaxCalculationContext,
TransactionBaseService,
} from "../interfaces"
import { TaxLinesMaps, TaxServiceRate } from "../types/tax-service" import { TaxLinesMaps, TaxServiceRate } from "../types/tax-service"
import EventBusService from "./event-bus"
import TaxRateService from "./tax-rate" import TaxRateService from "./tax-rate"
import EventBusService from "./event-bus"
type RegionDetails = { type RegionDetails = {
id: string id: string

View File

@@ -6,16 +6,15 @@ import {
orderExportPropertiesDescriptors, orderExportPropertiesDescriptors,
} from "." } from "."
import { AdminPostBatchesReq } from "../../../api" import { AdminPostBatchesReq } from "../../../api"
import { IFileService } from "../../../interfaces" import { AbstractBatchJobStrategy, IFileService } from "../../../interfaces"
import { AbstractBatchJobStrategy } from "../../../interfaces" import SalesChannelFeatureFlag from "../../../loaders/feature-flags/sales-channels"
import { Order } from "../../../models" import { Order } from "../../../models"
import { OrderService } from "../../../services" import { OrderService } from "../../../services"
import BatchJobService from "../../../services/batch-job" import BatchJobService from "../../../services/batch-job"
import { BatchJobStatus } from "../../../types/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 { FindConfig } from "../../../types/common"
import { FlagRouter } from "../../../utils/flag-router"
import { prepareListQuery } from "../../../utils/get-query-config"
type InjectedDependencies = { type InjectedDependencies = {
fileService: IFileService 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 { EntityManager } from "typeorm"
import {
BatchJobService,
EventBusService,
StrategyResolverService,
} from "../services"
type InjectedDependencies = { type InjectedDependencies = {
eventBusService: EventBusService eventBusService: EventBusService
@@ -27,9 +29,15 @@ class BatchJobSubscriber {
this.strategyResolver_ = strategyResolverService this.strategyResolver_ = strategyResolverService
this.manager_ = manager this.manager_ = manager
this.eventBusService_ this.eventBusService_.subscribe(
.subscribe(BatchJobService.Events.CREATED, this.preProcessBatchJob) BatchJobService.Events.CREATED,
.subscribe(BatchJobService.Events.CONFIRMED, this.processBatchJob) this.preProcessBatchJob
) as EventBusService
this.eventBusService_.subscribe(
BatchJobService.Events.CONFIRMED,
this.processBatchJob
) as EventBusService
} }
preProcessBatchJob = async (data): Promise<void> => { 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 { EntityManager } from "typeorm"
import { CartService, EventBusService } from "../services"
type InjectedDependencies = { type InjectedDependencies = {
eventBusService: EventBusService eventBusService: EventBusService

View File

@@ -1,21 +1,21 @@
import { EventBusTypes } from "@medusajs/types"
import { indexTypes } from "medusa-core-utils" import { indexTypes } from "medusa-core-utils"
import { ISearchService } from "../interfaces" import { ISearchService } from "../interfaces"
import ProductCategoryFeatureFlag from "../loaders/feature-flags/product-categories" import ProductCategoryFeatureFlag from "../loaders/feature-flags/product-categories"
import { SEARCH_INDEX_EVENT } from "../loaders/search-index" import { SEARCH_INDEX_EVENT } from "../loaders/search-index"
import { Product } from "../models" import { Product } from "../models"
import EventBusService from "../services/event-bus"
import ProductService from "../services/product" import ProductService from "../services/product"
import { FlagRouter } from "../utils/flag-router" import { FlagRouter } from "../utils/flag-router"
type InjectedDependencies = { type InjectedDependencies = {
eventBusService: EventBusService eventBusService: EventBusTypes.IEventBusService
searchService: ISearchService searchService: ISearchService
productService: ProductService productService: ProductService
featureFlagRouter: FlagRouter featureFlagRouter: FlagRouter
} }
class SearchIndexingSubscriber { class SearchIndexingSubscriber {
private readonly eventBusService_: EventBusService private readonly eventBusService_: EventBusTypes.IEventBusService
private readonly searchService_: ISearchService private readonly searchService_: ISearchService
private readonly productService_: ProductService private readonly productService_: ProductService
private readonly featureFlagRouter_: FlagRouter private readonly featureFlagRouter_: FlagRouter

View File

@@ -1,13 +1,8 @@
import { import { CommonTypes } from "@medusajs/types"
ExternalModuleDeclaration,
InternalModuleDeclaration,
} from "@medusajs/modules-sdk"
import { Request } from "express" import { Request } from "express"
import { MedusaContainer as coreMedusaContainer } from "medusa-core-utils" import { MedusaContainer as coreMedusaContainer } from "medusa-core-utils"
import { LoggerOptions } from "typeorm"
import { Logger as _Logger } from "winston" import { Logger as _Logger } from "winston"
import { Customer, User } from "../models" import { Customer, User } from "../models"
import { EmitOptions } from "../services/event-bus"
import { FindConfig, RequestQueryFields } from "./common" import { FindConfig, RequestQueryFields } from "./common"
declare global { declare global {
@@ -44,67 +39,4 @@ export type Logger = _Logger & {
export type Constructor<T> = new (...args: any[]) => T export type Constructor<T> = new (...args: any[]) => T
type SessionOptions = { export type ConfigModule = CommonTypes.ConfigModule
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
)[]
}

View File

@@ -1,5 +1,3 @@
export async function sleep(ms: number) { import { promisify } from "util"
return new Promise((resolve) => {
setTimeout(resolve, ms) export const sleep = promisify(setTimeout)
})
}

View File

@@ -1,6 +1,18 @@
import { ModuleDefinition, MODULE_RESOURCE_TYPE, MODULE_SCOPE } from "./types" import { ModuleDefinition, MODULE_RESOURCE_TYPE, MODULE_SCOPE } from "./types"
export const MODULE_DEFINITIONS: ModuleDefinition[] = [ 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", key: "stockLocationService",
registrationName: "stockLocationService", registrationName: "stockLocationService",

View File

@@ -1,15 +1,16 @@
import { IEventBusService, IStockLocationService } from "@medusajs/medusa" import { IStockLocationService } from "@medusajs/medusa"
import { import {
ExternalModuleDeclaration, ExternalModuleDeclaration,
InternalModuleDeclaration, InternalModuleDeclaration,
MedusaModule, MedusaModule
} from "@medusajs/modules-sdk" } from "@medusajs/modules-sdk"
import { EventBusTypes } from "@medusajs/types"
import { StockLocationServiceInitializeOptions } from "../types" import { StockLocationServiceInitializeOptions } from "../types"
export const initialize = async ( export const initialize = async (
options?: StockLocationServiceInitializeOptions | ExternalModuleDeclaration, options?: StockLocationServiceInitializeOptions | ExternalModuleDeclaration,
injectedDependencies?: { injectedDependencies?: {
eventBusService: IEventBusService eventBusService: EventBusTypes.IEventBusService
} }
): Promise<IStockLocationService> => { ): Promise<IStockLocationService> => {
const serviceKey = "stockLocationService" const serviceKey = "stockLocationService"

View File

@@ -3,13 +3,12 @@ import {
CreateStockLocationInput, CreateStockLocationInput,
FilterableStockLocationProps, FilterableStockLocationProps,
FindConfig, FindConfig,
IEventBusService,
setMetadata, setMetadata,
StockLocationAddressInput, StockLocationAddressInput,
UpdateStockLocationInput, UpdateStockLocationInput
} from "@medusajs/medusa" } from "@medusajs/medusa"
import { InternalModuleDeclaration } from "@medusajs/modules-sdk" import { InternalModuleDeclaration } from "@medusajs/modules-sdk"
import { SharedContext } from "@medusajs/types" import { EventBusTypes, SharedContext } from "@medusajs/types"
import { InjectEntityManager, MedusaContext } from "@medusajs/utils" import { InjectEntityManager, MedusaContext } from "@medusajs/utils"
import { isDefined, MedusaError } from "medusa-core-utils" import { isDefined, MedusaError } from "medusa-core-utils"
import { EntityManager } from "typeorm" import { EntityManager } from "typeorm"
@@ -17,7 +16,7 @@ import { StockLocation, StockLocationAddress } from "../models"
type InjectedDependencies = { type InjectedDependencies = {
manager: EntityManager manager: EntityManager
eventBusService: IEventBusService eventBusService: EventBusTypes.IEventBusService
} }
/** /**
@@ -32,7 +31,7 @@ export default class StockLocationService {
} }
protected readonly manager_: EntityManager protected readonly manager_: EntityManager
protected readonly eventBusService_: IEventBusService protected readonly eventBusService_: EventBusTypes.IEventBusService
constructor( constructor(
{ eventBusService, manager }: InjectedDependencies, { eventBusService, manager }: InjectedDependencies,

View File

@@ -16,6 +16,9 @@
], ],
"author": "Medusa", "author": "Medusa",
"license": "MIT", "license": "MIT",
"dependencies": {
"@medusajs/modules-sdk": "^0.0.1"
},
"devDependencies": { "devDependencies": {
"cross-env": "^5.2.1", "cross-env": "^5.2.1",
"typeorm": "^0.3.11", "typeorm": "^0.3.11",
@@ -24,6 +27,7 @@
"scripts": { "scripts": {
"prepare": "cross-env NODE_ENV=production yarn run build", "prepare": "cross-env NODE_ENV=production yarn run build",
"build": "tsc --build", "build": "tsc --build",
"watch": "tsc --build --watch",
"test": "exit 0" "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