@wavezync/nestjs-pgboss 5.1.1 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -52,7 +52,9 @@ export class PgBossService {
52
52
  await this.pgBoss.schedule(name, cron, data ?? {}, options ?? {});
53
53
  await this.pgBoss.work<TData>(
54
54
  name,
55
- { ...transformOptions(options), includeMetadata: true },
55
+ { ...transformOptions(options), includeMetadata: true } as WorkOptions & {
56
+ includeMetadata: true;
57
+ },
56
58
  handler,
57
59
  );
58
60
  }
@@ -4,24 +4,26 @@ import { mergeMap, retryWhen, delay } from "rxjs/operators";
4
4
  export function handleRetry(
5
5
  retryAttempts = 9,
6
6
  retryDelay = 3000,
7
- toRetry: (err: any) => boolean = (_err: any) => true,
7
+ toRetry: (err: unknown) => boolean = () => true,
8
8
  ) {
9
9
  return <T>(source: import("rxjs").Observable<T>) =>
10
10
  source.pipe(
11
11
  retryWhen((attempts) =>
12
12
  attempts.pipe(
13
- mergeMap((error, index) => {
13
+ mergeMap((error: unknown, index: number) => {
14
14
  const includeError = toRetry(error);
15
15
 
16
16
  if (includeError) {
17
17
  if (index + 1 >= retryAttempts) {
18
- return throwError(() => new Error(error.message));
18
+ const message =
19
+ error instanceof Error ? error.message : "Unknown error";
20
+ return throwError(() => new Error(message));
19
21
  }
20
22
 
21
23
  return of(error).pipe(delay(retryDelay));
22
24
  }
23
25
 
24
- return throwError(() => error);
26
+ return throwError(() => error as Error);
25
27
  }),
26
28
  ),
27
29
  ),
@@ -1,20 +1,23 @@
1
1
  import { WorkOptions, ScheduleOptions } from "pg-boss";
2
2
 
3
- export function transformOptions(options?: WorkOptions | ScheduleOptions) {
3
+ export function transformOptions(
4
+ options?: WorkOptions | ScheduleOptions,
5
+ ): Record<string, unknown> {
4
6
  if (!options) return {};
5
7
 
6
- const transformedOptions: any = { ...options };
8
+ const transformedOptions: Record<string, unknown> = { ...options };
9
+ const opts = options as Record<string, unknown>;
7
10
 
8
- if (typeof (options as any).priority === "number") {
9
- transformedOptions.priority = (options as any).priority > 0;
11
+ if (typeof opts.priority === "number") {
12
+ transformedOptions.priority = opts.priority > 0;
10
13
  }
11
14
 
12
15
  return transformedOptions;
13
16
  }
14
17
 
15
- export function normalizeJob(job: any) {
16
- if (typeof job === "object" && "0" in job) {
17
- return job[0];
18
+ export function normalizeJob(job: unknown): unknown {
19
+ if (typeof job === "object" && job !== null && "0" in job) {
20
+ return (job as Record<string, unknown>)["0"];
18
21
  }
19
22
  return job;
20
23
  }
package/package.json CHANGED
@@ -1,19 +1,19 @@
1
1
  {
2
2
  "name": "@wavezync/nestjs-pgboss",
3
- "version": "5.1.1",
3
+ "version": "6.0.0",
4
4
  "description": "A NestJS module that integrates pg-boss for job scheduling and handling.",
5
5
  "license": "MIT",
6
- "author": "samaratungajs@wavezync.com",
6
+ "author": "dev@wavezync.com",
7
7
  "main": "dist/index.js",
8
8
  "types": "dist/index.d.ts",
9
9
  "scripts": {
10
10
  "build": "nest build",
11
- "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
11
+ "format": "prettier --write \"lib/**/*.ts\" \"test/**/*.ts\"",
12
12
  "start": "nest start",
13
13
  "start:dev": "nest start --watch",
14
14
  "start:debug": "nest start --debug --watch",
15
15
  "start:prod": "node dist/main",
16
- "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
16
+ "lint": "eslint \"{lib,test}/**/*.ts\" --fix",
17
17
  "test": "jest",
18
18
  "test:watch": "jest --watch",
19
19
  "test:cov": "jest --coverage",
@@ -23,7 +23,7 @@
23
23
  "peerDependencies": {
24
24
  "@nestjs/common": "^11",
25
25
  "@nestjs/core": "^11",
26
- "pg-boss": "^12",
26
+ "pg-boss": ">=12.6.0",
27
27
  "reflect-metadata": "^0.1.13 || ^0.2.0",
28
28
  "rxjs": "^7.2.0"
29
29
  },
@@ -31,15 +31,14 @@
31
31
  "@nestjs/cli": "^11.0.0",
32
32
  "@nestjs/schematics": "^11.0.0",
33
33
  "@nestjs/testing": "^11.0.0",
34
- "@types/express": "^4.17.17",
35
34
  "@types/jest": "^29.5.2",
36
35
  "@types/node": "^20.3.1",
37
36
  "@types/supertest": "^6.0.0",
38
- "@typescript-eslint/eslint-plugin": "^8.0.0",
39
- "@typescript-eslint/parser": "^8.0.0",
40
- "eslint": "^8.42.0",
41
- "eslint-config-prettier": "^9.0.0",
37
+ "@eslint/js": "^9.0.0",
38
+ "eslint": "^9.0.0",
42
39
  "eslint-plugin-prettier": "^5.0.0",
40
+ "globals": "^16.0.0",
41
+ "typescript-eslint": "^8.0.0",
43
42
  "jest": "^29.5.0",
44
43
  "prettier": "^3.0.0",
45
44
  "source-map-support": "^0.5.21",
@@ -0,0 +1,82 @@
1
+ import "reflect-metadata";
2
+ import {
3
+ Job,
4
+ CronJob,
5
+ JOB_NAME,
6
+ JOB_OPTIONS,
7
+ CRON_EXPRESSION,
8
+ CRON_OPTIONS,
9
+ PG_BOSS_JOB_METADATA,
10
+ } from "../lib/decorators/job.decorator";
11
+
12
+ /* eslint-disable @typescript-eslint/unbound-method */
13
+ describe("Job Decorator", () => {
14
+ it("should set JOB_NAME, JOB_OPTIONS, and PG_BOSS_JOB_METADATA with defaults", () => {
15
+ class TestClass {
16
+ @Job("test-job")
17
+ handle() {}
18
+ }
19
+
20
+ const method = TestClass.prototype.handle;
21
+
22
+ expect(Reflect.getMetadata(JOB_NAME, method)).toBe("test-job");
23
+ expect(Reflect.getMetadata(JOB_OPTIONS, method)).toEqual({});
24
+ expect(Reflect.getMetadata(PG_BOSS_JOB_METADATA, method)).toEqual({
25
+ jobName: "test-job",
26
+ workOptions: {},
27
+ });
28
+ });
29
+
30
+ it("should pass custom WorkOptions through", () => {
31
+ const options = { teamSize: 5, teamConcurrency: 2 } as any;
32
+
33
+ class TestClass {
34
+ @Job("custom-job", options)
35
+ handle() {}
36
+ }
37
+
38
+ const method = TestClass.prototype.handle;
39
+
40
+ expect(Reflect.getMetadata(JOB_OPTIONS, method)).toEqual(options);
41
+ expect(Reflect.getMetadata(PG_BOSS_JOB_METADATA, method)).toEqual({
42
+ jobName: "custom-job",
43
+ workOptions: options,
44
+ });
45
+ });
46
+ });
47
+
48
+ describe("CronJob Decorator", () => {
49
+ it("should set JOB_NAME, CRON_EXPRESSION, CRON_OPTIONS, and PG_BOSS_JOB_METADATA with defaults", () => {
50
+ class TestClass {
51
+ @CronJob("cron-job", "* * * * *")
52
+ handle() {}
53
+ }
54
+
55
+ const method = TestClass.prototype.handle;
56
+
57
+ expect(Reflect.getMetadata(JOB_NAME, method)).toBe("cron-job");
58
+ expect(Reflect.getMetadata(CRON_EXPRESSION, method)).toBe("* * * * *");
59
+ expect(Reflect.getMetadata(CRON_OPTIONS, method)).toEqual({});
60
+ expect(Reflect.getMetadata(PG_BOSS_JOB_METADATA, method)).toEqual({
61
+ jobName: "cron-job",
62
+ workOptions: {},
63
+ });
64
+ });
65
+
66
+ it("should pass custom ScheduleOptions through", () => {
67
+ const options = { tz: "America/New_York" };
68
+
69
+ class TestClass {
70
+ @CronJob("cron-custom", "0 9 * * *", options)
71
+ handle() {}
72
+ }
73
+
74
+ const method = TestClass.prototype.handle;
75
+
76
+ expect(Reflect.getMetadata(CRON_OPTIONS, method)).toEqual(options);
77
+ expect(Reflect.getMetadata(PG_BOSS_JOB_METADATA, method)).toEqual({
78
+ jobName: "cron-custom",
79
+ workOptions: options,
80
+ });
81
+ });
82
+ });
@@ -0,0 +1,69 @@
1
+ import { Observable, of, lastValueFrom } from "rxjs";
2
+ import { handleRetry } from "../lib/utils/handleRetry";
3
+
4
+ describe("handleRetry", () => {
5
+ it("should pass through successful values without retrying", async () => {
6
+ const result = await lastValueFrom(of("success").pipe(handleRetry()));
7
+ expect(result).toBe("success");
8
+ });
9
+
10
+ it("should retry on error and eventually succeed", async () => {
11
+ let attempt = 0;
12
+ const source$ = new Observable<string>((subscriber) => {
13
+ attempt++;
14
+ if (attempt < 3) {
15
+ subscriber.error(new Error("fail"));
16
+ } else {
17
+ subscriber.next("success");
18
+ subscriber.complete();
19
+ }
20
+ });
21
+
22
+ const result = await lastValueFrom(source$.pipe(handleRetry(5, 1)));
23
+ expect(result).toBe("success");
24
+ expect(attempt).toBe(3);
25
+ });
26
+
27
+ it("should throw after exhausting retry attempts", async () => {
28
+ const source$ = new Observable<string>((subscriber) => {
29
+ subscriber.error(new Error("persistent failure"));
30
+ });
31
+
32
+ await expect(
33
+ lastValueFrom(source$.pipe(handleRetry(3, 1))),
34
+ ).rejects.toThrow("persistent failure");
35
+ });
36
+
37
+ it("should respect custom retryAttempts count", async () => {
38
+ let attempts = 0;
39
+ const source$ = new Observable<string>((subscriber) => {
40
+ attempts++;
41
+ subscriber.error(new Error("fail"));
42
+ });
43
+
44
+ await expect(
45
+ lastValueFrom(source$.pipe(handleRetry(2, 1))),
46
+ ).rejects.toThrow("fail");
47
+ expect(attempts).toBe(2);
48
+ });
49
+
50
+ it("should throw immediately when toRetry predicate returns false", async () => {
51
+ const source$ = new Observable<string>((subscriber) => {
52
+ subscriber.error(new Error("not retryable"));
53
+ });
54
+
55
+ await expect(
56
+ lastValueFrom(source$.pipe(handleRetry(5, 1, () => false))),
57
+ ).rejects.toThrow("not retryable");
58
+ });
59
+
60
+ it('should use "Unknown error" for non-Error throws', async () => {
61
+ const source$ = new Observable<string>((subscriber) => {
62
+ subscriber.error("string error");
63
+ });
64
+
65
+ await expect(
66
+ lastValueFrom(source$.pipe(handleRetry(1, 1))),
67
+ ).rejects.toThrow("Unknown error");
68
+ });
69
+ });
@@ -1,82 +1,283 @@
1
- jest.mock("pg-boss", () => ({}));
1
+ jest.mock("pg-boss", () => {
2
+ return {
3
+ PgBoss: jest.fn().mockImplementation(() => ({
4
+ on: jest.fn(),
5
+ start: jest.fn(),
6
+ stop: jest.fn(),
7
+ send: jest.fn(),
8
+ schedule: jest.fn(),
9
+ work: jest.fn(),
10
+ createQueue: jest.fn(),
11
+ })),
12
+ };
13
+ });
2
14
 
3
15
  import { Test, TestingModule } from "@nestjs/testing";
4
16
  import { Reflector, ModulesContainer } from "@nestjs/core";
5
17
  import { HandlerScannerService } from "../lib/handler-scanner.service";
6
18
  import { PgBossService } from "../lib/pgboss.service";
7
- import { JOB_NAME, JOB_OPTIONS } from "../lib/decorators/job.decorator";
19
+ import {
20
+ JOB_NAME,
21
+ JOB_OPTIONS,
22
+ CRON_EXPRESSION,
23
+ CRON_OPTIONS,
24
+ } from "../lib/decorators/job.decorator";
8
25
 
9
26
  describe("HandlerScannerService", () => {
10
- let service: HandlerScannerService;
11
- let mockPgBossService: any;
12
- let mockReflector: any;
13
- let mockModulesContainer: Map<string, any>;
14
-
15
- class TestHandler {
16
- handle() {}
17
- }
27
+ let scanner: HandlerScannerService;
28
+ let pgBossService: jest.Mocked<
29
+ Pick<PgBossService, "registerJob" | "registerCronJob">
30
+ >;
31
+ let reflector: Reflector;
32
+ let modulesContainer: ModulesContainer;
18
33
 
19
34
  beforeEach(async () => {
20
- mockPgBossService = {
35
+ pgBossService = {
21
36
  registerJob: jest.fn().mockResolvedValue(undefined),
22
37
  registerCronJob: jest.fn().mockResolvedValue(undefined),
23
38
  };
24
39
 
25
- mockReflector = { get: jest.fn() };
26
- mockModulesContainer = new Map();
27
-
28
40
  const module: TestingModule = await Test.createTestingModule({
29
41
  providers: [
30
42
  HandlerScannerService,
31
- { provide: PgBossService, useValue: mockPgBossService },
32
- { provide: Reflector, useValue: mockReflector },
33
- { provide: ModulesContainer, useValue: mockModulesContainer },
43
+ { provide: PgBossService, useValue: pgBossService },
44
+ Reflector,
45
+ { provide: ModulesContainer, useValue: new Map() },
34
46
  ],
35
47
  }).compile();
36
48
 
37
- service = module.get<HandlerScannerService>(HandlerScannerService);
49
+ scanner = module.get<HandlerScannerService>(HandlerScannerService);
50
+ reflector = module.get<Reflector>(Reflector);
51
+ modulesContainer = module.get<ModulesContainer>(ModulesContainer);
38
52
  });
39
53
 
40
- afterEach(() => {
41
- jest.clearAllMocks();
54
+ it("should be defined", () => {
55
+ expect(scanner).toBeDefined();
42
56
  });
43
57
 
44
- describe("teamSize", () => {
45
- const setupHandler = (jobOptions?: { teamSize?: number }) => {
46
- const instance = new TestHandler();
47
- mockModulesContainer.set("TestModule", {
48
- providers: new Map([["TestHandler", { instance }]]),
49
- });
50
- mockReflector.get.mockImplementation((key: string, target: any) => {
51
- if (key === JOB_NAME && target === instance.handle) return "my-job";
52
- if (key === JOB_OPTIONS && target === instance.handle)
53
- return jobOptions;
54
- return undefined;
55
- });
58
+ it("should register @Job-decorated methods via registerJob", async () => {
59
+ const handler = jest.fn();
60
+ const instance = {
61
+ handle: handler,
56
62
  };
63
+ Object.setPrototypeOf(
64
+ instance,
65
+ Object.create(null, {
66
+ constructor: { value: class {} },
67
+ handle: { value: handler, enumerable: true },
68
+ }),
69
+ );
57
70
 
58
- it("should register job once by default", async () => {
59
- setupHandler();
60
- await service.scanAndRegisterHandlers();
61
- expect(mockPgBossService.registerJob).toHaveBeenCalledTimes(1);
71
+ const reflectorGetSpy = jest.spyOn(reflector, "get");
72
+ reflectorGetSpy.mockImplementation((key: any, target: any) => {
73
+ if (target === handler) {
74
+ if (key === JOB_NAME) return "test-job";
75
+ if (key === JOB_OPTIONS) return { teamSize: 2 };
76
+ }
77
+ return undefined;
62
78
  });
63
79
 
64
- it("should register job multiple times when teamSize > 1", async () => {
65
- setupHandler({ teamSize: 3 });
66
- await service.scanAndRegisterHandlers();
67
- expect(mockPgBossService.registerJob).toHaveBeenCalledTimes(3);
80
+ const fakeModule = {
81
+ providers: new Map([
82
+ [
83
+ "TestProvider",
84
+ {
85
+ instance,
86
+ metatype: class {},
87
+ },
88
+ ],
89
+ ]),
90
+ };
91
+ (modulesContainer as Map<string, any>).set("TestModule", fakeModule);
92
+
93
+ await scanner.scanAndRegisterHandlers();
94
+
95
+ expect(pgBossService.registerJob).toHaveBeenCalledWith(
96
+ "test-job",
97
+ expect.any(Function),
98
+ { teamSize: 2 },
99
+ );
100
+ });
101
+
102
+ it("should register @CronJob-decorated methods via registerCronJob", async () => {
103
+ const handler = jest.fn();
104
+ const instance = {
105
+ handle: handler,
106
+ };
107
+ Object.setPrototypeOf(
108
+ instance,
109
+ Object.create(null, {
110
+ constructor: { value: class {} },
111
+ handle: { value: handler, enumerable: true },
112
+ }),
113
+ );
114
+
115
+ const reflectorGetSpy = jest.spyOn(reflector, "get");
116
+ reflectorGetSpy.mockImplementation((key: any, target: any) => {
117
+ if (target === handler) {
118
+ if (key === JOB_NAME) return "cron-job";
119
+ if (key === CRON_EXPRESSION) return "* * * * *";
120
+ if (key === CRON_OPTIONS) return { tz: "UTC" };
121
+ }
122
+ return undefined;
123
+ });
124
+
125
+ const fakeModule = {
126
+ providers: new Map([
127
+ [
128
+ "TestProvider",
129
+ {
130
+ instance,
131
+ metatype: class {},
132
+ },
133
+ ],
134
+ ]),
135
+ };
136
+ (modulesContainer as Map<string, any>).set("TestModule", fakeModule);
137
+
138
+ await scanner.scanAndRegisterHandlers();
139
+
140
+ expect(pgBossService.registerCronJob).toHaveBeenCalledWith(
141
+ "cron-job",
142
+ "* * * * *",
143
+ expect.any(Function),
144
+ {},
145
+ { tz: "UTC" },
146
+ );
147
+ });
148
+
149
+ it("should skip providers with no instance", async () => {
150
+ const fakeModule = {
151
+ providers: new Map([
152
+ ["NullProvider", { instance: null, metatype: class {} }],
153
+ ]),
154
+ };
155
+ (modulesContainer as Map<string, any>).set("TestModule", fakeModule);
156
+
157
+ await scanner.scanAndRegisterHandlers();
158
+
159
+ expect(pgBossService.registerJob).not.toHaveBeenCalled();
160
+ expect(pgBossService.registerCronJob).not.toHaveBeenCalled();
161
+ });
162
+
163
+ it("should skip methods without job metadata", async () => {
164
+ const instance = {
165
+ someMethod: jest.fn(),
166
+ };
167
+ Object.setPrototypeOf(
168
+ instance,
169
+ Object.create(null, {
170
+ constructor: { value: class {} },
171
+ someMethod: { value: jest.fn(), enumerable: true },
172
+ }),
173
+ );
174
+
175
+ jest.spyOn(reflector, "get").mockReturnValue(undefined);
176
+
177
+ const fakeModule = {
178
+ providers: new Map([["TestProvider", { instance, metatype: class {} }]]),
179
+ };
180
+ (modulesContainer as Map<string, any>).set("TestModule", fakeModule);
181
+
182
+ await scanner.scanAndRegisterHandlers();
183
+
184
+ expect(pgBossService.registerJob).not.toHaveBeenCalled();
185
+ expect(pgBossService.registerCronJob).not.toHaveBeenCalled();
186
+ });
187
+
188
+ it("should log errors when registration fails", async () => {
189
+ const handler = jest.fn();
190
+ const instance = {
191
+ handle: handler,
192
+ };
193
+ Object.setPrototypeOf(
194
+ instance,
195
+ Object.create(null, {
196
+ constructor: { value: class {} },
197
+ handle: { value: handler, enumerable: true },
198
+ }),
199
+ );
200
+
201
+ jest.spyOn(reflector, "get").mockImplementation((key: any, target: any) => {
202
+ if (target === handler) {
203
+ if (key === JOB_NAME) return "failing-job";
204
+ if (key === JOB_OPTIONS) return {};
205
+ }
206
+ return undefined;
68
207
  });
69
208
 
70
- it("should default to 1 when teamSize is 0", async () => {
71
- setupHandler({ teamSize: 0 });
72
- await service.scanAndRegisterHandlers();
73
- expect(mockPgBossService.registerJob).toHaveBeenCalledTimes(1);
209
+ pgBossService.registerJob.mockRejectedValue(
210
+ new Error("registration failed"),
211
+ );
212
+
213
+ const fakeModule = {
214
+ providers: new Map([["TestProvider", { instance, metatype: class {} }]]),
215
+ };
216
+ (modulesContainer as Map<string, any>).set("TestModule", fakeModule);
217
+
218
+ // Should not throw
219
+ await expect(scanner.scanAndRegisterHandlers()).resolves.not.toThrow();
220
+ });
221
+
222
+ it("should bind handler to the correct instance context", async () => {
223
+ const handler = jest.fn();
224
+ const instance = {
225
+ handle: handler,
226
+ };
227
+ Object.setPrototypeOf(
228
+ instance,
229
+ Object.create(null, {
230
+ constructor: { value: class {} },
231
+ handle: { value: handler, enumerable: true },
232
+ }),
233
+ );
234
+
235
+ jest.spyOn(reflector, "get").mockImplementation((key: any, target: any) => {
236
+ if (target === handler) {
237
+ if (key === JOB_NAME) return "bound-job";
238
+ if (key === JOB_OPTIONS) return {};
239
+ }
240
+ return undefined;
74
241
  });
75
242
 
76
- it("should default to 1 when teamSize is negative", async () => {
77
- setupHandler({ teamSize: -5 });
78
- await service.scanAndRegisterHandlers();
79
- expect(mockPgBossService.registerJob).toHaveBeenCalledTimes(1);
243
+ const fakeModule = {
244
+ providers: new Map([["TestProvider", { instance, metatype: class {} }]]),
245
+ };
246
+ (modulesContainer as Map<string, any>).set("TestModule", fakeModule);
247
+
248
+ await scanner.scanAndRegisterHandlers();
249
+
250
+ // The handler passed to registerJob should be bound to the instance
251
+ const boundHandler = pgBossService.registerJob.mock.calls[0][1];
252
+ expect(typeof boundHandler).toBe("function");
253
+ });
254
+
255
+ it("should skip prototype getters that throw (e.g. TypeORM DataSource.mongoManager)", async () => {
256
+ const proto = Object.create(null);
257
+ Object.defineProperty(proto, "constructor", { value: class {} });
258
+ Object.defineProperty(proto, "mongoManager", {
259
+ get() {
260
+ throw new Error(
261
+ "MongoEntityManager is only available for MongoDB databases.",
262
+ );
263
+ },
264
+ enumerable: true,
80
265
  });
266
+ Object.defineProperty(proto, "handle", {
267
+ value: jest.fn(),
268
+ enumerable: true,
269
+ });
270
+
271
+ const instance = Object.create(proto);
272
+
273
+ jest.spyOn(reflector, "get").mockReturnValue(undefined);
274
+
275
+ const fakeModule = {
276
+ providers: new Map([["TestProvider", { instance, metatype: class {} }]]),
277
+ };
278
+ (modulesContainer as Map<string, any>).set("TestModule", fakeModule);
279
+
280
+ // Should not throw despite the getter
281
+ await expect(scanner.scanAndRegisterHandlers()).resolves.not.toThrow();
81
282
  });
82
283
  });
@@ -0,0 +1,47 @@
1
+ import { transformOptions, normalizeJob } from "../lib/utils/helpers";
2
+
3
+ describe("transformOptions", () => {
4
+ it("should return {} when options is undefined", () => {
5
+ expect(transformOptions(undefined)).toEqual({});
6
+ });
7
+
8
+ it("should spread options through unchanged when no priority", () => {
9
+ const options = { tz: "UTC" };
10
+ expect(transformOptions(options)).toEqual({ tz: "UTC" });
11
+ });
12
+
13
+ it("should convert numeric priority > 0 to true", () => {
14
+ const options = { priority: 5 } as any;
15
+ expect(transformOptions(options)).toEqual({ priority: true });
16
+ });
17
+
18
+ it("should convert numeric priority <= 0 to false", () => {
19
+ expect(transformOptions({ priority: 0 } as any)).toEqual({
20
+ priority: false,
21
+ });
22
+ expect(transformOptions({ priority: -1 } as any)).toEqual({
23
+ priority: false,
24
+ });
25
+ });
26
+ });
27
+
28
+ describe("normalizeJob", () => {
29
+ it('should return the "0" property from array-like objects', () => {
30
+ const job = { "0": { id: "job-1", data: {} } };
31
+ expect(normalizeJob(job)).toEqual({ id: "job-1", data: {} });
32
+ });
33
+
34
+ it("should return the job as-is for plain objects", () => {
35
+ const job = { id: "job-1", data: {} };
36
+ expect(normalizeJob(job)).toEqual({ id: "job-1", data: {} });
37
+ });
38
+
39
+ it("should return primitives unchanged", () => {
40
+ expect(normalizeJob("test")).toBe("test");
41
+ expect(normalizeJob(42)).toBe(42);
42
+ });
43
+
44
+ it("should handle null input", () => {
45
+ expect(normalizeJob(null)).toBeNull();
46
+ });
47
+ });