@checkstack/dependency-backend 1.2.0 → 1.3.1
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 +104 -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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,109 @@
|
|
|
1
1
|
# @checkstack/dependency-backend
|
|
2
2
|
|
|
3
|
+
## 1.3.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [a57f7db]
|
|
8
|
+
- Updated dependencies [0d9e5d8]
|
|
9
|
+
- @checkstack/backend-api@0.20.0
|
|
10
|
+
- @checkstack/healthcheck-backend@1.5.0
|
|
11
|
+
- @checkstack/automation-backend@0.4.0
|
|
12
|
+
- @checkstack/catalog-backend@1.3.1
|
|
13
|
+
- @checkstack/gitops-backend@0.4.1
|
|
14
|
+
|
|
15
|
+
## 1.3.0
|
|
16
|
+
|
|
17
|
+
### Minor Changes
|
|
18
|
+
|
|
19
|
+
- b995afb: Make `dependency-edge` a plugin-backed reactive entity via the Model-B entity state machine + rewire cross-plugin consumers.
|
|
20
|
+
|
|
21
|
+
Dependency defines a `dependency-edge` entity `{ sourceSystemId, targetSystemId, impactType, transitive }` keyed by dependency id. The `dependencies` table is BOTH authoritative AND the entity's current-state storage - there is no framework `entity_state` row for a dependency edge. `defineEntity` is given a plugin `read` accessor (`DependencyService.getManyEntityStates`) that projects the reactive subset straight off that table, and every reactive-state write goes through `handle.mutate` / `handle.remove`: `apply` performs the REAL `dependencies` write (the plugin's own db/tx, including the cycle/duplicate validation that may throw) and returns the new state; the framework snapshots `prev` via `read` BEFORE the write, appends the transition log, and emits `ENTITY_CHANGED` AFTER the write commits. Covered sites: create, update, delete (tombstone), plus the `dependency.create` / `dependency.remove` automation actions. Create sites pre-generate the id so the create's `prev` snapshot reads the not-yet-existing row as absent; `createDependency` accepts an optional pre-generated `id` (server-owned either way). The `dependency_derived_states` propagation cursor is declared non-reactive (bookkeeping).
|
|
22
|
+
|
|
23
|
+
A change -> trigger-event deriver reproduces the existing `dependency.created` / `.updated` / `.deleted` qualified events so automations keep firing. The old `dependency.created` / `.updated` / `.deleted` change hooks are removed; the catalog + healthcheck consumers switched from `onHook(<hook>)` to `onEntityChanged({ kind })`, all keeping `work-queue` delivery (cleanup + downstream-propagation are side-effecting writes that must run once per cluster):
|
|
24
|
+
|
|
25
|
+
- `dependency-system-cleanup`: reacts to `catalog-system` tombstones (`change.next === null`).
|
|
26
|
+
- `dependency-notification-evaluator` / `-recovery`: react to `health` changes filtered to a degraded / recovered transition via `classifyHealthChange`, reproducing the old `systemDegraded` / `systemHealthy` predicates.
|
|
27
|
+
|
|
28
|
+
`@checkstack/automation-backend` adds `makeEntityDrivenTriggerSetup()` - a no-op `setup` factory so a migrated domain's lifecycle triggers stay in the editor's trigger catalog (and register cleanly) while being fired by the entity change deriver via Stage-1 routing rather than a hook.
|
|
29
|
+
|
|
30
|
+
BREAKING CHANGES:
|
|
31
|
+
|
|
32
|
+
- The `dependency.created` / `dependency.updated` / `dependency.deleted` cross-plugin hooks (the `createHook` descriptors) are removed. Dependency lifecycle is now the reactive `dependency-edge` entity; the matching trigger events still fire (via the entity change deriver), so existing automations on `dependency.created/.updated/.deleted` keep working. The `dependency.impact_propagated` hook is KEPT (a derived fan-out signal, not a single mutable field). No in-repo plugin subscribed to the removed hooks.
|
|
33
|
+
- On the RPC create path, the `dependency.created` entity emit (via `mutate`) now precedes the `DEPENDENCY_CHANGED` realtime signal broadcast (previously the signal fired first, then the mirror); both still fire on a successful create.
|
|
34
|
+
- NARROWING: `dependency.updated` now fires only on a change to the REACTIVE state (`impactType`, `source`, `target`, or `transitive`). A label-only edit no longer fires `dependency.updated` (the label is not reactive entity state). Re-author any automation that needed to react to a label-only dependency edit against a different signal.
|
|
35
|
+
|
|
36
|
+
- b995afb: Restore the documented domain payload fields on entity-driven automation triggers.
|
|
37
|
+
|
|
38
|
+
Migrated triggers declare domain-named `payloadSchema`s (incident `incidentId`; health `systemId` / `previousStatus`; catalog `systemId` / `changedFields`; dependency `dependencyId`), but Stage-2 dispatch built `trigger.payload` from the generic entity-change shape (`{ kind, id, prev, next, delta, ...next }`). Operator filters and templates reading `trigger.payload.incidentId` / `.systemId` / `.previousStatus` silently resolved to `undefined` — a regression vs the legacy hook payloads.
|
|
39
|
+
|
|
40
|
+
Changes:
|
|
41
|
+
|
|
42
|
+
- `@checkstack/automation-backend`: `registerChangeDeriver` now accepts an optional per-kind `toPayload(changed) => Record<string, unknown>` mapper (at most one per kind; a second distinct mapper throws). Stage-2's `changedToPayload` uses the registered mapper to build `trigger.payload` so it matches the kind's declared `payloadSchema`, falling back to the generic change shape for kinds without a mapper. New exported type `EntityChangePayloadMapper`.
|
|
43
|
+
- `@checkstack/incident-backend`, `@checkstack/healthcheck-backend`, `@checkstack/catalog-backend`, `@checkstack/dependency-backend`: implement and register a `toPayload` for each entity-driven kind so `trigger.payload` carries the legacy domain keys again.
|
|
44
|
+
|
|
45
|
+
Descriptive incident payload fields not derivable from the reactive entity state (`title`, `description`, `createdAt`, `resolvedAt`) are now OPTIONAL on the incident trigger `payloadSchema`s — they were always absent from an entity-driven payload.
|
|
46
|
+
|
|
47
|
+
### Patch Changes
|
|
48
|
+
|
|
49
|
+
- b995afb: Extract a shared `withEntityWrite` / `withEntityRemove` guard for PLUGIN-BACKED (Model B) reactive entities and refactor the per-domain copies onto it.
|
|
50
|
+
|
|
51
|
+
Every plugin-backed domain (incident, catalog, dependency, maintenance, slo, satellite) reimplemented the same "no handle wired → run the plugin write directly; handle wired → route through `handle.mutate` / `handle.remove`" guard, varying only in the id-key name. `@checkstack/automation-backend` now exports `withEntityWrite` / `withEntityRemove` (from the entity barrel) and each domain's thin, well-named wrappers (`writeIncidentEntity`, `writeMaintenanceEntity`, satellite's `mirror`, …) delegate to it, so the branch lives in exactly one place. Behavior is unchanged.
|
|
52
|
+
|
|
53
|
+
`writeHealthEntity` (healthcheck-backend) is intentionally NOT migrated onto the helper — it is genuinely bespoke (closure-captured durable state, distinct rethrow-vs-fail-soft branches, a per-system serializer, and it returns the computed state). SLO keeps its fail-soft `onError` wrapper around the shared guard.
|
|
54
|
+
|
|
55
|
+
- Updated dependencies [270ef29]
|
|
56
|
+
- Updated dependencies [b995afb]
|
|
57
|
+
- Updated dependencies [b995afb]
|
|
58
|
+
- Updated dependencies [b995afb]
|
|
59
|
+
- Updated dependencies [270ef29]
|
|
60
|
+
- Updated dependencies [270ef29]
|
|
61
|
+
- Updated dependencies [270ef29]
|
|
62
|
+
- Updated dependencies [270ef29]
|
|
63
|
+
- Updated dependencies [270ef29]
|
|
64
|
+
- Updated dependencies [270ef29]
|
|
65
|
+
- Updated dependencies [270ef29]
|
|
66
|
+
- Updated dependencies [270ef29]
|
|
67
|
+
- Updated dependencies [270ef29]
|
|
68
|
+
- Updated dependencies [b995afb]
|
|
69
|
+
- Updated dependencies [b995afb]
|
|
70
|
+
- Updated dependencies [b995afb]
|
|
71
|
+
- Updated dependencies [b995afb]
|
|
72
|
+
- Updated dependencies [270ef29]
|
|
73
|
+
- Updated dependencies [b995afb]
|
|
74
|
+
- Updated dependencies [270ef29]
|
|
75
|
+
- Updated dependencies [b995afb]
|
|
76
|
+
- Updated dependencies [b995afb]
|
|
77
|
+
- Updated dependencies [270ef29]
|
|
78
|
+
- Updated dependencies [b995afb]
|
|
79
|
+
- Updated dependencies [b995afb]
|
|
80
|
+
- Updated dependencies [270ef29]
|
|
81
|
+
- Updated dependencies [b995afb]
|
|
82
|
+
- Updated dependencies [b995afb]
|
|
83
|
+
- Updated dependencies [b995afb]
|
|
84
|
+
- Updated dependencies [b995afb]
|
|
85
|
+
- Updated dependencies [b995afb]
|
|
86
|
+
- Updated dependencies [b995afb]
|
|
87
|
+
- Updated dependencies [b995afb]
|
|
88
|
+
- Updated dependencies [270ef29]
|
|
89
|
+
- Updated dependencies [270ef29]
|
|
90
|
+
- Updated dependencies [270ef29]
|
|
91
|
+
- Updated dependencies [270ef29]
|
|
92
|
+
- Updated dependencies [270ef29]
|
|
93
|
+
- Updated dependencies [270ef29]
|
|
94
|
+
- Updated dependencies [270ef29]
|
|
95
|
+
- Updated dependencies [270ef29]
|
|
96
|
+
- Updated dependencies [b995afb]
|
|
97
|
+
- Updated dependencies [b995afb]
|
|
98
|
+
- @checkstack/backend-api@0.19.0
|
|
99
|
+
- @checkstack/automation-backend@0.3.0
|
|
100
|
+
- @checkstack/gitops-common@0.5.0
|
|
101
|
+
- @checkstack/gitops-backend@0.4.0
|
|
102
|
+
- @checkstack/healthcheck-backend@1.4.0
|
|
103
|
+
- @checkstack/healthcheck-common@1.4.0
|
|
104
|
+
- @checkstack/maintenance-common@1.3.0
|
|
105
|
+
- @checkstack/catalog-backend@1.3.0
|
|
106
|
+
|
|
3
107
|
## 1.2.0
|
|
4
108
|
|
|
5
109
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/dependency-backend",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"license": "Elastic-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -15,28 +15,28 @@
|
|
|
15
15
|
"test": "bun test"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@checkstack/backend-api": "0.
|
|
19
|
-
"@checkstack/automation-backend": "0.
|
|
20
|
-
"@checkstack/dependency-common": "1.1.
|
|
21
|
-
"@checkstack/catalog-common": "2.2.
|
|
22
|
-
"@checkstack/catalog-backend": "1.
|
|
23
|
-
"@checkstack/healthcheck-common": "1.
|
|
24
|
-
"@checkstack/healthcheck-backend": "1.
|
|
25
|
-
"@checkstack/maintenance-common": "1.2.
|
|
26
|
-
"@checkstack/incident-common": "1.3.
|
|
27
|
-
"@checkstack/notification-common": "1.2.
|
|
28
|
-
"@checkstack/signal-common": "0.2.
|
|
29
|
-
"@checkstack/gitops-backend": "0.3.
|
|
30
|
-
"@checkstack/gitops-common": "0.4.
|
|
31
|
-
"@checkstack/common": "0.
|
|
18
|
+
"@checkstack/backend-api": "0.18.0",
|
|
19
|
+
"@checkstack/automation-backend": "0.2.0",
|
|
20
|
+
"@checkstack/dependency-common": "1.1.3",
|
|
21
|
+
"@checkstack/catalog-common": "2.2.3",
|
|
22
|
+
"@checkstack/catalog-backend": "1.2.0",
|
|
23
|
+
"@checkstack/healthcheck-common": "1.3.0",
|
|
24
|
+
"@checkstack/healthcheck-backend": "1.3.0",
|
|
25
|
+
"@checkstack/maintenance-common": "1.2.3",
|
|
26
|
+
"@checkstack/incident-common": "1.3.1",
|
|
27
|
+
"@checkstack/notification-common": "1.2.1",
|
|
28
|
+
"@checkstack/signal-common": "0.2.5",
|
|
29
|
+
"@checkstack/gitops-backend": "0.3.7",
|
|
30
|
+
"@checkstack/gitops-common": "0.4.2",
|
|
31
|
+
"@checkstack/common": "0.12.0",
|
|
32
32
|
"drizzle-orm": "^0.45.0",
|
|
33
33
|
"zod": "^4.2.1",
|
|
34
34
|
"@orpc/server": "^1.13.2"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@checkstack/drizzle-helper": "0.0.5",
|
|
38
|
-
"@checkstack/scripts": "0.3.
|
|
39
|
-
"@checkstack/test-utils-backend": "0.1.
|
|
38
|
+
"@checkstack/scripts": "0.3.4",
|
|
39
|
+
"@checkstack/test-utils-backend": "0.1.31",
|
|
40
40
|
"@checkstack/tsconfig": "0.0.7",
|
|
41
41
|
"@types/bun": "^1.0.0",
|
|
42
42
|
"drizzle-kit": "^0.31.10",
|
package/src/automations.test.ts
CHANGED
|
@@ -14,7 +14,6 @@ import {
|
|
|
14
14
|
dependencyTriggers,
|
|
15
15
|
dependencyUpdatedTrigger,
|
|
16
16
|
} from "./automations";
|
|
17
|
-
import { dependencyHooks } from "./hooks";
|
|
18
17
|
import type { DependencyService } from "./services/dependency-service";
|
|
19
18
|
|
|
20
19
|
const logger = createMockLogger() as Logger;
|
|
@@ -184,7 +183,28 @@ describe("dependency.create", () => {
|
|
|
184
183
|
},
|
|
185
184
|
});
|
|
186
185
|
const emitHook = mock(async (_hook: unknown, _payload: unknown) => {});
|
|
187
|
-
|
|
186
|
+
// The action now drives the create through `handle.mutate({ id, apply })`:
|
|
187
|
+
// it pre-generates the dependency id (keying the handle), runs `apply()`
|
|
188
|
+
// (the real `createDependency`), and the handle records the resulting
|
|
189
|
+
// reactive state.
|
|
190
|
+
const mutateCalls: Array<{ id: string; next: unknown }> = [];
|
|
191
|
+
const getDependencyEntity = () =>
|
|
192
|
+
({
|
|
193
|
+
kind: "dependency-edge",
|
|
194
|
+
async mutate(input: {
|
|
195
|
+
id: string;
|
|
196
|
+
apply: () => Promise<unknown>;
|
|
197
|
+
}) {
|
|
198
|
+
const next = await input.apply();
|
|
199
|
+
mutateCalls.push({ id: input.id, next });
|
|
200
|
+
return next;
|
|
201
|
+
},
|
|
202
|
+
}) as never;
|
|
203
|
+
const [create] = createDependencyActions({
|
|
204
|
+
service,
|
|
205
|
+
emitHook: emitHook as never,
|
|
206
|
+
getDependencyEntity,
|
|
207
|
+
});
|
|
188
208
|
|
|
189
209
|
const result = await create!.execute({
|
|
190
210
|
...ctxBase,
|
|
@@ -201,9 +221,24 @@ describe("dependency.create", () => {
|
|
|
201
221
|
if (!result.success) return;
|
|
202
222
|
expect(result.externalId).toBe("dep-1");
|
|
203
223
|
expect((result.artifact as { dependencyId: string }).dependencyId).toBe("dep-1");
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
224
|
+
// The old `dependencyCreated` hook emission was replaced by driving the
|
|
225
|
+
// create through the reactive `dependency-edge` entity via `handle.mutate`
|
|
226
|
+
// (§10.5): the handle is keyed by the pre-generated dependency id, and
|
|
227
|
+
// `apply` returns the resulting reactive state.
|
|
228
|
+
expect(emitHook).not.toHaveBeenCalled();
|
|
229
|
+
expect(mutateCalls).toHaveLength(1);
|
|
230
|
+
// The id keying the handle is the pre-generated id passed into the
|
|
231
|
+
// service create (server-owned uuid), not asserted to a literal here.
|
|
232
|
+
expect(typeof mutateCalls[0]!.id).toBe("string");
|
|
233
|
+
expect(mutateCalls[0]!.next).toEqual({
|
|
234
|
+
sourceSystemId: "sys-a",
|
|
235
|
+
targetSystemId: "sys-b",
|
|
236
|
+
impactType: "critical",
|
|
237
|
+
transitive: false,
|
|
238
|
+
});
|
|
239
|
+
// The create was driven with the pre-generated id as its second arg.
|
|
240
|
+
expect(service.createMock).toHaveBeenCalledTimes(1);
|
|
241
|
+
expect(service.createMock.mock.calls[0]![1]).toBe(mutateCalls[0]!.id);
|
|
207
242
|
});
|
|
208
243
|
|
|
209
244
|
it("returns a failure when service.createDependency throws (e.g. cycle detected)", async () => {
|
|
@@ -247,7 +282,23 @@ describe("dependency.remove", () => {
|
|
|
247
282
|
deleteResult: true,
|
|
248
283
|
});
|
|
249
284
|
const emitHook = mock(async (_hook: unknown, _payload: unknown) => {});
|
|
250
|
-
|
|
285
|
+
// The action now drives the delete through `handle.remove({ id, apply })`:
|
|
286
|
+
// it runs `apply()` (the real `deleteDependency`) and the handle records
|
|
287
|
+
// the tombstoned id.
|
|
288
|
+
const removeCalls: string[] = [];
|
|
289
|
+
const getDependencyEntity = () =>
|
|
290
|
+
({
|
|
291
|
+
kind: "dependency-edge",
|
|
292
|
+
async remove(input: { id: string; apply: () => Promise<void> }) {
|
|
293
|
+
await input.apply();
|
|
294
|
+
removeCalls.push(input.id);
|
|
295
|
+
},
|
|
296
|
+
}) as never;
|
|
297
|
+
const [, remove] = createDependencyActions({
|
|
298
|
+
service,
|
|
299
|
+
emitHook: emitHook as never,
|
|
300
|
+
getDependencyEntity,
|
|
301
|
+
});
|
|
251
302
|
|
|
252
303
|
const result = await remove!.execute({
|
|
253
304
|
...ctxBase,
|
|
@@ -258,8 +309,12 @@ describe("dependency.remove", () => {
|
|
|
258
309
|
expect(result.success).toBe(true);
|
|
259
310
|
if (!result.success) return;
|
|
260
311
|
expect(result.externalId).toBe("dep-1");
|
|
261
|
-
|
|
262
|
-
|
|
312
|
+
// The old `dependencyDeleted` hook emission was replaced by driving the
|
|
313
|
+
// tombstone through the reactive `dependency-edge` entity via
|
|
314
|
+
// `handle.remove` (§10.5). The real delete ran inside `apply`.
|
|
315
|
+
expect(emitHook).not.toHaveBeenCalled();
|
|
316
|
+
expect(service.deleteMock).toHaveBeenCalledTimes(1);
|
|
317
|
+
expect(removeCalls).toEqual(["dep-1"]);
|
|
263
318
|
});
|
|
264
319
|
|
|
265
320
|
it("returns failure if the dependency does not exist", async () => {
|
package/src/automations.ts
CHANGED
|
@@ -24,14 +24,22 @@ import type {
|
|
|
24
24
|
ActionDefinition,
|
|
25
25
|
TriggerDefinition,
|
|
26
26
|
} from "@checkstack/automation-backend";
|
|
27
|
+
import { makeEntityDrivenTriggerSetup } from "@checkstack/automation-backend";
|
|
27
28
|
import { extractErrorMessage } from "@checkstack/common";
|
|
28
29
|
import {
|
|
29
30
|
DerivedStateSchema,
|
|
30
31
|
ImpactTypeSchema,
|
|
31
32
|
} from "@checkstack/dependency-common";
|
|
32
33
|
|
|
34
|
+
import type { EntityHandle } from "@checkstack/automation-backend";
|
|
33
35
|
import { dependencyHooks } from "./hooks";
|
|
34
36
|
import type { DependencyService } from "./services/dependency-service";
|
|
37
|
+
import {
|
|
38
|
+
removeDependencyEdge,
|
|
39
|
+
toDependencyEdgeState,
|
|
40
|
+
writeDependencyEdge,
|
|
41
|
+
type DependencyEdgeState,
|
|
42
|
+
} from "./dependency-entity";
|
|
35
43
|
|
|
36
44
|
// ─── Payload schemas — match the hook payloads exactly ─────────────────
|
|
37
45
|
|
|
@@ -69,6 +77,10 @@ const dependencyImpactPropagatedPayloadSchema = z.object({
|
|
|
69
77
|
|
|
70
78
|
// ─── Triggers ──────────────────────────────────────────────────────────
|
|
71
79
|
|
|
80
|
+
// These three triggers are ENTITY-DRIVEN (§10.5): the `dependency-edge`
|
|
81
|
+
// entity's change deriver fires `dependency.created/.updated/.deleted` via
|
|
82
|
+
// Stage-1 routing, so they no longer subscribe to a hook. A no-op `setup`
|
|
83
|
+
// keeps them in the editor's trigger catalog without re-introducing a hook.
|
|
72
84
|
export const dependencyCreatedTrigger: TriggerDefinition<
|
|
73
85
|
z.infer<typeof dependencyCreatedPayloadSchema>
|
|
74
86
|
> = {
|
|
@@ -78,7 +90,9 @@ export const dependencyCreatedTrigger: TriggerDefinition<
|
|
|
78
90
|
category: "Dependencies",
|
|
79
91
|
icon: "Network",
|
|
80
92
|
payloadSchema: dependencyCreatedPayloadSchema,
|
|
81
|
-
|
|
93
|
+
setup: makeEntityDrivenTriggerSetup<
|
|
94
|
+
z.infer<typeof dependencyCreatedPayloadSchema>
|
|
95
|
+
>(),
|
|
82
96
|
contextKey: (p) => p.dependencyId,
|
|
83
97
|
};
|
|
84
98
|
|
|
@@ -87,11 +101,14 @@ export const dependencyUpdatedTrigger: TriggerDefinition<
|
|
|
87
101
|
> = {
|
|
88
102
|
id: "updated",
|
|
89
103
|
displayName: "Dependency Updated",
|
|
90
|
-
description:
|
|
104
|
+
description:
|
|
105
|
+
"Fires when an existing dependency's reactive state changes (impact type, source, target, or transitivity). A label-only edit does not fire this trigger.",
|
|
91
106
|
category: "Dependencies",
|
|
92
107
|
icon: "Network",
|
|
93
108
|
payloadSchema: dependencyUpdatedPayloadSchema,
|
|
94
|
-
|
|
109
|
+
setup: makeEntityDrivenTriggerSetup<
|
|
110
|
+
z.infer<typeof dependencyUpdatedPayloadSchema>
|
|
111
|
+
>(),
|
|
95
112
|
contextKey: (p) => p.dependencyId,
|
|
96
113
|
};
|
|
97
114
|
|
|
@@ -104,7 +121,9 @@ export const dependencyDeletedTrigger: TriggerDefinition<
|
|
|
104
121
|
category: "Dependencies",
|
|
105
122
|
icon: "Network",
|
|
106
123
|
payloadSchema: dependencyDeletedPayloadSchema,
|
|
107
|
-
|
|
124
|
+
setup: makeEntityDrivenTriggerSetup<
|
|
125
|
+
z.infer<typeof dependencyDeletedPayloadSchema>
|
|
126
|
+
>(),
|
|
108
127
|
contextKey: (p) => p.dependencyId,
|
|
109
128
|
};
|
|
110
129
|
|
|
@@ -178,6 +197,8 @@ export const dependencyArtifactType = {
|
|
|
178
197
|
export interface DependencyActionDeps {
|
|
179
198
|
service: DependencyService;
|
|
180
199
|
emitHook: <T>(hook: Hook<T>, payload: T) => Promise<void>;
|
|
200
|
+
/** Resolver for the reactive `dependency-edge` entity (§10.5). */
|
|
201
|
+
getDependencyEntity?: () => EntityHandle<DependencyEdgeState> | undefined;
|
|
181
202
|
}
|
|
182
203
|
|
|
183
204
|
export function createDependencyActions(
|
|
@@ -199,19 +220,33 @@ export function createDependencyActions(
|
|
|
199
220
|
produces: "dependency.edge",
|
|
200
221
|
execute: async ({ config, logger }) => {
|
|
201
222
|
try {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
223
|
+
// Drive the create through the reactive `dependency-edge` entity
|
|
224
|
+
// (§10.5): the REAL create (with cycle/duplicate validation that may
|
|
225
|
+
// throw) runs INSIDE `apply`, so `prev` is snapshotted (absent →
|
|
226
|
+
// null) BEFORE the insert and the deriver fires `dependency.created`.
|
|
227
|
+
// The id is generated up front so the create's `prev` snapshot reads
|
|
228
|
+
// the not-yet-existing row as absent.
|
|
229
|
+
const dependencyId = crypto.randomUUID();
|
|
230
|
+
let created!: Awaited<
|
|
231
|
+
ReturnType<typeof deps.service.createDependency>
|
|
232
|
+
>;
|
|
233
|
+
await writeDependencyEdge({
|
|
234
|
+
handle: deps.getDependencyEntity?.(),
|
|
235
|
+
dependencyId,
|
|
236
|
+
apply: async () => {
|
|
237
|
+
created = await deps.service.createDependency(
|
|
238
|
+
{
|
|
239
|
+
sourceSystemId: config.sourceSystemId,
|
|
240
|
+
targetSystemId: config.targetSystemId,
|
|
241
|
+
impactType: config.impactType,
|
|
242
|
+
transitive: config.transitive,
|
|
243
|
+
label: config.label,
|
|
244
|
+
healthCheckRules: [],
|
|
245
|
+
},
|
|
246
|
+
dependencyId,
|
|
247
|
+
);
|
|
248
|
+
return toDependencyEdgeState(created);
|
|
249
|
+
},
|
|
215
250
|
});
|
|
216
251
|
logger.info(`Automation created dependency ${created.id}`);
|
|
217
252
|
return {
|
|
@@ -254,18 +289,23 @@ export function createDependencyActions(
|
|
|
254
289
|
error: `Dependency not found: ${config.dependencyId}`,
|
|
255
290
|
};
|
|
256
291
|
}
|
|
257
|
-
|
|
292
|
+
// Drive the delete through the reactive `dependency-edge` entity
|
|
293
|
+
// tombstone (§10.5); the REAL delete runs INSIDE `apply`, so `prev` is
|
|
294
|
+
// snapshotted before it and the deriver fires `dependency.deleted`.
|
|
295
|
+
let removed = false;
|
|
296
|
+
await removeDependencyEdge({
|
|
297
|
+
handle: deps.getDependencyEntity?.(),
|
|
298
|
+
dependencyId: existing.id,
|
|
299
|
+
apply: async () => {
|
|
300
|
+
removed = await deps.service.deleteDependency(config.dependencyId);
|
|
301
|
+
},
|
|
302
|
+
});
|
|
258
303
|
if (!removed) {
|
|
259
304
|
return {
|
|
260
305
|
success: false,
|
|
261
306
|
error: `Dependency ${config.dependencyId} disappeared mid-delete`,
|
|
262
307
|
};
|
|
263
308
|
}
|
|
264
|
-
await deps.emitHook(dependencyHooks.dependencyDeleted, {
|
|
265
|
-
dependencyId: existing.id,
|
|
266
|
-
sourceSystemId: existing.sourceSystemId,
|
|
267
|
-
targetSystemId: existing.targetSystemId,
|
|
268
|
-
});
|
|
269
309
|
logger.info(`Automation removed dependency ${existing.id}`);
|
|
270
310
|
return {
|
|
271
311
|
success: true,
|