@checkstack/healthcheck-backend 0.5.0 → 0.7.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.
@@ -0,0 +1,50 @@
1
+ import { describe, it, expect, mock, beforeEach } from "bun:test";
2
+ import { HealthCheckService } from "./service";
3
+ import { createMockDb } from "@checkstack/test-utils-backend";
4
+
5
+ describe("HealthCheckService - pause/resume", () => {
6
+ let mockDb: ReturnType<typeof createMockDb>;
7
+ let service: HealthCheckService;
8
+ let mockUpdate: ReturnType<typeof mock>;
9
+ let mockSet: ReturnType<typeof mock>;
10
+ let mockWhere: ReturnType<typeof mock>;
11
+
12
+ beforeEach(() => {
13
+ mockDb = createMockDb();
14
+ mockWhere = mock(() => Promise.resolve());
15
+ mockSet = mock(() => ({ where: mockWhere }));
16
+ mockUpdate = mock(() => ({ set: mockSet }));
17
+ (mockDb.update as any) = mockUpdate;
18
+ service = new HealthCheckService(mockDb as any);
19
+ });
20
+
21
+ describe("pauseConfiguration", () => {
22
+ it("should update paused to true and set updatedAt", async () => {
23
+ await service.pauseConfiguration("config-123");
24
+
25
+ expect(mockUpdate).toHaveBeenCalled();
26
+ expect(mockSet).toHaveBeenCalledWith(
27
+ expect.objectContaining({
28
+ paused: true,
29
+ updatedAt: expect.any(Date),
30
+ }),
31
+ );
32
+ expect(mockWhere).toHaveBeenCalled();
33
+ });
34
+ });
35
+
36
+ describe("resumeConfiguration", () => {
37
+ it("should update paused to false and set updatedAt", async () => {
38
+ await service.resumeConfiguration("config-456");
39
+
40
+ expect(mockUpdate).toHaveBeenCalled();
41
+ expect(mockSet).toHaveBeenCalledWith(
42
+ expect.objectContaining({
43
+ paused: false,
44
+ updatedAt: expect.any(Date),
45
+ }),
46
+ );
47
+ expect(mockWhere).toHaveBeenCalled();
48
+ });
49
+ });
50
+ });
package/src/service.ts CHANGED
@@ -105,6 +105,20 @@ export class HealthCheckService {
105
105
  .where(eq(healthCheckConfigurations.id, id));
106
106
  }
107
107
 
108
+ async pauseConfiguration(id: string): Promise<void> {
109
+ await this.db
110
+ .update(healthCheckConfigurations)
111
+ .set({ paused: true, updatedAt: new Date() })
112
+ .where(eq(healthCheckConfigurations.id, id));
113
+ }
114
+
115
+ async resumeConfiguration(id: string): Promise<void> {
116
+ await this.db
117
+ .update(healthCheckConfigurations)
118
+ .set({ paused: false, updatedAt: new Date() })
119
+ .where(eq(healthCheckConfigurations.id, id));
120
+ }
121
+
108
122
  async getConfigurations(): Promise<HealthCheckConfiguration[]> {
109
123
  const configs = await this.db.select().from(healthCheckConfigurations);
110
124
  return configs.map((c) => this.mapConfig(c));
@@ -400,7 +414,7 @@ export class HealthCheckService {
400
414
  const sparklineLimit = 25;
401
415
 
402
416
  for (const assoc of associations) {
403
- // Get last 25 runs for sparkline
417
+ // Get last 25 runs for sparkline (newest first, then reverse for chronological display)
404
418
  const runs = await this.db
405
419
  .select({
406
420
  id: healthCheckRuns.id,
@@ -417,15 +431,18 @@ export class HealthCheckService {
417
431
  .orderBy(desc(healthCheckRuns.timestamp))
418
432
  .limit(sparklineLimit);
419
433
 
434
+ // Reverse to chronological order (oldest first) for sparkline display
435
+ const chronologicalRuns = runs.toReversed();
436
+
420
437
  // Migrate and extract thresholds
421
438
  let thresholds: StateThresholds | undefined;
422
439
  if (assoc.stateThresholds) {
423
440
  thresholds = await stateThresholds.parse(assoc.stateThresholds);
424
441
  }
425
442
 
426
- // Evaluate current status
443
+ // Evaluate current status (runs are in DESC order - newest first - as evaluateHealthStatus expects)
427
444
  const status = evaluateHealthStatus({
428
- runs: runs as Array<{ status: HealthCheckStatus; timestamp: Date }>,
445
+ runs,
429
446
  thresholds,
430
447
  });
431
448
 
@@ -437,7 +454,7 @@ export class HealthCheckService {
437
454
  enabled: assoc.enabled,
438
455
  status,
439
456
  stateThresholds: thresholds,
440
- recentRuns: runs.map((r) => ({
457
+ recentRuns: chronologicalRuns.map((r) => ({
441
458
  id: r.id,
442
459
  status: r.status,
443
460
  timestamp: r.timestamp,
@@ -450,6 +467,7 @@ export class HealthCheckService {
450
467
 
451
468
  /**
452
469
  * Get paginated health check run history (public - no result data).
470
+ * @param sortOrder - 'asc' for chronological (oldest first), 'desc' for reverse (newest first)
453
471
  */
454
472
  async getHistory(props: {
455
473
  systemId?: string;
@@ -458,6 +476,7 @@ export class HealthCheckService {
458
476
  endDate?: Date;
459
477
  limit?: number;
460
478
  offset?: number;
479
+ sortOrder: "asc" | "desc";
461
480
  }) {
462
481
  const {
463
482
  systemId,
@@ -466,6 +485,7 @@ export class HealthCheckService {
466
485
  endDate,
467
486
  limit = 10,
468
487
  offset = 0,
488
+ sortOrder,
469
489
  } = props;
470
490
 
471
491
  const conditions = [];
@@ -481,16 +501,17 @@ export class HealthCheckService {
481
501
  // Get total count using drizzle $count
482
502
  const total = await this.db.$count(healthCheckRuns, whereClause);
483
503
 
484
- // Get paginated runs
504
+ // Get paginated runs with requested sort order
485
505
  let query = this.db.select().from(healthCheckRuns);
486
506
  if (whereClause) {
487
507
  // @ts-expect-error drizzle-orm type mismatch
488
508
  query = query.where(whereClause);
489
509
  }
490
- const runs = await query
491
- .orderBy(desc(healthCheckRuns.timestamp))
492
- .limit(limit)
493
- .offset(offset);
510
+ const orderColumn =
511
+ sortOrder === "desc"
512
+ ? desc(healthCheckRuns.timestamp)
513
+ : healthCheckRuns.timestamp;
514
+ const runs = await query.orderBy(orderColumn).limit(limit).offset(offset);
494
515
 
495
516
  // Return without result field for public access (latencyMs is public data)
496
517
  return {
@@ -509,6 +530,7 @@ export class HealthCheckService {
509
530
  /**
510
531
  * Get detailed health check run history with full result data.
511
532
  * Restricted to users with manage access.
533
+ * @param sortOrder - 'asc' for chronological (oldest first), 'desc' for reverse (newest first)
512
534
  */
513
535
  async getDetailedHistory(props: {
514
536
  systemId?: string;
@@ -517,6 +539,7 @@ export class HealthCheckService {
517
539
  endDate?: Date;
518
540
  limit?: number;
519
541
  offset?: number;
542
+ sortOrder: "asc" | "desc";
520
543
  }) {
521
544
  const {
522
545
  systemId,
@@ -525,6 +548,7 @@ export class HealthCheckService {
525
548
  endDate,
526
549
  limit = 10,
527
550
  offset = 0,
551
+ sortOrder,
528
552
  } = props;
529
553
 
530
554
  const conditions = [];
@@ -542,10 +566,11 @@ export class HealthCheckService {
542
566
  // @ts-expect-error drizzle-orm type mismatch
543
567
  query = query.where(whereClause);
544
568
  }
545
- const runs = await query
546
- .orderBy(desc(healthCheckRuns.timestamp))
547
- .limit(limit)
548
- .offset(offset);
569
+ const orderColumn =
570
+ sortOrder === "desc"
571
+ ? desc(healthCheckRuns.timestamp)
572
+ : healthCheckRuns.timestamp;
573
+ const runs = await query.orderBy(orderColumn).limit(limit).offset(offset);
549
574
 
550
575
  // Return with full result data for manage access
551
576
  return {
@@ -562,6 +587,32 @@ export class HealthCheckService {
562
587
  };
563
588
  }
564
589
 
590
+ /**
591
+ * Get a single health check run by its ID.
592
+ */
593
+ async getRunById(props: { runId: string }) {
594
+ const run = await this.db
595
+ .select()
596
+ .from(healthCheckRuns)
597
+ .where(eq(healthCheckRuns.id, props.runId))
598
+ .limit(1);
599
+
600
+ if (run.length === 0) {
601
+ return;
602
+ }
603
+
604
+ const r = run[0];
605
+ return {
606
+ id: r.id,
607
+ configurationId: r.configurationId,
608
+ systemId: r.systemId,
609
+ status: r.status,
610
+ result: r.result ?? {},
611
+ timestamp: r.timestamp,
612
+ latencyMs: r.latencyMs ?? undefined,
613
+ };
614
+ }
615
+
565
616
  /**
566
617
  * Get aggregated health check history with dynamically-sized buckets.
567
618
  * Queries all three tiers (raw, hourly, daily) and merges with priority.
@@ -704,6 +755,7 @@ export class HealthCheckService {
704
755
  sourceBuckets: mergedBuckets,
705
756
  targetIntervalMs: bucketIntervalMs,
706
757
  rangeStart: startDate,
758
+ rangeEnd: endDate,
707
759
  });
708
760
 
709
761
  // Convert to output format
@@ -717,6 +769,7 @@ export class HealthCheckService {
717
769
 
718
770
  const baseBucket = {
719
771
  bucketStart: bucket.bucketStart,
772
+ bucketEnd: new Date(bucket.bucketEndMs),
720
773
  bucketIntervalSeconds,
721
774
  runCount: bucket.runCount,
722
775
  healthyCount: bucket.healthyCount,
@@ -884,6 +937,7 @@ export class HealthCheckService {
884
937
  config: row.config,
885
938
  collectors: row.collectors ?? undefined,
886
939
  intervalSeconds: row.intervalSeconds,
940
+ paused: row.paused,
887
941
  createdAt: row.createdAt,
888
942
  updatedAt: row.updatedAt,
889
943
  };