@checkstack/dependency-backend 1.2.0 → 1.3.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 +92 -0
- package/package.json +17 -17
- package/src/automations.test.ts +63 -8
- package/src/automations.ts +63 -23
- package/src/dependency-entity.test.ts +270 -0
- package/src/dependency-entity.ts +157 -0
- package/src/hooks.ts +7 -32
- package/src/index.ts +110 -27
- package/src/router.ts +72 -29
- package/src/services/dependency-service.ts +67 -3
package/src/index.ts
CHANGED
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
automationActionExtensionPoint,
|
|
13
13
|
automationArtifactTypeExtensionPoint,
|
|
14
14
|
automationTriggerExtensionPoint,
|
|
15
|
+
entityExtensionPoint,
|
|
16
|
+
type EntityHandle,
|
|
15
17
|
} from "@checkstack/automation-backend";
|
|
16
18
|
import { DependencyService } from "./services/dependency-service";
|
|
17
19
|
import { WarningEvaluationService } from "./services/warning-evaluation-service";
|
|
@@ -28,9 +30,20 @@ import { HealthCheckApi } from "@checkstack/healthcheck-common";
|
|
|
28
30
|
import { MaintenanceApi } from "@checkstack/maintenance-common";
|
|
29
31
|
import { IncidentApi } from "@checkstack/incident-common";
|
|
30
32
|
import { NotificationApi } from "@checkstack/notification-common";
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
33
|
+
import { CATALOG_SYSTEM_ENTITY_KIND } from "@checkstack/catalog-backend";
|
|
34
|
+
import {
|
|
35
|
+
HEALTH_ENTITY_KIND,
|
|
36
|
+
classifyHealthChange,
|
|
37
|
+
} from "@checkstack/healthcheck-backend";
|
|
33
38
|
import { evaluateAndNotifyDownstream } from "./notifications";
|
|
39
|
+
import {
|
|
40
|
+
DEPENDENCY_EDGE_ENTITY_KIND,
|
|
41
|
+
DependencyEdgeStateSchema,
|
|
42
|
+
createDependencyEntityRead,
|
|
43
|
+
dependencyChangeToPayload,
|
|
44
|
+
deriveDependencyTriggerEvents,
|
|
45
|
+
type DependencyEdgeState,
|
|
46
|
+
} from "./dependency-entity";
|
|
34
47
|
import { entityKindExtensionPoint } from "@checkstack/gitops-backend";
|
|
35
48
|
import { registerDependencyGitOpsKinds } from "./dependency-gitops-kinds";
|
|
36
49
|
|
|
@@ -38,6 +51,17 @@ import { registerDependencyGitOpsKinds } from "./dependency-gitops-kinds";
|
|
|
38
51
|
// Plugin Definition
|
|
39
52
|
// =============================================================================
|
|
40
53
|
|
|
54
|
+
// Reactive `dependency-edge` entity handle (§10.5). Defined in register();
|
|
55
|
+
// mutated from the router/actions onward.
|
|
56
|
+
let dependencyEntity: EntityHandle<DependencyEdgeState> | undefined;
|
|
57
|
+
|
|
58
|
+
// The DependencyService is created in init() (it needs the resolved
|
|
59
|
+
// database), but the PLUGIN-BACKED entity `read` accessor must be supplied at
|
|
60
|
+
// `defineEntity` time in register(). This holder bridges the two: the `read`
|
|
61
|
+
// closure resolves the service lazily, and init() sets it before any mutation
|
|
62
|
+
// runs (the registry only mutates from init() onward).
|
|
63
|
+
let dependencyServiceRef: DependencyService | undefined;
|
|
64
|
+
|
|
41
65
|
export default createBackendPlugin({
|
|
42
66
|
metadata: pluginMetadata,
|
|
43
67
|
register(env) {
|
|
@@ -58,6 +82,40 @@ export default createBackendPlugin({
|
|
|
58
82
|
.getExtensionPoint(automationArtifactTypeExtensionPoint)
|
|
59
83
|
.registerArtifactType(dependencyArtifactType, pluginMetadata);
|
|
60
84
|
|
|
85
|
+
// ─── Reactive `dependency-edge` entity (§10.5) ─────────────────────
|
|
86
|
+
// PLUGIN-BACKED (Model B): the `dependencies` table IS the current-state
|
|
87
|
+
// storage. `read` routes straight to the service's batched authoritative
|
|
88
|
+
// read — no framework `entity_state` row, so no `indexes` (those only
|
|
89
|
+
// apply to store-backed kinds). The `read` closure resolves the service
|
|
90
|
+
// set by init() (mutations only happen from init on).
|
|
91
|
+
const entityPoint = env.getExtensionPoint(entityExtensionPoint);
|
|
92
|
+
dependencyEntity = entityPoint.defineEntity<DependencyEdgeState>({
|
|
93
|
+
kind: DEPENDENCY_EDGE_ENTITY_KIND,
|
|
94
|
+
state: DependencyEdgeStateSchema,
|
|
95
|
+
read: (ids) => {
|
|
96
|
+
const svc = dependencyServiceRef;
|
|
97
|
+
if (!svc) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
"dependency entity read before init: service not yet resolved",
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
return createDependencyEntityRead(svc)(ids);
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
entityPoint.registerChangeDeriver({
|
|
106
|
+
kind: DEPENDENCY_EDGE_ENTITY_KIND,
|
|
107
|
+
derive: deriveDependencyTriggerEvents,
|
|
108
|
+
toPayload: dependencyChangeToPayload,
|
|
109
|
+
});
|
|
110
|
+
const onEntityChanged = entityPoint.onEntityChanged;
|
|
111
|
+
// The propagation cursor is internal bookkeeping (§5): derived
|
|
112
|
+
// per-system state is reachable via the `health` entity, not this table.
|
|
113
|
+
entityPoint.declareNonReactiveState({
|
|
114
|
+
table: "dependency_derived_states",
|
|
115
|
+
reason: "bookkeeping",
|
|
116
|
+
note: "Propagation cursor for downstream impact evaluation. Derived per-system state is reachable via the health entity.",
|
|
117
|
+
});
|
|
118
|
+
|
|
61
119
|
// ─── GitOps Entity Kind Registration ─────────────────────────────
|
|
62
120
|
let gitopsService: DependencyService | undefined;
|
|
63
121
|
const kindRegistry = env.getExtensionPoint(entityKindExtensionPoint);
|
|
@@ -88,6 +146,9 @@ export default createBackendPlugin({
|
|
|
88
146
|
database as SafeDatabase<typeof schema>,
|
|
89
147
|
);
|
|
90
148
|
gitopsService = service;
|
|
149
|
+
// Publish the service for the PLUGIN-BACKED entity `read` accessor
|
|
150
|
+
// (defined in register()). Mutations only run from here onward.
|
|
151
|
+
dependencyServiceRef = service;
|
|
91
152
|
const warningService = new WarningEvaluationService();
|
|
92
153
|
|
|
93
154
|
const router = createRouter({
|
|
@@ -97,6 +158,7 @@ export default createBackendPlugin({
|
|
|
97
158
|
catalogClient,
|
|
98
159
|
healthCheckClient,
|
|
99
160
|
logger,
|
|
161
|
+
getDependencyEntity: () => dependencyEntity,
|
|
100
162
|
});
|
|
101
163
|
rpc.registerRouter(router, dependencyContract);
|
|
102
164
|
|
|
@@ -106,7 +168,6 @@ export default createBackendPlugin({
|
|
|
106
168
|
database,
|
|
107
169
|
rpcClient,
|
|
108
170
|
logger,
|
|
109
|
-
onHook,
|
|
110
171
|
emitHook,
|
|
111
172
|
signalService,
|
|
112
173
|
}) => {
|
|
@@ -132,7 +193,11 @@ export default createBackendPlugin({
|
|
|
132
193
|
const automationActions = env.getExtensionPoint(
|
|
133
194
|
automationActionExtensionPoint,
|
|
134
195
|
);
|
|
135
|
-
for (const action of createDependencyActions({
|
|
196
|
+
for (const action of createDependencyActions({
|
|
197
|
+
service,
|
|
198
|
+
emitHook,
|
|
199
|
+
getDependencyEntity: () => dependencyEntity,
|
|
200
|
+
})) {
|
|
136
201
|
automationActions.registerAction(action, pluginMetadata);
|
|
137
202
|
}
|
|
138
203
|
|
|
@@ -203,27 +268,42 @@ export default createBackendPlugin({
|
|
|
203
268
|
return statuses;
|
|
204
269
|
}
|
|
205
270
|
|
|
206
|
-
//
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
271
|
+
// Cross-plugin consumers now react to the reactive `catalog-system`
|
|
272
|
+
// / `health` ENTITY changes via `onEntityChanged` instead of the
|
|
273
|
+
// (being-removed) catalog/healthcheck hooks (§10.5). All keep
|
|
274
|
+
// `work-queue` delivery: cleanup + downstream-propagation are
|
|
275
|
+
// side-effecting writes that must run exactly once per cluster, not
|
|
276
|
+
// per-instance (broadcast would re-run them N times).
|
|
277
|
+
|
|
278
|
+
// Subscribe to catalog system deletion (tombstone) to clean up edges
|
|
279
|
+
onEntityChanged({
|
|
280
|
+
kind: CATALOG_SYSTEM_ENTITY_KIND,
|
|
281
|
+
handler: async (change) => {
|
|
282
|
+
if (change.next !== null) return; // tombstone only
|
|
283
|
+
const systemId = change.id;
|
|
210
284
|
logger.debug(
|
|
211
|
-
`Cleaning up dependencies for deleted system: ${
|
|
285
|
+
`Cleaning up dependencies for deleted system: ${systemId}`,
|
|
212
286
|
);
|
|
213
|
-
await service.removeSystemDependencies(
|
|
287
|
+
await service.removeSystemDependencies(systemId);
|
|
214
288
|
},
|
|
215
|
-
|
|
216
|
-
|
|
289
|
+
delivery: {
|
|
290
|
+
mode: "work-queue",
|
|
291
|
+
workerGroup: "dependency-system-cleanup",
|
|
292
|
+
},
|
|
293
|
+
});
|
|
217
294
|
|
|
218
|
-
//
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
async (
|
|
295
|
+
// Upstream health DEGRADED → evaluate downstream dependents
|
|
296
|
+
onEntityChanged({
|
|
297
|
+
kind: HEALTH_ENTITY_KIND,
|
|
298
|
+
handler: async (change) => {
|
|
299
|
+
const { systemId, degraded, previousStatus, newStatus } =
|
|
300
|
+
classifyHealthChange(change);
|
|
301
|
+
if (!degraded) return;
|
|
222
302
|
logger.debug(
|
|
223
|
-
`Upstream ${
|
|
303
|
+
`Upstream ${systemId} degraded (${previousStatus} → ${newStatus}), evaluating downstream dependencies`,
|
|
224
304
|
);
|
|
225
305
|
await evaluateAndNotifyDownstream({
|
|
226
|
-
changedSystemId:
|
|
306
|
+
changedSystemId: systemId,
|
|
227
307
|
db: typedDb,
|
|
228
308
|
dependencyService: service,
|
|
229
309
|
warningService,
|
|
@@ -237,20 +317,23 @@ export default createBackendPlugin({
|
|
|
237
317
|
emitImpactPropagated,
|
|
238
318
|
});
|
|
239
319
|
},
|
|
240
|
-
{
|
|
320
|
+
delivery: {
|
|
241
321
|
mode: "work-queue",
|
|
242
322
|
workerGroup: "dependency-notification-evaluator",
|
|
243
323
|
},
|
|
244
|
-
);
|
|
324
|
+
});
|
|
245
325
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
326
|
+
// Upstream health RECOVERED → evaluate downstream dependents
|
|
327
|
+
onEntityChanged({
|
|
328
|
+
kind: HEALTH_ENTITY_KIND,
|
|
329
|
+
handler: async (change) => {
|
|
330
|
+
const { systemId, recovered } = classifyHealthChange(change);
|
|
331
|
+
if (!recovered) return;
|
|
249
332
|
logger.debug(
|
|
250
|
-
`Upstream ${
|
|
333
|
+
`Upstream ${systemId} recovered, evaluating downstream dependencies`,
|
|
251
334
|
);
|
|
252
335
|
await evaluateAndNotifyDownstream({
|
|
253
|
-
changedSystemId:
|
|
336
|
+
changedSystemId: systemId,
|
|
254
337
|
db: typedDb,
|
|
255
338
|
dependencyService: service,
|
|
256
339
|
warningService,
|
|
@@ -264,11 +347,11 @@ export default createBackendPlugin({
|
|
|
264
347
|
emitImpactPropagated,
|
|
265
348
|
});
|
|
266
349
|
},
|
|
267
|
-
{
|
|
350
|
+
delivery: {
|
|
268
351
|
mode: "work-queue",
|
|
269
352
|
workerGroup: "dependency-notification-recovery",
|
|
270
353
|
},
|
|
271
|
-
);
|
|
354
|
+
});
|
|
272
355
|
|
|
273
356
|
logger.debug("✅ Dependency Backend afterPluginsReady complete.");
|
|
274
357
|
},
|
package/src/router.ts
CHANGED
|
@@ -16,11 +16,17 @@ import type {
|
|
|
16
16
|
WarningEvaluationService,
|
|
17
17
|
SystemStatus,
|
|
18
18
|
} from "./services/warning-evaluation-service";
|
|
19
|
-
import { dependencyHooks } from "./hooks";
|
|
20
19
|
import type { InferClient } from "@checkstack/common";
|
|
21
20
|
import { extractErrorMessage } from "@checkstack/common";
|
|
22
21
|
import { CatalogApi } from "@checkstack/catalog-common";
|
|
23
22
|
import { HealthCheckApi } from "@checkstack/healthcheck-common";
|
|
23
|
+
import type { EntityHandle } from "@checkstack/automation-backend";
|
|
24
|
+
import {
|
|
25
|
+
removeDependencyEdge,
|
|
26
|
+
toDependencyEdgeState,
|
|
27
|
+
writeDependencyEdge,
|
|
28
|
+
type DependencyEdgeState,
|
|
29
|
+
} from "./dependency-entity";
|
|
24
30
|
|
|
25
31
|
export function createRouter({
|
|
26
32
|
service,
|
|
@@ -29,6 +35,7 @@ export function createRouter({
|
|
|
29
35
|
catalogClient,
|
|
30
36
|
healthCheckClient,
|
|
31
37
|
logger,
|
|
38
|
+
getDependencyEntity,
|
|
32
39
|
}: {
|
|
33
40
|
service: DependencyService;
|
|
34
41
|
warningService: WarningEvaluationService;
|
|
@@ -36,6 +43,8 @@ export function createRouter({
|
|
|
36
43
|
catalogClient: InferClient<typeof CatalogApi>;
|
|
37
44
|
healthCheckClient: InferClient<typeof HealthCheckApi>;
|
|
38
45
|
logger: Logger;
|
|
46
|
+
/** Resolver for the reactive `dependency-edge` entity (§10.5). */
|
|
47
|
+
getDependencyEntity?: () => EntityHandle<DependencyEdgeState> | undefined;
|
|
39
48
|
}) {
|
|
40
49
|
/**
|
|
41
50
|
* Fetch system statuses for warning evaluation using the bulk health status API.
|
|
@@ -181,9 +190,27 @@ export function createRouter({
|
|
|
181
190
|
),
|
|
182
191
|
|
|
183
192
|
createDependency: os.createDependency.handler(
|
|
184
|
-
async ({ input
|
|
193
|
+
async ({ input }) => {
|
|
185
194
|
try {
|
|
186
|
-
|
|
195
|
+
// Drive the create through the reactive `dependency-edge` entity
|
|
196
|
+
// (§10.5): `apply` performs the REAL `dependencies` write (the
|
|
197
|
+
// plugin's own db/tx, including cycle/duplicate validation that may
|
|
198
|
+
// throw) and returns the new reactive state; the deriver fires
|
|
199
|
+
// `dependency.created` from the resulting change. The id is
|
|
200
|
+
// generated up front so the handle is keyed on it and the create's
|
|
201
|
+
// `prev` snapshot reads the not-yet-existing row as absent. A
|
|
202
|
+
// throwing `apply` means no transition is appended and nothing is
|
|
203
|
+
// emitted (the create never happened).
|
|
204
|
+
const dependencyId = crypto.randomUUID();
|
|
205
|
+
let result!: Awaited<ReturnType<typeof service.createDependency>>;
|
|
206
|
+
await writeDependencyEdge({
|
|
207
|
+
handle: getDependencyEntity?.(),
|
|
208
|
+
dependencyId,
|
|
209
|
+
apply: async () => {
|
|
210
|
+
result = await service.createDependency(input, dependencyId);
|
|
211
|
+
return toDependencyEdgeState(result);
|
|
212
|
+
},
|
|
213
|
+
});
|
|
187
214
|
|
|
188
215
|
// Broadcast signal
|
|
189
216
|
await signalService.broadcast(DEPENDENCY_CHANGED, {
|
|
@@ -193,14 +220,6 @@ export function createRouter({
|
|
|
193
220
|
action: "created",
|
|
194
221
|
});
|
|
195
222
|
|
|
196
|
-
// Emit hook
|
|
197
|
-
await context.emitHook(dependencyHooks.dependencyCreated, {
|
|
198
|
-
dependencyId: result.id,
|
|
199
|
-
sourceSystemId: result.sourceSystemId,
|
|
200
|
-
targetSystemId: result.targetSystemId,
|
|
201
|
-
impactType: result.impactType,
|
|
202
|
-
});
|
|
203
|
-
|
|
204
223
|
// Notify affected systems about warning changes
|
|
205
224
|
await signalService.broadcast(DEPENDENCY_WARNINGS_CHANGED, {
|
|
206
225
|
affectedSystemIds: [result.sourceSystemId],
|
|
@@ -221,26 +240,43 @@ export function createRouter({
|
|
|
221
240
|
),
|
|
222
241
|
|
|
223
242
|
updateDependency: os.updateDependency.handler(
|
|
224
|
-
async ({ input
|
|
225
|
-
|
|
226
|
-
|
|
243
|
+
async ({ input }) => {
|
|
244
|
+
// Probe existence first so a missing dependency still surfaces as
|
|
245
|
+
// NOT_FOUND without driving an entity write.
|
|
246
|
+
const exists = await service.getDependencyById(input.id);
|
|
247
|
+
if (!exists) {
|
|
227
248
|
throw new ORPCError("NOT_FOUND", {
|
|
228
249
|
message: "Dependency not found",
|
|
229
250
|
});
|
|
230
251
|
}
|
|
231
252
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
253
|
+
// Drive the update through the reactive `dependency-edge` entity
|
|
254
|
+
// (§10.5); the REAL update runs INSIDE `apply`, so `prev` is
|
|
255
|
+
// snapshotted before the write and the deriver fires
|
|
256
|
+
// `dependency.updated` from the resulting change.
|
|
257
|
+
let result!: NonNullable<
|
|
258
|
+
Awaited<ReturnType<typeof service.updateDependency>>
|
|
259
|
+
>;
|
|
260
|
+
await writeDependencyEdge({
|
|
261
|
+
handle: getDependencyEntity?.(),
|
|
262
|
+
dependencyId: input.id,
|
|
263
|
+
apply: async () => {
|
|
264
|
+
const updated = await service.updateDependency(input);
|
|
265
|
+
if (!updated) {
|
|
266
|
+
throw new ORPCError("NOT_FOUND", {
|
|
267
|
+
message: "Dependency not found",
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
result = updated;
|
|
271
|
+
return toDependencyEdgeState(result);
|
|
272
|
+
},
|
|
237
273
|
});
|
|
238
274
|
|
|
239
|
-
await
|
|
275
|
+
await signalService.broadcast(DEPENDENCY_CHANGED, {
|
|
240
276
|
dependencyId: result.id,
|
|
241
277
|
sourceSystemId: result.sourceSystemId,
|
|
242
278
|
targetSystemId: result.targetSystemId,
|
|
243
|
-
|
|
279
|
+
action: "updated",
|
|
244
280
|
});
|
|
245
281
|
|
|
246
282
|
await signalService.broadcast(DEPENDENCY_WARNINGS_CHANGED, {
|
|
@@ -252,9 +288,22 @@ export function createRouter({
|
|
|
252
288
|
),
|
|
253
289
|
|
|
254
290
|
deleteDependency: os.deleteDependency.handler(
|
|
255
|
-
async ({ input
|
|
291
|
+
async ({ input }) => {
|
|
256
292
|
const existing = await service.getDependencyById(input.id);
|
|
257
|
-
|
|
293
|
+
|
|
294
|
+
// Drive the delete through the reactive `dependency-edge` entity
|
|
295
|
+
// tombstone (§10.5). The REAL delete runs INSIDE `apply`, so `prev`
|
|
296
|
+
// is snapshotted before it and the deriver fires `dependency.deleted`
|
|
297
|
+
// from the tombstone. `success` tracks whether the row was actually
|
|
298
|
+
// deleted.
|
|
299
|
+
let success = false;
|
|
300
|
+
await removeDependencyEdge({
|
|
301
|
+
handle: getDependencyEntity?.(),
|
|
302
|
+
dependencyId: input.id,
|
|
303
|
+
apply: async () => {
|
|
304
|
+
success = await service.deleteDependency(input.id);
|
|
305
|
+
},
|
|
306
|
+
});
|
|
258
307
|
|
|
259
308
|
if (success && existing) {
|
|
260
309
|
await signalService.broadcast(DEPENDENCY_CHANGED, {
|
|
@@ -264,12 +313,6 @@ export function createRouter({
|
|
|
264
313
|
action: "deleted",
|
|
265
314
|
});
|
|
266
315
|
|
|
267
|
-
await context.emitHook(dependencyHooks.dependencyDeleted, {
|
|
268
|
-
dependencyId: input.id,
|
|
269
|
-
sourceSystemId: existing.sourceSystemId,
|
|
270
|
-
targetSystemId: existing.targetSystemId,
|
|
271
|
-
});
|
|
272
|
-
|
|
273
316
|
await signalService.broadcast(DEPENDENCY_WARNINGS_CHANGED, {
|
|
274
317
|
affectedSystemIds: [existing.sourceSystemId],
|
|
275
318
|
});
|
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
CreateDependencyInput,
|
|
12
12
|
UpdateDependencyInput,
|
|
13
13
|
NodePosition,
|
|
14
|
+
ImpactType,
|
|
14
15
|
} from "@checkstack/dependency-common";
|
|
15
16
|
|
|
16
17
|
type Db = SafeDatabase<typeof schema>;
|
|
@@ -59,8 +60,17 @@ export class DependencyService {
|
|
|
59
60
|
/**
|
|
60
61
|
* Create a new dependency with cycle detection.
|
|
61
62
|
* Throws if the dependency would create a circular chain.
|
|
63
|
+
*
|
|
64
|
+
* `id` may be supplied by the caller so the reactive `dependency-edge`
|
|
65
|
+
* entity can be keyed on a known id BEFORE the insert runs (the create's
|
|
66
|
+
* `prev` snapshot must read the not-yet-existing row as absent — see
|
|
67
|
+
* §10.5). When omitted, a fresh id is generated. The id is server-owned
|
|
68
|
+
* either way.
|
|
62
69
|
*/
|
|
63
|
-
async createDependency(
|
|
70
|
+
async createDependency(
|
|
71
|
+
input: CreateDependencyInput,
|
|
72
|
+
id: string = generateId(),
|
|
73
|
+
): Promise<Dependency> {
|
|
64
74
|
// Check for duplicate edge
|
|
65
75
|
const [existing] = await this.db
|
|
66
76
|
.select()
|
|
@@ -90,7 +100,6 @@ export class DependencyService {
|
|
|
90
100
|
);
|
|
91
101
|
}
|
|
92
102
|
|
|
93
|
-
const id = generateId();
|
|
94
103
|
await this.db.insert(dependencies).values({
|
|
95
104
|
id,
|
|
96
105
|
sourceSystemId: input.sourceSystemId,
|
|
@@ -213,12 +222,67 @@ export class DependencyService {
|
|
|
213
222
|
|
|
214
223
|
return {
|
|
215
224
|
...row,
|
|
216
|
-
|
|
225
|
+
|
|
217
226
|
label: row.label ?? null,
|
|
218
227
|
healthCheckRules: rules.length > 0 ? rules : undefined,
|
|
219
228
|
};
|
|
220
229
|
}
|
|
221
230
|
|
|
231
|
+
/**
|
|
232
|
+
* Batched reactive-state read for the `dependency-edge` entity (Model B
|
|
233
|
+
* plugin-backed `read` accessor). Given dependency ids, return the reactive
|
|
234
|
+
* subset `{ sourceSystemId, targetSystemId, impactType, transitive }` for
|
|
235
|
+
* each that exists (missing ids omitted). Reads the AUTHORITATIVE
|
|
236
|
+
* `dependencies` table — no framework `entity_state` storage. This is the
|
|
237
|
+
* single source of truth `handle.mutate` snapshots `prev` from and
|
|
238
|
+
* `get`/`getMany`/scope enrichment route through.
|
|
239
|
+
*/
|
|
240
|
+
async getManyEntityStates(
|
|
241
|
+
ids: ReadonlyArray<string>,
|
|
242
|
+
): Promise<
|
|
243
|
+
Record<
|
|
244
|
+
string,
|
|
245
|
+
{
|
|
246
|
+
sourceSystemId: string;
|
|
247
|
+
targetSystemId: string;
|
|
248
|
+
impactType: ImpactType;
|
|
249
|
+
transitive: boolean;
|
|
250
|
+
}
|
|
251
|
+
>
|
|
252
|
+
> {
|
|
253
|
+
if (ids.length === 0) return {};
|
|
254
|
+
|
|
255
|
+
const rows = await this.db
|
|
256
|
+
.select({
|
|
257
|
+
id: dependencies.id,
|
|
258
|
+
sourceSystemId: dependencies.sourceSystemId,
|
|
259
|
+
targetSystemId: dependencies.targetSystemId,
|
|
260
|
+
impactType: dependencies.impactType,
|
|
261
|
+
transitive: dependencies.transitive,
|
|
262
|
+
})
|
|
263
|
+
.from(dependencies)
|
|
264
|
+
.where(inArray(dependencies.id, [...ids]));
|
|
265
|
+
|
|
266
|
+
const out: Record<
|
|
267
|
+
string,
|
|
268
|
+
{
|
|
269
|
+
sourceSystemId: string;
|
|
270
|
+
targetSystemId: string;
|
|
271
|
+
impactType: ImpactType;
|
|
272
|
+
transitive: boolean;
|
|
273
|
+
}
|
|
274
|
+
> = {};
|
|
275
|
+
for (const row of rows) {
|
|
276
|
+
out[row.id] = {
|
|
277
|
+
sourceSystemId: row.sourceSystemId,
|
|
278
|
+
targetSystemId: row.targetSystemId,
|
|
279
|
+
impactType: row.impactType,
|
|
280
|
+
transitive: row.transitive,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
return out;
|
|
284
|
+
}
|
|
285
|
+
|
|
222
286
|
// ===========================================================================
|
|
223
287
|
// NODE POSITIONS
|
|
224
288
|
// ===========================================================================
|