@checkstack/incident-backend 1.8.1 → 1.8.3

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,75 @@
1
1
  # @checkstack/incident-backend
2
2
 
3
+ ## 1.8.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 8cad340: refactor: typed router-factory args and structured logging
8
+
9
+ Internal router factories that took long positional argument lists
10
+ (`incident-backend`, `maintenance-backend`, and `notification-backend`'s
11
+ `createNotificationRouter`) now take a single typed `deps` object, matching the
12
+ `RouterDeps` convention already used by sibling routers and removing a class of
13
+ easy-to-transpose call sites.
14
+
15
+ Backend code paths that wrote to `console.*` now use the injected structured
16
+ `Logger` so they respect log levels and correlation: the catalog router's
17
+ notification-resource lifecycle warnings, the notification OAuth callback
18
+ handler's errors, and the command router's search-provider failures. The
19
+ command router factory now takes a typed `{ logger }` object.
20
+
21
+ - Updated dependencies [8cad340]
22
+ - Updated dependencies [8cad340]
23
+ - Updated dependencies [8cad340]
24
+ - Updated dependencies [8cad340]
25
+ - Updated dependencies [8cad340]
26
+ - Updated dependencies [8cad340]
27
+ - Updated dependencies [8cad340]
28
+ - Updated dependencies [8cad340]
29
+ - Updated dependencies [8cad340]
30
+ - Updated dependencies [8cad340]
31
+ - Updated dependencies [8cad340]
32
+ - Updated dependencies [8cad340]
33
+ - Updated dependencies [8cad340]
34
+ - Updated dependencies [8cad340]
35
+ - Updated dependencies [8cad340]
36
+ - Updated dependencies [8cad340]
37
+ - Updated dependencies [8cad340]
38
+ - Updated dependencies [8cad340]
39
+ - Updated dependencies [8cad340]
40
+ - @checkstack/ai-backend@0.8.0
41
+ - @checkstack/ai-common@0.6.0
42
+ - @checkstack/automation-backend@0.9.3
43
+ - @checkstack/status-page-backend@0.3.0
44
+ - @checkstack/backend-api@0.25.0
45
+ - @checkstack/notification-common@1.4.0
46
+ - @checkstack/common@0.17.0
47
+ - @checkstack/auth-common@0.11.0
48
+ - @checkstack/integration-backend@0.6.5
49
+ - @checkstack/command-backend@0.2.12
50
+ - @checkstack/catalog-backend@1.5.3
51
+ - @checkstack/status-page-common@0.3.0
52
+ - @checkstack/catalog-common@2.4.2
53
+ - @checkstack/incident-common@1.6.2
54
+ - @checkstack/automation-common@0.7.1
55
+ - @checkstack/cache-api@0.3.14
56
+ - @checkstack/integration-common@0.9.2
57
+ - @checkstack/signal-common@0.2.11
58
+ - @checkstack/cache-utils@0.2.19
59
+
60
+ ## 1.8.2
61
+
62
+ ### Patch Changes
63
+
64
+ - Updated dependencies [2ec8f64]
65
+ - @checkstack/backend-api@0.24.1
66
+ - @checkstack/catalog-backend@1.5.2
67
+ - @checkstack/automation-backend@0.9.2
68
+ - @checkstack/ai-backend@0.7.2
69
+ - @checkstack/command-backend@0.2.11
70
+ - @checkstack/integration-backend@0.6.4
71
+ - @checkstack/status-page-backend@0.2.1
72
+
3
73
  ## 1.8.1
4
74
 
5
75
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/incident-backend",
3
- "version": "1.8.1",
3
+ "version": "1.8.3",
4
4
  "license": "Elastic-2.0",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -14,34 +14,34 @@
14
14
  "lint:code": "eslint . --max-warnings 0"
15
15
  },
16
16
  "dependencies": {
17
- "@checkstack/ai-backend": "0.7.1",
18
- "@checkstack/ai-common": "0.5.0",
19
- "@checkstack/backend-api": "0.24.0",
20
- "@checkstack/cache-api": "0.3.13",
21
- "@checkstack/cache-utils": "0.2.18",
22
- "@checkstack/incident-common": "1.6.1",
23
- "@checkstack/catalog-common": "2.4.1",
24
- "@checkstack/catalog-backend": "1.5.1",
25
- "@checkstack/notification-common": "1.3.4",
26
- "@checkstack/auth-common": "0.10.0",
27
- "@checkstack/command-backend": "0.2.10",
28
- "@checkstack/signal-common": "0.2.10",
29
- "@checkstack/status-page-backend": "0.2.0",
30
- "@checkstack/status-page-common": "0.2.0",
31
- "@checkstack/integration-backend": "0.6.3",
32
- "@checkstack/integration-common": "0.9.1",
33
- "@checkstack/automation-backend": "0.9.1",
34
- "@checkstack/automation-common": "0.7.0",
35
- "@checkstack/common": "0.16.0",
17
+ "@checkstack/ai-backend": "0.8.0",
18
+ "@checkstack/ai-common": "0.6.0",
19
+ "@checkstack/backend-api": "0.25.0",
20
+ "@checkstack/cache-api": "0.3.14",
21
+ "@checkstack/cache-utils": "0.2.19",
22
+ "@checkstack/incident-common": "1.6.2",
23
+ "@checkstack/catalog-common": "2.4.2",
24
+ "@checkstack/catalog-backend": "1.5.3",
25
+ "@checkstack/notification-common": "1.4.0",
26
+ "@checkstack/auth-common": "0.11.0",
27
+ "@checkstack/command-backend": "0.2.12",
28
+ "@checkstack/signal-common": "0.2.11",
29
+ "@checkstack/status-page-backend": "0.3.0",
30
+ "@checkstack/status-page-common": "0.3.0",
31
+ "@checkstack/integration-backend": "0.6.5",
32
+ "@checkstack/integration-common": "0.9.2",
33
+ "@checkstack/automation-backend": "0.9.3",
34
+ "@checkstack/automation-common": "0.7.1",
35
+ "@checkstack/common": "0.17.0",
36
36
  "drizzle-orm": "^0.45.0",
37
37
  "zod": "^4.2.1",
38
38
  "@orpc/contract": "^1.14.4",
39
39
  "@orpc/server": "^1.14.4"
40
40
  },
41
41
  "devDependencies": {
42
- "@checkstack/drizzle-helper": "0.0.5",
43
- "@checkstack/scripts": "0.6.2",
44
- "@checkstack/test-utils-backend": "0.1.44",
42
+ "@checkstack/drizzle-helper": "0.0.6",
43
+ "@checkstack/scripts": "0.6.3",
44
+ "@checkstack/test-utils-backend": "0.1.46",
45
45
  "@checkstack/tsconfig": "0.0.7",
46
46
  "@types/bun": "^1.0.0",
47
47
  "drizzle-kit": "^0.31.10",
@@ -0,0 +1,229 @@
1
+ import { describe, test, expect, mock } from "bun:test";
2
+ import type {
3
+ CacheManager,
4
+ CacheProvider,
5
+ } from "@checkstack/cache-api";
6
+ import type { Logger } from "@checkstack/backend-api";
7
+ import type {
8
+ IncidentDetail,
9
+ IncidentWithSystems,
10
+ } from "@checkstack/incident-common";
11
+ import { createIncidentCache, type IncidentCache } from "./cache";
12
+
13
+ // Loader signatures derived from the cache's own surface, so the fakes match
14
+ // the real `IncidentService` return types without re-declaring those shapes.
15
+ type IncidentLoader = Parameters<IncidentCache["wrapIncident"]>[1];
16
+ type ListLoader = Parameters<IncidentCache["wrapList"]>[1];
17
+ type SystemLoader = Parameters<IncidentCache["wrapSystem"]>[1];
18
+
19
+ const sampleIncident: IncidentDetail = {
20
+ id: "i1",
21
+ title: "DB down",
22
+ status: "investigating",
23
+ severity: "major",
24
+ suppressNotifications: false,
25
+ createdAt: new Date("2026-06-01T00:00:00Z"),
26
+ updatedAt: new Date("2026-06-01T00:00:00Z"),
27
+ systemIds: ["s1"],
28
+ updates: [],
29
+ links: [],
30
+ };
31
+
32
+ const sampleWithSystems: IncidentWithSystems = {
33
+ id: sampleIncident.id,
34
+ title: sampleIncident.title,
35
+ status: sampleIncident.status,
36
+ severity: sampleIncident.severity,
37
+ suppressNotifications: sampleIncident.suppressNotifications,
38
+ createdAt: sampleIncident.createdAt,
39
+ updatedAt: sampleIncident.updatedAt,
40
+ systemIds: sampleIncident.systemIds,
41
+ };
42
+
43
+ // Loaders return non-undefined values so the read-through cache actually
44
+ // stores them (the scope treats `undefined` as a miss).
45
+ const incidentLoaderReturning: IncidentLoader = async () => sampleIncident;
46
+ const listLoaderReturning: ListLoader = async () => [sampleWithSystems];
47
+ const systemLoaderReturning: SystemLoader = async () => [sampleWithSystems];
48
+
49
+ function createMemoryProvider(): {
50
+ provider: CacheProvider;
51
+ store: Map<string, unknown>;
52
+ } {
53
+ const store = new Map<string, unknown>();
54
+ const provider: CacheProvider = {
55
+ get: async <T>(key: string) => store.get(key) as T | undefined,
56
+ set: async (key, value) => {
57
+ store.set(key, value);
58
+ },
59
+ delete: async (key) => {
60
+ store.delete(key);
61
+ },
62
+ deleteByPrefix: async (prefix) => {
63
+ let removed = 0;
64
+ for (const key of [...store.keys()]) {
65
+ if (key.startsWith(prefix)) {
66
+ store.delete(key);
67
+ removed++;
68
+ }
69
+ }
70
+ return removed;
71
+ },
72
+ has: async (key) => store.has(key),
73
+ };
74
+ return { provider, store };
75
+ }
76
+
77
+ function createManager(provider: CacheProvider): CacheManager {
78
+ return {
79
+ getProvider: () => provider,
80
+ getActivePlugin: () => "incident",
81
+ getActiveConfig: () => ({}),
82
+ setActiveBackend: async () => {},
83
+ shutdown: async () => {},
84
+ };
85
+ }
86
+
87
+ const noopLogger: Logger = {
88
+ debug: () => {},
89
+ info: () => {},
90
+ warn: () => {},
91
+ error: () => {},
92
+ } as unknown as Logger;
93
+
94
+ function makeCache() {
95
+ const { provider, store } = createMemoryProvider();
96
+ const cache = createIncidentCache({
97
+ cacheManager: createManager(provider),
98
+ logger: noopLogger,
99
+ });
100
+ return { cache, store };
101
+ }
102
+
103
+ describe("incident cache — read-through wrapping", () => {
104
+ test("wrapIncident caches per id and only calls the loader once for a hit", async () => {
105
+ const { cache } = makeCache();
106
+ const loader = mock(incidentLoaderReturning);
107
+
108
+ const a = await cache.wrapIncident("i1", loader);
109
+ const b = await cache.wrapIncident("i1", loader);
110
+
111
+ expect(a).toEqual(sampleIncident);
112
+ expect(b).toEqual(sampleIncident);
113
+ // Second read is served from cache: the loader ran only once.
114
+ expect(loader).toHaveBeenCalledTimes(1);
115
+ });
116
+
117
+ test("wrapList keys by filter shape — different filters do not share a slot", async () => {
118
+ const { cache } = makeCache();
119
+ const loaderA = mock(listLoaderReturning);
120
+ const loaderB = mock(listLoaderReturning);
121
+
122
+ const resA = await cache.wrapList({ status: "open" }, loaderA);
123
+ const resB = await cache.wrapList({ status: "resolved" }, loaderB);
124
+
125
+ expect(resA).toEqual([sampleWithSystems]);
126
+ expect(resB).toEqual([sampleWithSystems]);
127
+ expect(loaderA).toHaveBeenCalledTimes(1);
128
+ expect(loaderB).toHaveBeenCalledTimes(1);
129
+ });
130
+
131
+ test("wrapList collapses key-order-different but equal filter shapes to one slot", async () => {
132
+ const { cache } = makeCache();
133
+ const loader = mock(listLoaderReturning);
134
+
135
+ await cache.wrapList({ a: 1, b: 2 }, loader);
136
+ // Same logical filter, keys in a different order -> same cache key.
137
+ const second = await cache.wrapList({ b: 2, a: 1 }, loader);
138
+
139
+ expect(second).toEqual([sampleWithSystems]);
140
+ expect(loader).toHaveBeenCalledTimes(1);
141
+ });
142
+
143
+ test("wrapList treats undefined filters and {} as the same slot", async () => {
144
+ const { cache } = makeCache();
145
+ const loader = mock(listLoaderReturning);
146
+
147
+ await cache.wrapList(undefined, loader);
148
+ await cache.wrapList({}, loader);
149
+
150
+ expect(loader).toHaveBeenCalledTimes(1);
151
+ });
152
+ });
153
+
154
+ describe("incident cache — invalidation fan-out", () => {
155
+ test("invalidateForMutation drops the incident, its systems, and all list shapes", async () => {
156
+ const { cache } = makeCache();
157
+ const incidentLoader = mock(incidentLoaderReturning);
158
+ const systemLoader = mock(systemLoaderReturning);
159
+ const listLoader = mock(listLoaderReturning);
160
+
161
+ await cache.wrapIncident("i1", incidentLoader);
162
+ await cache.wrapSystem("s1", systemLoader);
163
+ await cache.wrapList({ status: "open" }, listLoader);
164
+
165
+ await cache.invalidateForMutation({ incidentId: "i1", systemIds: ["s1"] });
166
+
167
+ // All three must re-load on next read.
168
+ await cache.wrapIncident("i1", incidentLoader);
169
+ await cache.wrapSystem("s1", systemLoader);
170
+ await cache.wrapList({ status: "open" }, listLoader);
171
+
172
+ expect(incidentLoader).toHaveBeenCalledTimes(2);
173
+ expect(systemLoader).toHaveBeenCalledTimes(2);
174
+ expect(listLoader).toHaveBeenCalledTimes(2);
175
+ });
176
+
177
+ test("invalidateForMutation does NOT touch an unrelated incident's cache", async () => {
178
+ const { cache } = makeCache();
179
+ const otherLoader = mock(incidentLoaderReturning);
180
+
181
+ await cache.wrapIncident("i2", otherLoader);
182
+ await cache.invalidateForMutation({ incidentId: "i1", systemIds: ["s1"] });
183
+ await cache.wrapIncident("i2", otherLoader);
184
+
185
+ expect(otherLoader).toHaveBeenCalledTimes(1);
186
+ });
187
+
188
+ test("invalidateForMutation drops every affected system in one call", async () => {
189
+ const { cache } = makeCache();
190
+ const s1 = mock(systemLoaderReturning);
191
+ const s2 = mock(systemLoaderReturning);
192
+
193
+ await cache.wrapSystem("s1", s1);
194
+ await cache.wrapSystem("s2", s2);
195
+
196
+ await cache.invalidateForMutation({
197
+ incidentId: "i1",
198
+ systemIds: ["s1", "s2"],
199
+ });
200
+
201
+ await cache.wrapSystem("s1", s1);
202
+ await cache.wrapSystem("s2", s2);
203
+
204
+ expect(s1).toHaveBeenCalledTimes(2);
205
+ expect(s2).toHaveBeenCalledTimes(2);
206
+ });
207
+
208
+ test("invalidateSystem drops one system's cache plus all lists, leaving incidents alone", async () => {
209
+ const { cache } = makeCache();
210
+ const systemLoader = mock(systemLoaderReturning);
211
+ const listLoader = mock(listLoaderReturning);
212
+ const incidentLoader = mock(incidentLoaderReturning);
213
+
214
+ await cache.wrapSystem("s1", systemLoader);
215
+ await cache.wrapList({}, listLoader);
216
+ await cache.wrapIncident("i1", incidentLoader);
217
+
218
+ await cache.invalidateSystem("s1");
219
+
220
+ await cache.wrapSystem("s1", systemLoader);
221
+ await cache.wrapList({}, listLoader);
222
+ await cache.wrapIncident("i1", incidentLoader);
223
+
224
+ expect(systemLoader).toHaveBeenCalledTimes(2);
225
+ expect(listLoader).toHaveBeenCalledTimes(2);
226
+ // The incident entry survives a system-only invalidation.
227
+ expect(incidentLoader).toHaveBeenCalledTimes(1);
228
+ });
229
+ });
package/src/index.ts CHANGED
@@ -195,7 +195,7 @@ export default createBackendPlugin({
195
195
 
196
196
  const cache = createIncidentCache({ cacheManager, logger });
197
197
  incidentCache = cache;
198
- const router = createRouter(
198
+ const router = createRouter({
199
199
  service,
200
200
  signalService,
201
201
  catalogClient,
@@ -203,8 +203,8 @@ export default createBackendPlugin({
203
203
  authClient,
204
204
  logger,
205
205
  cache,
206
- () => incidentEntity,
207
- );
206
+ getIncidentEntity: () => incidentEntity,
207
+ });
208
208
  rpc.registerRouter(router, incidentContract);
209
209
 
210
210
  // Register incident actions with the Automation platform. We
package/src/router.ts CHANGED
@@ -28,19 +28,30 @@ import {
28
28
  type IncidentEntityState,
29
29
  } from "./incident-entity";
30
30
 
31
- export function createRouter(
32
- service: IncidentService,
33
- signalService: SignalService,
34
- catalogClient: InferClient<typeof CatalogApi>,
31
+ export interface IncidentRouterDeps {
32
+ service: IncidentService;
33
+ signalService: SignalService;
34
+ catalogClient: InferClient<typeof CatalogApi>;
35
35
  notificationClient: InferClient<
36
36
  typeof import("@checkstack/notification-common").NotificationApi
37
- >,
38
- authClient: InferClient<typeof AuthApi>,
39
- logger: Logger,
40
- cache: IncidentCache,
37
+ >;
38
+ authClient: InferClient<typeof AuthApi>;
39
+ logger: Logger;
40
+ cache: IncidentCache;
41
41
  /** Resolver for the reactive `incident` entity (§10.1). Undefined in tests. */
42
- getIncidentEntity?: () => EntityHandle<IncidentEntityState> | undefined,
43
- ) {
42
+ getIncidentEntity?: () => EntityHandle<IncidentEntityState> | undefined;
43
+ }
44
+
45
+ export function createRouter({
46
+ service,
47
+ signalService,
48
+ catalogClient,
49
+ notificationClient,
50
+ authClient,
51
+ logger,
52
+ cache,
53
+ getIncidentEntity,
54
+ }: IncidentRouterDeps) {
44
55
  /**
45
56
  * Resolve user IDs to profile names for a list of updates.
46
57
  * Falls back to "Unknown User" if the user cannot be found.