@checkstack/healthcheck-postgres-backend 0.1.14 → 0.2.1

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,53 @@
1
1
  # @checkstack/healthcheck-postgres-backend
2
2
 
3
+ ## 0.2.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 869b4ab: ## Health Check Execution Improvements
8
+
9
+ ### Breaking Changes (backend-api)
10
+
11
+ - `HealthCheckStrategy.createClient()` now accepts `unknown` instead of `TConfig` due to TypeScript contravariance constraints. Implementations should use `this.config.validate(config)` to narrow the type.
12
+
13
+ ### Features
14
+
15
+ - **Platform-level hard timeout**: The executor now wraps the entire health check execution (connection + all collectors) in a single timeout, ensuring checks never hang indefinitely.
16
+ - **Parallel collector execution**: Collectors now run in parallel using `Promise.allSettled()`, improving performance while ensuring all collectors complete regardless of individual failures.
17
+ - **Base strategy config schema**: All strategy configs now extend `baseStrategyConfigSchema` which provides a standardized `timeout` field with sensible defaults (30s, min 100ms).
18
+
19
+ ### Fixes
20
+
21
+ - Fixed HTTP and Jenkins strategies clearing timeouts before reading the full response body.
22
+ - Simplified registry type signatures by using default type parameters.
23
+
24
+ - Updated dependencies [869b4ab]
25
+ - @checkstack/backend-api@0.8.0
26
+
27
+ ## 0.2.0
28
+
29
+ ### Minor Changes
30
+
31
+ - 3dd1914: Migrate health check strategies to VersionedAggregated with \_type discriminator
32
+
33
+ All 13 health check strategies now use `VersionedAggregated` for their `aggregatedResult` property, enabling automatic bucket merging with 100% mathematical fidelity.
34
+
35
+ **Key changes:**
36
+
37
+ - **`_type` discriminator**: All aggregated state objects now include a required `_type` field (`"average"`, `"rate"`, `"counter"`, `"minmax"`) for reliable type detection
38
+ - The `HealthCheckStrategy` interface now requires `aggregatedResult` to be a `VersionedAggregated<AggregatedResultShape>`
39
+ - Strategy/collector `mergeResult` methods return state objects with `_type` (e.g., `{ _type: "average", _sum, _count, avg }`)
40
+ - `mergeAggregatedBucketResults`, `combineBuckets`, and `reaggregateBuckets` now require `registry` and `strategyId` parameters
41
+ - `HealthCheckService` constructor now requires both `registry` and `collectorRegistry` parameters
42
+ - Frontend `extractComputedValue` now uses `_type` discriminator for robust type detection
43
+
44
+ **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.
45
+
46
+ ### Patch Changes
47
+
48
+ - Updated dependencies [3dd1914]
49
+ - @checkstack/backend-api@0.7.0
50
+
3
51
  ## 0.1.14
4
52
 
5
53
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/healthcheck-postgres-backend",
3
- "version": "0.1.14",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "scripts": {
@@ -9,9 +9,9 @@
9
9
  "lint:code": "eslint . --max-warnings 0"
10
10
  },
11
11
  "dependencies": {
12
- "@checkstack/backend-api": "0.5.2",
13
- "@checkstack/common": "0.6.1",
14
- "@checkstack/healthcheck-common": "0.8.1",
12
+ "@checkstack/backend-api": "0.7.0",
13
+ "@checkstack/common": "0.6.2",
14
+ "@checkstack/healthcheck-common": "0.8.2",
15
15
  "pg": "^8.11.0"
16
16
  },
17
17
  "devDependencies": {
@@ -89,8 +89,8 @@ describe("QueryCollector", () => {
89
89
  let aggregated = collector.mergeResult(undefined, runs[0]);
90
90
  aggregated = collector.mergeResult(aggregated, runs[1]);
91
91
 
92
- expect(aggregated.avgExecutionTimeMs).toBe(75);
93
- expect(aggregated.successRate).toBe(100);
92
+ expect(aggregated.avgExecutionTimeMs.avg).toBe(75);
93
+ expect(aggregated.successRate.rate).toBe(100);
94
94
  });
95
95
 
96
96
  it("should calculate success rate correctly", () => {
@@ -117,7 +117,7 @@ describe("QueryCollector", () => {
117
117
  let aggregated = collector.mergeResult(undefined, runs[0]);
118
118
  aggregated = collector.mergeResult(aggregated, runs[1]);
119
119
 
120
- expect(aggregated.successRate).toBe(50);
120
+ expect(aggregated.successRate.rate).toBe(50);
121
121
  });
122
122
  });
123
123
 
@@ -6,10 +6,10 @@ import {
6
6
  type CollectorStrategy,
7
7
  mergeAverage,
8
8
  mergeRate,
9
- averageStateSchema,
10
- rateStateSchema,
11
- type AverageState,
12
- type RateState,
9
+ VersionedAggregated,
10
+ aggregatedAverage,
11
+ aggregatedRate,
12
+ type InferAggregatedResult,
13
13
  } from "@checkstack/backend-api";
14
14
  import {
15
15
  healthResultNumber,
@@ -51,29 +51,24 @@ const queryResultSchema = healthResultSchema({
51
51
 
52
52
  export type QueryResult = z.infer<typeof queryResultSchema>;
53
53
 
54
- const queryAggregatedDisplaySchema = healthResultSchema({
55
- avgExecutionTimeMs: healthResultNumber({
54
+ // Aggregated result fields definition
55
+ const queryAggregatedFields = {
56
+ avgExecutionTimeMs: aggregatedAverage({
56
57
  "x-chart-type": "line",
57
58
  "x-chart-label": "Avg Execution Time",
58
59
  "x-chart-unit": "ms",
59
60
  }),
60
- successRate: healthResultNumber({
61
+ successRate: aggregatedRate({
61
62
  "x-chart-type": "gauge",
62
63
  "x-chart-label": "Success Rate",
63
64
  "x-chart-unit": "%",
64
65
  }),
65
- });
66
-
67
- const queryAggregatedInternalSchema = z.object({
68
- _executionTime: averageStateSchema.optional(),
69
- _success: rateStateSchema.optional(),
70
- });
66
+ };
71
67
 
72
- const queryAggregatedSchema = queryAggregatedDisplaySchema.merge(
73
- queryAggregatedInternalSchema,
74
- );
75
-
76
- export type QueryAggregatedResult = z.infer<typeof queryAggregatedSchema>;
68
+ // Type inferred from field definitions
69
+ export type QueryAggregatedResult = InferAggregatedResult<
70
+ typeof queryAggregatedFields
71
+ >;
77
72
 
78
73
  // ============================================================================
79
74
  // QUERY COLLECTOR
@@ -99,9 +94,9 @@ export class QueryCollector implements CollectorStrategy<
99
94
 
100
95
  config = new Versioned({ version: 1, schema: queryConfigSchema });
101
96
  result = new Versioned({ version: 1, schema: queryResultSchema });
102
- aggregatedResult = new Versioned({
97
+ aggregatedResult = new VersionedAggregated({
103
98
  version: 1,
104
- schema: queryAggregatedSchema,
99
+ fields: queryAggregatedFields,
105
100
  });
106
101
 
107
102
  async execute({
@@ -133,21 +128,12 @@ export class QueryCollector implements CollectorStrategy<
133
128
  ): QueryAggregatedResult {
134
129
  const metadata = run.metadata;
135
130
 
136
- const executionTimeState = mergeAverage(
137
- existing?._executionTime as AverageState | undefined,
138
- metadata?.executionTimeMs,
139
- );
140
-
141
- const successState = mergeRate(
142
- existing?._success as RateState | undefined,
143
- metadata?.success,
144
- );
145
-
146
131
  return {
147
- avgExecutionTimeMs: executionTimeState.avg,
148
- successRate: successState.rate,
149
- _executionTime: executionTimeState,
150
- _success: successState,
132
+ avgExecutionTimeMs: mergeAverage(
133
+ existing?.avgExecutionTimeMs,
134
+ metadata?.executionTimeMs,
135
+ ),
136
+ successRate: mergeRate(existing?.successRate, metadata?.success),
151
137
  };
152
138
  }
153
139
  }
@@ -162,9 +162,9 @@ describe("PostgresHealthCheckStrategy", () => {
162
162
  let aggregated = strategy.mergeResult(undefined, runs[0]);
163
163
  aggregated = strategy.mergeResult(aggregated, runs[1]);
164
164
 
165
- expect(aggregated.avgConnectionTime).toBe(75);
166
- expect(aggregated.successRate).toBe(100);
167
- 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);
168
168
  });
169
169
 
170
170
  it("should count errors", () => {
@@ -184,8 +184,8 @@ describe("PostgresHealthCheckStrategy", () => {
184
184
 
185
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,23 +3,22 @@ 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,
11
- mergeAverage,
12
- mergeRate,
13
- mergeCounter,
14
- mergeMinMax,
15
- averageStateSchema,
16
- minMaxStateSchema,
17
- rateStateSchema,
18
- counterStateSchema,
19
- type AverageState,
20
- type RateState,
21
- type CounterState,
22
- type MinMaxState,
20
+ type InferAggregatedResult,
21
+ baseStrategyConfigSchema,
23
22
  } from "@checkstack/backend-api";
24
23
  import {
25
24
  healthResultBoolean,
@@ -40,7 +39,7 @@ import type {
40
39
  /**
41
40
  * Configuration schema for PostgreSQL health checks.
42
41
  */
43
- export const postgresConfigSchema = z.object({
42
+ export const postgresConfigSchema = baseStrategyConfigSchema.extend({
44
43
  host: configString({}).describe("PostgreSQL server hostname"),
45
44
  port: configNumber({})
46
45
  .int()
@@ -52,10 +51,6 @@ export const postgresConfigSchema = z.object({
52
51
  user: configString({}).describe("Database user"),
53
52
  password: configString({ "x-secret": true }).describe("Database password"),
54
53
  ssl: configBoolean({}).default(false).describe("Use SSL connection"),
55
- timeout: configNumber({})
56
- .min(100)
57
- .default(10_000)
58
- .describe("Connection timeout in milliseconds"),
59
54
  });
60
55
 
61
56
  export type PostgresConfig = z.infer<typeof postgresConfigSchema>;
@@ -82,45 +77,32 @@ const postgresResultSchema = healthResultSchema({
82
77
 
83
78
  type PostgresResult = z.infer<typeof postgresResultSchema>;
84
79
 
85
- /**
86
- * Aggregated metadata for buckets.
87
- */
88
- // UI-visible aggregated fields (for charts)
89
- const postgresAggregatedDisplaySchema = healthResultSchema({
90
- avgConnectionTime: healthResultNumber({
80
+ /** Aggregated field definitions for bucket merging */
81
+ const postgresAggregatedFields = {
82
+ avgConnectionTime: aggregatedAverage({
91
83
  "x-chart-type": "line",
92
84
  "x-chart-label": "Avg Connection Time",
93
85
  "x-chart-unit": "ms",
94
86
  }),
95
- maxConnectionTime: healthResultNumber({
87
+ maxConnectionTime: aggregatedMinMax({
96
88
  "x-chart-type": "line",
97
89
  "x-chart-label": "Max Connection Time",
98
90
  "x-chart-unit": "ms",
99
91
  }),
100
- successRate: healthResultNumber({
92
+ successRate: aggregatedRate({
101
93
  "x-chart-type": "gauge",
102
94
  "x-chart-label": "Success Rate",
103
95
  "x-chart-unit": "%",
104
96
  }),
105
- errorCount: healthResultNumber({
97
+ errorCount: aggregatedCounter({
106
98
  "x-chart-type": "counter",
107
99
  "x-chart-label": "Errors",
108
100
  }),
109
- });
110
-
111
- // Internal state for incremental aggregation
112
- const postgresAggregatedInternalSchema = z.object({
113
- _connectionTime: averageStateSchema.optional(),
114
- _maxConnectionTime: minMaxStateSchema.optional(),
115
- _success: rateStateSchema.optional(),
116
- _errors: counterStateSchema.optional(),
117
- });
118
-
119
- const postgresAggregatedSchema = postgresAggregatedDisplaySchema.merge(
120
- postgresAggregatedInternalSchema,
121
- );
101
+ };
122
102
 
123
- type PostgresAggregatedResult = z.infer<typeof postgresAggregatedSchema>;
103
+ type PostgresAggregatedResult = InferAggregatedResult<
104
+ typeof postgresAggregatedFields
105
+ >;
124
106
 
125
107
  // ============================================================================
126
108
  // DATABASE CLIENT INTERFACE (for testability)
@@ -162,7 +144,7 @@ export class PostgresHealthCheckStrategy implements HealthCheckStrategy<
162
144
  PostgresConfig,
163
145
  PostgresTransportClient,
164
146
  PostgresResult,
165
- PostgresAggregatedResult
147
+ typeof postgresAggregatedFields
166
148
  > {
167
149
  id = "postgres";
168
150
  displayName = "PostgreSQL Health Check";
@@ -200,9 +182,9 @@ export class PostgresHealthCheckStrategy implements HealthCheckStrategy<
200
182
  ],
201
183
  });
202
184
 
203
- aggregatedResult: Versioned<PostgresAggregatedResult> = new Versioned({
185
+ aggregatedResult = new VersionedAggregated({
204
186
  version: 1,
205
- schema: postgresAggregatedSchema,
187
+ fields: postgresAggregatedFields,
206
188
  });
207
189
 
208
190
  mergeResult(
@@ -211,40 +193,23 @@ export class PostgresHealthCheckStrategy implements HealthCheckStrategy<
211
193
  ): PostgresAggregatedResult {
212
194
  const metadata = run.metadata;
213
195
 
214
- // Merge connection time average
215
- const connectionTimeState = mergeAverage(
216
- existing?._connectionTime as AverageState | undefined,
196
+ const avgConnectionTime = mergeAverage(
197
+ existing?.avgConnectionTime,
217
198
  metadata?.connectionTimeMs,
218
199
  );
219
200
 
220
- // Merge max connection time
221
- const maxConnectionTimeState = mergeMinMax(
222
- existing?._maxConnectionTime as MinMaxState | undefined,
201
+ const maxConnectionTime = mergeMinMax(
202
+ existing?.maxConnectionTime,
223
203
  metadata?.connectionTimeMs,
224
204
  );
225
205
 
226
- // Merge success rate
227
- const successState = mergeRate(
228
- existing?._success as RateState | undefined,
229
- metadata?.connected,
230
- );
206
+ const isSuccess = metadata?.connected ?? false;
207
+ const successRate = mergeRate(existing?.successRate, isSuccess);
231
208
 
232
- // Merge error count
233
- const errorState = mergeCounter(
234
- existing?._errors as CounterState | undefined,
235
- metadata?.error !== undefined,
236
- );
209
+ const hasError = metadata?.error !== undefined;
210
+ const errorCount = mergeCounter(existing?.errorCount, hasError);
237
211
 
238
- return {
239
- avgConnectionTime: connectionTimeState.avg,
240
- maxConnectionTime: maxConnectionTimeState.max,
241
- successRate: successState.rate,
242
- errorCount: errorState.count,
243
- _connectionTime: connectionTimeState,
244
- _maxConnectionTime: maxConnectionTimeState,
245
- _success: successState,
246
- _errors: errorState,
247
- };
212
+ return { avgConnectionTime, maxConnectionTime, successRate, errorCount };
248
213
  }
249
214
 
250
215
  async createClient(