@nicnocquee/dataqueue 1.20.0 → 1.21.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/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
+ });
package/src/queue.ts CHANGED
@@ -421,7 +421,7 @@ export const cancelAllUpcomingJobs = async (
421
421
  filters?: {
422
422
  jobType?: string;
423
423
  priority?: number;
424
- runAt?: Date;
424
+ runAt?: Date | { gt?: Date; gte?: Date; lt?: Date; lte?: Date; eq?: Date };
425
425
  tags?: { values: string[]; mode?: TagQueryMode };
426
426
  },
427
427
  ): Promise<number> => {
@@ -443,8 +443,32 @@ export const cancelAllUpcomingJobs = async (
443
443
  params.push(filters.priority);
444
444
  }
445
445
  if (filters.runAt) {
446
- query += ` AND run_at = $${paramIdx++}`;
447
- params.push(filters.runAt);
446
+ if (filters.runAt instanceof Date) {
447
+ query += ` AND run_at = $${paramIdx++}`;
448
+ params.push(filters.runAt);
449
+ } else if (typeof filters.runAt === 'object') {
450
+ const ops = filters.runAt;
451
+ if (ops.gt) {
452
+ query += ` AND run_at > $${paramIdx++}`;
453
+ params.push(ops.gt);
454
+ }
455
+ if (ops.gte) {
456
+ query += ` AND run_at >= $${paramIdx++}`;
457
+ params.push(ops.gte);
458
+ }
459
+ if (ops.lt) {
460
+ query += ` AND run_at < $${paramIdx++}`;
461
+ params.push(ops.lt);
462
+ }
463
+ if (ops.lte) {
464
+ query += ` AND run_at <= $${paramIdx++}`;
465
+ params.push(ops.lte);
466
+ }
467
+ if (ops.eq) {
468
+ query += ` AND run_at = $${paramIdx++}`;
469
+ params.push(ops.eq);
470
+ }
471
+ }
448
472
  }
449
473
  if (
450
474
  filters.tags &&
@@ -661,3 +685,123 @@ export const getJobsByTags = async <
661
685
  client.release();
662
686
  }
663
687
  };
688
+
689
+ export const getJobs = async <PayloadMap, T extends keyof PayloadMap & string>(
690
+ pool: Pool,
691
+ filters?: {
692
+ jobType?: string;
693
+ priority?: number;
694
+ runAt?: Date | { gt?: Date; gte?: Date; lt?: Date; lte?: Date; eq?: Date };
695
+ tags?: { values: string[]; mode?: TagQueryMode };
696
+ },
697
+ limit = 100,
698
+ offset = 0,
699
+ ): Promise<JobRecord<PayloadMap, T>[]> => {
700
+ const client = await pool.connect();
701
+ try {
702
+ let query = `SELECT id, job_type AS "jobType", payload, status, max_attempts AS "maxAttempts", attempts, priority, run_at AS "runAt", timeout_ms AS "timeoutMs", created_at AS "createdAt", updated_at AS "updatedAt", started_at AS "startedAt", completed_at AS "completedAt", last_failed_at AS "lastFailedAt", locked_at AS "lockedAt", locked_by AS "lockedBy", error_history AS "errorHistory", failure_reason AS "failureReason", next_attempt_at AS "nextAttemptAt", last_failed_at AS "lastFailedAt", last_retried_at AS "lastRetriedAt", last_cancelled_at AS "lastCancelledAt", pending_reason AS "pendingReason", tags FROM job_queue`;
703
+ const params: any[] = [];
704
+ let where: string[] = [];
705
+ let paramIdx = 1;
706
+ if (filters) {
707
+ if (filters.jobType) {
708
+ where.push(`job_type = $${paramIdx++}`);
709
+ params.push(filters.jobType);
710
+ }
711
+ if (filters.priority !== undefined) {
712
+ where.push(`priority = $${paramIdx++}`);
713
+ params.push(filters.priority);
714
+ }
715
+ if (filters.runAt) {
716
+ if (filters.runAt instanceof Date) {
717
+ where.push(`run_at = $${paramIdx++}`);
718
+ params.push(filters.runAt);
719
+ } else if (
720
+ typeof filters.runAt === 'object' &&
721
+ (filters.runAt.gt !== undefined ||
722
+ filters.runAt.gte !== undefined ||
723
+ filters.runAt.lt !== undefined ||
724
+ filters.runAt.lte !== undefined ||
725
+ filters.runAt.eq !== undefined)
726
+ ) {
727
+ const ops = filters.runAt as {
728
+ gt?: Date;
729
+ gte?: Date;
730
+ lt?: Date;
731
+ lte?: Date;
732
+ eq?: Date;
733
+ };
734
+ if (ops.gt) {
735
+ where.push(`run_at > $${paramIdx++}`);
736
+ params.push(ops.gt);
737
+ }
738
+ if (ops.gte) {
739
+ where.push(`run_at >= $${paramIdx++}`);
740
+ params.push(ops.gte);
741
+ }
742
+ if (ops.lt) {
743
+ where.push(`run_at < $${paramIdx++}`);
744
+ params.push(ops.lt);
745
+ }
746
+ if (ops.lte) {
747
+ where.push(`run_at <= $${paramIdx++}`);
748
+ params.push(ops.lte);
749
+ }
750
+ if (ops.eq) {
751
+ where.push(`run_at = $${paramIdx++}`);
752
+ params.push(ops.eq);
753
+ }
754
+ }
755
+ }
756
+ if (
757
+ filters.tags &&
758
+ filters.tags.values &&
759
+ filters.tags.values.length > 0
760
+ ) {
761
+ const mode = filters.tags.mode || 'all';
762
+ const tagValues = filters.tags.values;
763
+ switch (mode) {
764
+ case 'exact':
765
+ where.push(`tags = $${paramIdx++}`);
766
+ params.push(tagValues);
767
+ break;
768
+ case 'all':
769
+ where.push(`tags @> $${paramIdx++}`);
770
+ params.push(tagValues);
771
+ break;
772
+ case 'any':
773
+ where.push(`tags && $${paramIdx++}`);
774
+ params.push(tagValues);
775
+ break;
776
+ case 'none':
777
+ where.push(`NOT (tags && $${paramIdx++})`);
778
+ params.push(tagValues);
779
+ break;
780
+ default:
781
+ where.push(`tags @> $${paramIdx++}`);
782
+ params.push(tagValues);
783
+ }
784
+ }
785
+ }
786
+ if (where.length > 0) {
787
+ query += ` WHERE ${where.join(' AND ')}`;
788
+ }
789
+ // Always add LIMIT and OFFSET as the last parameters
790
+ paramIdx = params.length + 1;
791
+ query += ` ORDER BY created_at DESC LIMIT $${paramIdx++} OFFSET $${paramIdx}`;
792
+ params.push(limit, offset);
793
+ const result = await client.query(query, params);
794
+ log(`Found ${result.rows.length} jobs`);
795
+ return result.rows.map((job) => ({
796
+ ...job,
797
+ payload: job.payload,
798
+ timeoutMs: job.timeoutMs,
799
+ failureReason: job.failureReason,
800
+ }));
801
+ } catch (error) {
802
+ log(`Error getting jobs: ${error}`);
803
+ throw error;
804
+ } finally {
805
+ client.release();
806
+ }
807
+ };
package/src/types.ts CHANGED
@@ -225,6 +225,17 @@ export interface JobQueue<PayloadMap> {
225
225
  limit?: number,
226
226
  offset?: number,
227
227
  ) => Promise<JobRecord<PayloadMap, T>[]>;
228
+ /**
229
+ * Get jobs by filters.
230
+ /**
231
+ * Get jobs by filters.
232
+ */
233
+ getJobs: <T extends JobType<PayloadMap>>(filters?: {
234
+ jobType?: string;
235
+ priority?: number;
236
+ runAt?: Date | { gt?: Date; gte?: Date; lt?: Date; lte?: Date; eq?: Date };
237
+ tags?: { values: string[]; mode?: TagQueryMode };
238
+ }) => Promise<JobRecord<PayloadMap, T>[]>;
228
239
  /**
229
240
  * Retry a job given its ID.
230
241
  * - This will set the job status back to 'pending', clear the locked_at and locked_by, and allow it to be picked up by other workers.
@@ -253,13 +264,13 @@ export interface JobQueue<PayloadMap> {
253
264
  * - The filters are:
254
265
  * - jobType: The job type to cancel.
255
266
  * - priority: The priority of the job to cancel.
256
- * - runAt: The time the job is scheduled to run at.
267
+ * - runAt: The time the job is scheduled to run at (now supports gt/gte/lt/lte/eq).
257
268
  * - tags: An object with 'values' (string[]) and 'mode' (TagQueryMode) for tag-based cancellation.
258
269
  */
259
270
  cancelAllUpcomingJobs: (filters?: {
260
271
  jobType?: string;
261
272
  priority?: number;
262
- runAt?: Date;
273
+ runAt?: Date | { gt?: Date; gte?: Date; lt?: Date; lte?: Date; eq?: Date };
263
274
  tags?: { values: string[]; mode?: TagQueryMode };
264
275
  }) => Promise<number>;
265
276
  /**