@checkstack/healthcheck-http-backend 0.3.23 → 0.4.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 +65 -0
- package/package.json +3 -3
- package/src/migration-chain-contract.test.ts +43 -0
- package/src/request-collector.test.ts +20 -0
- package/src/request-collector.ts +46 -3
- package/src/strategy.test.ts +31 -0
- package/src/strategy.ts +52 -15
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,70 @@
|
|
|
1
1
|
# @checkstack/healthcheck-http-backend
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 9dcc848: Harden config-versioning so stored configs always migrate-then-validate and broken migration chains fail fast at boot.
|
|
8
|
+
|
|
9
|
+
- `@checkstack/backend-api` `Versioned<T>` gains `parseAssumingV1` (migrate-from-v1 then validate leniently, runtime path), `parseStrictAssumingV1` (migrate then validate strictly, editor path), and `validateMigrationChainFromV1()`. A standalone pure helper `assertMigrationChainFromV1({ version, migrations })` is the single shared implementation behind the constructor guard and `validateMigrationChainFromV1`.
|
|
10
|
+
- `Versioned` now validates its own v1 -> `version` chain in the constructor, which runs at module import / plugin registration. A new `no-restricted-syntax` ESLint rule bans calling `parse` / `safeParse` / `parseAsync` / `strict` directly on a `Versioned`'s `.schema` member.
|
|
11
|
+
- Auth strategy migration chains are validated at the `betterAuthExtensionPoint.addStrategy` chokepoint (`@checkstack/auth-backend`).
|
|
12
|
+
- Automation action AND trigger configs migrate-then-validate (lenient at dispatch, strict in the editor validator, recursing into `choose`/`parallel`/`repeat`/`sequence` blocks). The `run_script` / `run_shell` action configs bump to `version: 2` dropping the removed `sandbox` key, fixing the editor's `Unrecognized key: sandbox` error.
|
|
13
|
+
- Anomaly read path now validates: `getAnomalyConfig` / `getAnomalyAssignmentConfig` run stored records through `Versioned.parseRecord`; `PartialAnomalySettingsSchema` moved to `@checkstack/anomaly-common`. Notification ConfigService reads thread the migrations argument, and per-strategy `userConfig` is migrate-then-validated before `send()`.
|
|
14
|
+
- gitops-apply migrate-then-validates authored health-check config; integration connection validation routes through `safeValidate`. The latent HTTP health-check `result` schema (at `version: 3` with no migrations) now ships a pass-through v1 -> v2 -> v3 chain.
|
|
15
|
+
|
|
16
|
+
BREAKING CHANGES (fail-fast at boot, intended):
|
|
17
|
+
|
|
18
|
+
- Any `Versioned` config with `version > 1` and an incomplete or non-contiguous migration chain now throws at construction (boot) instead of failing lazily on first read. This covers every `Versioned` instance repo-wide, including future plugin types. Out-of-tree plugins shipping such a config must add the missing migration step(s); all in-repo strategies already have complete chains.
|
|
19
|
+
- An auth strategy declaring `configVersion > 1` without a complete chain throws at registration.
|
|
20
|
+
- A trigger's per-automation config is now a versioned `config: Versioned<TConfig>` instead of a bare `configSchema?`. Plugins registering triggers with `configSchema:` must wrap it: `config: new Versioned({ version: 1, schema })`. The underlying schema stays reachable via `config.schema`; triggers without per-automation config are unaffected.
|
|
21
|
+
|
|
22
|
+
State and scale: all affected reads resolve from shared Postgres / in-process registries, so every pod sees the same migrated answer. No new framework-owned current-state store.
|
|
23
|
+
|
|
24
|
+
This is a beta minor.
|
|
25
|
+
|
|
26
|
+
- 9dcc848: Add environments as a first-class catalog primitive, with per-environment health-check fan-out, config templating, per-environment reactive health, and script run-context exposure.
|
|
27
|
+
|
|
28
|
+
- Catalog primitive: an environment is a sibling of groups - a named, instance-global record carrying free-form custom fields (baseUrl, region, tier, ...) that any system can belong to many-to-many. New `environments` + `systems_environments` tables, `EnvironmentSchema` + create/update schemas, `EntityService` environment CRUD and membership joins, RPC endpoints gated by a new `catalogAccess.environment` access rule, a GitOps `Environment` kind + `System.environments` extension, and frontend management (an `EnvironmentEditor`, an Environments management panel, and a per-system environment picker). The Environments card's Add/Edit/Delete affordances are gated on `catalogAccess.environment.manage`.
|
|
29
|
+
- Per-environment fan-out: run identity becomes `(systemId, configurationId, environmentId)`. Runs, aggregates, and state transitions gain a nullable `environmentId`. The health-check assignment gains an `environmentIds` selector with three modes (All / Specific / None; `null` and `[]` are distinct). The queue executor resolves the effective environment set via the catalog `resolveSystemEnvironments` read and executes one isolated run per environment.
|
|
30
|
+
- Config templating: a new `x-templatable` config-field marker renders a string field through the template engine at execute time, against `{ environment, check, system }`. A shared `renderTemplatableConfig` and a `renderTemplatePreview` helper (re-exported from `@checkstack/template-engine`) keep editor previews identical to the run-time render. The HTTP collector's `url`, `headers[].value`, and `body` are templatable, rendered per environment (the strategy client build moves inside the per-env loop); the `url`'s `.url()` validation moves post-render. Secrets resolve before templating; a field marked both secret and `x-templatable` is rejected at plugin load. `DynamicForm` shows a live "Preview" line, and the catalog `EnvironmentPreviewPicker` ("Preview as: <environment>") drives it in the collector editor (only when the schema has a templatable field).
|
|
31
|
+
- Script run-context: `CollectorRunContext` gains an optional `environment` field (`{ id, name, fields }`, metadata only). Shell collectors receive `CHECKSTACK_ENV_ID` / `_NAME` / `CHECKSTACK_ENV_<FIELD>` vars; inline TS collectors read `globalThis.context.environment`; the editor test panel mirrors both. The env-less path is unchanged.
|
|
32
|
+
- Per-environment reactive health (see BREAKING below), env-keyed read/write paths, env-qualified serialization locks, an optional `trigger.payload.environmentId`, per-environment isolation, and an `ENVIRONMENT_RESOLUTION_FAILED` signal when catalog resolution degrades to a single env-less run.
|
|
33
|
+
|
|
34
|
+
BREAKING CHANGES: the reactive `health` entity's id-shape and cardinality change. It now encodes two views: per-environment (id `"<systemId>::<environmentId>"`) and a system rollup (id `"<systemId>"`, the worst status across environments + env-less runs). The rollup PRESERVES the pre-existing system-level contract - dashboards, status badges, and automations referencing health by `systemId` keep working without re-authoring - but the entity's contract surface changed (new id-shape, higher cardinality, new payload field), so it is flagged breaking. `getBulkHealthState` parses env-qualified ids and keys results by the original id.
|
|
35
|
+
|
|
36
|
+
State and scale: membership and custom fields live only in catalog Postgres and are re-read every tick via the cross-plugin RPC; env-keyed health reads from shared `health_check_runs` / aggregates / transitions (compute-on-read). Every pod resolves the same effective set and the same per-environment health. No pod-local environment state.
|
|
37
|
+
|
|
38
|
+
Also: `unwrapSchema` in `zod-config.ts` loops instead of single-pass-stripping so multi-layer wrappers (`.optional().default()`) still resolve `x-templatable` meta. The env-less `{{ environment.* }}` run notice logs at `debug` (a legitimate recurring configuration), while the post-render HTTP `.url()` check still fails a genuinely-broken empty render with a clear "Rendered URL is invalid" error.
|
|
39
|
+
|
|
40
|
+
This is a beta minor.
|
|
41
|
+
|
|
42
|
+
- 9dcc848: Health-check strategy and collector configs now migrate-then-validate when loaded, instead of being cast/rendered raw.
|
|
43
|
+
|
|
44
|
+
These configs declared `version: 2`/`3` migrations but the load path never ran them: stored values are persisted UNVERSIONED, and the executor cast them straight to the strategy/collector type. Both the execution path (`queue-executor`) and the read API (`mapConfig`, feeding router / frontend / gitops `getConfiguration`) now use assume-v1-on-read (`Versioned.parseAssumingV1`): wrap as version 1, run the declared chain, then validate. Order is preserved: migrate -> secret resolve -> template render -> execute. An unregistered strategy/collector or a failed migrate falls back to the raw stored blob rather than dropping the configuration. Every reshaper migration is now IDEMPOTENT, guarding on its legacy discriminator so already-current data passes through untouched.
|
|
45
|
+
|
|
46
|
+
BREAKING CHANGE: for any config GENUINELY at version 1 in the database (e.g. an HTTP strategy still carrying `url`/`method`, or an execute collector still carrying `command`/`args`), the declared migrations now actually RUN on load, so the loaded/returned shape changes for such rows. This is the intended fix - those fields were already supposed to have been migrated away. Configs already at the current shape are unaffected. No data backfill is performed; migration is applied on every read.
|
|
47
|
+
|
|
48
|
+
This is a beta minor.
|
|
49
|
+
|
|
50
|
+
### Patch Changes
|
|
51
|
+
|
|
52
|
+
- Updated dependencies [9dcc848]
|
|
53
|
+
- Updated dependencies [9dcc848]
|
|
54
|
+
- Updated dependencies [9dcc848]
|
|
55
|
+
- Updated dependencies [9dcc848]
|
|
56
|
+
- Updated dependencies [9dcc848]
|
|
57
|
+
- Updated dependencies [9dcc848]
|
|
58
|
+
- Updated dependencies [9dcc848]
|
|
59
|
+
- Updated dependencies [9dcc848]
|
|
60
|
+
- Updated dependencies [9dcc848]
|
|
61
|
+
- Updated dependencies [9dcc848]
|
|
62
|
+
- Updated dependencies [9dcc848]
|
|
63
|
+
- Updated dependencies [9dcc848]
|
|
64
|
+
- @checkstack/backend-api@0.21.0
|
|
65
|
+
- @checkstack/healthcheck-common@1.5.0
|
|
66
|
+
- @checkstack/common@0.13.0
|
|
67
|
+
|
|
3
68
|
## 0.3.23
|
|
4
69
|
|
|
5
70
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/healthcheck-http-backend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"checkstack": {
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
"pack": "bunx @checkstack/scripts plugin-pack"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@checkstack/backend-api": "0.
|
|
18
|
-
"@checkstack/healthcheck-common": "1.
|
|
17
|
+
"@checkstack/backend-api": "0.20.0",
|
|
18
|
+
"@checkstack/healthcheck-common": "1.4.0",
|
|
19
19
|
"jsonpath-plus": "^10.3.0",
|
|
20
20
|
"@checkstack/common": "0.12.0"
|
|
21
21
|
},
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract test: every HTTP health-check Versioned schema that is stored
|
|
3
|
+
* UNVERSIONED and read back via assume-v1-on-read MUST have a COMPLETE,
|
|
4
|
+
* contiguous migration chain from version 1 to its current `version`.
|
|
5
|
+
*
|
|
6
|
+
* This is a pure STRUCTURAL check on the `Versioned.migrations` array
|
|
7
|
+
* (`validateMigrationChainFromV1` — no `migrate()` is run), so it carries
|
|
8
|
+
* zero per-config upkeep: the day someone bumps a schema's `version` without
|
|
9
|
+
* shipping a covering migration, `parseAssumingV1` would silently fail at
|
|
10
|
+
* runtime on a genuinely-v1 stored blob — this test turns that into a CI
|
|
11
|
+
* failure instead. It enumerates every Versioned field (config + result +
|
|
12
|
+
* aggregatedResult) of the strategy + collectors this plugin registers, so a
|
|
13
|
+
* new collector or a bumped result schema is covered automatically.
|
|
14
|
+
*/
|
|
15
|
+
import { describe, expect, it } from "bun:test";
|
|
16
|
+
import { HttpHealthCheckStrategy } from "./strategy";
|
|
17
|
+
import { RequestCollector } from "./request-collector";
|
|
18
|
+
|
|
19
|
+
describe("http health-check migration-chain contract", () => {
|
|
20
|
+
const strategy = new HttpHealthCheckStrategy();
|
|
21
|
+
const collector = new RequestCollector();
|
|
22
|
+
const configs = [
|
|
23
|
+
{ name: "http strategy config", config: strategy.config },
|
|
24
|
+
{ name: "http strategy result", config: strategy.result },
|
|
25
|
+
{ name: "http strategy aggregatedResult", config: strategy.aggregatedResult },
|
|
26
|
+
{ name: "request collector config", config: collector.config },
|
|
27
|
+
{ name: "request collector result", config: collector.result },
|
|
28
|
+
{
|
|
29
|
+
name: "request collector aggregatedResult",
|
|
30
|
+
config: collector.aggregatedResult,
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
it("every registered Versioned schema has a complete v1->version chain", () => {
|
|
35
|
+
for (const { name, config } of configs) {
|
|
36
|
+
const problem = config.validateMigrationChainFromV1();
|
|
37
|
+
expect(
|
|
38
|
+
problem,
|
|
39
|
+
`${name} (version ${config.version}) has a broken migration chain: ${problem}`,
|
|
40
|
+
).toBeUndefined();
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -93,6 +93,26 @@ describe("RequestCollector", () => {
|
|
|
93
93
|
);
|
|
94
94
|
});
|
|
95
95
|
|
|
96
|
+
it("returns a clear config error when the rendered URL is invalid", async () => {
|
|
97
|
+
// The executor renders `{{ environment.* }}` into `config.url` before
|
|
98
|
+
// execute. An empty environment can produce a non-URL (e.g. "/healthz"
|
|
99
|
+
// when baseUrl is missing). The collector must surface this as a clear
|
|
100
|
+
// config error, not attempt the request.
|
|
101
|
+
const collector = new RequestCollector();
|
|
102
|
+
const client = createMockClient();
|
|
103
|
+
|
|
104
|
+
const result = await collector.execute({
|
|
105
|
+
config: { url: "/healthz", method: "GET", timeout: 5000 },
|
|
106
|
+
client,
|
|
107
|
+
pluginId: "test",
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
expect(result.result.success).toBe(false);
|
|
111
|
+
expect(result.result.statusCode).toBe(0);
|
|
112
|
+
expect(result.error).toContain("Rendered URL is invalid");
|
|
113
|
+
expect(client.exec).not.toHaveBeenCalled();
|
|
114
|
+
});
|
|
115
|
+
|
|
96
116
|
it("should pass body to client", async () => {
|
|
97
117
|
const collector = new RequestCollector();
|
|
98
118
|
const client = createMockClient();
|
package/src/request-collector.ts
CHANGED
|
@@ -27,20 +27,34 @@ import type { HttpTransportClient } from "./transport-client";
|
|
|
27
27
|
// ============================================================================
|
|
28
28
|
|
|
29
29
|
const requestConfigSchema = z.object({
|
|
30
|
-
|
|
30
|
+
// Templatable: supports `{{ environment.baseUrl }}` etc. so one config
|
|
31
|
+
// covers N environments. The `.url()` validation moves to POST-RENDER (see
|
|
32
|
+
// `execute`) because the stored value `{{ environment.baseUrl }}/healthz` is
|
|
33
|
+
// not itself a valid URL.
|
|
34
|
+
url: configString({ "x-templatable": true }).describe(
|
|
35
|
+
"Full URL to request. Supports templating, e.g. {{ environment.baseUrl }}/healthz",
|
|
36
|
+
),
|
|
31
37
|
method: z
|
|
32
38
|
.enum(["GET", "POST", "PUT", "DELETE", "HEAD"])
|
|
33
39
|
.default("GET")
|
|
34
40
|
.describe("HTTP method"),
|
|
35
41
|
headers: z
|
|
36
|
-
.array(
|
|
42
|
+
.array(
|
|
43
|
+
z.object({
|
|
44
|
+
name: z.string(),
|
|
45
|
+
// Templatable: header values are a natural per-env case (auth hosts,
|
|
46
|
+
// tenant ids). `name` stays literal.
|
|
47
|
+
value: configString({ "x-templatable": true }),
|
|
48
|
+
}),
|
|
49
|
+
)
|
|
37
50
|
.optional()
|
|
38
51
|
.describe("Request headers"),
|
|
39
52
|
body: configString({
|
|
40
53
|
"x-editor-types": ["none", "raw", "json", "yaml", "xml", "formdata"],
|
|
54
|
+
"x-templatable": true,
|
|
41
55
|
})
|
|
42
56
|
.optional()
|
|
43
|
-
.describe("Request body"),
|
|
57
|
+
.describe("Request body. Supports templating, e.g. {{ environment.* }}"),
|
|
44
58
|
timeout: z
|
|
45
59
|
.number()
|
|
46
60
|
.min(100)
|
|
@@ -48,6 +62,15 @@ const requestConfigSchema = z.object({
|
|
|
48
62
|
.describe("Timeout in milliseconds"),
|
|
49
63
|
});
|
|
50
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Post-render validator for the HTTP `url`. The stored value is a plain
|
|
67
|
+
* templatable string (no `.url()`); the executor renders `{{ environment.* }}`
|
|
68
|
+
* per environment, then this re-validates the CONCRETE rendered URL. A bad
|
|
69
|
+
* render (e.g. an empty environment yielding `/healthz`) surfaces as a clear
|
|
70
|
+
* config error instead of a silent pass.
|
|
71
|
+
*/
|
|
72
|
+
const renderedUrlSchema = z.string().url();
|
|
73
|
+
|
|
51
74
|
export type RequestConfig = z.infer<typeof requestConfigSchema>;
|
|
52
75
|
|
|
53
76
|
// ============================================================================
|
|
@@ -156,6 +179,26 @@ export class RequestCollector implements CollectorStrategy<
|
|
|
156
179
|
}): Promise<CollectorResult<RequestResult>> {
|
|
157
180
|
const startTime = Date.now();
|
|
158
181
|
|
|
182
|
+
// Post-render validation: the stored `url` is a plain templatable string,
|
|
183
|
+
// so `.url()` cannot run at store time. The executor has already rendered
|
|
184
|
+
// `{{ environment.* }}` into `config.url` by the time we get here; validate
|
|
185
|
+
// the CONCRETE rendered value now. A bad render (e.g. an empty environment)
|
|
186
|
+
// yields a clear config error rather than a confusing fetch failure.
|
|
187
|
+
const urlValidation = renderedUrlSchema.safeParse(config.url);
|
|
188
|
+
if (!urlValidation.success) {
|
|
189
|
+
return {
|
|
190
|
+
result: {
|
|
191
|
+
statusCode: 0,
|
|
192
|
+
statusText: "Invalid URL",
|
|
193
|
+
responseTimeMs: Date.now() - startTime,
|
|
194
|
+
body: "",
|
|
195
|
+
bodyLength: 0,
|
|
196
|
+
success: false,
|
|
197
|
+
},
|
|
198
|
+
error: `Rendered URL is invalid: ${JSON.stringify(config.url)}`,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
159
202
|
// Convert headers array to record
|
|
160
203
|
const headers: Record<string, string> = {};
|
|
161
204
|
for (const h of config.headers ?? []) {
|
package/src/strategy.test.ts
CHANGED
|
@@ -8,6 +8,37 @@ describe("HttpHealthCheckStrategy", () => {
|
|
|
8
8
|
spyOn(globalThis, "fetch").mockRestore();
|
|
9
9
|
});
|
|
10
10
|
|
|
11
|
+
describe("config migration (assume-v1-on-read)", () => {
|
|
12
|
+
it("migrates a genuine v1 blob (url/method/...) down to {timeout}", async () => {
|
|
13
|
+
const migrated = await strategy.config.parseAssumingV1({
|
|
14
|
+
url: "https://example.com",
|
|
15
|
+
method: "GET",
|
|
16
|
+
headers: [{ name: "Accept", value: "application/json" }],
|
|
17
|
+
body: "payload",
|
|
18
|
+
});
|
|
19
|
+
// v1->v2 fabricates the default timeout, v2->v3 strips the moved fields.
|
|
20
|
+
expect(migrated).toEqual({ timeout: 30_000 });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("carries a v1 timeout through both migration steps", async () => {
|
|
24
|
+
const migrated = await strategy.config.parseAssumingV1({
|
|
25
|
+
url: "https://example.com",
|
|
26
|
+
method: "POST",
|
|
27
|
+
timeout: 12_345,
|
|
28
|
+
});
|
|
29
|
+
expect(migrated).toEqual({ timeout: 12_345 });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("is idempotent: an already-current {timeout} blob is unchanged", async () => {
|
|
33
|
+
const migrated = await strategy.config.parseAssumingV1({ timeout: 5000 });
|
|
34
|
+
expect(migrated).toEqual({ timeout: 5000 });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("has a complete v1->version migration chain", () => {
|
|
38
|
+
expect(strategy.config.validateMigrationChainFromV1()).toBeUndefined();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
11
42
|
describe("createClient", () => {
|
|
12
43
|
it("should return a connected client", async () => {
|
|
13
44
|
const connectedClient = await strategy.createClient({ timeout: 5000 });
|
package/src/strategy.ts
CHANGED
|
@@ -33,16 +33,19 @@ export const httpHealthCheckConfigSchema = baseStrategyConfigSchema.extend({});
|
|
|
33
33
|
|
|
34
34
|
export type HttpHealthCheckConfig = z.infer<typeof httpHealthCheckConfigSchema>;
|
|
35
35
|
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
36
|
+
// The migrate input is `unknown` per the versioning chain, so narrowing is
|
|
37
|
+
// done with `typeof`/`in` guards (no casts).
|
|
38
|
+
|
|
39
|
+
/** Type guard: the migrate input is a plain object whose keys can be probed. */
|
|
40
|
+
function isRecord(data: unknown): data is Record<string, unknown> {
|
|
41
|
+
return typeof data === "object" && data !== null;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
/** Read a numeric `timeout` field from a legacy/current config blob. */
|
|
45
|
+
function readTimeout(data: unknown): number | undefined {
|
|
46
|
+
if (!isRecord(data)) return undefined;
|
|
47
|
+
const value = data.timeout;
|
|
48
|
+
return typeof value === "number" ? value : undefined;
|
|
46
49
|
}
|
|
47
50
|
|
|
48
51
|
/** Per-run result metadata */
|
|
@@ -91,19 +94,34 @@ export class HttpHealthCheckStrategy implements HealthCheckStrategy<
|
|
|
91
94
|
fromVersion: 1,
|
|
92
95
|
toVersion: 2,
|
|
93
96
|
description: "Add timeout field",
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
// IDEMPOTENT: only a genuine v1 blob carries url/method. If neither is
|
|
98
|
+
// present the data is already v2+ — pass it through untouched.
|
|
99
|
+
migrate: (data: unknown): unknown => {
|
|
100
|
+
if (isRecord(data) && ("url" in data || "method" in data)) {
|
|
101
|
+
return { ...data, timeout: readTimeout(data) ?? 30_000 };
|
|
102
|
+
}
|
|
103
|
+
return data;
|
|
104
|
+
},
|
|
98
105
|
},
|
|
99
106
|
{
|
|
100
107
|
fromVersion: 2,
|
|
101
108
|
toVersion: 3,
|
|
102
109
|
description:
|
|
103
110
|
"Remove url/method/headers/body (moved to RequestCollector)",
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
111
|
+
// IDEMPOTENT: only a genuine v2 blob still carries the moved fields. An
|
|
112
|
+
// already-v3 blob (just `{ timeout }`) passes through untouched.
|
|
113
|
+
migrate: (data: unknown): unknown => {
|
|
114
|
+
if (
|
|
115
|
+
isRecord(data) &&
|
|
116
|
+
("url" in data ||
|
|
117
|
+
"method" in data ||
|
|
118
|
+
"headers" in data ||
|
|
119
|
+
"body" in data)
|
|
120
|
+
) {
|
|
121
|
+
return { timeout: readTimeout(data) };
|
|
122
|
+
}
|
|
123
|
+
return data;
|
|
124
|
+
},
|
|
107
125
|
},
|
|
108
126
|
],
|
|
109
127
|
});
|
|
@@ -111,6 +129,25 @@ export class HttpHealthCheckStrategy implements HealthCheckStrategy<
|
|
|
111
129
|
result: Versioned<HttpResultMetadata> = new Versioned({
|
|
112
130
|
version: 3,
|
|
113
131
|
schema: httpResultMetadataSchema,
|
|
132
|
+
// The result version tracks the strategy version (bumped in lockstep
|
|
133
|
+
// with the createClient refactor); the result SHAPE never changed, so
|
|
134
|
+
// these are pass-through migrations. They are required for a COMPLETE
|
|
135
|
+
// v1->v3 chain (enforced at construction) so a genuinely-v1 stored
|
|
136
|
+
// result still reads back via assume-v1-on-read.
|
|
137
|
+
migrations: [
|
|
138
|
+
{
|
|
139
|
+
fromVersion: 1,
|
|
140
|
+
toVersion: 2,
|
|
141
|
+
description: "Migrate to createClient pattern (no result changes)",
|
|
142
|
+
migrate: (data: unknown) => data,
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
fromVersion: 2,
|
|
146
|
+
toVersion: 3,
|
|
147
|
+
description: "Move request params to RequestCollector (no result changes)",
|
|
148
|
+
migrate: (data: unknown) => data,
|
|
149
|
+
},
|
|
150
|
+
],
|
|
114
151
|
});
|
|
115
152
|
|
|
116
153
|
aggregatedResult = new VersionedAggregated({
|