From dd375962a4dc60088fe2724a6e18d4f39b93de27 Mon Sep 17 00:00:00 2001 From: Abdul Rafay Modi Date: Sat, 13 Jun 2026 20:49:06 +0500 Subject: [PATCH] feat: add jobsPath configuration option and enhance job import functionality This links to #11. This code was mostly generated by AI but tested by humans. --- bin/test.ts | 12 +++++ define_config.ts | 1 + jobs.ts | 15 ++++-- stubs/config/resque.stub | 7 +++ .../fixtures/app/contacts/jobs/contact_job.ts | 7 +++ tests/fixtures/app/jobs/email_job.ts | 7 +++ .../app/reminders/jobs/reminder_job.ts | 7 +++ tests/integration/import_all_jobs.spec.ts | 52 +++++++++++++++++++ tests/unit/resolve_job_paths.spec.ts | 21 ++++++++ tsconfig.json | 5 ++ 10 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 bin/test.ts create mode 100644 tests/fixtures/app/contacts/jobs/contact_job.ts create mode 100644 tests/fixtures/app/jobs/email_job.ts create mode 100644 tests/fixtures/app/reminders/jobs/reminder_job.ts create mode 100644 tests/integration/import_all_jobs.spec.ts create mode 100644 tests/unit/resolve_job_paths.spec.ts diff --git a/bin/test.ts b/bin/test.ts new file mode 100644 index 0000000..f1c2238 --- /dev/null +++ b/bin/test.ts @@ -0,0 +1,12 @@ +import { configure, processCLIArgs, run } from '@japa/runner' +import { expect } from '@japa/expect' +import { expectTypeOf } from '@japa/expect-type' + +processCLIArgs(process.argv.splice(2)) + +configure({ + files: ['tests/**/*.spec.ts'], + plugins: [expect(), expectTypeOf()], +}) + +run() diff --git a/define_config.ts b/define_config.ts index b6c13bd..9c0a44e 100644 --- a/define_config.ts +++ b/define_config.ts @@ -12,6 +12,7 @@ export function defineConfig(config: { queueNameForWorkers: string logger: string | null verbose: boolean + jobsPath?: string | string[] }) { return config } \ No newline at end of file diff --git a/jobs.ts b/jobs.ts index c607ee6..7d4942f 100644 --- a/jobs.ts +++ b/jobs.ts @@ -2,11 +2,20 @@ import app from "@adonisjs/core/services/app" import { fsImportAll } from "@poppinss/utils" import Job from "./base_job.js" import { NodeResqueJob } from './types.js' +import { getConfig } from './index.js' + +export function resolveJobPaths(configured?: string | string[]): string[] { + if (!configured) return ['app/jobs'] + return Array.isArray(configured) ? configured : [configured] +} export async function importAllJobs() { - const jobs: Record = await fsImportAll(app.makePath('app/jobs'), { - ignoreMissingRoot: true - }) + const paths = resolveJobPaths(getConfig('jobsPath')) + + const jobsArrays = await Promise.all( + paths.map((p) => fsImportAll(app.makePath(p), { ignoreMissingRoot: true })) + ) + const jobs: Record = Object.assign({}, ...jobsArrays) /** * Duck typing check * @param job diff --git a/stubs/config/resque.stub b/stubs/config/resque.stub index e27bad7..94c8568 100644 --- a/stubs/config/resque.stub +++ b/stubs/config/resque.stub @@ -46,6 +46,13 @@ const resqueConfig = defineConfig({ */ logger: null, verbose: true, + /** + * path(s) to scan for job files, relative to the app root + * accepts a single path or an array of paths for modular architectures + * e.g. ['app/contacts/jobs', 'app/reminders/jobs'] + * defaults to 'app/jobs' when omitted + */ + // jobsPath: 'app/jobs', }) export default resqueConfig diff --git a/tests/fixtures/app/contacts/jobs/contact_job.ts b/tests/fixtures/app/contacts/jobs/contact_job.ts new file mode 100644 index 0000000..48560fd --- /dev/null +++ b/tests/fixtures/app/contacts/jobs/contact_job.ts @@ -0,0 +1,7 @@ +export default class ContactJob { + plugins: any[] = [] + + async perform(_contactId: number) {} + + async enqueue(_contactId: number) {} +} diff --git a/tests/fixtures/app/jobs/email_job.ts b/tests/fixtures/app/jobs/email_job.ts new file mode 100644 index 0000000..c51c8eb --- /dev/null +++ b/tests/fixtures/app/jobs/email_job.ts @@ -0,0 +1,7 @@ +export default class EmailJob { + plugins: any[] = [] + + async perform(_to: string) {} + + async enqueue(_to: string) {} +} diff --git a/tests/fixtures/app/reminders/jobs/reminder_job.ts b/tests/fixtures/app/reminders/jobs/reminder_job.ts new file mode 100644 index 0000000..32ebcca --- /dev/null +++ b/tests/fixtures/app/reminders/jobs/reminder_job.ts @@ -0,0 +1,7 @@ +export default class ReminderJob { + plugins: any[] = [] + + async perform(_message: string) {} + + async enqueue(_message: string) {} +} diff --git a/tests/integration/import_all_jobs.spec.ts b/tests/integration/import_all_jobs.spec.ts new file mode 100644 index 0000000..4af1bed --- /dev/null +++ b/tests/integration/import_all_jobs.spec.ts @@ -0,0 +1,52 @@ +import { test } from '@japa/runner' +import { fsImportAll } from '@poppinss/utils' +import { fileURLToPath } from 'node:url' +import { join } from 'node:path' + +/** + * Tests the multi-path file loading logic that importAllJobs delegates to. + * We test fsImportAll + Object.assign directly because spinning up a full + * AdonisJS app in a library test is unnecessary — the wiring is a one-liner + * and the path resolution is already covered in the unit tests. + */ +const FIXTURES = fileURLToPath(new URL('../fixtures/', import.meta.url)) + +function resolveFixture(p: string) { + return join(FIXTURES, p) +} + +test.group('multi-path job loading', () => { + test('discovers jobs from the default path', async ({ expect }) => { + const jobs = await fsImportAll(resolveFixture('app/jobs'), { ignoreMissingRoot: true }) + + const names = Object.values(jobs).map((v: any) => v.name) + expect(names).toContain('EmailJob') + }) + + test('discovers jobs from a single custom path', async ({ expect }) => { + const jobs = await fsImportAll(resolveFixture('app/contacts/jobs'), { ignoreMissingRoot: true }) + + const names = Object.values(jobs).map((v: any) => v.name) + expect(names).toContain('ContactJob') + expect(names).not.toContain('EmailJob') + }) + + test('merges jobs from multiple paths (modular architecture)', async ({ expect }) => { + const results = await Promise.all([ + fsImportAll(resolveFixture('app/contacts/jobs'), { ignoreMissingRoot: true }), + fsImportAll(resolveFixture('app/reminders/jobs'), { ignoreMissingRoot: true }), + ]) + const jobs = Object.assign({}, ...results) + + const names = Object.values(jobs).map((v: any) => v.name) + expect(names).toContain('ContactJob') + expect(names).toContain('ReminderJob') + expect(names).not.toContain('EmailJob') + }) + + test('returns empty object for a missing directory', async ({ expect }) => { + const jobs = await fsImportAll(resolveFixture('app/nonexistent/jobs'), { ignoreMissingRoot: true }) + + expect(Object.keys(jobs)).toHaveLength(0) + }) +}) diff --git a/tests/unit/resolve_job_paths.spec.ts b/tests/unit/resolve_job_paths.spec.ts new file mode 100644 index 0000000..91246c7 --- /dev/null +++ b/tests/unit/resolve_job_paths.spec.ts @@ -0,0 +1,21 @@ +import { test } from '@japa/runner' +import { resolveJobPaths } from '../../jobs.js' + +test.group('resolveJobPaths', () => { + test('returns ["app/jobs"] when no path is configured', ({ expect }) => { + expect(resolveJobPaths(undefined)).toEqual(['app/jobs']) + }) + + test('wraps a single string in an array', ({ expect }) => { + expect(resolveJobPaths('app/billing/jobs')).toEqual(['app/billing/jobs']) + }) + + test('returns an array of paths as-is', ({ expect }) => { + const paths = ['app/contacts/jobs', 'app/reminders/jobs'] + expect(resolveJobPaths(paths)).toEqual(paths) + }) + + test('returns a one-element array unchanged', ({ expect }) => { + expect(resolveJobPaths(['app/custom/jobs'])).toEqual(['app/custom/jobs']) + }) +}) diff --git a/tsconfig.json b/tsconfig.json index 4ef23fa..98443df 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,5 +4,10 @@ "rootDir": "./", "outDir": "./build", "resolveJsonModule": false + }, + "ts-node": { + "swc": false, + "esm": true, + "transpileOnly": true } } \ No newline at end of file