@checkstack/healthcheck-postgres-backend 0.1.13 → 0.2.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/CHANGELOG.md CHANGED
@@ -1,5 +1,59 @@
1
1
  # @checkstack/healthcheck-postgres-backend
2
2
 
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 3dd1914: Migrate health check strategies to VersionedAggregated with \_type discriminator
8
+
9
+ All 13 health check strategies now use `VersionedAggregated` for their `aggregatedResult` property, enabling automatic bucket merging with 100% mathematical fidelity.
10
+
11
+ **Key changes:**
12
+
13
+ - **`_type` discriminator**: All aggregated state objects now include a required `_type` field (`"average"`, `"rate"`, `"counter"`, `"minmax"`) for reliable type detection
14
+ - The `HealthCheckStrategy` interface now requires `aggregatedResult` to be a `VersionedAggregated<AggregatedResultShape>`
15
+ - Strategy/collector `mergeResult` methods return state objects with `_type` (e.g., `{ _type: "average", _sum, _count, avg }`)
16
+ - `mergeAggregatedBucketResults`, `combineBuckets`, and `reaggregateBuckets` now require `registry` and `strategyId` parameters
17
+ - `HealthCheckService` constructor now requires both `registry` and `collectorRegistry` parameters
18
+ - Frontend `extractComputedValue` now uses `_type` discriminator for robust type detection
19
+
20
+ **Breaking Change**: State objects now require `_type`. Merge functions automatically add `_type` to output. The bucket merging functions and `HealthCheckService` now require additional required parameters.
21
+
22
+ ### Patch Changes
23
+
24
+ - Updated dependencies [3dd1914]
25
+ - @checkstack/backend-api@0.7.0
26
+
27
+ ## 0.1.14
28
+
29
+ ### Patch Changes
30
+
31
+ - 48c2080: Migrate aggregation from batch to incremental (`mergeResult`)
32
+
33
+ ### Breaking Changes (Internal)
34
+
35
+ - Replaced `aggregateResult(runs[])` with `mergeResult(existing, run)` interface across all HealthCheckStrategy and CollectorStrategy implementations
36
+
37
+ ### New Features
38
+
39
+ - Added incremental aggregation utilities in `@checkstack/backend-api`:
40
+ - `mergeCounter()` - track occurrences
41
+ - `mergeAverage()` - track sum/count, compute avg
42
+ - `mergeRate()` - track success/total, compute %
43
+ - `mergeMinMax()` - track min/max values
44
+ - Exported Zod schemas for internal state: `averageStateSchema`, `rateStateSchema`, `minMaxStateSchema`, `counterStateSchema`
45
+
46
+ ### Improvements
47
+
48
+ - Enables O(1) storage overhead by maintaining incremental aggregation state
49
+ - Prepares for real-time hourly aggregation without batch accumulation
50
+
51
+ - Updated dependencies [f676e11]
52
+ - Updated dependencies [48c2080]
53
+ - @checkstack/common@0.6.2
54
+ - @checkstack/backend-api@0.6.0
55
+ - @checkstack/healthcheck-common@0.8.2
56
+
3
57
  ## 0.1.13
4
58
 
5
59
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/healthcheck-postgres-backend",
3
- "version": "0.1.13",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "scripts": {
@@ -9,16 +9,16 @@
9
9
  "lint:code": "eslint . --max-warnings 0"
10
10
  },
11
11
  "dependencies": {
12
- "@checkstack/backend-api": "0.5.1",
13
- "@checkstack/common": "0.6.0",
14
- "@checkstack/healthcheck-common": "0.7.0",
12
+ "@checkstack/backend-api": "0.5.2",
13
+ "@checkstack/common": "0.6.1",
14
+ "@checkstack/healthcheck-common": "0.8.1",
15
15
  "pg": "^8.11.0"
16
16
  },
17
17
  "devDependencies": {
18
18
  "@types/bun": "^1.0.0",
19
19
  "@types/pg": "^8.11.0",
20
20
  "typescript": "^5.0.0",
21
- "@checkstack/tsconfig": "0.0.2",
22
- "@checkstack/scripts": "0.1.0"
21
+ "@checkstack/tsconfig": "0.0.3",
22
+ "@checkstack/scripts": "0.1.1"
23
23
  }
24
24
  }
@@ -64,7 +64,7 @@ describe("QueryCollector", () => {
64
64
  });
65
65
  });
66
66
 
67
- describe("aggregateResult", () => {
67
+ describe("mergeResult", () => {
68
68
  it("should calculate average execution time and success rate", () => {
69
69
  const collector = new QueryCollector();
70
70
  const runs = [
@@ -86,10 +86,11 @@ describe("QueryCollector", () => {
86
86
  },
87
87
  ];
88
88
 
89
- const aggregated = collector.aggregateResult(runs);
89
+ let aggregated = collector.mergeResult(undefined, runs[0]);
90
+ aggregated = collector.mergeResult(aggregated, runs[1]);
90
91
 
91
- expect(aggregated.avgExecutionTimeMs).toBe(75);
92
- expect(aggregated.successRate).toBe(100);
92
+ expect(aggregated.avgExecutionTimeMs.avg).toBe(75);
93
+ expect(aggregated.successRate.rate).toBe(100);
93
94
  });
94
95
 
95
96
  it("should calculate success rate correctly", () => {
@@ -113,9 +114,10 @@ describe("QueryCollector", () => {
113
114
  },
114
115
  ];
115
116
 
116
- const aggregated = collector.aggregateResult(runs);
117
+ let aggregated = collector.mergeResult(undefined, runs[0]);
118
+ aggregated = collector.mergeResult(aggregated, runs[1]);
117
119
 
118
- expect(aggregated.successRate).toBe(50);
120
+ expect(aggregated.successRate.rate).toBe(50);
119
121
  });
120
122
  });
121
123
 
@@ -4,6 +4,12 @@ import {
4
4
  type HealthCheckRunForAggregation,
5
5
  type CollectorResult,
6
6
  type CollectorStrategy,
7
+ mergeAverage,
8
+ mergeRate,
9
+ VersionedAggregated,
10
+ aggregatedAverage,
11
+ aggregatedRate,
12
+ type InferAggregatedResult,
7
13
  } from "@checkstack/backend-api";
8
14
  import {
9
15
  healthResultNumber,
@@ -45,20 +51,24 @@ const queryResultSchema = healthResultSchema({
45
51
 
46
52
  export type QueryResult = z.infer<typeof queryResultSchema>;
47
53
 
48
- const queryAggregatedSchema = healthResultSchema({
49
- avgExecutionTimeMs: healthResultNumber({
54
+ // Aggregated result fields definition
55
+ const queryAggregatedFields = {
56
+ avgExecutionTimeMs: aggregatedAverage({
50
57
  "x-chart-type": "line",
51
58
  "x-chart-label": "Avg Execution Time",
52
59
  "x-chart-unit": "ms",
53
60
  }),
54
- successRate: healthResultNumber({
61
+ successRate: aggregatedRate({
55
62
  "x-chart-type": "gauge",
56
63
  "x-chart-label": "Success Rate",
57
64
  "x-chart-unit": "%",
58
65
  }),
59
- });
66
+ };
60
67
 
61
- export type QueryAggregatedResult = z.infer<typeof queryAggregatedSchema>;
68
+ // Type inferred from field definitions
69
+ export type QueryAggregatedResult = InferAggregatedResult<
70
+ typeof queryAggregatedFields
71
+ >;
62
72
 
63
73
  // ============================================================================
64
74
  // QUERY COLLECTOR
@@ -68,15 +78,12 @@ export type QueryAggregatedResult = z.infer<typeof queryAggregatedSchema>;
68
78
  * Built-in PostgreSQL query collector.
69
79
  * Executes SQL queries and checks results.
70
80
  */
71
- export class QueryCollector
72
- implements
73
- CollectorStrategy<
74
- PostgresTransportClient,
75
- QueryConfig,
76
- QueryResult,
77
- QueryAggregatedResult
78
- >
79
- {
81
+ export class QueryCollector implements CollectorStrategy<
82
+ PostgresTransportClient,
83
+ QueryConfig,
84
+ QueryResult,
85
+ QueryAggregatedResult
86
+ > {
80
87
  id = "query";
81
88
  displayName = "SQL Query";
82
89
  description = "Execute a SQL query and check the result";
@@ -87,9 +94,9 @@ export class QueryCollector
87
94
 
88
95
  config = new Versioned({ version: 1, schema: queryConfigSchema });
89
96
  result = new Versioned({ version: 1, schema: queryResultSchema });
90
- aggregatedResult = new Versioned({
97
+ aggregatedResult = new VersionedAggregated({
91
98
  version: 1,
92
- schema: queryAggregatedSchema,
99
+ fields: queryAggregatedFields,
93
100
  });
94
101
 
95
102
  async execute({
@@ -115,28 +122,18 @@ export class QueryCollector
115
122
  };
116
123
  }
117
124
 
118
- aggregateResult(
119
- runs: HealthCheckRunForAggregation<QueryResult>[]
125
+ mergeResult(
126
+ existing: QueryAggregatedResult | undefined,
127
+ run: HealthCheckRunForAggregation<QueryResult>,
120
128
  ): QueryAggregatedResult {
121
- const times = runs
122
- .map((r) => r.metadata?.executionTimeMs)
123
- .filter((v): v is number => typeof v === "number");
124
-
125
- const successes = runs
126
- .map((r) => r.metadata?.success)
127
- .filter((v): v is boolean => typeof v === "boolean");
128
-
129
- const successCount = successes.filter(Boolean).length;
129
+ const metadata = run.metadata;
130
130
 
131
131
  return {
132
- avgExecutionTimeMs:
133
- times.length > 0
134
- ? Math.round(times.reduce((a, b) => a + b, 0) / times.length)
135
- : 0,
136
- successRate:
137
- successes.length > 0
138
- ? Math.round((successCount / successes.length) * 100)
139
- : 0,
132
+ avgExecutionTimeMs: mergeAverage(
133
+ existing?.avgExecutionTimeMs,
134
+ metadata?.executionTimeMs,
135
+ ),
136
+ successRate: mergeRate(existing?.successRate, metadata?.success),
140
137
  };
141
138
  }
142
139
  }
@@ -8,7 +8,7 @@ describe("PostgresHealthCheckStrategy", () => {
8
8
  rowCount?: number;
9
9
  queryError?: Error;
10
10
  connectError?: Error;
11
- } = {}
11
+ } = {},
12
12
  ): DbClient => ({
13
13
  connect: mock(() =>
14
14
  config.connectError
@@ -17,10 +17,10 @@ describe("PostgresHealthCheckStrategy", () => {
17
17
  query: mock(() =>
18
18
  config.queryError
19
19
  ? Promise.reject(config.queryError)
20
- : Promise.resolve({ rowCount: config.rowCount ?? 1 })
20
+ : Promise.resolve({ rowCount: config.rowCount ?? 1 }),
21
21
  ),
22
22
  end: mock(() => Promise.resolve()),
23
- })
23
+ }),
24
24
  ),
25
25
  });
26
26
 
@@ -46,7 +46,7 @@ describe("PostgresHealthCheckStrategy", () => {
46
46
 
47
47
  it("should throw for connection error", async () => {
48
48
  const strategy = new PostgresHealthCheckStrategy(
49
- createMockClient({ connectError: new Error("Connection refused") })
49
+ createMockClient({ connectError: new Error("Connection refused") }),
50
50
  );
51
51
 
52
52
  await expect(
@@ -57,7 +57,7 @@ describe("PostgresHealthCheckStrategy", () => {
57
57
  user: "postgres",
58
58
  password: "secret",
59
59
  timeout: 5000,
60
- })
60
+ }),
61
61
  ).rejects.toThrow("Connection refused");
62
62
  });
63
63
  });
@@ -85,7 +85,7 @@ describe("PostgresHealthCheckStrategy", () => {
85
85
 
86
86
  it("should return error for query error", async () => {
87
87
  const strategy = new PostgresHealthCheckStrategy(
88
- createMockClient({ queryError: new Error("Syntax error") })
88
+ createMockClient({ queryError: new Error("Syntax error") }),
89
89
  );
90
90
  const connectedClient = await strategy.createClient({
91
91
  host: "localhost",
@@ -107,7 +107,7 @@ describe("PostgresHealthCheckStrategy", () => {
107
107
 
108
108
  it("should return custom row count", async () => {
109
109
  const strategy = new PostgresHealthCheckStrategy(
110
- createMockClient({ rowCount: 5 })
110
+ createMockClient({ rowCount: 5 }),
111
111
  );
112
112
  const connectedClient = await strategy.createClient({
113
113
  host: "localhost",
@@ -128,8 +128,8 @@ describe("PostgresHealthCheckStrategy", () => {
128
128
  });
129
129
  });
130
130
 
131
- describe("aggregateResult", () => {
132
- it("should calculate averages correctly", () => {
131
+ describe("mergeResult", () => {
132
+ it("should calculate averages correctly through incremental merging", () => {
133
133
  const strategy = new PostgresHealthCheckStrategy();
134
134
  const runs = [
135
135
  {
@@ -158,34 +158,34 @@ describe("PostgresHealthCheckStrategy", () => {
158
158
  },
159
159
  ];
160
160
 
161
- const aggregated = strategy.aggregateResult(runs);
161
+ // Merge incrementally
162
+ let aggregated = strategy.mergeResult(undefined, runs[0]);
163
+ aggregated = strategy.mergeResult(aggregated, runs[1]);
162
164
 
163
- expect(aggregated.avgConnectionTime).toBe(75);
164
- expect(aggregated.successRate).toBe(100);
165
- expect(aggregated.errorCount).toBe(0);
165
+ expect(aggregated.avgConnectionTime.avg).toBe(75);
166
+ expect(aggregated.successRate.rate).toBe(100);
167
+ expect(aggregated.errorCount.count).toBe(0);
166
168
  });
167
169
 
168
170
  it("should count errors", () => {
169
171
  const strategy = new PostgresHealthCheckStrategy();
170
- const runs = [
171
- {
172
- id: "1",
173
- status: "unhealthy" as const,
174
- latencyMs: 100,
175
- checkId: "c1",
176
- timestamp: new Date(),
177
- metadata: {
178
- connected: false,
179
- connectionTimeMs: 100,
180
- error: "Connection refused",
181
- },
172
+ const run = {
173
+ id: "1",
174
+ status: "unhealthy" as const,
175
+ latencyMs: 100,
176
+ checkId: "c1",
177
+ timestamp: new Date(),
178
+ metadata: {
179
+ connected: false,
180
+ connectionTimeMs: 100,
181
+ error: "Connection refused",
182
182
  },
183
- ];
183
+ };
184
184
 
185
- const aggregated = strategy.aggregateResult(runs);
185
+ const aggregated = strategy.mergeResult(undefined, run);
186
186
 
187
- expect(aggregated.errorCount).toBe(1);
188
- expect(aggregated.successRate).toBe(0);
187
+ expect(aggregated.errorCount.count).toBe(1);
188
+ expect(aggregated.successRate.rate).toBe(0);
189
189
  });
190
190
  });
191
191
  });
package/src/strategy.ts CHANGED
@@ -3,11 +3,21 @@ import {
3
3
  HealthCheckStrategy,
4
4
  HealthCheckRunForAggregation,
5
5
  Versioned,
6
+ VersionedAggregated,
7
+ aggregatedAverage,
8
+ aggregatedMinMax,
9
+ aggregatedRate,
10
+ aggregatedCounter,
11
+ mergeAverage,
12
+ mergeRate,
13
+ mergeCounter,
14
+ mergeMinMax,
6
15
  z,
7
16
  configString,
8
17
  configNumber,
9
18
  configBoolean,
10
19
  type ConnectedClient,
20
+ type InferAggregatedResult,
11
21
  } from "@checkstack/backend-api";
12
22
  import {
13
23
  healthResultBoolean,
@@ -70,32 +80,32 @@ const postgresResultSchema = healthResultSchema({
70
80
 
71
81
  type PostgresResult = z.infer<typeof postgresResultSchema>;
72
82
 
73
- /**
74
- * Aggregated metadata for buckets.
75
- */
76
- const postgresAggregatedSchema = healthResultSchema({
77
- avgConnectionTime: healthResultNumber({
83
+ /** Aggregated field definitions for bucket merging */
84
+ const postgresAggregatedFields = {
85
+ avgConnectionTime: aggregatedAverage({
78
86
  "x-chart-type": "line",
79
87
  "x-chart-label": "Avg Connection Time",
80
88
  "x-chart-unit": "ms",
81
89
  }),
82
- maxConnectionTime: healthResultNumber({
90
+ maxConnectionTime: aggregatedMinMax({
83
91
  "x-chart-type": "line",
84
92
  "x-chart-label": "Max Connection Time",
85
93
  "x-chart-unit": "ms",
86
94
  }),
87
- successRate: healthResultNumber({
95
+ successRate: aggregatedRate({
88
96
  "x-chart-type": "gauge",
89
97
  "x-chart-label": "Success Rate",
90
98
  "x-chart-unit": "%",
91
99
  }),
92
- errorCount: healthResultNumber({
100
+ errorCount: aggregatedCounter({
93
101
  "x-chart-type": "counter",
94
102
  "x-chart-label": "Errors",
95
103
  }),
96
- });
104
+ };
97
105
 
98
- type PostgresAggregatedResult = z.infer<typeof postgresAggregatedSchema>;
106
+ type PostgresAggregatedResult = InferAggregatedResult<
107
+ typeof postgresAggregatedFields
108
+ >;
99
109
 
100
110
  // ============================================================================
101
111
  // DATABASE CLIENT INTERFACE (for testability)
@@ -133,15 +143,12 @@ const defaultDbClient: DbClient = {
133
143
  // STRATEGY
134
144
  // ============================================================================
135
145
 
136
- export class PostgresHealthCheckStrategy
137
- implements
138
- HealthCheckStrategy<
139
- PostgresConfig,
140
- PostgresTransportClient,
141
- PostgresResult,
142
- PostgresAggregatedResult
143
- >
144
- {
146
+ export class PostgresHealthCheckStrategy implements HealthCheckStrategy<
147
+ PostgresConfig,
148
+ PostgresTransportClient,
149
+ PostgresResult,
150
+ typeof postgresAggregatedFields
151
+ > {
145
152
  id = "postgres";
146
153
  displayName = "PostgreSQL Health Check";
147
154
  description = "PostgreSQL database connectivity and query health check";
@@ -178,58 +185,38 @@ export class PostgresHealthCheckStrategy
178
185
  ],
179
186
  });
180
187
 
181
- aggregatedResult: Versioned<PostgresAggregatedResult> = new Versioned({
188
+ aggregatedResult = new VersionedAggregated({
182
189
  version: 1,
183
- schema: postgresAggregatedSchema,
190
+ fields: postgresAggregatedFields,
184
191
  });
185
192
 
186
- aggregateResult(
187
- runs: HealthCheckRunForAggregation<PostgresResult>[]
193
+ mergeResult(
194
+ existing: PostgresAggregatedResult | undefined,
195
+ run: HealthCheckRunForAggregation<PostgresResult>,
188
196
  ): PostgresAggregatedResult {
189
- const validRuns = runs.filter((r) => r.metadata);
197
+ const metadata = run.metadata;
190
198
 
191
- if (validRuns.length === 0) {
192
- return {
193
- avgConnectionTime: 0,
194
- maxConnectionTime: 0,
195
- successRate: 0,
196
- errorCount: 0,
197
- };
198
- }
199
+ const avgConnectionTime = mergeAverage(
200
+ existing?.avgConnectionTime,
201
+ metadata?.connectionTimeMs,
202
+ );
199
203
 
200
- const connectionTimes = validRuns
201
- .map((r) => r.metadata?.connectionTimeMs)
202
- .filter((t): t is number => typeof t === "number");
204
+ const maxConnectionTime = mergeMinMax(
205
+ existing?.maxConnectionTime,
206
+ metadata?.connectionTimeMs,
207
+ );
203
208
 
204
- const avgConnectionTime =
205
- connectionTimes.length > 0
206
- ? Math.round(
207
- connectionTimes.reduce((a, b) => a + b, 0) / connectionTimes.length
208
- )
209
- : 0;
209
+ const isSuccess = metadata?.connected ?? false;
210
+ const successRate = mergeRate(existing?.successRate, isSuccess);
210
211
 
211
- const maxConnectionTime =
212
- connectionTimes.length > 0 ? Math.max(...connectionTimes) : 0;
212
+ const hasError = metadata?.error !== undefined;
213
+ const errorCount = mergeCounter(existing?.errorCount, hasError);
213
214
 
214
- const successCount = validRuns.filter(
215
- (r) => r.metadata?.connected === true
216
- ).length;
217
- const successRate = Math.round((successCount / validRuns.length) * 100);
218
-
219
- const errorCount = validRuns.filter(
220
- (r) => r.metadata?.error !== undefined
221
- ).length;
222
-
223
- return {
224
- avgConnectionTime,
225
- maxConnectionTime,
226
- successRate,
227
- errorCount,
228
- };
215
+ return { avgConnectionTime, maxConnectionTime, successRate, errorCount };
229
216
  }
230
217
 
231
218
  async createClient(
232
- config: PostgresConfigInput
219
+ config: PostgresConfigInput,
233
220
  ): Promise<ConnectedClient<PostgresTransportClient>> {
234
221
  const validatedConfig = this.config.validate(config);
235
222