@checkstack/backend 0.0.2 → 0.1.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 +45 -0
- package/package.json +1 -1
- package/src/health-check-plugin-integration.test.ts +12 -5
- package/src/plugin-manager/api-router.ts +5 -4
- package/src/plugin-manager/core-services.ts +23 -7
- package/src/plugin-manager.ts +16 -1
- package/src/services/collector-registry.ts +115 -0
- package/src/services/health-check-registry.test.ts +62 -21
- package/src/services/health-check-registry.ts +81 -10
- package/src/test-preload.ts +15 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,50 @@
|
|
|
1
1
|
# @checkstack/backend
|
|
2
2
|
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- f5b1f49: Added collector registry lifecycle cleanup during plugin unloading.
|
|
8
|
+
|
|
9
|
+
- Added `unregisterByOwner(pluginId)` to remove collectors owned by unloading plugins
|
|
10
|
+
- Added `unregisterByMissingStrategies(loadedPluginIds)` for dependency-based pruning
|
|
11
|
+
- Integrated registry cleanup into `PluginManager.deregisterPlugin()`
|
|
12
|
+
- Updated `registerCoreServices` to return global registries for lifecycle management
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- f5b1f49: Added JSONPath assertions for response body validation and fully qualified strategy IDs.
|
|
17
|
+
|
|
18
|
+
**JSONPath Assertions:**
|
|
19
|
+
|
|
20
|
+
- Added `healthResultJSONPath()` factory in healthcheck-common for fields supporting JSONPath queries
|
|
21
|
+
- Extended AssertionBuilder with jsonpath field type showing path input (e.g., `$.data.status`)
|
|
22
|
+
- Added `jsonPath` field to `CollectorAssertionSchema` for persistence
|
|
23
|
+
- HTTP Request collector body field now supports JSONPath assertions
|
|
24
|
+
|
|
25
|
+
**Fully Qualified Strategy IDs:**
|
|
26
|
+
|
|
27
|
+
- HealthCheckRegistry now uses scoped factories like CollectorRegistry
|
|
28
|
+
- Strategies are stored with `pluginId.strategyId` format
|
|
29
|
+
- Added `getStrategiesWithMeta()` method to HealthCheckRegistry interface
|
|
30
|
+
- Router returns qualified IDs so frontend can correctly fetch collectors
|
|
31
|
+
|
|
32
|
+
**UI Improvements:**
|
|
33
|
+
|
|
34
|
+
- Save button disabled when collector configs have invalid required fields
|
|
35
|
+
- Fixed nested button warning in CollectorList accordion
|
|
36
|
+
|
|
37
|
+
- Updated dependencies [f5b1f49]
|
|
38
|
+
- Updated dependencies [f5b1f49]
|
|
39
|
+
- Updated dependencies [f5b1f49]
|
|
40
|
+
- @checkstack/backend-api@0.1.0
|
|
41
|
+
- @checkstack/common@0.0.3
|
|
42
|
+
- @checkstack/queue-api@0.0.3
|
|
43
|
+
- @checkstack/signal-backend@0.0.3
|
|
44
|
+
- @checkstack/api-docs-common@0.0.3
|
|
45
|
+
- @checkstack/auth-common@0.0.3
|
|
46
|
+
- @checkstack/signal-common@0.0.3
|
|
47
|
+
|
|
3
48
|
## 0.0.2
|
|
4
49
|
|
|
5
50
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -30,10 +30,13 @@ describe("HealthCheck Plugin Integration", () => {
|
|
|
30
30
|
newResponse: mock(),
|
|
31
31
|
} as never;
|
|
32
32
|
|
|
33
|
-
// Define a mock
|
|
34
|
-
const
|
|
33
|
+
// Define a mock createClient function for the strategy
|
|
34
|
+
const mockCreateClient = mock(async () => ({
|
|
35
|
+
client: { exec: async () => ({}) },
|
|
36
|
+
close: () => {},
|
|
37
|
+
}));
|
|
35
38
|
|
|
36
|
-
// 1. Define a mock strategy
|
|
39
|
+
// 1. Define a mock strategy with createClient pattern
|
|
37
40
|
const mockStrategy: HealthCheckStrategy = {
|
|
38
41
|
id: "test-strategy",
|
|
39
42
|
displayName: "Test Strategy",
|
|
@@ -42,11 +45,15 @@ describe("HealthCheck Plugin Integration", () => {
|
|
|
42
45
|
version: 1,
|
|
43
46
|
schema: z.object({}),
|
|
44
47
|
}),
|
|
48
|
+
result: new Versioned({
|
|
49
|
+
version: 1,
|
|
50
|
+
schema: z.record(z.string(), z.unknown()),
|
|
51
|
+
}),
|
|
45
52
|
aggregatedResult: new Versioned({
|
|
46
53
|
version: 1,
|
|
47
54
|
schema: z.record(z.string(), z.unknown()),
|
|
48
55
|
}),
|
|
49
|
-
|
|
56
|
+
createClient: mockCreateClient,
|
|
50
57
|
aggregateResult: mock(() => ({})),
|
|
51
58
|
};
|
|
52
59
|
|
|
@@ -88,6 +95,6 @@ describe("HealthCheck Plugin Integration", () => {
|
|
|
88
95
|
expect(retrieved).toBe(mockStrategy);
|
|
89
96
|
expect(retrieved?.displayName).toBe("Test Strategy");
|
|
90
97
|
expect(retrieved?.id).toBe("test-strategy");
|
|
91
|
-
expect(retrieved?.
|
|
98
|
+
expect(retrieved?.createClient).toBe(mockCreateClient);
|
|
92
99
|
});
|
|
93
100
|
});
|
|
@@ -8,13 +8,11 @@ import {
|
|
|
8
8
|
Logger,
|
|
9
9
|
Fetch,
|
|
10
10
|
HealthCheckRegistry,
|
|
11
|
+
CollectorRegistry,
|
|
11
12
|
type EmitHookFn,
|
|
12
13
|
type Hook,
|
|
13
14
|
} from "@checkstack/backend-api";
|
|
14
|
-
import type {
|
|
15
|
-
QueuePluginRegistry,
|
|
16
|
-
QueueManager,
|
|
17
|
-
} from "@checkstack/queue-api";
|
|
15
|
+
import type { QueuePluginRegistry, QueueManager } from "@checkstack/queue-api";
|
|
18
16
|
import type { ServiceRegistry } from "../services/service-registry";
|
|
19
17
|
import type { EventBus } from "@checkstack/backend-api";
|
|
20
18
|
import type { PluginMetadata } from "@checkstack/common";
|
|
@@ -70,6 +68,7 @@ export function createApiRouteHandler({
|
|
|
70
68
|
const healthCheckRegistry = await getService(
|
|
71
69
|
coreServices.healthCheckRegistry
|
|
72
70
|
);
|
|
71
|
+
const collectorRegistry = await getService(coreServices.collectorRegistry);
|
|
73
72
|
const queuePluginRegistry = await getService(
|
|
74
73
|
coreServices.queuePluginRegistry
|
|
75
74
|
);
|
|
@@ -82,6 +81,7 @@ export function createApiRouteHandler({
|
|
|
82
81
|
!db ||
|
|
83
82
|
!fetch ||
|
|
84
83
|
!healthCheckRegistry ||
|
|
84
|
+
!collectorRegistry ||
|
|
85
85
|
!queuePluginRegistry ||
|
|
86
86
|
!queueManager ||
|
|
87
87
|
!eventBus
|
|
@@ -111,6 +111,7 @@ export function createApiRouteHandler({
|
|
|
111
111
|
db: db as NodePgDatabase<Record<string, unknown>>,
|
|
112
112
|
fetch: fetch as Fetch,
|
|
113
113
|
healthCheckRegistry: healthCheckRegistry as HealthCheckRegistry,
|
|
114
|
+
collectorRegistry: collectorRegistry as CollectorRegistry,
|
|
114
115
|
queuePluginRegistry: queuePluginRegistry as QueuePluginRegistry,
|
|
115
116
|
queueManager: queueManager as QueueManager,
|
|
116
117
|
user,
|
|
@@ -16,7 +16,14 @@ import type { ServiceRegistry } from "../services/service-registry";
|
|
|
16
16
|
import { rootLogger } from "../logger";
|
|
17
17
|
import { db } from "../db";
|
|
18
18
|
import { jwtService } from "../services/jwt";
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
CoreHealthCheckRegistry,
|
|
21
|
+
createScopedHealthCheckRegistry,
|
|
22
|
+
} from "../services/health-check-registry";
|
|
23
|
+
import {
|
|
24
|
+
CoreCollectorRegistry,
|
|
25
|
+
createScopedCollectorRegistry,
|
|
26
|
+
} from "../services/collector-registry";
|
|
20
27
|
import { EventBus } from "../services/event-bus.js";
|
|
21
28
|
import { getPluginSchemaName } from "@checkstack/drizzle-helper";
|
|
22
29
|
|
|
@@ -34,6 +41,7 @@ async function schemaExists(pool: Pool, schemaName: string): Promise<boolean> {
|
|
|
34
41
|
/**
|
|
35
42
|
* Registers all core services with the service registry.
|
|
36
43
|
* Extracted from PluginManager for better organization.
|
|
44
|
+
* Returns the global registries for lifecycle cleanup.
|
|
37
45
|
*/
|
|
38
46
|
export function registerCoreServices({
|
|
39
47
|
registry,
|
|
@@ -47,7 +55,7 @@ export function registerCoreServices({
|
|
|
47
55
|
pluginRpcRouters: Map<string, unknown>;
|
|
48
56
|
pluginHttpHandlers: Map<string, (req: Request) => Promise<Response>>;
|
|
49
57
|
pluginContractRegistry: Map<string, unknown>;
|
|
50
|
-
}) {
|
|
58
|
+
}): { collectorRegistry: CoreCollectorRegistry } {
|
|
51
59
|
// 1. Database Factory (Scoped)
|
|
52
60
|
registry.registerFactory(coreServices.database, async (metadata) => {
|
|
53
61
|
const { pluginId, previousPluginIds } = metadata;
|
|
@@ -258,11 +266,16 @@ export function registerCoreServices({
|
|
|
258
266
|
return rpcClient;
|
|
259
267
|
});
|
|
260
268
|
|
|
261
|
-
// 6. Health Check Registry (
|
|
262
|
-
const
|
|
263
|
-
registry.registerFactory(
|
|
264
|
-
|
|
265
|
-
|
|
269
|
+
// 6. Health Check Registry (Scoped Factory - auto-prefixes strategy IDs with pluginId)
|
|
270
|
+
const globalHealthCheckRegistry = new CoreHealthCheckRegistry();
|
|
271
|
+
registry.registerFactory(coreServices.healthCheckRegistry, (metadata) =>
|
|
272
|
+
createScopedHealthCheckRegistry(globalHealthCheckRegistry, metadata)
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
// 6b. Collector Registry (Scoped Factory - injects ownerPlugin automatically)
|
|
276
|
+
const globalCollectorRegistry = new CoreCollectorRegistry();
|
|
277
|
+
registry.registerFactory(coreServices.collectorRegistry, (metadata) =>
|
|
278
|
+
createScopedCollectorRegistry(globalCollectorRegistry, metadata)
|
|
266
279
|
);
|
|
267
280
|
|
|
268
281
|
// 7. RPC Service (Scoped Factory - uses pluginId for path derivation)
|
|
@@ -309,4 +322,7 @@ export function registerCoreServices({
|
|
|
309
322
|
}
|
|
310
323
|
return eventBusInstance;
|
|
311
324
|
});
|
|
325
|
+
|
|
326
|
+
// Return global registries for lifecycle cleanup
|
|
327
|
+
return { collectorRegistry: globalCollectorRegistry };
|
|
312
328
|
}
|
package/src/plugin-manager.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Hono } from "hono";
|
|
2
2
|
import { adminPool, db } from "./db";
|
|
3
3
|
import { ServiceRegistry } from "./services/service-registry";
|
|
4
|
+
import type { CoreCollectorRegistry } from "./services/collector-registry";
|
|
4
5
|
import {
|
|
5
6
|
BackendPlugin,
|
|
6
7
|
ServiceRef,
|
|
@@ -46,14 +47,18 @@ export class PluginManager {
|
|
|
46
47
|
// Hook subscriptions per plugin (for bulk unsubscribe)
|
|
47
48
|
private hookSubscriptions = new Map<string, HookUnsubscribe[]>();
|
|
48
49
|
|
|
50
|
+
// Global collector registry reference for cleanup
|
|
51
|
+
private collectorRegistry: CoreCollectorRegistry;
|
|
52
|
+
|
|
49
53
|
constructor() {
|
|
50
|
-
registerCoreServices({
|
|
54
|
+
const registries = registerCoreServices({
|
|
51
55
|
registry: this.registry,
|
|
52
56
|
adminPool,
|
|
53
57
|
pluginRpcRouters: this.pluginRpcRouters,
|
|
54
58
|
pluginHttpHandlers: this.pluginHttpHandlers,
|
|
55
59
|
pluginContractRegistry: this.pluginContractRegistry,
|
|
56
60
|
});
|
|
61
|
+
this.collectorRegistry = registries.collectorRegistry;
|
|
57
62
|
}
|
|
58
63
|
|
|
59
64
|
registerExtensionPoint<T>(ref: ExtensionPoint<T>, impl: T) {
|
|
@@ -163,6 +168,16 @@ export class PluginManager {
|
|
|
163
168
|
this.pluginContractRegistry.delete(pluginId);
|
|
164
169
|
rootLogger.debug(` -> Removed routers and contracts for ${pluginId}`);
|
|
165
170
|
|
|
171
|
+
// 4b. Cleanup collectors
|
|
172
|
+
// - Remove collectors owned by this plugin
|
|
173
|
+
this.collectorRegistry.unregisterByOwner(pluginId);
|
|
174
|
+
// - Remove collectors that have no loaded strategy plugins
|
|
175
|
+
// (exclude the current plugin being deregistered)
|
|
176
|
+
const loadedPluginIds = new Set(
|
|
177
|
+
[...this.pluginMetadataRegistry.keys()].filter((id) => id !== pluginId)
|
|
178
|
+
);
|
|
179
|
+
this.collectorRegistry.unregisterByMissingStrategies(loadedPluginIds);
|
|
180
|
+
|
|
166
181
|
// 5. Remove permissions from registry
|
|
167
182
|
const beforeCount = this.registeredPermissions.length;
|
|
168
183
|
this.registeredPermissions = this.registeredPermissions.filter(
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { PluginMetadata } from "@checkstack/common";
|
|
2
|
+
import type {
|
|
3
|
+
CollectorStrategy,
|
|
4
|
+
TransportClient,
|
|
5
|
+
RegisteredCollector,
|
|
6
|
+
} from "@checkstack/backend-api";
|
|
7
|
+
import { rootLogger } from "../logger";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Core implementation of the CollectorRegistry storage.
|
|
11
|
+
* This is the global singleton that stores all collectors.
|
|
12
|
+
*/
|
|
13
|
+
export class CoreCollectorRegistry {
|
|
14
|
+
private collectors = new Map<string, RegisteredCollector>();
|
|
15
|
+
|
|
16
|
+
registerWithOwner(
|
|
17
|
+
collector: CollectorStrategy<TransportClient<unknown, unknown>>,
|
|
18
|
+
ownerPlugin: PluginMetadata
|
|
19
|
+
): void {
|
|
20
|
+
if (this.collectors.has(collector.id)) {
|
|
21
|
+
rootLogger.warn(
|
|
22
|
+
`CollectorStrategy '${collector.id}' is already registered. Overwriting.`
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
this.collectors.set(collector.id, { collector, ownerPlugin });
|
|
26
|
+
rootLogger.debug(
|
|
27
|
+
`✅ Registered CollectorStrategy: ${ownerPlugin.pluginId}.${collector.id}`
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Unregister all collectors owned by a specific plugin.
|
|
33
|
+
* Called when the collector-providing plugin is unloaded.
|
|
34
|
+
*/
|
|
35
|
+
unregisterByOwner(ownerPluginId: string): void {
|
|
36
|
+
const toRemove: string[] = [];
|
|
37
|
+
for (const [id, entry] of this.collectors) {
|
|
38
|
+
if (entry.ownerPlugin.pluginId === ownerPluginId) {
|
|
39
|
+
toRemove.push(id);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
for (const id of toRemove) {
|
|
43
|
+
this.collectors.delete(id);
|
|
44
|
+
rootLogger.debug(
|
|
45
|
+
`🗑️ Unregistered CollectorStrategy: ${id} (owner plugin unloaded)`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Remove collectors that no longer have any supported strategies loaded.
|
|
52
|
+
* Called when a healthcheck strategy plugin is unloaded.
|
|
53
|
+
* @param loadedStrategyPluginIds - Set of plugin IDs that are still loaded
|
|
54
|
+
*/
|
|
55
|
+
unregisterByMissingStrategies(loadedStrategyPluginIds: Set<string>): void {
|
|
56
|
+
const toRemove: string[] = [];
|
|
57
|
+
for (const [id, entry] of this.collectors) {
|
|
58
|
+
// Check if ANY of the collector's supported plugins are still loaded
|
|
59
|
+
const hasLoadedStrategy = entry.collector.supportedPlugins.some((p) =>
|
|
60
|
+
loadedStrategyPluginIds.has(p.pluginId)
|
|
61
|
+
);
|
|
62
|
+
// If none of the supported plugins are loaded, mark for removal
|
|
63
|
+
if (!hasLoadedStrategy) {
|
|
64
|
+
toRemove.push(id);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
for (const id of toRemove) {
|
|
68
|
+
this.collectors.delete(id);
|
|
69
|
+
rootLogger.debug(
|
|
70
|
+
`🗑️ Unregistered CollectorStrategy: ${id} (no supported strategies loaded)`
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
getCollector(id: string): RegisteredCollector | undefined {
|
|
76
|
+
return this.collectors.get(id);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
getCollectorsForPlugin(
|
|
80
|
+
pluginMetadata: PluginMetadata
|
|
81
|
+
): RegisteredCollector[] {
|
|
82
|
+
return [...this.collectors.values()].filter((entry) =>
|
|
83
|
+
entry.collector.supportedPlugins.some(
|
|
84
|
+
(p) => p.pluginId === pluginMetadata.pluginId
|
|
85
|
+
)
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
getCollectors(): RegisteredCollector[] {
|
|
90
|
+
return [...this.collectors.values()];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Creates a scoped CollectorRegistry that auto-injects owning plugin metadata.
|
|
96
|
+
*/
|
|
97
|
+
export function createScopedCollectorRegistry(
|
|
98
|
+
globalRegistry: CoreCollectorRegistry,
|
|
99
|
+
ownerPlugin: PluginMetadata
|
|
100
|
+
) {
|
|
101
|
+
return {
|
|
102
|
+
register(collector: CollectorStrategy<TransportClient<unknown, unknown>>) {
|
|
103
|
+
globalRegistry.registerWithOwner(collector, ownerPlugin);
|
|
104
|
+
},
|
|
105
|
+
getCollector(id: string) {
|
|
106
|
+
return globalRegistry.getCollector(id);
|
|
107
|
+
},
|
|
108
|
+
getCollectorsForPlugin(pluginMetadata: PluginMetadata) {
|
|
109
|
+
return globalRegistry.getCollectorsForPlugin(pluginMetadata);
|
|
110
|
+
},
|
|
111
|
+
getCollectors() {
|
|
112
|
+
return globalRegistry.getCollectors();
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, mock } from "bun:test";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
CoreHealthCheckRegistry,
|
|
4
|
+
createScopedHealthCheckRegistry,
|
|
5
|
+
} from "./health-check-registry";
|
|
3
6
|
import { HealthCheckStrategy, Versioned } from "@checkstack/backend-api";
|
|
4
7
|
import { createMockLogger } from "@checkstack/test-utils-backend";
|
|
5
8
|
import { z } from "zod";
|
|
9
|
+
import type { PluginMetadata } from "@checkstack/common";
|
|
6
10
|
|
|
7
11
|
// Mock logger
|
|
8
12
|
const mockLogger = createMockLogger();
|
|
@@ -13,6 +17,8 @@ mock.module("../logger", () => ({
|
|
|
13
17
|
describe("CoreHealthCheckRegistry", () => {
|
|
14
18
|
let registry: CoreHealthCheckRegistry;
|
|
15
19
|
|
|
20
|
+
const mockOwner: PluginMetadata = { pluginId: "test-plugin" };
|
|
21
|
+
|
|
16
22
|
const mockStrategy1: HealthCheckStrategy = {
|
|
17
23
|
id: "test-strategy-1",
|
|
18
24
|
displayName: "Test Strategy 1",
|
|
@@ -21,11 +27,17 @@ describe("CoreHealthCheckRegistry", () => {
|
|
|
21
27
|
version: 1,
|
|
22
28
|
schema: z.object({}),
|
|
23
29
|
}),
|
|
30
|
+
result: new Versioned({
|
|
31
|
+
version: 1,
|
|
32
|
+
schema: z.record(z.string(), z.unknown()),
|
|
33
|
+
}),
|
|
24
34
|
aggregatedResult: new Versioned({
|
|
25
35
|
version: 1,
|
|
26
36
|
schema: z.record(z.string(), z.unknown()),
|
|
27
37
|
}),
|
|
28
|
-
|
|
38
|
+
createClient: mock(() =>
|
|
39
|
+
Promise.resolve({ client: { exec: async () => ({}) }, close: () => {} })
|
|
40
|
+
),
|
|
29
41
|
aggregateResult: mock(() => ({})),
|
|
30
42
|
};
|
|
31
43
|
|
|
@@ -37,12 +49,16 @@ describe("CoreHealthCheckRegistry", () => {
|
|
|
37
49
|
version: 1,
|
|
38
50
|
schema: z.object({}),
|
|
39
51
|
}),
|
|
52
|
+
result: new Versioned({
|
|
53
|
+
version: 1,
|
|
54
|
+
schema: z.record(z.string(), z.unknown()),
|
|
55
|
+
}),
|
|
40
56
|
aggregatedResult: new Versioned({
|
|
41
57
|
version: 1,
|
|
42
58
|
schema: z.record(z.string(), z.unknown()),
|
|
43
59
|
}),
|
|
44
|
-
|
|
45
|
-
Promise.resolve({
|
|
60
|
+
createClient: mock(() =>
|
|
61
|
+
Promise.resolve({ client: { exec: async () => ({}) }, close: () => {} })
|
|
46
62
|
),
|
|
47
63
|
aggregateResult: mock(() => ({})),
|
|
48
64
|
};
|
|
@@ -51,31 +67,33 @@ describe("CoreHealthCheckRegistry", () => {
|
|
|
51
67
|
registry = new CoreHealthCheckRegistry();
|
|
52
68
|
});
|
|
53
69
|
|
|
54
|
-
describe("
|
|
55
|
-
it("should register a new health check strategy", () => {
|
|
56
|
-
registry.
|
|
57
|
-
|
|
70
|
+
describe("registerWithOwner", () => {
|
|
71
|
+
it("should register a new health check strategy with qualified ID", () => {
|
|
72
|
+
registry.registerWithOwner(mockStrategy1, mockOwner);
|
|
73
|
+
// Should be stored with qualified ID: ownerPluginId.strategyId
|
|
74
|
+
const qualifiedId = `${mockOwner.pluginId}.${mockStrategy1.id}`;
|
|
75
|
+
expect(registry.getStrategy(qualifiedId)).toBe(mockStrategy1);
|
|
58
76
|
});
|
|
59
77
|
|
|
60
|
-
it("should overwrite an existing strategy with the same ID", () => {
|
|
61
|
-
const overwritingStrategy: HealthCheckStrategy
|
|
78
|
+
it("should overwrite an existing strategy with the same qualified ID", () => {
|
|
79
|
+
const overwritingStrategy: HealthCheckStrategy = {
|
|
62
80
|
...mockStrategy1,
|
|
63
81
|
displayName: "New Name",
|
|
64
82
|
};
|
|
65
|
-
registry.
|
|
66
|
-
registry.
|
|
83
|
+
registry.registerWithOwner(mockStrategy1, mockOwner);
|
|
84
|
+
registry.registerWithOwner(overwritingStrategy, mockOwner);
|
|
67
85
|
|
|
68
|
-
|
|
69
|
-
expect(registry.getStrategy(
|
|
70
|
-
|
|
71
|
-
);
|
|
86
|
+
const qualifiedId = `${mockOwner.pluginId}.${mockStrategy1.id}`;
|
|
87
|
+
expect(registry.getStrategy(qualifiedId)).toBe(overwritingStrategy);
|
|
88
|
+
expect(registry.getStrategy(qualifiedId)?.displayName).toBe("New Name");
|
|
72
89
|
});
|
|
73
90
|
});
|
|
74
91
|
|
|
75
|
-
describe("
|
|
92
|
+
describe("getStrategy", () => {
|
|
76
93
|
it("should return the strategy if it exists", () => {
|
|
77
|
-
registry.
|
|
78
|
-
|
|
94
|
+
registry.registerWithOwner(mockStrategy1, mockOwner);
|
|
95
|
+
const qualifiedId = `${mockOwner.pluginId}.${mockStrategy1.id}`;
|
|
96
|
+
expect(registry.getStrategy(qualifiedId)).toBe(mockStrategy1);
|
|
79
97
|
});
|
|
80
98
|
|
|
81
99
|
it("should return undefined if the strategy does not exist", () => {
|
|
@@ -85,8 +103,8 @@ describe("CoreHealthCheckRegistry", () => {
|
|
|
85
103
|
|
|
86
104
|
describe("getStrategies", () => {
|
|
87
105
|
it("should return all registered strategies", () => {
|
|
88
|
-
registry.
|
|
89
|
-
registry.
|
|
106
|
+
registry.registerWithOwner(mockStrategy1, mockOwner);
|
|
107
|
+
registry.registerWithOwner(mockStrategy2, mockOwner);
|
|
90
108
|
|
|
91
109
|
const strategies = registry.getStrategies();
|
|
92
110
|
expect(strategies).toHaveLength(2);
|
|
@@ -98,4 +116,27 @@ describe("CoreHealthCheckRegistry", () => {
|
|
|
98
116
|
expect(registry.getStrategies()).toEqual([]);
|
|
99
117
|
});
|
|
100
118
|
});
|
|
119
|
+
|
|
120
|
+
describe("createScopedHealthCheckRegistry", () => {
|
|
121
|
+
it("should auto-qualify strategy IDs on register", () => {
|
|
122
|
+
const scoped = createScopedHealthCheckRegistry(registry, mockOwner);
|
|
123
|
+
scoped.register(mockStrategy1);
|
|
124
|
+
|
|
125
|
+
// The scoped registry should auto-prefix with pluginId
|
|
126
|
+
const qualifiedId = `${mockOwner.pluginId}.${mockStrategy1.id}`;
|
|
127
|
+
expect(registry.getStrategy(qualifiedId)).toBe(mockStrategy1);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("should lookup strategies by both qualified and unqualified ID", () => {
|
|
131
|
+
const scoped = createScopedHealthCheckRegistry(registry, mockOwner);
|
|
132
|
+
scoped.register(mockStrategy1);
|
|
133
|
+
|
|
134
|
+
// Should be able to lookup by unqualified ID in scoped registry
|
|
135
|
+
expect(scoped.getStrategy(mockStrategy1.id)).toBe(mockStrategy1);
|
|
136
|
+
|
|
137
|
+
// Should also work with qualified ID
|
|
138
|
+
const qualifiedId = `${mockOwner.pluginId}.${mockStrategy1.id}`;
|
|
139
|
+
expect(scoped.getStrategy(qualifiedId)).toBe(mockStrategy1);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
101
142
|
});
|
|
@@ -1,27 +1,98 @@
|
|
|
1
|
+
import type { PluginMetadata } from "@checkstack/common";
|
|
1
2
|
import {
|
|
2
3
|
HealthCheckRegistry,
|
|
3
4
|
HealthCheckStrategy,
|
|
4
5
|
} from "@checkstack/backend-api";
|
|
5
6
|
import { rootLogger } from "../logger";
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Registered strategy with its fully qualified ID and owner info.
|
|
10
|
+
*/
|
|
11
|
+
interface RegisteredStrategy {
|
|
12
|
+
strategy: HealthCheckStrategy;
|
|
13
|
+
ownerPlugin: PluginMetadata;
|
|
14
|
+
qualifiedId: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Core implementation of the HealthCheckRegistry storage.
|
|
19
|
+
* This is the global singleton that stores all strategies with fully qualified IDs.
|
|
20
|
+
*/
|
|
21
|
+
export class CoreHealthCheckRegistry {
|
|
22
|
+
private strategies = new Map<string, RegisteredStrategy>();
|
|
9
23
|
|
|
10
|
-
|
|
11
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Register a strategy with its owning plugin.
|
|
26
|
+
* The strategy ID is stored as: ownerPluginId.strategyId
|
|
27
|
+
*/
|
|
28
|
+
registerWithOwner(
|
|
29
|
+
strategy: HealthCheckStrategy,
|
|
30
|
+
ownerPlugin: PluginMetadata
|
|
31
|
+
): void {
|
|
32
|
+
const qualifiedId = `${ownerPlugin.pluginId}.${strategy.id}`;
|
|
33
|
+
if (this.strategies.has(qualifiedId)) {
|
|
12
34
|
rootLogger.warn(
|
|
13
|
-
`HealthCheckStrategy '${
|
|
35
|
+
`HealthCheckStrategy '${qualifiedId}' is already registered. Overwriting.`
|
|
14
36
|
);
|
|
15
37
|
}
|
|
16
|
-
this.strategies.set(strategy
|
|
17
|
-
rootLogger.debug(`✅ Registered HealthCheckStrategy: ${
|
|
38
|
+
this.strategies.set(qualifiedId, { strategy, ownerPlugin, qualifiedId });
|
|
39
|
+
rootLogger.debug(`✅ Registered HealthCheckStrategy: ${qualifiedId}`);
|
|
18
40
|
}
|
|
19
41
|
|
|
20
|
-
getStrategy(
|
|
21
|
-
return this.strategies.get(
|
|
42
|
+
getStrategy(qualifiedId: string) {
|
|
43
|
+
return this.strategies.get(qualifiedId)?.strategy;
|
|
22
44
|
}
|
|
23
45
|
|
|
24
46
|
getStrategies() {
|
|
25
|
-
return [...this.strategies.values()];
|
|
47
|
+
return [...this.strategies.values()].map((r) => r.strategy);
|
|
26
48
|
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get the owner plugin ID for a strategy.
|
|
52
|
+
*/
|
|
53
|
+
getOwnerPluginId(qualifiedId: string): string | undefined {
|
|
54
|
+
return this.strategies.get(qualifiedId)?.ownerPlugin.pluginId;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get all registered strategies with their metadata.
|
|
59
|
+
*/
|
|
60
|
+
getStrategiesWithMeta(): Array<{
|
|
61
|
+
strategy: HealthCheckStrategy;
|
|
62
|
+
ownerPluginId: string;
|
|
63
|
+
qualifiedId: string;
|
|
64
|
+
}> {
|
|
65
|
+
return [...this.strategies.values()].map((r) => ({
|
|
66
|
+
strategy: r.strategy,
|
|
67
|
+
ownerPluginId: r.ownerPlugin.pluginId,
|
|
68
|
+
qualifiedId: r.qualifiedId,
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Creates a scoped HealthCheckRegistry that auto-prefixes strategy IDs with plugin ID.
|
|
75
|
+
*/
|
|
76
|
+
export function createScopedHealthCheckRegistry(
|
|
77
|
+
globalRegistry: CoreHealthCheckRegistry,
|
|
78
|
+
ownerPlugin: PluginMetadata
|
|
79
|
+
): HealthCheckRegistry {
|
|
80
|
+
return {
|
|
81
|
+
register(strategy: HealthCheckStrategy) {
|
|
82
|
+
globalRegistry.registerWithOwner(strategy, ownerPlugin);
|
|
83
|
+
},
|
|
84
|
+
getStrategy(id: string) {
|
|
85
|
+
// Support both qualified and unqualified lookups
|
|
86
|
+
return (
|
|
87
|
+
globalRegistry.getStrategy(id) ||
|
|
88
|
+
globalRegistry.getStrategy(`${ownerPlugin.pluginId}.${id}`)
|
|
89
|
+
);
|
|
90
|
+
},
|
|
91
|
+
getStrategies() {
|
|
92
|
+
return globalRegistry.getStrategies();
|
|
93
|
+
},
|
|
94
|
+
getStrategiesWithMeta() {
|
|
95
|
+
return globalRegistry.getStrategiesWithMeta();
|
|
96
|
+
},
|
|
97
|
+
};
|
|
27
98
|
}
|
package/src/test-preload.ts
CHANGED
|
@@ -110,5 +110,20 @@ mock.module(coreServicesPath, () => ({
|
|
|
110
110
|
// eslint-disable-next-line unicorn/consistent-function-scoping
|
|
111
111
|
subscribe: async () => () => {},
|
|
112
112
|
}));
|
|
113
|
+
|
|
114
|
+
// Return the registries object to match actual function signature
|
|
115
|
+
// Create a mock collector registry
|
|
116
|
+
const collectors = new Map<string, unknown>();
|
|
117
|
+
return {
|
|
118
|
+
collectorRegistry: {
|
|
119
|
+
register: (collector: { id: string }) => {
|
|
120
|
+
collectors.set(collector.id, collector);
|
|
121
|
+
},
|
|
122
|
+
getCollector: (id: string) => collectors.get(id),
|
|
123
|
+
getAllCollectors: () => [...collectors.values()],
|
|
124
|
+
unregisterByOwner: () => {},
|
|
125
|
+
unregisterByMissingStrategies: () => {},
|
|
126
|
+
},
|
|
127
|
+
};
|
|
113
128
|
},
|
|
114
129
|
}));
|