@nicnocquee/dataqueue 1.20.0 → 1.22.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/dist/cli.cjs +6 -0
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +6 -0
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +185 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +48 -4
- package/dist/index.d.ts +48 -4
- package/dist/index.js +181 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +6 -0
- package/src/db-util.test.ts +56 -1
- package/src/db-util.ts +77 -6
- package/src/index.ts +19 -1
- package/src/queue.test.ts +449 -0
- package/src/queue.ts +147 -3
- package/src/types.ts +33 -3
package/src/db-util.test.ts
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
|
-
import { describe, it, expect, afterAll } from 'vitest';
|
|
1
|
+
import { describe, it, expect, afterAll, vi } from 'vitest';
|
|
2
2
|
import { createPool } from './db-util.js';
|
|
3
|
+
import fs from 'fs';
|
|
3
4
|
|
|
4
5
|
// Use a test schema name that is unlikely to exist
|
|
5
6
|
const TEST_SCHEMA = 'testschema';
|
|
6
7
|
const TEST_CONN = `postgres://postgres:postgres@localhost:5432/postgres?search_path=${TEST_SCHEMA}`;
|
|
7
8
|
|
|
9
|
+
// Dummy PEM string for testing
|
|
10
|
+
const DUMMY_PEM =
|
|
11
|
+
'-----BEGIN CERTIFICATE-----\nDUMMY\n-----END CERTIFICATE-----';
|
|
12
|
+
|
|
13
|
+
// Helper to mock fs.readFileSync
|
|
14
|
+
function mockReadFileSync(content: string) {
|
|
15
|
+
return vi.spyOn(fs, 'readFileSync').mockImplementation(() => content);
|
|
16
|
+
}
|
|
17
|
+
|
|
8
18
|
describe('createPool', () => {
|
|
9
19
|
const pool = createPool({ connectionString: TEST_CONN });
|
|
10
20
|
|
|
@@ -22,4 +32,49 @@ describe('createPool', () => {
|
|
|
22
32
|
client.release();
|
|
23
33
|
}
|
|
24
34
|
});
|
|
35
|
+
|
|
36
|
+
it('should use PEM string directly for ssl.ca', () => {
|
|
37
|
+
const pool = createPool({
|
|
38
|
+
connectionString: TEST_CONN,
|
|
39
|
+
ssl: { ca: DUMMY_PEM, rejectUnauthorized: true },
|
|
40
|
+
});
|
|
41
|
+
const ssl = pool.options.ssl;
|
|
42
|
+
if (typeof ssl === 'object' && ssl !== null) {
|
|
43
|
+
expect(ssl.ca).toBe(DUMMY_PEM);
|
|
44
|
+
} else {
|
|
45
|
+
throw new Error('ssl is not an object');
|
|
46
|
+
}
|
|
47
|
+
pool.end();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should load ca from file when using file:// path', () => {
|
|
51
|
+
const spy = mockReadFileSync(DUMMY_PEM);
|
|
52
|
+
const pool = createPool({
|
|
53
|
+
connectionString: TEST_CONN,
|
|
54
|
+
ssl: { ca: 'file:///dummy/path/ca.crt', rejectUnauthorized: true },
|
|
55
|
+
});
|
|
56
|
+
const ssl = pool.options.ssl;
|
|
57
|
+
if (typeof ssl === 'object' && ssl !== null) {
|
|
58
|
+
expect(spy).toHaveBeenCalledWith('/dummy/path/ca.crt', 'utf8');
|
|
59
|
+
expect(ssl.ca).toBe(DUMMY_PEM);
|
|
60
|
+
} else {
|
|
61
|
+
throw new Error('ssl is not an object');
|
|
62
|
+
}
|
|
63
|
+
pool.end();
|
|
64
|
+
spy.mockRestore();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should set rejectUnauthorized to false for self-signed certs', () => {
|
|
68
|
+
const pool = createPool({
|
|
69
|
+
connectionString: TEST_CONN,
|
|
70
|
+
ssl: { ca: DUMMY_PEM, rejectUnauthorized: false },
|
|
71
|
+
});
|
|
72
|
+
const ssl = pool.options.ssl;
|
|
73
|
+
if (typeof ssl === 'object' && ssl !== null) {
|
|
74
|
+
expect(ssl.rejectUnauthorized).toBe(false);
|
|
75
|
+
} else {
|
|
76
|
+
throw new Error('ssl is not an object');
|
|
77
|
+
}
|
|
78
|
+
pool.end();
|
|
79
|
+
});
|
|
25
80
|
});
|
package/src/db-util.ts
CHANGED
|
@@ -1,31 +1,102 @@
|
|
|
1
1
|
import { Pool } from 'pg';
|
|
2
2
|
import { JobQueueConfig } from './types.js';
|
|
3
3
|
import { parse } from 'pg-connection-string';
|
|
4
|
+
import fs from 'fs';
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Helper to load a PEM string or file. Only values starting with 'file://' are loaded from file.
|
|
8
|
+
*/
|
|
9
|
+
function loadPemOrFile(value?: string): string | undefined {
|
|
10
|
+
if (!value) return undefined;
|
|
11
|
+
if (value.startsWith('file://')) {
|
|
12
|
+
const filePath = value.slice(7);
|
|
13
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Create a database connection pool with flexible SSL certificate loading.
|
|
20
|
+
*
|
|
21
|
+
* SSL config example (for local file paths):
|
|
22
|
+
* ssl: {
|
|
23
|
+
* ca: process.env.PGSSLROOTCERT, // PEM string or 'file://...'
|
|
24
|
+
* cert: process.env.PGSSLCERT, // optional, PEM string or 'file://...'
|
|
25
|
+
* key: process.env.PGSSLKEY, // optional, PEM string or 'file://...'
|
|
26
|
+
* rejectUnauthorized: true
|
|
27
|
+
* }
|
|
28
|
+
*/
|
|
6
29
|
export const createPool = (config: JobQueueConfig['databaseConfig']): Pool => {
|
|
7
30
|
let searchPath: string | undefined;
|
|
31
|
+
let ssl: any = undefined;
|
|
32
|
+
let customCA: string | undefined;
|
|
33
|
+
let sslmode: string | undefined;
|
|
34
|
+
|
|
8
35
|
if (config.connectionString) {
|
|
9
|
-
// Parse the connection string to extract search_path from query params
|
|
10
36
|
try {
|
|
11
37
|
const url = new URL(config.connectionString);
|
|
12
38
|
searchPath = url.searchParams.get('search_path') || undefined;
|
|
39
|
+
sslmode = url.searchParams.get('sslmode') || undefined;
|
|
40
|
+
if (sslmode === 'no-verify') {
|
|
41
|
+
ssl = { rejectUnauthorized: false };
|
|
42
|
+
}
|
|
13
43
|
} catch (e) {
|
|
14
|
-
// fallback: try pg-connection-string parse (for non-standard URLs)
|
|
15
44
|
const parsed = parse(config.connectionString);
|
|
16
45
|
if (parsed.options) {
|
|
17
|
-
// options might look like '-c search_path=myschema'
|
|
18
46
|
const match = parsed.options.match(/search_path=([^\s]+)/);
|
|
19
47
|
if (match) {
|
|
20
48
|
searchPath = match[1];
|
|
21
49
|
}
|
|
22
50
|
}
|
|
51
|
+
sslmode = typeof parsed.sslmode === 'string' ? parsed.sslmode : undefined;
|
|
52
|
+
if (sslmode === 'no-verify') {
|
|
53
|
+
ssl = { rejectUnauthorized: false };
|
|
54
|
+
}
|
|
23
55
|
}
|
|
24
56
|
}
|
|
25
57
|
|
|
26
|
-
|
|
58
|
+
// Flexible SSL loading: only support file:// for file loading
|
|
59
|
+
if (config.ssl) {
|
|
60
|
+
if (typeof config.ssl.ca === 'string') {
|
|
61
|
+
customCA = config.ssl.ca;
|
|
62
|
+
} else if (typeof process.env.PGSSLROOTCERT === 'string') {
|
|
63
|
+
customCA = process.env.PGSSLROOTCERT;
|
|
64
|
+
} else {
|
|
65
|
+
customCA = undefined;
|
|
66
|
+
}
|
|
67
|
+
const caValue =
|
|
68
|
+
typeof customCA === 'string' ? loadPemOrFile(customCA) : undefined;
|
|
69
|
+
ssl = {
|
|
70
|
+
...ssl,
|
|
71
|
+
...(caValue ? { ca: caValue } : {}),
|
|
72
|
+
cert: loadPemOrFile(
|
|
73
|
+
typeof config.ssl.cert === 'string'
|
|
74
|
+
? config.ssl.cert
|
|
75
|
+
: process.env.PGSSLCERT,
|
|
76
|
+
),
|
|
77
|
+
key: loadPemOrFile(
|
|
78
|
+
typeof config.ssl.key === 'string'
|
|
79
|
+
? config.ssl.key
|
|
80
|
+
: process.env.PGSSLKEY,
|
|
81
|
+
),
|
|
82
|
+
rejectUnauthorized:
|
|
83
|
+
config.ssl.rejectUnauthorized !== undefined
|
|
84
|
+
? config.ssl.rejectUnauthorized
|
|
85
|
+
: true,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Warn if both sslmode (any value) and a custom CA are set
|
|
90
|
+
if (sslmode && customCA) {
|
|
91
|
+
const warning = `\n\n\x1b[33m**************************************************\n\u26A0\uFE0F WARNING: SSL CONFIGURATION ISSUE\n**************************************************\nBoth sslmode ('${sslmode}') is set in the connection string\nand a custom CA is provided (via config.ssl.ca or PGSSLROOTCERT).\nThis combination may cause connection failures or unexpected behavior.\n\nRecommended: Remove sslmode from the connection string when using a custom CA.\n**************************************************\x1b[0m\n`;
|
|
92
|
+
console.warn(warning);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const pool = new Pool({
|
|
96
|
+
...config,
|
|
97
|
+
...(ssl ? { ssl } : {}),
|
|
98
|
+
});
|
|
27
99
|
|
|
28
|
-
// If search_path is specified, set it for every new connection
|
|
29
100
|
if (searchPath) {
|
|
30
101
|
pool.on('connect', (client) => {
|
|
31
102
|
client.query(`SET search_path TO ${searchPath}`);
|
package/src/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
reclaimStuckJobs,
|
|
11
11
|
getJobEvents,
|
|
12
12
|
getJobsByTags,
|
|
13
|
+
getJobs,
|
|
13
14
|
} from './queue.js';
|
|
14
15
|
import { createProcessor } from './processor.js';
|
|
15
16
|
import {
|
|
@@ -56,6 +57,21 @@ export const initJobQueue = <PayloadMap = any>(
|
|
|
56
57
|
getAllJobs<PayloadMap, any>(pool, limit, offset),
|
|
57
58
|
config.verbose ?? false,
|
|
58
59
|
),
|
|
60
|
+
getJobs: withLogContext(
|
|
61
|
+
(
|
|
62
|
+
filters?: {
|
|
63
|
+
jobType?: string;
|
|
64
|
+
priority?: number;
|
|
65
|
+
runAt?:
|
|
66
|
+
| Date
|
|
67
|
+
| { gt?: Date; gte?: Date; lt?: Date; lte?: Date; eq?: Date };
|
|
68
|
+
tags?: { values: string[]; mode?: import('./types.js').TagQueryMode };
|
|
69
|
+
},
|
|
70
|
+
limit?: number,
|
|
71
|
+
offset?: number,
|
|
72
|
+
) => getJobs<PayloadMap, any>(pool, filters, limit, offset),
|
|
73
|
+
config.verbose ?? false,
|
|
74
|
+
),
|
|
59
75
|
retryJob: (jobId: number) => retryJob(pool, jobId),
|
|
60
76
|
cleanupOldJobs: (daysToKeep?: number) => cleanupOldJobs(pool, daysToKeep),
|
|
61
77
|
cancelJob: withLogContext(
|
|
@@ -66,7 +82,9 @@ export const initJobQueue = <PayloadMap = any>(
|
|
|
66
82
|
(filters?: {
|
|
67
83
|
jobType?: string;
|
|
68
84
|
priority?: number;
|
|
69
|
-
runAt?:
|
|
85
|
+
runAt?:
|
|
86
|
+
| Date
|
|
87
|
+
| { gt?: Date; gte?: Date; lt?: Date; lte?: Date; eq?: Date };
|
|
70
88
|
tags?: { values: string[]; mode?: import('./types.js').TagQueryMode };
|
|
71
89
|
}) => cancelAllUpcomingJobs(pool, filters),
|
|
72
90
|
config.verbose ?? false,
|
package/src/queue.test.ts
CHANGED
|
@@ -748,3 +748,452 @@ describe('tags feature', () => {
|
|
|
748
748
|
expect(job3?.status).toBe('cancelled');
|
|
749
749
|
});
|
|
750
750
|
});
|
|
751
|
+
|
|
752
|
+
describe('cancelAllUpcomingJobs with runAt object filter', () => {
|
|
753
|
+
let pool: Pool;
|
|
754
|
+
let dbName: string;
|
|
755
|
+
|
|
756
|
+
beforeEach(async () => {
|
|
757
|
+
const setup = await createTestDbAndPool();
|
|
758
|
+
pool = setup.pool;
|
|
759
|
+
dbName = setup.dbName;
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
afterEach(async () => {
|
|
763
|
+
await pool.end();
|
|
764
|
+
await destroyTestDb(dbName);
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
it('should cancel jobs with runAt > filter (gt)', async () => {
|
|
768
|
+
const now = new Date();
|
|
769
|
+
const past = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
|
770
|
+
const future = new Date(now.getTime() + 24 * 60 * 60 * 1000);
|
|
771
|
+
const jobIdPast = await queue.addJob<{ email: { to: string } }, 'email'>(
|
|
772
|
+
pool,
|
|
773
|
+
{
|
|
774
|
+
jobType: 'email',
|
|
775
|
+
payload: { to: 'past@example.com' },
|
|
776
|
+
runAt: past,
|
|
777
|
+
},
|
|
778
|
+
);
|
|
779
|
+
const jobIdNow = await queue.addJob<{ email: { to: string } }, 'email'>(
|
|
780
|
+
pool,
|
|
781
|
+
{
|
|
782
|
+
jobType: 'email',
|
|
783
|
+
payload: { to: 'now@example.com' },
|
|
784
|
+
runAt: now,
|
|
785
|
+
},
|
|
786
|
+
);
|
|
787
|
+
const jobIdFuture = await queue.addJob<{ email: { to: string } }, 'email'>(
|
|
788
|
+
pool,
|
|
789
|
+
{
|
|
790
|
+
jobType: 'email',
|
|
791
|
+
payload: { to: 'future@example.com' },
|
|
792
|
+
runAt: future,
|
|
793
|
+
},
|
|
794
|
+
);
|
|
795
|
+
const cancelled = await queue.cancelAllUpcomingJobs(pool, {
|
|
796
|
+
runAt: { gt: now },
|
|
797
|
+
});
|
|
798
|
+
expect(cancelled).toBe(1);
|
|
799
|
+
const jobPast = await queue.getJob(pool, jobIdPast);
|
|
800
|
+
const jobNow = await queue.getJob(pool, jobIdNow);
|
|
801
|
+
const jobFuture = await queue.getJob(pool, jobIdFuture);
|
|
802
|
+
expect(jobPast?.status).toBe('pending');
|
|
803
|
+
expect(jobNow?.status).toBe('pending');
|
|
804
|
+
expect(jobFuture?.status).toBe('cancelled');
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
it('should cancel jobs with runAt >= filter (gte)', async () => {
|
|
808
|
+
const now = new Date();
|
|
809
|
+
const past = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
|
810
|
+
const future = new Date(now.getTime() + 24 * 60 * 60 * 1000);
|
|
811
|
+
const jobIdPast = await queue.addJob<{ email: { to: string } }, 'email'>(
|
|
812
|
+
pool,
|
|
813
|
+
{
|
|
814
|
+
jobType: 'email',
|
|
815
|
+
payload: { to: 'past@example.com' },
|
|
816
|
+
runAt: past,
|
|
817
|
+
},
|
|
818
|
+
);
|
|
819
|
+
const jobIdNow = await queue.addJob<{ email: { to: string } }, 'email'>(
|
|
820
|
+
pool,
|
|
821
|
+
{
|
|
822
|
+
jobType: 'email',
|
|
823
|
+
payload: { to: 'now@example.com' },
|
|
824
|
+
runAt: now,
|
|
825
|
+
},
|
|
826
|
+
);
|
|
827
|
+
const jobIdFuture = await queue.addJob<{ email: { to: string } }, 'email'>(
|
|
828
|
+
pool,
|
|
829
|
+
{
|
|
830
|
+
jobType: 'email',
|
|
831
|
+
payload: { to: 'future@example.com' },
|
|
832
|
+
runAt: future,
|
|
833
|
+
},
|
|
834
|
+
);
|
|
835
|
+
const cancelled = await queue.cancelAllUpcomingJobs(pool, {
|
|
836
|
+
runAt: { gte: now },
|
|
837
|
+
});
|
|
838
|
+
expect(cancelled).toBe(2);
|
|
839
|
+
const jobPast = await queue.getJob(pool, jobIdPast);
|
|
840
|
+
const jobNow = await queue.getJob(pool, jobIdNow);
|
|
841
|
+
const jobFuture = await queue.getJob(pool, jobIdFuture);
|
|
842
|
+
expect(jobPast?.status).toBe('pending');
|
|
843
|
+
expect(jobNow?.status).toBe('cancelled');
|
|
844
|
+
expect(jobFuture?.status).toBe('cancelled');
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
it('should cancel jobs with runAt < filter (lt)', async () => {
|
|
848
|
+
const now = new Date();
|
|
849
|
+
const past = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
|
850
|
+
const future = new Date(now.getTime() + 24 * 60 * 60 * 1000);
|
|
851
|
+
const jobIdPast = await queue.addJob<{ email: { to: string } }, 'email'>(
|
|
852
|
+
pool,
|
|
853
|
+
{
|
|
854
|
+
jobType: 'email',
|
|
855
|
+
payload: { to: 'past@example.com' },
|
|
856
|
+
runAt: past,
|
|
857
|
+
},
|
|
858
|
+
);
|
|
859
|
+
const jobIdNow = await queue.addJob<{ email: { to: string } }, 'email'>(
|
|
860
|
+
pool,
|
|
861
|
+
{
|
|
862
|
+
jobType: 'email',
|
|
863
|
+
payload: { to: 'now@example.com' },
|
|
864
|
+
runAt: now,
|
|
865
|
+
},
|
|
866
|
+
);
|
|
867
|
+
const jobIdFuture = await queue.addJob<{ email: { to: string } }, 'email'>(
|
|
868
|
+
pool,
|
|
869
|
+
{
|
|
870
|
+
jobType: 'email',
|
|
871
|
+
payload: { to: 'future@example.com' },
|
|
872
|
+
runAt: future,
|
|
873
|
+
},
|
|
874
|
+
);
|
|
875
|
+
const cancelled = await queue.cancelAllUpcomingJobs(pool, {
|
|
876
|
+
runAt: { lt: now },
|
|
877
|
+
});
|
|
878
|
+
expect(cancelled).toBe(1);
|
|
879
|
+
const jobPast = await queue.getJob(pool, jobIdPast);
|
|
880
|
+
const jobNow = await queue.getJob(pool, jobIdNow);
|
|
881
|
+
const jobFuture = await queue.getJob(pool, jobIdFuture);
|
|
882
|
+
expect(jobPast?.status).toBe('cancelled');
|
|
883
|
+
expect(jobNow?.status).toBe('pending');
|
|
884
|
+
expect(jobFuture?.status).toBe('pending');
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
it('should cancel jobs with runAt <= filter (lte)', async () => {
|
|
888
|
+
const now = new Date();
|
|
889
|
+
const past = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
|
890
|
+
const future = new Date(now.getTime() + 24 * 60 * 60 * 1000);
|
|
891
|
+
const jobIdPast = await queue.addJob<{ email: { to: string } }, 'email'>(
|
|
892
|
+
pool,
|
|
893
|
+
{
|
|
894
|
+
jobType: 'email',
|
|
895
|
+
payload: { to: 'past@example.com' },
|
|
896
|
+
runAt: past,
|
|
897
|
+
},
|
|
898
|
+
);
|
|
899
|
+
const jobIdNow = await queue.addJob<{ email: { to: string } }, 'email'>(
|
|
900
|
+
pool,
|
|
901
|
+
{
|
|
902
|
+
jobType: 'email',
|
|
903
|
+
payload: { to: 'now@example.com' },
|
|
904
|
+
runAt: now,
|
|
905
|
+
},
|
|
906
|
+
);
|
|
907
|
+
const jobIdFuture = await queue.addJob<{ email: { to: string } }, 'email'>(
|
|
908
|
+
pool,
|
|
909
|
+
{
|
|
910
|
+
jobType: 'email',
|
|
911
|
+
payload: { to: 'future@example.com' },
|
|
912
|
+
runAt: future,
|
|
913
|
+
},
|
|
914
|
+
);
|
|
915
|
+
const cancelled = await queue.cancelAllUpcomingJobs(pool, {
|
|
916
|
+
runAt: { lte: now },
|
|
917
|
+
});
|
|
918
|
+
expect(cancelled).toBe(2);
|
|
919
|
+
const jobPast = await queue.getJob(pool, jobIdPast);
|
|
920
|
+
const jobNow = await queue.getJob(pool, jobIdNow);
|
|
921
|
+
const jobFuture = await queue.getJob(pool, jobIdFuture);
|
|
922
|
+
expect(jobPast?.status).toBe('cancelled');
|
|
923
|
+
expect(jobNow?.status).toBe('cancelled');
|
|
924
|
+
expect(jobFuture?.status).toBe('pending');
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
it('should cancel jobs with runAt eq filter (eq)', async () => {
|
|
928
|
+
const now = new Date();
|
|
929
|
+
const past = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
|
930
|
+
const future = new Date(now.getTime() + 24 * 60 * 60 * 1000);
|
|
931
|
+
const jobIdPast = await queue.addJob<{ email: { to: string } }, 'email'>(
|
|
932
|
+
pool,
|
|
933
|
+
{
|
|
934
|
+
jobType: 'email',
|
|
935
|
+
payload: { to: 'past@example.com' },
|
|
936
|
+
runAt: past,
|
|
937
|
+
},
|
|
938
|
+
);
|
|
939
|
+
const jobIdNow = await queue.addJob<{ email: { to: string } }, 'email'>(
|
|
940
|
+
pool,
|
|
941
|
+
{
|
|
942
|
+
jobType: 'email',
|
|
943
|
+
payload: { to: 'now@example.com' },
|
|
944
|
+
runAt: now,
|
|
945
|
+
},
|
|
946
|
+
);
|
|
947
|
+
const jobIdFuture = await queue.addJob<{ email: { to: string } }, 'email'>(
|
|
948
|
+
pool,
|
|
949
|
+
{
|
|
950
|
+
jobType: 'email',
|
|
951
|
+
payload: { to: 'future@example.com' },
|
|
952
|
+
runAt: future,
|
|
953
|
+
},
|
|
954
|
+
);
|
|
955
|
+
const cancelled = await queue.cancelAllUpcomingJobs(pool, {
|
|
956
|
+
runAt: { eq: now },
|
|
957
|
+
});
|
|
958
|
+
expect(cancelled).toBe(1);
|
|
959
|
+
const jobPast = await queue.getJob(pool, jobIdPast);
|
|
960
|
+
const jobNow = await queue.getJob(pool, jobIdNow);
|
|
961
|
+
const jobFuture = await queue.getJob(pool, jobIdFuture);
|
|
962
|
+
expect(jobPast?.status).toBe('pending');
|
|
963
|
+
expect(jobNow?.status).toBe('cancelled');
|
|
964
|
+
expect(jobFuture?.status).toBe('pending');
|
|
965
|
+
});
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
describe('getJobs', () => {
|
|
969
|
+
let pool: Pool;
|
|
970
|
+
let dbName: string;
|
|
971
|
+
|
|
972
|
+
beforeEach(async () => {
|
|
973
|
+
const setup = await createTestDbAndPool();
|
|
974
|
+
pool = setup.pool;
|
|
975
|
+
dbName = setup.dbName;
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
afterEach(async () => {
|
|
979
|
+
await pool.end();
|
|
980
|
+
await destroyTestDb(dbName);
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
it('should filter by jobType', async () => {
|
|
984
|
+
const id1 = await queue.addJob<{ a: { n: number }; b: { n: number } }, 'a'>(
|
|
985
|
+
pool,
|
|
986
|
+
{ jobType: 'a', payload: { n: 1 } },
|
|
987
|
+
);
|
|
988
|
+
const id2 = await queue.addJob<{ a: { n: number }; b: { n: number } }, 'b'>(
|
|
989
|
+
pool,
|
|
990
|
+
{ jobType: 'b', payload: { n: 2 } },
|
|
991
|
+
);
|
|
992
|
+
const jobs = await queue.getJobs(pool, { jobType: 'a' });
|
|
993
|
+
expect(jobs.map((j) => j.id)).toContain(id1);
|
|
994
|
+
expect(jobs.map((j) => j.id)).not.toContain(id2);
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
it('should filter by priority', async () => {
|
|
998
|
+
const id1 = await queue.addJob<{ a: { n: number } }, 'a'>(pool, {
|
|
999
|
+
jobType: 'a',
|
|
1000
|
+
payload: { n: 1 },
|
|
1001
|
+
priority: 1,
|
|
1002
|
+
});
|
|
1003
|
+
const id2 = await queue.addJob<{ a: { n: number } }, 'a'>(pool, {
|
|
1004
|
+
jobType: 'a',
|
|
1005
|
+
payload: { n: 2 },
|
|
1006
|
+
priority: 2,
|
|
1007
|
+
});
|
|
1008
|
+
const jobs = await queue.getJobs(pool, { priority: 2 });
|
|
1009
|
+
expect(jobs.map((j) => j.id)).toContain(id2);
|
|
1010
|
+
expect(jobs.map((j) => j.id)).not.toContain(id1);
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
it('should filter by runAt', async () => {
|
|
1014
|
+
const runAt = new Date(Date.UTC(2030, 0, 1, 12, 0, 0, 0));
|
|
1015
|
+
const id1 = await queue.addJob<{ a: { n: number } }, 'a'>(pool, {
|
|
1016
|
+
jobType: 'a',
|
|
1017
|
+
payload: { n: 1 },
|
|
1018
|
+
runAt,
|
|
1019
|
+
});
|
|
1020
|
+
const id2 = await queue.addJob<{ a: { n: number } }, 'a'>(pool, {
|
|
1021
|
+
jobType: 'a',
|
|
1022
|
+
payload: { n: 2 },
|
|
1023
|
+
});
|
|
1024
|
+
const jobs = await queue.getJobs(pool, { runAt });
|
|
1025
|
+
expect(jobs.map((j) => j.id)).toContain(id1);
|
|
1026
|
+
expect(jobs.map((j) => j.id)).not.toContain(id2);
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
it('should filter jobs using runAt with gt/gte/lt/lte/eq', async () => {
|
|
1030
|
+
const now = new Date();
|
|
1031
|
+
const past = new Date(now.getTime() - 24 * 60 * 60 * 1000); // 1 day ago
|
|
1032
|
+
const future = new Date(now.getTime() + 24 * 60 * 60 * 1000); // 1 day ahead
|
|
1033
|
+
const jobIdPast = await queue.addJob<{ email: { to: string } }, 'email'>(
|
|
1034
|
+
pool,
|
|
1035
|
+
{
|
|
1036
|
+
jobType: 'email',
|
|
1037
|
+
payload: { to: 'past@example.com' },
|
|
1038
|
+
runAt: past,
|
|
1039
|
+
},
|
|
1040
|
+
);
|
|
1041
|
+
const jobIdNow = await queue.addJob<{ email: { to: string } }, 'email'>(
|
|
1042
|
+
pool,
|
|
1043
|
+
{
|
|
1044
|
+
jobType: 'email',
|
|
1045
|
+
payload: { to: 'now@example.com' },
|
|
1046
|
+
runAt: now,
|
|
1047
|
+
},
|
|
1048
|
+
);
|
|
1049
|
+
const jobIdFuture = await queue.addJob<{ email: { to: string } }, 'email'>(
|
|
1050
|
+
pool,
|
|
1051
|
+
{
|
|
1052
|
+
jobType: 'email',
|
|
1053
|
+
payload: { to: 'future@example.com' },
|
|
1054
|
+
runAt: future,
|
|
1055
|
+
},
|
|
1056
|
+
);
|
|
1057
|
+
// eq
|
|
1058
|
+
let jobs = await queue.getJobs(pool, { runAt: now });
|
|
1059
|
+
expect(jobs.map((j) => j.id)).toContain(jobIdNow);
|
|
1060
|
+
// gt
|
|
1061
|
+
jobs = await queue.getJobs(pool, { runAt: { gt: now } });
|
|
1062
|
+
expect(jobs.map((j) => j.id)).toContain(jobIdFuture);
|
|
1063
|
+
expect(jobs.map((j) => j.id)).not.toContain(jobIdNow);
|
|
1064
|
+
// gte
|
|
1065
|
+
jobs = await queue.getJobs(pool, { runAt: { gte: now } });
|
|
1066
|
+
expect(jobs.map((j) => j.id)).toContain(jobIdNow);
|
|
1067
|
+
expect(jobs.map((j) => j.id)).toContain(jobIdFuture);
|
|
1068
|
+
// lt
|
|
1069
|
+
jobs = await queue.getJobs(pool, { runAt: { lt: now } });
|
|
1070
|
+
expect(jobs.map((j) => j.id)).toContain(jobIdPast);
|
|
1071
|
+
expect(jobs.map((j) => j.id)).not.toContain(jobIdNow);
|
|
1072
|
+
// lte
|
|
1073
|
+
jobs = await queue.getJobs(pool, { runAt: { lte: now } });
|
|
1074
|
+
expect(jobs.map((j) => j.id)).toContain(jobIdPast);
|
|
1075
|
+
expect(jobs.map((j) => j.id)).toContain(jobIdNow);
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
it('should filter by tags (all mode)', async () => {
|
|
1079
|
+
const id1 = await queue.addJob<{ a: { n: number } }, 'a'>(pool, {
|
|
1080
|
+
jobType: 'a',
|
|
1081
|
+
payload: { n: 1 },
|
|
1082
|
+
tags: ['foo', 'bar'],
|
|
1083
|
+
});
|
|
1084
|
+
const id2 = await queue.addJob<{ a: { n: number } }, 'a'>(pool, {
|
|
1085
|
+
jobType: 'a',
|
|
1086
|
+
payload: { n: 2 },
|
|
1087
|
+
tags: ['foo'],
|
|
1088
|
+
});
|
|
1089
|
+
const jobs = await queue.getJobs(pool, {
|
|
1090
|
+
tags: { values: ['foo', 'bar'], mode: 'all' },
|
|
1091
|
+
});
|
|
1092
|
+
expect(jobs.map((j) => j.id)).toContain(id1);
|
|
1093
|
+
expect(jobs.map((j) => j.id)).not.toContain(id2);
|
|
1094
|
+
});
|
|
1095
|
+
|
|
1096
|
+
it('should filter by tags (any mode)', async () => {
|
|
1097
|
+
const id1 = await queue.addJob<{ a: { n: number } }, 'a'>(pool, {
|
|
1098
|
+
jobType: 'a',
|
|
1099
|
+
payload: { n: 1 },
|
|
1100
|
+
tags: ['foo'],
|
|
1101
|
+
});
|
|
1102
|
+
const id2 = await queue.addJob<{ a: { n: number } }, 'a'>(pool, {
|
|
1103
|
+
jobType: 'a',
|
|
1104
|
+
payload: { n: 2 },
|
|
1105
|
+
tags: ['bar'],
|
|
1106
|
+
});
|
|
1107
|
+
const jobs = await queue.getJobs(pool, {
|
|
1108
|
+
tags: { values: ['foo', 'bar'], mode: 'any' },
|
|
1109
|
+
});
|
|
1110
|
+
expect(jobs.map((j) => j.id)).toContain(id1);
|
|
1111
|
+
expect(jobs.map((j) => j.id)).toContain(id2);
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
it('should filter by tags (exact mode)', async () => {
|
|
1115
|
+
const id1 = await queue.addJob<{ a: { n: number } }, 'a'>(pool, {
|
|
1116
|
+
jobType: 'a',
|
|
1117
|
+
payload: { n: 1 },
|
|
1118
|
+
tags: ['foo', 'bar'],
|
|
1119
|
+
});
|
|
1120
|
+
const id2 = await queue.addJob<{ a: { n: number } }, 'a'>(pool, {
|
|
1121
|
+
jobType: 'a',
|
|
1122
|
+
payload: { n: 2 },
|
|
1123
|
+
tags: ['foo', 'bar', 'baz'],
|
|
1124
|
+
});
|
|
1125
|
+
const jobs = await queue.getJobs(pool, {
|
|
1126
|
+
tags: { values: ['foo', 'bar'], mode: 'exact' },
|
|
1127
|
+
});
|
|
1128
|
+
expect(jobs.map((j) => j.id)).toContain(id1);
|
|
1129
|
+
expect(jobs.map((j) => j.id)).not.toContain(id2);
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
it('should filter by tags (none mode)', async () => {
|
|
1133
|
+
const id1 = await queue.addJob<{ a: { n: number } }, 'a'>(pool, {
|
|
1134
|
+
jobType: 'a',
|
|
1135
|
+
payload: { n: 1 },
|
|
1136
|
+
tags: ['foo'],
|
|
1137
|
+
});
|
|
1138
|
+
const id2 = await queue.addJob<{ a: { n: number } }, 'a'>(pool, {
|
|
1139
|
+
jobType: 'a',
|
|
1140
|
+
payload: { n: 2 },
|
|
1141
|
+
tags: ['bar'],
|
|
1142
|
+
});
|
|
1143
|
+
const id3 = await queue.addJob<{ a: { n: number } }, 'a'>(pool, {
|
|
1144
|
+
jobType: 'a',
|
|
1145
|
+
payload: { n: 3 },
|
|
1146
|
+
tags: ['baz'],
|
|
1147
|
+
});
|
|
1148
|
+
const jobs = await queue.getJobs(pool, {
|
|
1149
|
+
tags: { values: ['foo', 'bar'], mode: 'none' },
|
|
1150
|
+
});
|
|
1151
|
+
expect(jobs.map((j) => j.id)).toContain(id3);
|
|
1152
|
+
expect(jobs.map((j) => j.id)).not.toContain(id1);
|
|
1153
|
+
expect(jobs.map((j) => j.id)).not.toContain(id2);
|
|
1154
|
+
});
|
|
1155
|
+
|
|
1156
|
+
it('should support pagination', async () => {
|
|
1157
|
+
const ids = [];
|
|
1158
|
+
for (let i = 0; i < 5; i++) {
|
|
1159
|
+
ids.push(
|
|
1160
|
+
await queue.addJob<{ a: { n: number } }, 'a'>(pool, {
|
|
1161
|
+
jobType: 'a',
|
|
1162
|
+
payload: { n: i },
|
|
1163
|
+
}),
|
|
1164
|
+
);
|
|
1165
|
+
}
|
|
1166
|
+
const firstTwo = await queue.getJobs(pool, {}, 2, 0);
|
|
1167
|
+
const nextTwo = await queue.getJobs(pool, {}, 2, 2);
|
|
1168
|
+
expect(firstTwo.length).toBe(2);
|
|
1169
|
+
expect(nextTwo.length).toBe(2);
|
|
1170
|
+
const firstIds = firstTwo.map((j) => j.id);
|
|
1171
|
+
const nextIds = nextTwo.map((j) => j.id);
|
|
1172
|
+
expect(firstIds.some((id) => nextIds.includes(id))).toBe(false);
|
|
1173
|
+
});
|
|
1174
|
+
|
|
1175
|
+
it('should filter by a combination of filters', async () => {
|
|
1176
|
+
const runAt = new Date(Date.UTC(2030, 0, 1, 12, 0, 0, 0));
|
|
1177
|
+
const id1 = await queue.addJob<{ a: { n: number } }, 'a'>(pool, {
|
|
1178
|
+
jobType: 'a',
|
|
1179
|
+
payload: { n: 1 },
|
|
1180
|
+
priority: 1,
|
|
1181
|
+
runAt,
|
|
1182
|
+
tags: ['foo', 'bar'],
|
|
1183
|
+
});
|
|
1184
|
+
const id2 = await queue.addJob<{ a: { n: number } }, 'a'>(pool, {
|
|
1185
|
+
jobType: 'a',
|
|
1186
|
+
payload: { n: 2 },
|
|
1187
|
+
priority: 2,
|
|
1188
|
+
tags: ['foo'],
|
|
1189
|
+
});
|
|
1190
|
+
const jobs = await queue.getJobs(pool, {
|
|
1191
|
+
jobType: 'a',
|
|
1192
|
+
priority: 1,
|
|
1193
|
+
runAt,
|
|
1194
|
+
tags: { values: ['foo', 'bar'], mode: 'all' },
|
|
1195
|
+
});
|
|
1196
|
+
expect(jobs.map((j) => j.id)).toContain(id1);
|
|
1197
|
+
expect(jobs.map((j) => j.id)).not.toContain(id2);
|
|
1198
|
+
});
|
|
1199
|
+
});
|