@checkstack/dependency-common 0.2.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 ADDED
@@ -0,0 +1,33 @@
1
+ # @checkstack/dependency-common
2
+
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 3f36a64: Add System Dependencies plugin
8
+
9
+ Introduces the system dependencies feature with three new core plugins and
10
+ extends the catalog with a new SystemEditorSlot extension point.
11
+
12
+ **New plugins:**
13
+
14
+ - **dependency-common**: Shared Zod schemas, RPC contract with resource-level access control, signal definitions, and routes
15
+ - **dependency-backend**: Drizzle schema, DependencyService with cycle detection, WarningEvaluationService with transitive impact matrix, RPC router with signal broadcasting, and per-user canvas node position persistence
16
+ - **dependency-frontend**: DependencyBadge (dashboard), DependencyAlert (system details), DependencyEditor (system editor dialog), and interactive DependencyMapPage (React Flow canvas)
17
+
18
+ **Catalog extensions:**
19
+
20
+ - **catalog-common**: New `SystemEditorSlot` for plugin-injected sections in the system editor dialog
21
+ - **catalog-frontend**: `SystemEditor` renders the slot after TeamAccessEditor for existing systems
22
+
23
+ **Key capabilities:**
24
+
25
+ - Directional dependency edges between systems (source depends on target)
26
+ - Three impact types: informational, degraded, critical
27
+ - Transitive multi-hop warning propagation with toggle switch
28
+ - Cycle detection at creation time with graphical chain visualization
29
+ - Health check-level dependency rules
30
+ - Interactive dependency map with drag-to-connect, edge click editor, and auto-saving node positions
31
+ - Inline editing of dependencies in both the system editor and the map canvas
32
+ - Team-based resource-level access control on all mutation endpoints
33
+ - Realtime signal-driven UI updates
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@checkstack/dependency-common",
3
+ "version": "0.2.0",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": {
7
+ "import": "./src/index.ts"
8
+ }
9
+ },
10
+ "dependencies": {
11
+ "@checkstack/common": "0.6.4",
12
+ "@checkstack/frontend-api": "0.3.8",
13
+ "@checkstack/signal-common": "0.1.8",
14
+ "@orpc/contract": "^1.13.14",
15
+ "zod": "^4.2.1"
16
+ },
17
+ "devDependencies": {
18
+ "typescript": "^5.7.2",
19
+ "@checkstack/tsconfig": "0.0.4",
20
+ "@checkstack/scripts": "0.1.2"
21
+ },
22
+ "scripts": {
23
+ "typecheck": "tsc --noEmit",
24
+ "lint": "bun run lint:code",
25
+ "lint:code": "eslint . --max-warnings 0"
26
+ },
27
+ "checkstack": {
28
+ "type": "common"
29
+ }
30
+ }
package/src/access.ts ADDED
@@ -0,0 +1,36 @@
1
+ import { accessPair } from "@checkstack/common";
2
+
3
+ /**
4
+ * Access rules for the Dependency plugin.
5
+ */
6
+ export const dependencyAccess = {
7
+ /**
8
+ * Dependency access with both read and manage levels.
9
+ * Read is public by default so all users can see dependency warnings.
10
+ */
11
+ dependency: accessPair(
12
+ "dependency",
13
+ {
14
+ read: {
15
+ description: "View system dependencies and dependency warnings",
16
+ isDefault: true,
17
+ isPublic: true,
18
+ },
19
+ manage: {
20
+ description:
21
+ "Manage system dependencies - create, edit, and delete dependency relationships",
22
+ },
23
+ },
24
+ {
25
+ idParam: "systemId",
26
+ },
27
+ ),
28
+ };
29
+
30
+ /**
31
+ * All access rules for registration with the plugin system.
32
+ */
33
+ export const dependencyAccessRules = [
34
+ dependencyAccess.dependency.read,
35
+ dependencyAccess.dependency.manage,
36
+ ];
package/src/index.ts ADDED
@@ -0,0 +1,61 @@
1
+ export { dependencyAccess, dependencyAccessRules } from "./access";
2
+ export {
3
+ dependencyContract,
4
+ DependencyApi,
5
+ type DependencyContract,
6
+ } from "./rpc-contract";
7
+ export {
8
+ ImpactTypeSchema,
9
+ DerivedStateSchema,
10
+ HealthCheckRuleSchema,
11
+ DependencySchema,
12
+ AffectedUpstreamSchema,
13
+ DependencyWarningSchema,
14
+ CreateHealthCheckRuleInputSchema,
15
+ CreateDependencyInputSchema,
16
+ UpdateDependencyInputSchema,
17
+ NodePositionSchema,
18
+ type ImpactType,
19
+ type DerivedState,
20
+ type HealthCheckRule,
21
+ type Dependency,
22
+ type AffectedUpstream,
23
+ type DependencyWarning,
24
+ type CreateDependencyInput,
25
+ type UpdateDependencyInput,
26
+ type NodePosition,
27
+ } from "./schemas";
28
+ export * from "./plugin-metadata";
29
+ export { dependencyRoutes } from "./routes";
30
+
31
+ // =============================================================================
32
+ // REALTIME SIGNALS
33
+ // =============================================================================
34
+
35
+ import { createSignal } from "@checkstack/signal-common";
36
+ import { z } from "zod";
37
+
38
+ /**
39
+ * Broadcast when dependency definitions change (created, updated, deleted).
40
+ * Frontend components can refetch the dependency graph.
41
+ */
42
+ export const DEPENDENCY_CHANGED = createSignal(
43
+ "dependency.changed",
44
+ z.object({
45
+ dependencyId: z.string(),
46
+ sourceSystemId: z.string(),
47
+ targetSystemId: z.string(),
48
+ action: z.enum(["created", "updated", "deleted"]),
49
+ }),
50
+ );
51
+
52
+ /**
53
+ * Broadcast when computed dependency warnings change for one or more systems.
54
+ * Badge components listen to this to refresh without polling.
55
+ */
56
+ export const DEPENDENCY_WARNINGS_CHANGED = createSignal(
57
+ "dependency.warnings.changed",
58
+ z.object({
59
+ affectedSystemIds: z.array(z.string()),
60
+ }),
61
+ );
@@ -0,0 +1,9 @@
1
+ import { definePluginMetadata } from "@checkstack/common";
2
+
3
+ /**
4
+ * Plugin metadata for the dependency plugin.
5
+ * Exported from the common package so both backend and frontend can reference it.
6
+ */
7
+ export const pluginMetadata = definePluginMetadata({
8
+ pluginId: "dependency",
9
+ });
package/src/routes.ts ADDED
@@ -0,0 +1,29 @@
1
+ import { createRoutes } from "@checkstack/common";
2
+
3
+ /**
4
+ * Route definitions for the dependency plugin.
5
+ * The Dependency Map page is registered under the Catalog namespace.
6
+ *
7
+ * @example Frontend plugin usage
8
+ * ```tsx
9
+ * import { dependencyRoutes } from "@checkstack/dependency-common";
10
+ *
11
+ * createFrontendPlugin({
12
+ * routes: [
13
+ * { route: dependencyRoutes.routes.map, element: <DependencyMapPage /> },
14
+ * ],
15
+ * });
16
+ * ```
17
+ *
18
+ * @example Link generation
19
+ * ```tsx
20
+ * import { dependencyRoutes } from "@checkstack/dependency-common";
21
+ * import { resolveRoute } from "@checkstack/common";
22
+ *
23
+ * const mapPath = resolveRoute(dependencyRoutes.routes.map);
24
+ * ```
25
+ */
26
+ export const dependencyRoutes = createRoutes("dependency", {
27
+ map: "/map",
28
+ systemDependencies: "/system/:systemId",
29
+ });
@@ -0,0 +1,128 @@
1
+ import { z } from "zod";
2
+ import { createClientDefinition, proc } from "@checkstack/common";
3
+ import { dependencyAccess } from "./access";
4
+ import { pluginMetadata } from "./plugin-metadata";
5
+ import {
6
+ DependencySchema,
7
+ DependencyWarningSchema,
8
+ CreateDependencyInputSchema,
9
+ UpdateDependencyInputSchema,
10
+ NodePositionSchema,
11
+ } from "./schemas";
12
+
13
+ export const dependencyContract = {
14
+ // ==========================================================================
15
+ // READ ENDPOINTS (public - accessible by anyone with read access)
16
+ // ==========================================================================
17
+
18
+ /** Get all dependencies for a system (both directions) */
19
+ getDependencies: proc({
20
+ operationType: "query",
21
+ userType: "public",
22
+ access: [dependencyAccess.dependency.read],
23
+ instanceAccess: { idParam: "systemId" },
24
+ })
25
+ .input(
26
+ z.object({
27
+ systemId: z.string(),
28
+ direction: z
29
+ .enum(["upstream", "downstream", "both"])
30
+ .optional()
31
+ .default("both"),
32
+ }),
33
+ )
34
+ .output(z.object({ dependencies: z.array(DependencySchema) })),
35
+
36
+ /** Get the full dependency graph (all dependencies, for the canvas) */
37
+ getAllDependencies: proc({
38
+ operationType: "query",
39
+ userType: "public",
40
+ access: [dependencyAccess.dependency.read],
41
+ }).output(z.object({ dependencies: z.array(DependencySchema) })),
42
+
43
+ /** Bulk-fetch derived warnings for multiple systems (for dashboard badges) */
44
+ getWarnings: proc({
45
+ operationType: "query",
46
+ userType: "public",
47
+ access: [dependencyAccess.dependency.read],
48
+ })
49
+ .input(z.object({ systemIds: z.array(z.string()) }))
50
+ .output(
51
+ z.object({
52
+ warnings: z.record(z.string(), DependencyWarningSchema),
53
+ }),
54
+ ),
55
+
56
+ /** Get dependency warnings for a single system */
57
+ getWarningsForSystem: proc({
58
+ operationType: "query",
59
+ userType: "public",
60
+ access: [dependencyAccess.dependency.read],
61
+ })
62
+ .input(z.object({ systemId: z.string() }))
63
+ .output(DependencyWarningSchema.nullable()),
64
+
65
+ // ==========================================================================
66
+ // MANAGEMENT ENDPOINTS (authenticated with manage access)
67
+ // ==========================================================================
68
+
69
+ /** Create a new dependency with cycle detection */
70
+ createDependency: proc({
71
+ operationType: "mutation",
72
+ userType: "authenticated",
73
+ access: [dependencyAccess.dependency.manage],
74
+ instanceAccess: { idParam: "sourceSystemId" },
75
+ })
76
+ .input(CreateDependencyInputSchema)
77
+ .output(DependencySchema),
78
+
79
+ /** Update an existing dependency */
80
+ updateDependency: proc({
81
+ operationType: "mutation",
82
+ userType: "authenticated",
83
+ access: [dependencyAccess.dependency.manage],
84
+ instanceAccess: { idParam: "systemId" },
85
+ })
86
+ .input(UpdateDependencyInputSchema)
87
+ .output(DependencySchema),
88
+
89
+ /** Delete a dependency */
90
+ deleteDependency: proc({
91
+ operationType: "mutation",
92
+ userType: "authenticated",
93
+ access: [dependencyAccess.dependency.manage],
94
+ instanceAccess: { idParam: "systemId" },
95
+ })
96
+ .input(z.object({ id: z.string(), systemId: z.string() }))
97
+ .output(z.object({ success: z.boolean() })),
98
+
99
+ // ==========================================================================
100
+ // NODE POSITIONS (authenticated - per-user canvas layout persistence)
101
+ // ==========================================================================
102
+
103
+ /** Get saved node positions for the dependency map canvas */
104
+ getNodePositions: proc({
105
+ operationType: "query",
106
+ userType: "user",
107
+ access: [dependencyAccess.dependency.read],
108
+ }).output(z.object({ positions: z.array(NodePositionSchema) })),
109
+
110
+ /** Save node positions for the dependency map canvas */
111
+ saveNodePositions: proc({
112
+ operationType: "mutation",
113
+ userType: "user",
114
+ access: [dependencyAccess.dependency.read],
115
+ })
116
+ .input(z.object({ positions: z.array(NodePositionSchema) }))
117
+ .output(z.object({ success: z.boolean() })),
118
+ };
119
+
120
+ // Export contract type
121
+ export type DependencyContract = typeof dependencyContract;
122
+
123
+ // Export client definition for type-safe forPlugin usage
124
+ // Use: const client = rpcApi.forPlugin(DependencyApi);
125
+ export const DependencyApi = createClientDefinition(
126
+ dependencyContract,
127
+ pluginMetadata,
128
+ );
package/src/schemas.ts ADDED
@@ -0,0 +1,148 @@
1
+ import { z } from "zod";
2
+
3
+ // =============================================================================
4
+ // ENUMS
5
+ // =============================================================================
6
+
7
+ /**
8
+ * Impact type determines how an upstream system's status affects the downstream.
9
+ * - informational: Show a link/badge, no status impact
10
+ * - degraded: Downstream shows as degraded when upstream is affected
11
+ * - critical: Downstream shows as degraded when upstream is degraded, down when upstream is down
12
+ */
13
+ export const ImpactTypeSchema = z.enum([
14
+ "informational",
15
+ "degraded",
16
+ "critical",
17
+ ]);
18
+ export type ImpactType = z.infer<typeof ImpactTypeSchema>;
19
+
20
+ /**
21
+ * Derived warning state computed from dependency evaluation.
22
+ */
23
+ export const DerivedStateSchema = z.enum(["info", "degraded", "down"]);
24
+ export type DerivedState = z.infer<typeof DerivedStateSchema>;
25
+
26
+ // =============================================================================
27
+ // HEALTH CHECK RULE
28
+ // =============================================================================
29
+
30
+ /**
31
+ * Optional advanced rule linking a dependency to a specific health check.
32
+ * When rules exist on a dependency, only specified checks trigger the impact.
33
+ */
34
+ export const HealthCheckRuleSchema = z.object({
35
+ id: z.string(),
36
+ dependencyId: z.string(),
37
+ healthCheckId: z.string(),
38
+ overrideImpactType: ImpactTypeSchema,
39
+ });
40
+ export type HealthCheckRule = z.infer<typeof HealthCheckRuleSchema>;
41
+
42
+ // =============================================================================
43
+ // DEPENDENCY ENTITY
44
+ // =============================================================================
45
+
46
+ /**
47
+ * Core dependency entity representing a directional edge between two systems.
48
+ * sourceSystemId (downstream) depends on targetSystemId (upstream).
49
+ */
50
+ export const DependencySchema = z.object({
51
+ id: z.string(),
52
+ sourceSystemId: z.string(),
53
+ targetSystemId: z.string(),
54
+ impactType: ImpactTypeSchema,
55
+ transitive: z.boolean(),
56
+ label: z.string().nullable(),
57
+ healthCheckRules: z.array(HealthCheckRuleSchema).optional(),
58
+ createdAt: z.date(),
59
+ updatedAt: z.date(),
60
+ });
61
+ export type Dependency = z.infer<typeof DependencySchema>;
62
+
63
+ // =============================================================================
64
+ // DEPENDENCY WARNING (Computed, not persisted)
65
+ // =============================================================================
66
+
67
+ /**
68
+ * Information about a single affected upstream system contributing to a warning.
69
+ */
70
+ export const AffectedUpstreamSchema = z.object({
71
+ systemId: z.string(),
72
+ systemName: z.string(),
73
+ ownStatus: z.string(),
74
+ impactType: ImpactTypeSchema,
75
+ dependencyLabel: z.string().nullable(),
76
+ });
77
+ export type AffectedUpstream = z.infer<typeof AffectedUpstreamSchema>;
78
+
79
+ /**
80
+ * Computed dependency warning for a system.
81
+ * Represents the worst derived state across all upstream dependencies.
82
+ */
83
+ export const DependencyWarningSchema = z.object({
84
+ systemId: z.string(),
85
+ derivedState: DerivedStateSchema,
86
+ affectedUpstreams: z.array(AffectedUpstreamSchema),
87
+ });
88
+ export type DependencyWarning = z.infer<typeof DependencyWarningSchema>;
89
+
90
+ // =============================================================================
91
+ // INPUT SCHEMAS
92
+ // =============================================================================
93
+
94
+ /**
95
+ * Input for creating a health check rule within a dependency.
96
+ */
97
+ export const CreateHealthCheckRuleInputSchema = z.object({
98
+ healthCheckId: z.string(),
99
+ overrideImpactType: ImpactTypeSchema,
100
+ });
101
+
102
+ /**
103
+ * Input for creating a new dependency.
104
+ */
105
+ export const CreateDependencyInputSchema = z
106
+ .object({
107
+ sourceSystemId: z.string().min(1, "Source system is required"),
108
+ targetSystemId: z.string().min(1, "Target system is required"),
109
+ impactType: ImpactTypeSchema,
110
+ transitive: z.boolean().optional().default(false),
111
+ label: z.string().optional(),
112
+ healthCheckRules: z
113
+ .array(CreateHealthCheckRuleInputSchema)
114
+ .optional()
115
+ .default([]),
116
+ })
117
+ .refine(
118
+ ({ sourceSystemId, targetSystemId }) => sourceSystemId !== targetSystemId,
119
+ { message: "A system cannot depend on itself" },
120
+ );
121
+ export type CreateDependencyInput = z.infer<typeof CreateDependencyInputSchema>;
122
+
123
+ /**
124
+ * Input for updating an existing dependency.
125
+ */
126
+ export const UpdateDependencyInputSchema = z.object({
127
+ id: z.string(),
128
+ systemId: z.string(),
129
+ impactType: ImpactTypeSchema.optional(),
130
+ transitive: z.boolean().optional(),
131
+ label: z.string().nullable().optional(),
132
+ healthCheckRules: z.array(CreateHealthCheckRuleInputSchema).optional(),
133
+ });
134
+ export type UpdateDependencyInput = z.infer<typeof UpdateDependencyInputSchema>;
135
+
136
+ // =============================================================================
137
+ // NODE POSITION (persisted server-side for the dependency map canvas)
138
+ // =============================================================================
139
+
140
+ /**
141
+ * Persisted node position for the dependency map canvas.
142
+ */
143
+ export const NodePositionSchema = z.object({
144
+ systemId: z.string(),
145
+ x: z.number(),
146
+ y: z.number(),
147
+ });
148
+ export type NodePosition = z.infer<typeof NodePositionSchema>;
@@ -0,0 +1,96 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import {
3
+ CreateDependencyInputSchema,
4
+ ImpactTypeSchema,
5
+ DerivedStateSchema,
6
+ } from "@checkstack/dependency-common";
7
+
8
+ describe("dependency-common schemas", () => {
9
+ describe("ImpactTypeSchema", () => {
10
+ test("accepts valid impact types", () => {
11
+ expect(ImpactTypeSchema.parse("informational")).toBe("informational");
12
+ expect(ImpactTypeSchema.parse("degraded")).toBe("degraded");
13
+ expect(ImpactTypeSchema.parse("critical")).toBe("critical");
14
+ });
15
+
16
+ test("rejects invalid impact types", () => {
17
+ expect(() => ImpactTypeSchema.parse("invalid")).toThrow();
18
+ });
19
+ });
20
+
21
+ describe("DerivedStateSchema", () => {
22
+ test("accepts valid derived states", () => {
23
+ expect(DerivedStateSchema.parse("info")).toBe("info");
24
+ expect(DerivedStateSchema.parse("degraded")).toBe("degraded");
25
+ expect(DerivedStateSchema.parse("down")).toBe("down");
26
+ });
27
+
28
+ test("rejects invalid derived states", () => {
29
+ expect(() => DerivedStateSchema.parse("invalid")).toThrow();
30
+ });
31
+ });
32
+
33
+ describe("CreateDependencyInputSchema", () => {
34
+ test("accepts valid input", () => {
35
+ const input = CreateDependencyInputSchema.parse({
36
+ sourceSystemId: "sys-a",
37
+ targetSystemId: "sys-b",
38
+ impactType: "degraded",
39
+ });
40
+
41
+ expect(input.sourceSystemId).toBe("sys-a");
42
+ expect(input.targetSystemId).toBe("sys-b");
43
+ expect(input.impactType).toBe("degraded");
44
+ expect(input.transitive).toBe(false); // default
45
+ expect(input.healthCheckRules).toEqual([]); // default
46
+ });
47
+
48
+ test("rejects self-referencing dependency", () => {
49
+ expect(() =>
50
+ CreateDependencyInputSchema.parse({
51
+ sourceSystemId: "sys-a",
52
+ targetSystemId: "sys-a",
53
+ impactType: "degraded",
54
+ }),
55
+ ).toThrow("A system cannot depend on itself");
56
+ });
57
+
58
+ test("rejects empty source system", () => {
59
+ expect(() =>
60
+ CreateDependencyInputSchema.parse({
61
+ sourceSystemId: "",
62
+ targetSystemId: "sys-b",
63
+ impactType: "degraded",
64
+ }),
65
+ ).toThrow();
66
+ });
67
+
68
+ test("rejects empty target system", () => {
69
+ expect(() =>
70
+ CreateDependencyInputSchema.parse({
71
+ sourceSystemId: "sys-a",
72
+ targetSystemId: "",
73
+ impactType: "degraded",
74
+ }),
75
+ ).toThrow();
76
+ });
77
+
78
+ test("accepts health check rules", () => {
79
+ const input = CreateDependencyInputSchema.parse({
80
+ sourceSystemId: "sys-a",
81
+ targetSystemId: "sys-b",
82
+ impactType: "degraded",
83
+ healthCheckRules: [
84
+ {
85
+ healthCheckId: "hc-1",
86
+ overrideImpactType: "critical",
87
+ },
88
+ ],
89
+ });
90
+
91
+ expect(input.healthCheckRules).toHaveLength(1);
92
+ expect(input.healthCheckRules[0].healthCheckId).toBe("hc-1");
93
+ expect(input.healthCheckRules[0].overrideImpactType).toBe("critical");
94
+ });
95
+ });
96
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "extends": "@checkstack/tsconfig/common.json",
3
+ "include": [
4
+ "src"
5
+ ]
6
+ }