@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.
- package/.github/workflows/release.yaml +49 -8
- package/CHANGELOG.md +146 -0
- package/CLAUDE.md +39 -0
- package/cliff.toml +49 -0
- package/dist/decorators/job.decorator.d.ts +3 -4
- package/dist/decorators/job.decorator.js.map +1 -1
- package/dist/handler-scanner.service.js +9 -8
- package/dist/handler-scanner.service.js.map +1 -1
- package/dist/interfaces/handler-metadata.interface.d.ts +1 -4
- package/dist/pgboss.service.js.map +1 -1
- package/dist/utils/handleRetry.d.ts +1 -1
- package/dist/utils/handleRetry.js +3 -2
- package/dist/utils/handleRetry.js.map +1 -1
- package/dist/utils/helpers.d.ts +2 -2
- package/dist/utils/helpers.js +5 -4
- package/dist/utils/helpers.js.map +1 -1
- package/eslint.config.mjs +48 -0
- package/lib/decorators/job.decorator.ts +5 -9
- package/lib/handler-scanner.service.ts +22 -17
- package/lib/interfaces/handler-metadata.interface.ts +1 -5
- package/lib/pgboss.service.ts +3 -1
- package/lib/utils/handleRetry.ts +6 -4
- package/lib/utils/helpers.ts +10 -7
- package/package.json +9 -10
- package/test/decorators.spec.ts +82 -0
- package/test/handleRetry.spec.ts +69 -0
- package/test/handler-scanner.service.spec.ts +249 -48
- package/test/helpers.spec.ts +47 -0
- package/test/pgboss.module.spec.ts +123 -0
- package/test/pgboss.service.spec.ts +61 -2
- package/.eslintrc.js +0 -33
|
@@ -0,0 +1,123 @@
|
|
|
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
|
+
})),
|
|
8
|
+
};
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
import { PgBossModule } from "../lib/pgboss.module";
|
|
12
|
+
import { HandlerScannerService } from "../lib/handler-scanner.service";
|
|
13
|
+
import { PgBossService } from "../lib/pgboss.service";
|
|
14
|
+
import { PGBOSS_OPTIONS, PGBOSS_TOKEN } from "../lib/utils/consts";
|
|
15
|
+
import { MetadataScanner } from "@nestjs/core";
|
|
16
|
+
|
|
17
|
+
describe("PgBossModule", () => {
|
|
18
|
+
let mockBoss: any;
|
|
19
|
+
let mockHandlerScanner: jest.Mocked<
|
|
20
|
+
Pick<HandlerScannerService, "scanAndRegisterHandlers">
|
|
21
|
+
>;
|
|
22
|
+
let module: PgBossModule;
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
mockBoss = {
|
|
26
|
+
on: jest.fn(),
|
|
27
|
+
stop: jest.fn().mockResolvedValue(undefined),
|
|
28
|
+
};
|
|
29
|
+
mockHandlerScanner = {
|
|
30
|
+
scanAndRegisterHandlers: jest.fn().mockResolvedValue(undefined),
|
|
31
|
+
};
|
|
32
|
+
module = new PgBossModule(mockBoss, mockHandlerScanner as any);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("should register error event listener on boss in constructor", () => {
|
|
36
|
+
expect(mockBoss.on).toHaveBeenCalledWith("error", expect.any(Function));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("onApplicationBootstrap", () => {
|
|
40
|
+
it("should call scanAndRegisterHandlers", async () => {
|
|
41
|
+
await module.onApplicationBootstrap();
|
|
42
|
+
expect(mockHandlerScanner.scanAndRegisterHandlers).toHaveBeenCalled();
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("onModuleDestroy", () => {
|
|
47
|
+
it("should call boss.stop()", async () => {
|
|
48
|
+
await module.onModuleDestroy();
|
|
49
|
+
expect(mockBoss.stop).toHaveBeenCalled();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("should catch and log errors from boss.stop()", async () => {
|
|
53
|
+
mockBoss.stop.mockRejectedValue(new Error("stop failed"));
|
|
54
|
+
// Should not throw
|
|
55
|
+
await expect(module.onModuleDestroy()).resolves.not.toThrow();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("forRootAsync", () => {
|
|
60
|
+
it("should return correct module structure with useFactory", () => {
|
|
61
|
+
const factory = jest.fn();
|
|
62
|
+
const result = PgBossModule.forRootAsync({
|
|
63
|
+
useFactory: factory,
|
|
64
|
+
inject: ["CONFIG"],
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(result.module).toBe(PgBossModule);
|
|
68
|
+
expect(result.exports).toContain(PgBossService);
|
|
69
|
+
expect(result.exports).toContain(PGBOSS_TOKEN);
|
|
70
|
+
|
|
71
|
+
const providerTokens = (result.providers as any[]).map(
|
|
72
|
+
(p: any) => p.provide || p,
|
|
73
|
+
);
|
|
74
|
+
expect(providerTokens).toContain(PGBOSS_OPTIONS);
|
|
75
|
+
expect(providerTokens).toContain(PGBOSS_TOKEN);
|
|
76
|
+
expect(providerTokens).toContain(PgBossService);
|
|
77
|
+
expect(providerTokens).toContain(HandlerScannerService);
|
|
78
|
+
expect(providerTokens).toContain(MetadataScanner);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("should return correct module structure with useClass", () => {
|
|
82
|
+
class TestOptionsFactory {
|
|
83
|
+
createPgBossOptions() {
|
|
84
|
+
return { connectionString: "postgres://localhost/test" };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const result = PgBossModule.forRootAsync({
|
|
89
|
+
useClass: TestOptionsFactory,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(result.module).toBe(PgBossModule);
|
|
93
|
+
expect(result.exports).toContain(PgBossService);
|
|
94
|
+
expect(result.exports).toContain(PGBOSS_TOKEN);
|
|
95
|
+
|
|
96
|
+
const providerTokens = (result.providers as any[]).map(
|
|
97
|
+
(p: any) => p.provide || p,
|
|
98
|
+
);
|
|
99
|
+
expect(providerTokens).toContain(PGBOSS_OPTIONS);
|
|
100
|
+
expect(providerTokens).toContain(PGBOSS_TOKEN);
|
|
101
|
+
// useClass also adds the class itself as a provider
|
|
102
|
+
expect(providerTokens).toContain(TestOptionsFactory);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("should pass imports through", () => {
|
|
106
|
+
const fakeModule = { module: class FakeModule {} } as any;
|
|
107
|
+
const result = PgBossModule.forRootAsync({
|
|
108
|
+
imports: [fakeModule],
|
|
109
|
+
useFactory: jest.fn(),
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
expect(result.imports).toContain(fakeModule);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("should default imports to empty array", () => {
|
|
116
|
+
const result = PgBossModule.forRootAsync({
|
|
117
|
+
useFactory: jest.fn(),
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
expect(result.imports).toEqual([]);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -19,10 +19,10 @@ jest.mock("pg-boss", () => {
|
|
|
19
19
|
|
|
20
20
|
describe("PgBossService", () => {
|
|
21
21
|
let service: PgBossService;
|
|
22
|
-
let mockPgBoss:
|
|
22
|
+
let mockPgBoss: jest.Mocked<PgBoss>;
|
|
23
23
|
|
|
24
24
|
beforeEach(async () => {
|
|
25
|
-
mockPgBoss = new PgBoss("connectionString")
|
|
25
|
+
mockPgBoss = new PgBoss("connectionString") as jest.Mocked<PgBoss>;
|
|
26
26
|
mockPgBoss.createQueue.mockResolvedValue(undefined);
|
|
27
27
|
mockPgBoss.send.mockResolvedValue("job-id");
|
|
28
28
|
mockPgBoss.schedule.mockResolvedValue(undefined);
|
|
@@ -49,6 +49,7 @@ describe("PgBossService", () => {
|
|
|
49
49
|
|
|
50
50
|
await service.registerJob("test-job", handler, options);
|
|
51
51
|
|
|
52
|
+
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
52
53
|
expect(mockPgBoss.work).toHaveBeenCalledWith(
|
|
53
54
|
"test-job",
|
|
54
55
|
{ includeMetadata: true },
|
|
@@ -63,8 +64,64 @@ describe("PgBossService", () => {
|
|
|
63
64
|
|
|
64
65
|
await service.scheduleJob("test-job", data, {});
|
|
65
66
|
|
|
67
|
+
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
66
68
|
expect(mockPgBoss.send).toHaveBeenCalledWith("test-job", data, {});
|
|
67
69
|
});
|
|
70
|
+
|
|
71
|
+
it("should call createQueue before send", async () => {
|
|
72
|
+
const callOrder: string[] = [];
|
|
73
|
+
mockPgBoss.createQueue.mockImplementation(() => {
|
|
74
|
+
callOrder.push("createQueue");
|
|
75
|
+
return Promise.resolve();
|
|
76
|
+
});
|
|
77
|
+
mockPgBoss.send.mockImplementation(() => {
|
|
78
|
+
callOrder.push("send");
|
|
79
|
+
return Promise.resolve("job-id");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
await service.scheduleJob("test-job", {});
|
|
83
|
+
|
|
84
|
+
expect(callOrder).toEqual(["createQueue", "send"]);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("scheduleCronJob", () => {
|
|
89
|
+
it("should call createQueue then schedule with correct params", async () => {
|
|
90
|
+
await service.scheduleCronJob(
|
|
91
|
+
"cron-job",
|
|
92
|
+
"0 * * * *",
|
|
93
|
+
{ key: "val" },
|
|
94
|
+
{ tz: "UTC" },
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
98
|
+
expect(mockPgBoss.createQueue).toHaveBeenCalledWith("cron-job");
|
|
99
|
+
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
100
|
+
expect(mockPgBoss.schedule).toHaveBeenCalledWith(
|
|
101
|
+
"cron-job",
|
|
102
|
+
"0 * * * *",
|
|
103
|
+
{ key: "val" },
|
|
104
|
+
{ tz: "UTC" },
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("should use {} defaults when data/options are omitted", async () => {
|
|
109
|
+
await service.scheduleCronJob("cron-job", "0 * * * *");
|
|
110
|
+
|
|
111
|
+
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
112
|
+
expect(mockPgBoss.schedule).toHaveBeenCalledWith(
|
|
113
|
+
"cron-job",
|
|
114
|
+
"0 * * * *",
|
|
115
|
+
{},
|
|
116
|
+
{},
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe("boss getter", () => {
|
|
122
|
+
it("should return the PgBoss instance", () => {
|
|
123
|
+
expect(service.boss).toBe(mockPgBoss);
|
|
124
|
+
});
|
|
68
125
|
});
|
|
69
126
|
|
|
70
127
|
describe("registerCronJob", () => {
|
|
@@ -82,12 +139,14 @@ describe("PgBossService", () => {
|
|
|
82
139
|
options,
|
|
83
140
|
);
|
|
84
141
|
|
|
142
|
+
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
85
143
|
expect(mockPgBoss.schedule).toHaveBeenCalledWith(
|
|
86
144
|
"test-cron-job",
|
|
87
145
|
cron,
|
|
88
146
|
data,
|
|
89
147
|
{ tz: "UTC" },
|
|
90
148
|
);
|
|
149
|
+
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
91
150
|
expect(mockPgBoss.work).toHaveBeenCalledWith(
|
|
92
151
|
"test-cron-job",
|
|
93
152
|
{ includeMetadata: true, tz: "UTC" },
|
package/.eslintrc.js
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
/** @type {import('eslint').ESLint.ConfigData} */
|
|
2
|
-
module.exports = {
|
|
3
|
-
parser: '@typescript-eslint/parser',
|
|
4
|
-
parserOptions: {
|
|
5
|
-
project: 'tsconfig.json',
|
|
6
|
-
sourceType: 'module',
|
|
7
|
-
},
|
|
8
|
-
plugins: ['@typescript-eslint/eslint-plugin'],
|
|
9
|
-
extends: [
|
|
10
|
-
'plugin:@typescript-eslint/recommended',
|
|
11
|
-
'plugin:prettier/recommended',
|
|
12
|
-
],
|
|
13
|
-
root: true,
|
|
14
|
-
env: {
|
|
15
|
-
node: true,
|
|
16
|
-
jest: true,
|
|
17
|
-
},
|
|
18
|
-
ignorePatterns: ['.eslintrc.js', 'tools/**'],
|
|
19
|
-
rules: {
|
|
20
|
-
'@typescript-eslint/interface-name-prefix': 'off',
|
|
21
|
-
'@typescript-eslint/explicit-function-return-type': 'off',
|
|
22
|
-
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
|
23
|
-
'@typescript-eslint/no-explicit-any': 'off',
|
|
24
|
-
"@typescript-eslint/no-unused-vars": [
|
|
25
|
-
"error",
|
|
26
|
-
{
|
|
27
|
-
"args": "all",
|
|
28
|
-
"argsIgnorePattern": "^_",
|
|
29
|
-
"varsIgnorePattern": "^_",
|
|
30
|
-
}
|
|
31
|
-
],
|
|
32
|
-
},
|
|
33
|
-
};
|