@nicnocquee/dataqueue 1.22.0 → 1.24.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
@@ -160,6 +160,528 @@ describe('queue integration', () => {
160
160
  expect(completedJob?.status).toBe('completed');
161
161
  });
162
162
 
163
+ it('should edit a pending job with all fields', async () => {
164
+ const jobId = await queue.addJob<{ email: { to: string } }, 'email'>(pool, {
165
+ jobType: 'email',
166
+ payload: { to: 'original@example.com' },
167
+ priority: 0,
168
+ maxAttempts: 3,
169
+ timeoutMs: 10000,
170
+ tags: ['original'],
171
+ });
172
+ const originalJob = await queue.getJob(pool, jobId);
173
+ const originalUpdatedAt = originalJob?.updatedAt;
174
+
175
+ // Wait a bit to ensure updated_at changes
176
+ await new Promise((r) => setTimeout(r, 10));
177
+
178
+ await queue.editJob<{ email: { to: string } }, 'email'>(pool, jobId, {
179
+ payload: { to: 'updated@example.com' },
180
+ priority: 10,
181
+ maxAttempts: 5,
182
+ runAt: new Date(Date.now() + 60000),
183
+ timeoutMs: 20000,
184
+ tags: ['updated', 'priority'],
185
+ });
186
+
187
+ const updatedJob = await queue.getJob(pool, jobId);
188
+ expect(updatedJob?.payload).toEqual({ to: 'updated@example.com' });
189
+ expect(updatedJob?.priority).toBe(10);
190
+ expect(updatedJob?.maxAttempts).toBe(5);
191
+ expect(updatedJob?.timeoutMs).toBe(20000);
192
+ expect(updatedJob?.tags).toEqual(['updated', 'priority']);
193
+ expect(updatedJob?.status).toBe('pending');
194
+ expect(updatedJob?.updatedAt.getTime()).toBeGreaterThan(
195
+ originalUpdatedAt?.getTime() || 0,
196
+ );
197
+ });
198
+
199
+ it('should edit a pending job with partial fields', async () => {
200
+ const jobId = await queue.addJob<{ email: { to: string } }, 'email'>(pool, {
201
+ jobType: 'email',
202
+ payload: { to: 'original@example.com' },
203
+ priority: 0,
204
+ maxAttempts: 3,
205
+ });
206
+
207
+ // Only update payload
208
+ await queue.editJob<{ email: { to: string } }, 'email'>(pool, jobId, {
209
+ payload: { to: 'updated@example.com' },
210
+ });
211
+
212
+ const updatedJob = await queue.getJob(pool, jobId);
213
+ expect(updatedJob?.payload).toEqual({ to: 'updated@example.com' });
214
+ expect(updatedJob?.priority).toBe(0); // Unchanged
215
+ expect(updatedJob?.maxAttempts).toBe(3); // Unchanged
216
+
217
+ // Only update priority
218
+ await queue.editJob<{ email: { to: string } }, 'email'>(pool, jobId, {
219
+ priority: 5,
220
+ });
221
+
222
+ const updatedJob2 = await queue.getJob(pool, jobId);
223
+ expect(updatedJob2?.payload).toEqual({ to: 'updated@example.com' }); // Still updated
224
+ expect(updatedJob2?.priority).toBe(5); // Now updated
225
+ });
226
+
227
+ it('should silently fail when editing a non-pending job', async () => {
228
+ const jobId = await queue.addJob<{ email: { to: string } }, 'email'>(pool, {
229
+ jobType: 'email',
230
+ payload: { to: 'original@example.com' },
231
+ });
232
+ await queue.completeJob(pool, jobId);
233
+
234
+ // Try to edit a completed job - should silently fail
235
+ await queue.editJob<{ email: { to: string } }, 'email'>(pool, jobId, {
236
+ payload: { to: 'updated@example.com' },
237
+ });
238
+
239
+ const job = await queue.getJob(pool, jobId);
240
+ expect(job?.status).toBe('completed');
241
+ expect(job?.payload).toEqual({ to: 'original@example.com' }); // Unchanged
242
+
243
+ // Try to edit a processing job
244
+ const jobId2 = await queue.addJob<{ email: { to: string } }, 'email'>(
245
+ pool,
246
+ {
247
+ jobType: 'email',
248
+ payload: { to: 'processing@example.com' },
249
+ },
250
+ );
251
+ await queue.getNextBatch(pool, 'worker-edit', 1);
252
+ await queue.editJob<{ email: { to: string } }, 'email'>(pool, jobId2, {
253
+ payload: { to: 'updated@example.com' },
254
+ });
255
+
256
+ const job2 = await queue.getJob(pool, jobId2);
257
+ expect(job2?.status).toBe('processing');
258
+ expect(job2?.payload).toEqual({ to: 'processing@example.com' }); // Unchanged
259
+ });
260
+
261
+ it('should record edited event when editing a job', async () => {
262
+ const jobId = await queue.addJob<{ email: { to: string } }, 'email'>(pool, {
263
+ jobType: 'email',
264
+ payload: { to: 'original@example.com' },
265
+ });
266
+
267
+ await queue.editJob<{ email: { to: string } }, 'email'>(pool, jobId, {
268
+ payload: { to: 'updated@example.com' },
269
+ priority: 10,
270
+ });
271
+
272
+ const res = await pool.query(
273
+ 'SELECT * FROM job_events WHERE job_id = $1 ORDER BY created_at ASC',
274
+ [jobId],
275
+ );
276
+ const events = res.rows.map(
277
+ (row) => objectKeysToCamelCase(row) as JobEvent,
278
+ );
279
+ const editEvent = events.find((e) => e.eventType === JobEventType.Edited);
280
+ expect(editEvent).not.toBeUndefined();
281
+ expect(editEvent?.metadata).toMatchObject({
282
+ payload: { to: 'updated@example.com' },
283
+ priority: 10,
284
+ });
285
+ });
286
+
287
+ it('should update updated_at timestamp when editing', async () => {
288
+ const jobId = await queue.addJob<{ email: { to: string } }, 'email'>(pool, {
289
+ jobType: 'email',
290
+ payload: { to: 'original@example.com' },
291
+ });
292
+ const originalJob = await queue.getJob(pool, jobId);
293
+ const originalUpdatedAt = originalJob?.updatedAt;
294
+
295
+ // Wait a bit to ensure timestamp difference
296
+ await new Promise((r) => setTimeout(r, 10));
297
+
298
+ await queue.editJob<{ email: { to: string } }, 'email'>(pool, jobId, {
299
+ priority: 5,
300
+ });
301
+
302
+ const updatedJob = await queue.getJob(pool, jobId);
303
+ expect(updatedJob?.updatedAt.getTime()).toBeGreaterThan(
304
+ originalUpdatedAt?.getTime() || 0,
305
+ );
306
+ });
307
+
308
+ it('should handle editing with null values', async () => {
309
+ const futureDate = new Date(Date.now() + 60000);
310
+ const jobId = await queue.addJob<{ email: { to: string } }, 'email'>(pool, {
311
+ jobType: 'email',
312
+ payload: { to: 'original@example.com' },
313
+ runAt: futureDate,
314
+ timeoutMs: 10000,
315
+ tags: ['original'],
316
+ });
317
+
318
+ await queue.editJob<{ email: { to: string } }, 'email'>(pool, jobId, {
319
+ runAt: null,
320
+ timeoutMs: null,
321
+ tags: null,
322
+ });
323
+
324
+ const updatedJob = await queue.getJob(pool, jobId);
325
+ expect(updatedJob?.runAt).not.toBeNull(); // runAt null means use default (now)
326
+ expect(updatedJob?.timeoutMs).toBeNull();
327
+ expect(updatedJob?.tags).toBeNull();
328
+ });
329
+
330
+ it('should do nothing when editing with no fields', async () => {
331
+ const jobId = await queue.addJob<{ email: { to: string } }, 'email'>(pool, {
332
+ jobType: 'email',
333
+ payload: { to: 'original@example.com' },
334
+ });
335
+ const originalJob = await queue.getJob(pool, jobId);
336
+
337
+ await queue.editJob<{ email: { to: string } }, 'email'>(pool, jobId, {});
338
+
339
+ const job = await queue.getJob(pool, jobId);
340
+ expect(job?.payload).toEqual(originalJob?.payload);
341
+ expect(job?.priority).toBe(originalJob?.priority);
342
+ });
343
+
344
+ it('should edit all pending jobs without filters', async () => {
345
+ // Add three pending jobs
346
+ const jobId1 = await queue.addJob<{ email: { to: string } }, 'email'>(
347
+ pool,
348
+ {
349
+ jobType: 'email',
350
+ payload: { to: 'batch1@example.com' },
351
+ priority: 0,
352
+ },
353
+ );
354
+ const jobId2 = await queue.addJob<{ email: { to: string } }, 'email'>(
355
+ pool,
356
+ {
357
+ jobType: 'email',
358
+ payload: { to: 'batch2@example.com' },
359
+ priority: 0,
360
+ },
361
+ );
362
+ const jobId3 = await queue.addJob<{ email: { to: string } }, 'email'>(
363
+ pool,
364
+ {
365
+ jobType: 'email',
366
+ payload: { to: 'batch3@example.com' },
367
+ priority: 0,
368
+ },
369
+ );
370
+ // Add a completed job
371
+ const jobId4 = await queue.addJob<{ email: { to: string } }, 'email'>(
372
+ pool,
373
+ {
374
+ jobType: 'email',
375
+ payload: { to: 'done@example.com' },
376
+ },
377
+ );
378
+ await queue.completeJob(pool, jobId4);
379
+
380
+ // Edit all pending jobs
381
+ const editedCount = await queue.editAllPendingJobs<
382
+ { email: { to: string } },
383
+ 'email'
384
+ >(pool, undefined, {
385
+ priority: 10,
386
+ });
387
+ expect(editedCount).toBeGreaterThanOrEqual(3);
388
+
389
+ // Check that all pending jobs are updated
390
+ const job1 = await queue.getJob(pool, jobId1);
391
+ const job2 = await queue.getJob(pool, jobId2);
392
+ const job3 = await queue.getJob(pool, jobId3);
393
+ expect(job1?.priority).toBe(10);
394
+ expect(job2?.priority).toBe(10);
395
+ expect(job3?.priority).toBe(10);
396
+
397
+ // Completed job should remain unchanged
398
+ const completedJob = await queue.getJob(pool, jobId4);
399
+ expect(completedJob?.priority).toBe(0);
400
+ });
401
+
402
+ it('should edit pending jobs filtered by jobType', async () => {
403
+ const emailJobId1 = await queue.addJob<{ email: { to: string } }, 'email'>(
404
+ pool,
405
+ {
406
+ jobType: 'email',
407
+ payload: { to: 'email1@example.com' },
408
+ priority: 0,
409
+ },
410
+ );
411
+ const emailJobId2 = await queue.addJob<{ email: { to: string } }, 'email'>(
412
+ pool,
413
+ {
414
+ jobType: 'email',
415
+ payload: { to: 'email2@example.com' },
416
+ priority: 0,
417
+ },
418
+ );
419
+ const smsJobId = await queue.addJob<{ sms: { to: string } }, 'sms'>(pool, {
420
+ jobType: 'sms',
421
+ payload: { to: 'sms@example.com' },
422
+ priority: 0,
423
+ });
424
+
425
+ // Edit only email jobs
426
+ const editedCount = await queue.editAllPendingJobs<
427
+ { email: { to: string }; sms: { to: string } },
428
+ 'email'
429
+ >(
430
+ pool,
431
+ { jobType: 'email' },
432
+ {
433
+ priority: 5,
434
+ },
435
+ );
436
+ expect(editedCount).toBeGreaterThanOrEqual(2);
437
+
438
+ const emailJob1 = await queue.getJob(pool, emailJobId1);
439
+ const emailJob2 = await queue.getJob(pool, emailJobId2);
440
+ const smsJob = await queue.getJob(pool, smsJobId);
441
+ expect(emailJob1?.priority).toBe(5);
442
+ expect(emailJob2?.priority).toBe(5);
443
+ expect(smsJob?.priority).toBe(0);
444
+ });
445
+
446
+ it('should edit pending jobs filtered by priority', async () => {
447
+ const lowPriorityJobId1 = await queue.addJob<
448
+ { email: { to: string } },
449
+ 'email'
450
+ >(pool, {
451
+ jobType: 'email',
452
+ payload: { to: 'low1@example.com' },
453
+ priority: 1,
454
+ });
455
+ const lowPriorityJobId2 = await queue.addJob<
456
+ { email: { to: string } },
457
+ 'email'
458
+ >(pool, {
459
+ jobType: 'email',
460
+ payload: { to: 'low2@example.com' },
461
+ priority: 1,
462
+ });
463
+ const highPriorityJobId = await queue.addJob<
464
+ { email: { to: string } },
465
+ 'email'
466
+ >(pool, {
467
+ jobType: 'email',
468
+ payload: { to: 'high@example.com' },
469
+ priority: 10,
470
+ });
471
+
472
+ // Edit only low priority jobs
473
+ const editedCount = await queue.editAllPendingJobs<
474
+ { email: { to: string } },
475
+ 'email'
476
+ >(
477
+ pool,
478
+ { priority: 1 },
479
+ {
480
+ priority: 5,
481
+ },
482
+ );
483
+ expect(editedCount).toBeGreaterThanOrEqual(2);
484
+
485
+ const lowJob1 = await queue.getJob(pool, lowPriorityJobId1);
486
+ const lowJob2 = await queue.getJob(pool, lowPriorityJobId2);
487
+ const highJob = await queue.getJob(pool, highPriorityJobId);
488
+ expect(lowJob1?.priority).toBe(5);
489
+ expect(lowJob2?.priority).toBe(5);
490
+ expect(highJob?.priority).toBe(10);
491
+ });
492
+
493
+ it('should edit pending jobs filtered by tags', async () => {
494
+ const taggedJobId1 = await queue.addJob<{ email: { to: string } }, 'email'>(
495
+ pool,
496
+ {
497
+ jobType: 'email',
498
+ payload: { to: 'tagged1@example.com' },
499
+ tags: ['urgent', 'priority'],
500
+ },
501
+ );
502
+ const taggedJobId2 = await queue.addJob<{ email: { to: string } }, 'email'>(
503
+ pool,
504
+ {
505
+ jobType: 'email',
506
+ payload: { to: 'tagged2@example.com' },
507
+ tags: ['urgent', 'priority'],
508
+ },
509
+ );
510
+ const untaggedJobId = await queue.addJob<
511
+ { email: { to: string } },
512
+ 'email'
513
+ >(pool, {
514
+ jobType: 'email',
515
+ payload: { to: 'untagged@example.com' },
516
+ tags: ['other'],
517
+ });
518
+
519
+ // Edit only jobs with 'urgent' tag
520
+ const editedCount = await queue.editAllPendingJobs<
521
+ { email: { to: string } },
522
+ 'email'
523
+ >(
524
+ pool,
525
+ { tags: { values: ['urgent'], mode: 'any' } },
526
+ {
527
+ priority: 10,
528
+ },
529
+ );
530
+ expect(editedCount).toBeGreaterThanOrEqual(2);
531
+
532
+ const taggedJob1 = await queue.getJob(pool, taggedJobId1);
533
+ const taggedJob2 = await queue.getJob(pool, taggedJobId2);
534
+ const untaggedJob = await queue.getJob(pool, untaggedJobId);
535
+ expect(taggedJob1?.priority).toBe(10);
536
+ expect(taggedJob2?.priority).toBe(10);
537
+ expect(untaggedJob?.priority).toBe(0);
538
+ });
539
+
540
+ it('should edit pending jobs filtered by runAt', async () => {
541
+ const futureDate = new Date(Date.now() + 60000);
542
+ const pastDate = new Date(Date.now() - 60000);
543
+
544
+ const futureJobId1 = await queue.addJob<{ email: { to: string } }, 'email'>(
545
+ pool,
546
+ {
547
+ jobType: 'email',
548
+ payload: { to: 'future1@example.com' },
549
+ runAt: futureDate,
550
+ },
551
+ );
552
+ const futureJobId2 = await queue.addJob<{ email: { to: string } }, 'email'>(
553
+ pool,
554
+ {
555
+ jobType: 'email',
556
+ payload: { to: 'future2@example.com' },
557
+ runAt: futureDate,
558
+ },
559
+ );
560
+ const pastJobId = await queue.addJob<{ email: { to: string } }, 'email'>(
561
+ pool,
562
+ {
563
+ jobType: 'email',
564
+ payload: { to: 'past@example.com' },
565
+ runAt: pastDate,
566
+ },
567
+ );
568
+
569
+ // Edit only jobs scheduled in the future
570
+ const editedCount = await queue.editAllPendingJobs<
571
+ { email: { to: string } },
572
+ 'email'
573
+ >(
574
+ pool,
575
+ { runAt: { gte: new Date() } },
576
+ {
577
+ priority: 10,
578
+ },
579
+ );
580
+ expect(editedCount).toBeGreaterThanOrEqual(2);
581
+
582
+ const futureJob1 = await queue.getJob(pool, futureJobId1);
583
+ const futureJob2 = await queue.getJob(pool, futureJobId2);
584
+ const pastJob = await queue.getJob(pool, pastJobId);
585
+ expect(futureJob1?.priority).toBe(10);
586
+ expect(futureJob2?.priority).toBe(10);
587
+ expect(pastJob?.priority).toBe(0);
588
+ });
589
+
590
+ it('should not edit non-pending jobs', async () => {
591
+ // Create processingJobId first so it gets picked up by getNextBatch
592
+ const processingJobId = await queue.addJob<
593
+ { email: { to: string } },
594
+ 'email'
595
+ >(pool, {
596
+ jobType: 'email',
597
+ payload: { to: 'processing@example.com' },
598
+ priority: 0,
599
+ });
600
+ const pendingJobId = await queue.addJob<{ email: { to: string } }, 'email'>(
601
+ pool,
602
+ {
603
+ jobType: 'email',
604
+ payload: { to: 'pending@example.com' },
605
+ priority: 0,
606
+ },
607
+ );
608
+ // Mark as processing (this will pick up processingJobId since it was created first)
609
+ await queue.getNextBatch(pool, 'worker-batch', 1);
610
+ const completedJobId = await queue.addJob<
611
+ { email: { to: string } },
612
+ 'email'
613
+ >(pool, {
614
+ jobType: 'email',
615
+ payload: { to: 'completed@example.com' },
616
+ priority: 0,
617
+ });
618
+ await queue.completeJob(pool, completedJobId);
619
+
620
+ // Edit all pending jobs
621
+ const editedCount = await queue.editAllPendingJobs<
622
+ { email: { to: string } },
623
+ 'email'
624
+ >(pool, undefined, {
625
+ priority: 10,
626
+ });
627
+
628
+ const pendingJob = await queue.getJob(pool, pendingJobId);
629
+ const processingJob = await queue.getJob(pool, processingJobId);
630
+ const completedJob = await queue.getJob(pool, completedJobId);
631
+ expect(pendingJob?.priority).toBe(10);
632
+ expect(processingJob?.priority).toBe(0);
633
+ expect(completedJob?.priority).toBe(0);
634
+ expect(editedCount).toBeGreaterThanOrEqual(1);
635
+ });
636
+
637
+ it('should record edit events for each edited job', async () => {
638
+ const jobId1 = await queue.addJob<{ email: { to: string } }, 'email'>(
639
+ pool,
640
+ {
641
+ jobType: 'email',
642
+ payload: { to: 'event1@example.com' },
643
+ },
644
+ );
645
+ const jobId2 = await queue.addJob<{ email: { to: string } }, 'email'>(
646
+ pool,
647
+ {
648
+ jobType: 'email',
649
+ payload: { to: 'event2@example.com' },
650
+ },
651
+ );
652
+
653
+ await queue.editAllPendingJobs<{ email: { to: string } }, 'email'>(
654
+ pool,
655
+ undefined,
656
+ {
657
+ priority: 10,
658
+ },
659
+ );
660
+
661
+ const events1 = await queue.getJobEvents(pool, jobId1);
662
+ const events2 = await queue.getJobEvents(pool, jobId2);
663
+ const editEvent1 = events1.find((e) => e.eventType === JobEventType.Edited);
664
+ const editEvent2 = events2.find((e) => e.eventType === JobEventType.Edited);
665
+ expect(editEvent1).not.toBeUndefined();
666
+ expect(editEvent2).not.toBeUndefined();
667
+ expect(editEvent1?.metadata).toMatchObject({ priority: 10 });
668
+ expect(editEvent2?.metadata).toMatchObject({ priority: 10 });
669
+ });
670
+
671
+ it('should return 0 when no fields to update', async () => {
672
+ await queue.addJob<{ email: { to: string } }, 'email'>(pool, {
673
+ jobType: 'email',
674
+ payload: { to: 'empty@example.com' },
675
+ });
676
+
677
+ const editedCount = await queue.editAllPendingJobs<
678
+ { email: { to: string } },
679
+ 'email'
680
+ >(pool, undefined, {});
681
+
682
+ expect(editedCount).toBe(0);
683
+ });
684
+
163
685
  it('should cancel all upcoming jobs', async () => {
164
686
  // Add three pending jobs
165
687
  const jobId1 = await queue.addJob<{ email: { to: string } }, 'email'>(