@checkstack/healthcheck-backend 0.4.0 → 0.4.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,45 @@
1
1
  # @checkstack/healthcheck-backend
2
2
 
3
+ ## 0.4.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 66a3963: Fix 500 error on `getDetailedAggregatedHistory` and update to SafeDatabase type
8
+
9
+ - Fixed runtime error caused by usage of Drizzle relational query API (`db.query`) in `getAggregatedHistory`
10
+ - Replaced `db.query.healthCheckConfigurations.findFirst()` with standard `db.select()` query
11
+ - Updated all database type declarations from `NodePgDatabase` to `SafeDatabase`
12
+
13
+ - Updated dependencies [2c0822d]
14
+ - Updated dependencies [66a3963]
15
+ - Updated dependencies [66a3963]
16
+ - Updated dependencies [66a3963]
17
+ - @checkstack/queue-api@0.2.0
18
+ - @checkstack/catalog-backend@0.2.7
19
+ - @checkstack/integration-backend@0.1.6
20
+ - @checkstack/backend-api@0.5.0
21
+ - @checkstack/command-backend@0.1.6
22
+
23
+ ## 0.4.1
24
+
25
+ ### Patch Changes
26
+
27
+ - Updated dependencies [8a87cd4]
28
+ - Updated dependencies [8a87cd4]
29
+ - Updated dependencies [8a87cd4]
30
+ - Updated dependencies [8a87cd4]
31
+ - Updated dependencies [8a87cd4]
32
+ - @checkstack/backend-api@0.4.1
33
+ - @checkstack/catalog-common@1.2.3
34
+ - @checkstack/common@0.5.0
35
+ - @checkstack/healthcheck-common@0.4.2
36
+ - @checkstack/maintenance-common@0.4.1
37
+ - @checkstack/catalog-backend@0.2.6
38
+ - @checkstack/command-backend@0.1.5
39
+ - @checkstack/integration-backend@0.1.5
40
+ - @checkstack/queue-api@0.1.3
41
+ - @checkstack/signal-common@0.1.3
42
+
3
43
  ## 0.4.0
4
44
 
5
45
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/healthcheck-backend",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "scripts": {
@@ -6,25 +6,27 @@ describe("HealthCheckService.getAggregatedHistory", () => {
6
6
  let mockDb: ReturnType<typeof createMockDb>;
7
7
  let mockRegistry: ReturnType<typeof createMockRegistry>;
8
8
  let service: HealthCheckService;
9
+ // Store mock data for different queries
10
+ let mockConfigResult: { id: string; strategyId: string } | null = null;
11
+ let mockRunsResult: unknown[] = [];
9
12
 
10
13
  function createMockDb() {
11
- return {
12
- select: mock(() => ({
13
- from: mock(() => ({
14
- where: mock(() => ({
15
- orderBy: mock(() => Promise.resolve([])),
16
- })),
14
+ // Create a mock that handles both config queries (with limit) and runs queries (with orderBy)
15
+ const createSelectChain = () => ({
16
+ from: mock(() => ({
17
+ where: mock(() => ({
18
+ // For config query: uses .limit(1)
19
+ limit: mock(() =>
20
+ Promise.resolve(mockConfigResult ? [mockConfigResult] : []),
21
+ ),
22
+ // For runs query: uses .orderBy()
23
+ orderBy: mock(() => Promise.resolve(mockRunsResult)),
17
24
  })),
18
25
  })),
19
- query: {
20
- healthCheckConfigurations: {
21
- findFirst: mock(() => Promise.resolve(null)) as ReturnType<
22
- typeof mock<
23
- () => Promise<{ id: string; strategyId: string } | null>
24
- >
25
- >,
26
- },
27
- },
26
+ });
27
+
28
+ return {
29
+ select: mock(createSelectChain),
28
30
  };
29
31
  }
30
32
 
@@ -47,6 +49,9 @@ describe("HealthCheckService.getAggregatedHistory", () => {
47
49
  }
48
50
 
49
51
  beforeEach(() => {
52
+ // Reset mock data
53
+ mockConfigResult = null;
54
+ mockRunsResult = [];
50
55
  mockDb = createMockDb();
51
56
  mockRegistry = createMockRegistry();
52
57
  service = new HealthCheckService(mockDb as never, mockRegistry as never);
@@ -65,7 +70,7 @@ describe("HealthCheckService.getAggregatedHistory", () => {
65
70
  endDate,
66
71
  bucketSize: "auto",
67
72
  },
68
- { includeAggregatedResult: true }
73
+ { includeAggregatedResult: true },
69
74
  );
70
75
 
71
76
  expect(result.buckets).toEqual([]);
@@ -83,7 +88,7 @@ describe("HealthCheckService.getAggregatedHistory", () => {
83
88
  endDate,
84
89
  bucketSize: "auto",
85
90
  },
86
- { includeAggregatedResult: true }
91
+ { includeAggregatedResult: true },
87
92
  );
88
93
 
89
94
  expect(result.buckets).toEqual([]);
@@ -122,18 +127,9 @@ describe("HealthCheckService.getAggregatedHistory", () => {
122
127
  },
123
128
  ];
124
129
 
125
- // Setup mock to return runs
126
- mockDb.select = mock(() => ({
127
- from: mock(() => ({
128
- where: mock(() => ({
129
- orderBy: mock(() => Promise.resolve(runs)),
130
- })),
131
- })),
132
- }));
133
-
134
- mockDb.query.healthCheckConfigurations.findFirst = mock(() =>
135
- Promise.resolve({ id: "config-1", strategyId: "http" })
136
- );
130
+ // Setup mock data
131
+ mockRunsResult = runs;
132
+ mockConfigResult = { id: "config-1", strategyId: "http" };
137
133
 
138
134
  const result = await service.getAggregatedHistory(
139
135
  {
@@ -143,14 +139,14 @@ describe("HealthCheckService.getAggregatedHistory", () => {
143
139
  endDate: new Date("2024-01-01T23:59:59Z"),
144
140
  bucketSize: "hourly",
145
141
  },
146
- { includeAggregatedResult: true }
142
+ { includeAggregatedResult: true },
147
143
  );
148
144
 
149
145
  expect(result.buckets).toHaveLength(2);
150
146
 
151
147
  // First bucket (10:00)
152
148
  const bucket10 = result.buckets.find(
153
- (b) => b.bucketStart.getHours() === 10
149
+ (b) => b.bucketStart.getHours() === 10,
154
150
  );
155
151
  expect(bucket10).toBeDefined();
156
152
  expect(bucket10!.runCount).toBe(2);
@@ -161,7 +157,7 @@ describe("HealthCheckService.getAggregatedHistory", () => {
161
157
 
162
158
  // Second bucket (11:00)
163
159
  const bucket11 = result.buckets.find(
164
- (b) => b.bucketStart.getHours() === 11
160
+ (b) => b.bucketStart.getHours() === 11,
165
161
  );
166
162
  expect(bucket11).toBeDefined();
167
163
  expect(bucket11!.runCount).toBe(1);
@@ -182,17 +178,9 @@ describe("HealthCheckService.getAggregatedHistory", () => {
182
178
  timestamp: new Date("2024-01-01T10:00:00Z"),
183
179
  }));
184
180
 
185
- mockDb.select = mock(() => ({
186
- from: mock(() => ({
187
- where: mock(() => ({
188
- orderBy: mock(() => Promise.resolve(runs)),
189
- })),
190
- })),
191
- }));
192
-
193
- mockDb.query.healthCheckConfigurations.findFirst = mock(() =>
194
- Promise.resolve({ id: "config-1", strategyId: "http" })
195
- );
181
+ // Setup mock data
182
+ mockRunsResult = runs;
183
+ mockConfigResult = { id: "config-1", strategyId: "http" };
196
184
 
197
185
  const result = await service.getAggregatedHistory(
198
186
  {
@@ -202,7 +190,7 @@ describe("HealthCheckService.getAggregatedHistory", () => {
202
190
  endDate: new Date("2024-01-01T23:59:59Z"),
203
191
  bucketSize: "hourly",
204
192
  },
205
- { includeAggregatedResult: true }
193
+ { includeAggregatedResult: true },
206
194
  );
207
195
 
208
196
  expect(result.buckets).toHaveLength(1);
@@ -224,17 +212,9 @@ describe("HealthCheckService.getAggregatedHistory", () => {
224
212
  },
225
213
  ];
226
214
 
227
- mockDb.select = mock(() => ({
228
- from: mock(() => ({
229
- where: mock(() => ({
230
- orderBy: mock(() => Promise.resolve(runs)),
231
- })),
232
- })),
233
- }));
234
-
235
- mockDb.query.healthCheckConfigurations.findFirst = mock(() =>
236
- Promise.resolve({ id: "config-1", strategyId: "http" })
237
- );
215
+ // Setup mock data
216
+ mockRunsResult = runs;
217
+ mockConfigResult = { id: "config-1", strategyId: "http" };
238
218
 
239
219
  const result = await service.getAggregatedHistory(
240
220
  {
@@ -244,7 +224,7 @@ describe("HealthCheckService.getAggregatedHistory", () => {
244
224
  endDate: new Date("2024-01-01T23:59:59Z"),
245
225
  bucketSize: "hourly",
246
226
  },
247
- { includeAggregatedResult: true }
227
+ { includeAggregatedResult: true },
248
228
  );
249
229
 
250
230
  const bucket = result.buckets[0];
@@ -270,18 +250,9 @@ describe("HealthCheckService.getAggregatedHistory", () => {
270
250
  },
271
251
  ];
272
252
 
273
- mockDb.select = mock(() => ({
274
- from: mock(() => ({
275
- where: mock(() => ({
276
- orderBy: mock(() => Promise.resolve(runs)),
277
- })),
278
- })),
279
- }));
280
-
281
- // No config found means no strategy
282
- mockDb.query.healthCheckConfigurations.findFirst = mock(() =>
283
- Promise.resolve(null)
284
- );
253
+ // Setup mock data - no config found means no strategy
254
+ mockRunsResult = runs;
255
+ mockConfigResult = null;
285
256
 
286
257
  const result = await service.getAggregatedHistory(
287
258
  {
@@ -291,12 +262,12 @@ describe("HealthCheckService.getAggregatedHistory", () => {
291
262
  endDate: new Date("2024-01-01T23:59:59Z"),
292
263
  bucketSize: "hourly",
293
264
  },
294
- { includeAggregatedResult: true }
265
+ { includeAggregatedResult: true },
295
266
  );
296
267
 
297
268
  const bucket = result.buckets[0];
298
269
  expect(
299
- "aggregatedResult" in bucket ? bucket.aggregatedResult : undefined
270
+ "aggregatedResult" in bucket ? bucket.aggregatedResult : undefined,
300
271
  ).toBeUndefined();
301
272
  });
302
273
  });
@@ -333,17 +304,9 @@ describe("HealthCheckService.getAggregatedHistory", () => {
333
304
  },
334
305
  ];
335
306
 
336
- mockDb.select = mock(() => ({
337
- from: mock(() => ({
338
- where: mock(() => ({
339
- orderBy: mock(() => Promise.resolve(runs)),
340
- })),
341
- })),
342
- }));
343
-
344
- mockDb.query.healthCheckConfigurations.findFirst = mock(() =>
345
- Promise.resolve({ id: "config-1", strategyId: "http" })
346
- );
307
+ // Setup mock data
308
+ mockRunsResult = runs;
309
+ mockConfigResult = { id: "config-1", strategyId: "http" };
347
310
 
348
311
  const result = await service.getAggregatedHistory(
349
312
  {
@@ -353,7 +316,7 @@ describe("HealthCheckService.getAggregatedHistory", () => {
353
316
  endDate: new Date("2024-01-03T00:00:00Z"),
354
317
  bucketSize: "daily",
355
318
  },
356
- { includeAggregatedResult: true }
319
+ { includeAggregatedResult: true },
357
320
  );
358
321
 
359
322
  expect(result.buckets).toHaveLength(2);
package/src/index.ts CHANGED
@@ -3,7 +3,6 @@ import {
3
3
  bootstrapHealthChecks,
4
4
  } from "./queue-executor";
5
5
  import * as schema from "./schema";
6
- import { NodePgDatabase } from "drizzle-orm/node-postgres";
7
6
  import {
8
7
  healthCheckAccessRules,
9
8
  healthCheckAccess,
@@ -15,6 +14,7 @@ import {
15
14
  createBackendPlugin,
16
15
  coreServices,
17
16
  type EmitHookFn,
17
+ type SafeDatabase,
18
18
  } from "@checkstack/backend-api";
19
19
  import { integrationEventExtensionPoint } from "@checkstack/integration-backend";
20
20
  import { z } from "zod";
@@ -130,7 +130,7 @@ export default createBackendPlugin({
130
130
  });
131
131
 
132
132
  const healthCheckRouter = createHealthCheckRouter(
133
- database as NodePgDatabase<typeof schema>,
133
+ database as SafeDatabase<typeof schema>,
134
134
  healthCheckRegistry,
135
135
  );
136
136
  rpc.registerRouter(healthCheckRouter, healthCheckContract);
@@ -4,6 +4,7 @@ import {
4
4
  type EmitHookFn,
5
5
  type CollectorRegistry,
6
6
  evaluateAssertions,
7
+ type SafeDatabase,
7
8
  } from "@checkstack/backend-api";
8
9
  import { QueueManager } from "@checkstack/queue-api";
9
10
  import {
@@ -13,7 +14,6 @@ import {
13
14
  } from "./schema";
14
15
  import * as schema from "./schema";
15
16
  import { eq, and, max } from "drizzle-orm";
16
- import { NodePgDatabase } from "drizzle-orm/node-postgres";
17
17
  import { type SignalService } from "@checkstack/signal-common";
18
18
  import {
19
19
  HEALTH_CHECK_RUN_COMPLETED,
@@ -25,7 +25,7 @@ import { resolveRoute, type InferClient } from "@checkstack/common";
25
25
  import { HealthCheckService } from "./service";
26
26
  import { healthCheckHooks } from "./hooks";
27
27
 
28
- type Db = NodePgDatabase<typeof schema>;
28
+ type Db = SafeDatabase<typeof schema>;
29
29
  type CatalogClient = InferClient<typeof CatalogApi>;
30
30
  type MaintenanceClient = InferClient<typeof MaintenanceApi>;
31
31
 
@@ -1,19 +1,20 @@
1
- import { NodePgDatabase } from "drizzle-orm/node-postgres";
1
+ import type {
2
+ HealthCheckRegistry,
3
+ Logger,
4
+ SafeDatabase,
5
+ } from "@checkstack/backend-api";
2
6
  import * as schema from "./schema";
3
7
  import {
4
8
  healthCheckRuns,
5
9
  systemHealthChecks,
6
10
  healthCheckAggregates,
11
+ healthCheckConfigurations,
7
12
  DEFAULT_RETENTION_CONFIG,
8
13
  } from "./schema";
9
14
  import { eq, and, lt, sql } from "drizzle-orm";
10
- import type {
11
- HealthCheckRegistry,
12
- Logger,
13
- } from "@checkstack/backend-api";
14
15
  import type { QueueManager } from "@checkstack/queue-api";
15
16
 
16
- type Db = NodePgDatabase<typeof schema>;
17
+ type Db = SafeDatabase<typeof schema>;
17
18
 
18
19
  interface RetentionJobDeps {
19
20
  db: Db;
@@ -46,7 +47,7 @@ export async function setupRetentionJob(deps: RetentionJobDeps) {
46
47
  await runRetentionJob({ db, registry, logger, queueManager });
47
48
  logger.info("Completed health check retention job");
48
49
  },
49
- { consumerGroup: "retention-worker" }
50
+ { consumerGroup: "retention-worker" },
50
51
  );
51
52
 
52
53
  // Schedule daily retention run (86400 seconds = 24 hours)
@@ -55,7 +56,7 @@ export async function setupRetentionJob(deps: RetentionJobDeps) {
55
56
  {
56
57
  jobId: "health-check-retention-daily",
57
58
  intervalSeconds: 24 * 60 * 60, // Daily (24 hours)
58
- }
59
+ },
59
60
  );
60
61
 
61
62
  logger.info("Health check retention job scheduled (runs daily)");
@@ -102,7 +103,7 @@ export async function runRetentionJob(deps: RetentionJobDeps) {
102
103
  } catch (error) {
103
104
  logger.error(
104
105
  `Retention job failed for ${assignment.systemId}/${assignment.configurationId}`,
105
- { error }
106
+ { error },
106
107
  );
107
108
  }
108
109
  }
@@ -127,9 +128,11 @@ async function aggregateRawRuns(params: AggregateRawRunsParams) {
127
128
  cutoffDate.setHours(cutoffDate.getHours(), 0, 0, 0); // Round to hour
128
129
 
129
130
  // Get strategy for metadata aggregation
130
- const config = await db.query.healthCheckConfigurations.findFirst({
131
- where: eq(schema.healthCheckConfigurations.id, configurationId),
132
- });
131
+ const [config] = await db
132
+ .select()
133
+ .from(healthCheckConfigurations)
134
+ .where(eq(healthCheckConfigurations.id, configurationId))
135
+ .limit(1);
133
136
  const strategy = config ? registry.getStrategy(config.strategyId) : undefined;
134
137
 
135
138
  // Query raw runs older than cutoff, grouped by hour
@@ -140,8 +143,8 @@ async function aggregateRawRuns(params: AggregateRawRunsParams) {
140
143
  and(
141
144
  eq(healthCheckRuns.systemId, systemId),
142
145
  eq(healthCheckRuns.configurationId, configurationId),
143
- lt(healthCheckRuns.timestamp, cutoffDate)
144
- )
146
+ lt(healthCheckRuns.timestamp, cutoffDate),
147
+ ),
145
148
  )
146
149
  .orderBy(healthCheckRuns.timestamp);
147
150
 
@@ -284,8 +287,8 @@ async function rollupHourlyAggregates(params: RollupParams) {
284
287
  eq(healthCheckAggregates.systemId, systemId),
285
288
  eq(healthCheckAggregates.configurationId, configurationId),
286
289
  eq(healthCheckAggregates.bucketSize, "hourly"),
287
- lt(healthCheckAggregates.bucketStart, cutoffDate)
288
- )
290
+ lt(healthCheckAggregates.bucketStart, cutoffDate),
291
+ ),
289
292
  );
290
293
 
291
294
  if (oldHourly.length === 0) return;
@@ -392,8 +395,8 @@ async function deleteExpiredAggregates(params: DeleteExpiredParams) {
392
395
  eq(healthCheckAggregates.systemId, systemId),
393
396
  eq(healthCheckAggregates.configurationId, configurationId),
394
397
  eq(healthCheckAggregates.bucketSize, "daily"),
395
- lt(healthCheckAggregates.bucketStart, cutoffDate)
396
- )
398
+ lt(healthCheckAggregates.bucketStart, cutoffDate),
399
+ ),
397
400
  );
398
401
  }
399
402
 
package/src/router.ts CHANGED
@@ -4,10 +4,10 @@ import {
4
4
  toJsonSchema,
5
5
  type RpcContext,
6
6
  type HealthCheckRegistry,
7
+ type SafeDatabase,
7
8
  } from "@checkstack/backend-api";
8
9
  import { healthCheckContract } from "@checkstack/healthcheck-common";
9
10
  import { HealthCheckService } from "./service";
10
- import { NodePgDatabase } from "drizzle-orm/node-postgres";
11
11
  import * as schema from "./schema";
12
12
  import { toJsonSchemaWithChartMeta } from "./schema-utils";
13
13
 
@@ -18,8 +18,8 @@ import { toJsonSchemaWithChartMeta } from "./schema-utils";
18
18
  * based on the contract's meta.userType and meta.access.
19
19
  */
20
20
  export const createHealthCheckRouter = (
21
- database: NodePgDatabase<typeof schema>,
22
- registry: HealthCheckRegistry
21
+ database: SafeDatabase<typeof schema>,
22
+ registry: HealthCheckRegistry,
23
23
  ) => {
24
24
  // Create service instance once - shared across all handlers
25
25
  const service = new HealthCheckService(database, registry);
@@ -40,7 +40,7 @@ export const createHealthCheckRouter = (
40
40
  ? toJsonSchemaWithChartMeta(r.strategy.result.schema)
41
41
  : undefined,
42
42
  aggregatedResultSchema: toJsonSchemaWithChartMeta(
43
- r.strategy.aggregatedResult.schema
43
+ r.strategy.aggregatedResult.schema,
44
44
  ),
45
45
  }));
46
46
  }),
@@ -48,7 +48,7 @@ export const createHealthCheckRouter = (
48
48
  getCollectors: os.getCollectors.handler(async ({ input, context }) => {
49
49
  // Get strategy to verify it exists
50
50
  const strategy = context.healthCheckRegistry.getStrategy(
51
- input.strategyId
51
+ input.strategyId,
52
52
  );
53
53
  if (!strategy) {
54
54
  return [];
@@ -103,13 +103,13 @@ export const createHealthCheckRouter = (
103
103
  getSystemConfigurations: os.getSystemConfigurations.handler(
104
104
  async ({ input }) => {
105
105
  return service.getSystemConfigurations(input);
106
- }
106
+ },
107
107
  ),
108
108
 
109
109
  getSystemAssociations: os.getSystemAssociations.handler(
110
110
  async ({ input }) => {
111
111
  return service.getSystemAssociations(input.systemId);
112
- }
112
+ },
113
113
  ),
114
114
 
115
115
  associateSystem: os.associateSystem.handler(async ({ input, context }) => {
@@ -123,7 +123,7 @@ export const createHealthCheckRouter = (
123
123
  // If enabling the health check, schedule it immediately
124
124
  if (input.body.enabled) {
125
125
  const config = await service.getConfiguration(
126
- input.body.configurationId
126
+ input.body.configurationId,
127
127
  );
128
128
  if (config) {
129
129
  const { scheduleHealthCheck } = await import("./queue-executor");
@@ -152,9 +152,9 @@ export const createHealthCheckRouter = (
152
152
  await service.updateRetentionConfig(
153
153
  input.systemId,
154
154
  input.configurationId,
155
- input.retentionConfig
155
+ input.retentionConfig,
156
156
  );
157
- }
157
+ },
158
158
  ),
159
159
 
160
160
  getHistory: os.getHistory.handler(async ({ input }) => {
@@ -176,12 +176,12 @@ export const createHealthCheckRouter = (
176
176
  return service.getAggregatedHistory(input, {
177
177
  includeAggregatedResult: true,
178
178
  });
179
- }
179
+ },
180
180
  ),
181
181
  getSystemHealthStatus: os.getSystemHealthStatus.handler(
182
182
  async ({ input }) => {
183
183
  return service.getSystemHealthStatus(input.systemId);
184
- }
184
+ },
185
185
  ),
186
186
 
187
187
  getBulkSystemHealthStatus: os.getBulkSystemHealthStatus.handler(
@@ -195,17 +195,17 @@ export const createHealthCheckRouter = (
195
195
  await Promise.all(
196
196
  input.systemIds.map(async (systemId) => {
197
197
  statuses[systemId] = await service.getSystemHealthStatus(systemId);
198
- })
198
+ }),
199
199
  );
200
200
 
201
201
  return { statuses };
202
- }
202
+ },
203
203
  ),
204
204
 
205
205
  getSystemHealthOverview: os.getSystemHealthOverview.handler(
206
206
  async ({ input }) => {
207
207
  return service.getSystemHealthOverview(input.systemId);
208
- }
208
+ },
209
209
  ),
210
210
  });
211
211
  };
package/src/service.ts CHANGED
@@ -14,14 +14,16 @@ import {
14
14
  } from "./schema";
15
15
  import * as schema from "./schema";
16
16
  import { eq, and, InferSelectModel, desc, gte, lte } from "drizzle-orm";
17
- import { NodePgDatabase } from "drizzle-orm/node-postgres";
18
17
  import { ORPCError } from "@orpc/server";
19
18
  import { evaluateHealthStatus } from "./state-evaluator";
20
19
  import { stateThresholds } from "./state-thresholds-migrations";
21
- import type { HealthCheckRegistry } from "@checkstack/backend-api";
20
+ import type {
21
+ HealthCheckRegistry,
22
+ SafeDatabase,
23
+ } from "@checkstack/backend-api";
22
24
 
23
- // Drizzle type helper
24
- type Db = NodePgDatabase<typeof schema>;
25
+ // Drizzle type helper - uses SafeDatabase to prevent relational query API usage
26
+ type Db = SafeDatabase<typeof schema>;
25
27
 
26
28
  interface SystemCheckStatus {
27
29
  configurationId: string;
@@ -38,10 +40,13 @@ interface SystemHealthStatusResponse {
38
40
  }
39
41
 
40
42
  export class HealthCheckService {
41
- constructor(private db: Db, private registry?: HealthCheckRegistry) {}
43
+ constructor(
44
+ private db: Db,
45
+ private registry?: HealthCheckRegistry,
46
+ ) {}
42
47
 
43
48
  async createConfiguration(
44
- data: CreateHealthCheckConfiguration
49
+ data: CreateHealthCheckConfiguration,
45
50
  ): Promise<HealthCheckConfiguration> {
46
51
  const [config] = await this.db
47
52
  .insert(healthCheckConfigurations)
@@ -58,7 +63,7 @@ export class HealthCheckService {
58
63
  }
59
64
 
60
65
  async getConfiguration(
61
- id: string
66
+ id: string,
62
67
  ): Promise<HealthCheckConfiguration | undefined> {
63
68
  const [config] = await this.db
64
69
  .select()
@@ -69,7 +74,7 @@ export class HealthCheckService {
69
74
 
70
75
  async updateConfiguration(
71
76
  id: string,
72
- data: UpdateHealthCheckConfiguration
77
+ data: UpdateHealthCheckConfiguration,
73
78
  ): Promise<HealthCheckConfiguration | undefined> {
74
79
  const [config] = await this.db
75
80
  .update(healthCheckConfigurations)
@@ -137,8 +142,8 @@ export class HealthCheckService {
137
142
  .where(
138
143
  and(
139
144
  eq(systemHealthChecks.systemId, systemId),
140
- eq(systemHealthChecks.configurationId, configurationId)
141
- )
145
+ eq(systemHealthChecks.configurationId, configurationId),
146
+ ),
142
147
  );
143
148
  }
144
149
 
@@ -147,7 +152,7 @@ export class HealthCheckService {
147
152
  */
148
153
  async getRetentionConfig(
149
154
  systemId: string,
150
- configurationId: string
155
+ configurationId: string,
151
156
  ): Promise<{ retentionConfig: RetentionConfig | null }> {
152
157
  const row = await this.db
153
158
  .select({ retentionConfig: systemHealthChecks.retentionConfig })
@@ -155,8 +160,8 @@ export class HealthCheckService {
155
160
  .where(
156
161
  and(
157
162
  eq(systemHealthChecks.systemId, systemId),
158
- eq(systemHealthChecks.configurationId, configurationId)
159
- )
163
+ eq(systemHealthChecks.configurationId, configurationId),
164
+ ),
160
165
  )
161
166
  .then((rows) => rows[0]);
162
167
 
@@ -170,7 +175,7 @@ export class HealthCheckService {
170
175
  async updateRetentionConfig(
171
176
  systemId: string,
172
177
  configurationId: string,
173
- retentionConfig: RetentionConfig | null
178
+ retentionConfig: RetentionConfig | null,
174
179
  ): Promise<void> {
175
180
  // Validate retention hierarchy: raw < hourly < daily
176
181
  if (retentionConfig) {
@@ -197,8 +202,8 @@ export class HealthCheckService {
197
202
  .where(
198
203
  and(
199
204
  eq(systemHealthChecks.systemId, systemId),
200
- eq(systemHealthChecks.configurationId, configurationId)
201
- )
205
+ eq(systemHealthChecks.configurationId, configurationId),
206
+ ),
202
207
  );
203
208
  }
204
209
 
@@ -213,7 +218,7 @@ export class HealthCheckService {
213
218
  }
214
219
 
215
220
  async getSystemConfigurations(
216
- systemId: string
221
+ systemId: string,
217
222
  ): Promise<HealthCheckConfiguration[]> {
218
223
  const rows = await this.db
219
224
  .select({
@@ -222,7 +227,7 @@ export class HealthCheckService {
222
227
  .from(systemHealthChecks)
223
228
  .innerJoin(
224
229
  healthCheckConfigurations,
225
- eq(systemHealthChecks.configurationId, healthCheckConfigurations.id)
230
+ eq(systemHealthChecks.configurationId, healthCheckConfigurations.id),
226
231
  )
227
232
  .where(eq(systemHealthChecks.systemId, systemId));
228
233
 
@@ -243,7 +248,7 @@ export class HealthCheckService {
243
248
  .from(systemHealthChecks)
244
249
  .innerJoin(
245
250
  healthCheckConfigurations,
246
- eq(systemHealthChecks.configurationId, healthCheckConfigurations.id)
251
+ eq(systemHealthChecks.configurationId, healthCheckConfigurations.id),
247
252
  )
248
253
  .where(eq(systemHealthChecks.systemId, systemId));
249
254
 
@@ -269,7 +274,7 @@ export class HealthCheckService {
269
274
  * Aggregates status from all health check configurations for this system.
270
275
  */
271
276
  async getSystemHealthStatus(
272
- systemId: string
277
+ systemId: string,
273
278
  ): Promise<SystemHealthStatusResponse> {
274
279
  // Get all associations for this system with their thresholds and config names
275
280
  const associations = await this.db
@@ -282,13 +287,13 @@ export class HealthCheckService {
282
287
  .from(systemHealthChecks)
283
288
  .innerJoin(
284
289
  healthCheckConfigurations,
285
- eq(systemHealthChecks.configurationId, healthCheckConfigurations.id)
290
+ eq(systemHealthChecks.configurationId, healthCheckConfigurations.id),
286
291
  )
287
292
  .where(
288
293
  and(
289
294
  eq(systemHealthChecks.systemId, systemId),
290
- eq(systemHealthChecks.enabled, true)
291
- )
295
+ eq(systemHealthChecks.enabled, true),
296
+ ),
292
297
  );
293
298
 
294
299
  if (associations.length === 0) {
@@ -314,8 +319,8 @@ export class HealthCheckService {
314
319
  .where(
315
320
  and(
316
321
  eq(healthCheckRuns.systemId, systemId),
317
- eq(healthCheckRuns.configurationId, assoc.configurationId)
318
- )
322
+ eq(healthCheckRuns.configurationId, assoc.configurationId),
323
+ ),
319
324
  )
320
325
  .orderBy(desc(healthCheckRuns.timestamp))
321
326
  .limit(maxWindowSize);
@@ -375,7 +380,7 @@ export class HealthCheckService {
375
380
  .from(systemHealthChecks)
376
381
  .innerJoin(
377
382
  healthCheckConfigurations,
378
- eq(systemHealthChecks.configurationId, healthCheckConfigurations.id)
383
+ eq(systemHealthChecks.configurationId, healthCheckConfigurations.id),
379
384
  )
380
385
  .where(eq(systemHealthChecks.systemId, systemId));
381
386
 
@@ -394,8 +399,8 @@ export class HealthCheckService {
394
399
  .where(
395
400
  and(
396
401
  eq(healthCheckRuns.systemId, systemId),
397
- eq(healthCheckRuns.configurationId, assoc.configurationId)
398
- )
402
+ eq(healthCheckRuns.configurationId, assoc.configurationId),
403
+ ),
399
404
  )
400
405
  .orderBy(desc(healthCheckRuns.timestamp))
401
406
  .limit(sparklineLimit);
@@ -558,7 +563,7 @@ export class HealthCheckService {
558
563
  endDate: Date;
559
564
  bucketSize: "hourly" | "daily" | "auto";
560
565
  },
561
- options: { includeAggregatedResult: boolean }
566
+ options: { includeAggregatedResult: boolean },
562
567
  ) {
563
568
  const { systemId, configurationId, startDate, endDate } = props;
564
569
  let bucketSize = props.bucketSize;
@@ -571,9 +576,13 @@ export class HealthCheckService {
571
576
  }
572
577
 
573
578
  // Get the configuration to find the strategy
574
- const config = await this.db.query.healthCheckConfigurations.findFirst({
575
- where: eq(healthCheckConfigurations.id, configurationId),
576
- });
579
+ // Note: Using standard select instead of relational query API
580
+ // as the relational API is blocked by the scoped database proxy
581
+ const [config] = await this.db
582
+ .select()
583
+ .from(healthCheckConfigurations)
584
+ .where(eq(healthCheckConfigurations.id, configurationId))
585
+ .limit(1);
577
586
 
578
587
  // Look up strategy for aggregateResult function (only if needed)
579
588
  const strategy =
@@ -590,8 +599,8 @@ export class HealthCheckService {
590
599
  eq(healthCheckRuns.systemId, systemId),
591
600
  eq(healthCheckRuns.configurationId, configurationId),
592
601
  gte(healthCheckRuns.timestamp, startDate),
593
- lte(healthCheckRuns.timestamp, endDate)
594
- )
602
+ lte(healthCheckRuns.timestamp, endDate),
603
+ ),
595
604
  )
596
605
  .orderBy(healthCheckRuns.timestamp);
597
606
 
@@ -631,13 +640,13 @@ export class HealthCheckService {
631
640
  const buckets = [...bucketMap.values()].map((bucket) => {
632
641
  const runCount = bucket.runs.length;
633
642
  const healthyCount = bucket.runs.filter(
634
- (r) => r.status === "healthy"
643
+ (r) => r.status === "healthy",
635
644
  ).length;
636
645
  const degradedCount = bucket.runs.filter(
637
- (r) => r.status === "degraded"
646
+ (r) => r.status === "degraded",
638
647
  ).length;
639
648
  const unhealthyCount = bucket.runs.filter(
640
- (r) => r.status === "unhealthy"
649
+ (r) => r.status === "unhealthy",
641
650
  ).length;
642
651
  const successRate = runCount > 0 ? healthyCount / runCount : 0;
643
652
 
@@ -691,7 +700,7 @@ export class HealthCheckService {
691
700
 
692
701
  private getBucketStart(
693
702
  timestamp: Date,
694
- bucketSize: "hourly" | "daily"
703
+ bucketSize: "hourly" | "daily",
695
704
  ): Date {
696
705
  const date = new Date(timestamp);
697
706
  if (bucketSize === "daily") {
@@ -709,7 +718,7 @@ export class HealthCheckService {
709
718
  }
710
719
 
711
720
  private mapConfig(
712
- row: InferSelectModel<typeof healthCheckConfigurations>
721
+ row: InferSelectModel<typeof healthCheckConfigurations>,
713
722
  ): HealthCheckConfiguration {
714
723
  return {
715
724
  id: row.id,