Chore/framework 6/n (#8356)

**What**
Refactor and improve job loader as well as move it to the framework

FIXES FRMW-2626
This commit is contained in:
Adrien de Peretti
2024-07-31 11:17:17 +02:00
committed by GitHub
parent 838eb0e34a
commit 6ccf83128c
13 changed files with 215 additions and 126 deletions

View File

@@ -12,7 +12,8 @@
"./config": "./dist/config/index.js",
"./logger": "./dist/logger/index.js",
"./database": "./dist/database/index.js",
"./subscribers": "./dist/subscribers/index.js"
"./subscribers": "./dist/subscribers/index.js",
"./jobs": "./dist/jobs/index.js"
},
"engines": {
"node": ">=20"
@@ -30,8 +31,8 @@
"scripts": {
"watch": "tsc --watch -p ./tsconfig.build.json",
"watch:test": "tsc --build tsconfig.spec.json --watch",
"prepublishOnly": "cross-env NODE_ENV=production tsc -p ./tsconfig.build.json && tsc-alias -p ./tsconfig.build.json",
"build": "rimraf dist && tsc --build && tsc-alias",
"prepublishOnly": "tsc -p ./tsconfig.build.json && tsc-alias -p ./tsconfig.build.json",
"build": "rimraf dist && tsc --noEmit && yarn prepublishOnly",
"test": "jest --runInBand --bail --passWithNoTests --forceExit -- src",
"test:integration": "jest --forceExit -- integration-tests/**/__tests__/**/*.ts"
},
@@ -47,6 +48,7 @@
"dependencies": {
"@medusajs/medusa-cli": "^1.3.22",
"@medusajs/utils": "^1.11.9",
"@medusajs/workflows-sdk": "^0.1.6",
"awilix": "^8.0.0",
"cookie-parser": "^1.4.6",
"express": "^4.18.2",

View File

@@ -4,4 +4,5 @@ export * from "./http"
export * from "./database"
export * from "./container"
export * from "./subscribers"
export * from "./jobs"
export * from "./feature-flags"

View File

@@ -0,0 +1,21 @@
import {
IDistributedSchedulerStorage,
SchedulerOptions,
} from "@medusajs/orchestration"
export class MockSchedulerStorage implements IDistributedSchedulerStorage {
async schedule(
jobDefinition: string | { jobId: string },
schedulerOptions: SchedulerOptions
): Promise<void> {
return Promise.resolve()
}
async remove(jobId: string): Promise<void> {
return Promise.resolve()
}
async removeAll(): Promise<void> {
return Promise.resolve()
}
}

View File

@@ -0,0 +1,11 @@
import { MedusaContainer } from "@medusajs/types"
export default async function handler(container: MedusaContainer) {
console.log(`You have received 5 orders today`)
}
export const config = {
name: "summarize-orders",
schedule: "* * * * * *",
numberOfExecutions: 2,
}

View File

@@ -0,0 +1,24 @@
import { join } from "path"
import { WorkflowManager, WorkflowScheduler } from "@medusajs/orchestration"
import { MockSchedulerStorage } from "../__fixtures__/mock-scheduler-storage"
import { JobLoader } from "../job-loader"
describe("register jobs", () => {
WorkflowScheduler.setStorage(new MockSchedulerStorage())
let jobLoader!: JobLoader
beforeAll(() => {
jobLoader = new JobLoader(join(__dirname, "../__fixtures__/plugin/jobs"))
})
it("registers jobs from plugins", async () => {
await jobLoader.load()
const workflow = WorkflowManager.getWorkflow("job-summarize-orders")
expect(workflow).toBeDefined()
expect(workflow?.options.schedule).toEqual({
cron: "* * * * * *",
numberOfExecutions: 2,
})
})
})

View File

@@ -0,0 +1 @@
export * from './job-loader'

View File

@@ -0,0 +1,180 @@
import {
createStep,
createWorkflow,
StepResponse,
} from "@medusajs/workflows-sdk"
import { isObject, MedusaError, promiseAll } from "@medusajs/utils"
import { SchedulerOptions } from "@medusajs/orchestration"
import { MedusaContainer } from "@medusajs/types"
import { logger } from "../logger"
import { access, readdir } from "fs/promises"
import { join } from "path"
type CronJobConfig = {
name: string
schedule: string
numberOfExecutions?: SchedulerOptions["numberOfExecutions"]
}
type CronJobHandler = (container: MedusaContainer) => Promise<any>
export class JobLoader {
/**
* The directory from which to load the jobs
* @private
*/
#sourceDir: string | string[]
/**
* The list of file names to exclude from the subscriber scan
* @private
*/
#excludes: RegExp[] = [
/index\.js/,
/index\.ts/,
/\.DS_Store/,
/(\.ts\.map|\.js\.map|\.d\.ts|\.md)/,
/^_[^/\\]*(\.[^/\\]+)?$/,
]
constructor(sourceDir: string | string[]) {
this.#sourceDir = sourceDir
}
/**
* Validate cron job configuration
* @param config
* @protected
*/
protected validateConfig(config: {
schedule: string | SchedulerOptions
name: string
}) {
if (!config) {
throw new MedusaError(
MedusaError.Types.INVALID_ARGUMENT,
"Config is required for scheduled jobs."
)
}
if (!config.schedule) {
throw new MedusaError(
MedusaError.Types.INVALID_ARGUMENT,
"Cron schedule definition is required for scheduled jobs."
)
}
if (!config.name) {
throw new MedusaError(
MedusaError.Types.INVALID_ARGUMENT,
"Job name is required for scheduled jobs."
)
}
}
/**
* Create a workflow to register a new cron job
* @param config
* @param handler
* @protected
*/
protected registerJob({
config,
handler,
}: {
config: CronJobConfig
handler: CronJobHandler
}) {
const workflowName = `job-${config.name}`
const step = createStep(
`${config.name}-as-step`,
async (_, stepContext) => {
const { container } = stepContext
try {
const res = await handler(container)
return new StepResponse(res, res)
} catch (error) {
logger.error(
`Scheduled job ${config.name} failed with error: ${error.message}`
)
throw error
}
}
)
const workflowConfig = {
name: workflowName,
schedule: isObject(config.schedule)
? config.schedule
: {
cron: config.schedule,
numberOfExecutions: config.numberOfExecutions,
},
}
createWorkflow(workflowConfig, () => {
step()
})
}
/**
* Load cron jobs from one or multiple source paths
*/
async load() {
const normalizedSourcePath = Array.isArray(this.#sourceDir)
? this.#sourceDir
: [this.#sourceDir]
const promises = normalizedSourcePath.map(async (sourcePath) => {
try {
await access(sourcePath)
} catch {
return
}
return await readdir(sourcePath, {
recursive: true,
withFileTypes: true,
}).then(async (entries) => {
const fileEntries = entries.filter((entry) => {
return (
!entry.isDirectory() &&
!this.#excludes.some((exclude) => exclude.test(entry.name))
)
})
logger.debug(`Registering jobs from ${sourcePath}.`)
return await promiseAll(
fileEntries.map(async (entry) => {
const fullPath = join(entry.path, entry.name)
const module_ = await import(fullPath)
const input = {
config: module_.config,
handler: module_.default,
}
this.validateConfig(input.config)
return input
})
)
})
})
const jobsInputs = await promiseAll(promises)
const flatJobsInput = jobsInputs.flat(1).filter(
(
job
): job is {
config: CronJobConfig
handler: CronJobHandler
} => !!job
)
flatJobsInput.map(this.registerJob)
logger.debug(`Job registered.`)
}
}

View File

@@ -130,10 +130,7 @@ export class SubscriberLoader {
withFileTypes: true,
}).then(async (entries) => {
return entries.flatMap(async (entry) => {
if (
this.#excludes.length &&
this.#excludes.some((exclude) => exclude.test(entry.name))
) {
if (this.#excludes.some((exclude) => exclude.test(entry.name))) {
return
}

View File

@@ -3,9 +3,9 @@
"include": ["src"],
"exclude": [
"dist",
"./src/**/__tests__",
"./src/**/__mocks__",
"./src/**/__fixtures__",
"src/**/__tests__",
"src/**/__mocks__",
"src/**/__fixtures__",
"node_modules"
],
}

View File

@@ -23,7 +23,6 @@
"paths": {
},
},
"include": ["src"],
"exclude": [
"dist",