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:
committed by
GitHub
parent
838eb0e34a
commit
6ccf83128c
@@ -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",
|
||||
|
||||
@@ -4,4 +4,5 @@ export * from "./http"
|
||||
export * from "./database"
|
||||
export * from "./container"
|
||||
export * from "./subscribers"
|
||||
export * from "./jobs"
|
||||
export * from "./feature-flags"
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
})
|
||||
})
|
||||
1
packages/framework/framework/src/jobs/index.ts
Normal file
1
packages/framework/framework/src/jobs/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './job-loader'
|
||||
180
packages/framework/framework/src/jobs/job-loader.ts
Normal file
180
packages/framework/framework/src/jobs/job-loader.ts
Normal 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.`)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
"include": ["src"],
|
||||
"exclude": [
|
||||
"dist",
|
||||
"./src/**/__tests__",
|
||||
"./src/**/__mocks__",
|
||||
"./src/**/__fixtures__",
|
||||
"src/**/__tests__",
|
||||
"src/**/__mocks__",
|
||||
"src/**/__fixtures__",
|
||||
"node_modules"
|
||||
],
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
"paths": {
|
||||
},
|
||||
},
|
||||
|
||||
"include": ["src"],
|
||||
"exclude": [
|
||||
"dist",
|
||||
|
||||
Reference in New Issue
Block a user