@checkstack/dependency-backend 1.1.6 → 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/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, context }) => {
193
+ async ({ input }) => {
185
194
  try {
186
- const result = await service.createDependency(input);
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, context }) => {
225
- const result = await service.updateDependency(input);
226
- if (!result) {
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
- await signalService.broadcast(DEPENDENCY_CHANGED, {
233
- dependencyId: result.id,
234
- sourceSystemId: result.sourceSystemId,
235
- targetSystemId: result.targetSystemId,
236
- action: "updated",
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 context.emitHook(dependencyHooks.dependencyUpdated, {
275
+ await signalService.broadcast(DEPENDENCY_CHANGED, {
240
276
  dependencyId: result.id,
241
277
  sourceSystemId: result.sourceSystemId,
242
278
  targetSystemId: result.targetSystemId,
243
- impactType: result.impactType,
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, context }) => {
291
+ async ({ input }) => {
256
292
  const existing = await service.getDependencyById(input.id);
257
- const success = await service.deleteDependency(input.id);
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(input: CreateDependencyInput): Promise<Dependency> {
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
  // ===========================================================================
package/tsconfig.json CHANGED
@@ -4,6 +4,9 @@
4
4
  "src"
5
5
  ],
6
6
  "references": [
7
+ {
8
+ "path": "../automation-backend"
9
+ },
7
10
  {
8
11
  "path": "../backend-api"
9
12
  },