Architecture
The "depends" project uses a directed graph model to manage entities as nodes with dependencies, ensuring no cycles exist. Each node has a state indicating its health, such as "green" (healthy), "yellow" (warning), or "red" (critical). This architecture leverages a SQLite database in Write-Ahead Logging (WAL) mode for concurrent reads and writes, storing relationships and enabling dynamic updates. Namespaces provide isolated environments, while authentication via Bearer tokens ensures secure access. For setup details, see Installation or Deployment.
Core Model
The core model represents entities as nodes in a directed graph, where nodes depend on others via edges. This structure manages dependencies without cycles, with the system checking for cycles during configuration import. Nodes are defined in configuration files and stored in the database with properties such as label, dependencies, default state, and metadata.
Nodes
Nodes are the fundamental entities in the graph, each with a unique ID. Properties include:
- label: A human-readable name (optional).
- depends_on: An array of node IDs that this node depends on, forming directed edges (optional).
- default_state: The initial state, which can be "green", "yellow", or "red" (optional; defaults to "yellow").
- meta: A key-value object for additional metadata (optional).
- ttl: A time-to-live value, after which the node transitions to "yellow" if no update is received.
Example configuration:
namespace: my-project
nodes:
database:
label: Database
depends_on:
- api-server
api-server:
label: API Server
default_state: yellow
meta:
version: 1.0
In the database, nodes are stored in a table with these properties, along with fields for state management and timestamps. Nodes can be managed through API endpoints, such as:
- Create or Update Node: PUT /v1/nodes/:namespace/:nodeId – Creates or updates a node, including its state, dependencies, and metadata, while checking for cycles.
- Get Node: GET /v1/nodes/:namespace/:nodeId – Retrieves details, including effective state and dependencies.
- Delete Node: DELETE /v1/nodes/:namespace/:nodeId – Deletes a specified node.
- List Nodes: GET /v1/nodes/:namespace – Lists all nodes in a namespace.
States
States indicate a node's health, with possible values of "green", "yellow", or "red", following a priority order where "red" is the most severe. A node's effective state is the worst state among itself and its transitive dependencies, computed using a breadth-first search (BFS) traversal.
The computation is handled as follows:
export function computeEffectiveState(
db: Database,
nsId: number,
nodeId: string
): string {
const node = db
.query("SELECT state, ttl, last_state_write FROM nodes WHERE ns_id = ? AND id = ?")
.get(nsId, nodeId) as { state: string; ttl: number | null; last_state_write: string | null } | null;
if (!node) throw new Error(`Node not found: ${nodeId}`);
let worst = resolveNodeState(node);
const visited = new Set<string>();
const queue = [nodeId];
while (queue.length > 0) {
const current = queue.shift()!;
if (visited.has(current)) continue;
visited.add(current);
const deps = db
.query("SELECT to_node FROM edges WHERE ns_id = ? AND from_node = ?")
.all(nsId, current) as { to_node: string }[];
for (const dep of deps) {
const depNode = db
.query("SELECT state, ttl, last_state_write FROM nodes WHERE ns_id = ? AND id = ?")
.get(nsId, dep.to_node) as { state: string; ttl: number | null; last_state_write: string | null } | null;
if (depNode) {
worst = worstState(worst, resolveNodeState(depNode));
if (!visited.has(dep.to_node)) {
queue.push(dep.to_node);
}
}
}
}
return worst;
}
States can be updated via:
- Update Node State: PUT /v1/state/:namespace/:nodeId/:state – Updates the state of a node and triggers notifications if changed.
API Overview
The API is the primary interface for interacting with the "depends" system, handling operations related to namespaces, nodes, graphs, events, and notifications. It uses RESTful principles, JSON for data exchange, and Bearer tokens for authentication. The verifyToken function ensures users can only access their own data.
Authentication
Tokens are verified against the database to confirm namespace ownership. Include the token in the Authorization header as Bearer .
Example:
export async function verifyToken(
db: Database,
namespace: string,
token: string,
isLocal: boolean = false
): Promise<AuthResult | null> {
// Verification logic as described
}
Endpoints
Endpoints are organized by resource type for modularity.
Namespaces
- POST /v1/namespaces – Creates a new namespace. This involves checking for a valid ID format (lowercase letters, numbers, and hyphens), ensuring uniqueness within the user's namespaces, and inserting it into the database linked to the token ID.
- DELETE /v1/namespaces/:namespace – Deletes a namespace and all associated data, such as nodes, edges, and events, due to foreign key constraints in the database schema. The
handleDeleteNamespacefunction manages this:
export function handleDeleteNamespace(
db: Database,
nsId: number
): Response {
db.query("DELETE FROM namespaces WHERE ns_id = ?").run(nsId);
return new Response(null, { status: 204 });
}
Nodes
As detailed in the Core Model section.
Events
- GET /v1/events/:namespace[/:nodeId] – Retrieves events, with optional query parameters like
sinceandlimit.
Graph
- GET /v1/graph/:namespace – Retrieves the full graph, optionally in YAML format.
- PUT /v1/graph/:namespace – Imports a graph from YAML, checking for cycles.
- GET /v1/graph/:namespace/:nodeId – Retrieves the subgraph for a specific node.
Notifications
- PUT /v1/notifications/:namespace – Creates or updates a notification rule.
- GET /v1/notifications/:namespace – Lists notification rules.
- DELETE /v1/notifications/:namespace/:ruleId – Deletes a rule.
Example configuration for a notification rule:
notifications:
alert-webhook:
watch: api-server
on: red
url: https://example.com/webhook
secret: mysecret
Usage
- GET /v1/usage/:namespace – Retrieves usage statistics.
For advanced topics, refer to Migration Guide or Log.
Querying the Graph
Graph queries retrieve related nodes, such as upstream and downstream dependencies, using BFS for efficiency. This design minimizes memory usage by querying the database on-the-fly.
The getUpstreamNodes function performs this search:
export function getUpstreamNodes(
db: Database,
nsId: number,
nodeId: string
): string[] {
const upstream: string[] = [];
const visited = new Set<string>();
const queue = [nodeId];
while (queue.length > 0) {
const current = queue.shift()!;
if (visited.has(current)) continue;
visited.add(current);
const deps = db
.query("SELECT to_node FROM edges WHERE ns_id = ? AND from_node = ?")
.all(nsId, current) as { to_node: string }[];
for (const dep of deps) {
if (!visited.has(dep.to_node)) {
upstream.push(dep.to_node);
queue.push(dep.to_node);
}
}
}
return upstream;
}
Design decisions, such as using BFS, ensure performance for large graphs and maintain a Directed Acyclic Graph (DAG). Effective state computation integrates with state updates, triggering notifications as needed.
Database
The database schema includes tables for tokens, namespaces, nodes, edges, notification rules, and events, with indexes for query optimization.
Example schema:
CREATE TABLE IF NOT EXISTS nodes (
ns_id INTEGER NOT NULL REFERENCES namespaces(ns_id) ON DELETE CASCADE,
id TEXT NOT NULL,
state TEXT NOT NULL DEFAULT 'yellow' CHECK (state IN ('green', 'yellow', 'red')),
-- Other fields...
PRIMARY KEY (ns_id, id)
);
CREATE TABLE IF NOT EXISTS edges (
ns_id INTEGER NOT NULL,
from_node TEXT NOT NULL,
to_node TEXT NOT NULL,
PRIMARY KEY (ns_id, from_node, to_node),
FOREIGN KEY (ns_id, from_node) REFERENCES nodes(ns_id, id) ON DELETE CASCADE,
FOREIGN KEY (ns_id, to_node) REFERENCES nodes(ns_id, id) ON DELETE CASCADE
);
Database interactions occur through API endpoints, such as those for namespaces and events.
Usage
The database stores and queries graphs dynamically. For example, updating a node's state triggers recomputation of effective states and notifications:
export async function handlePutState(
db: Database,
nsId: number,
namespace: string,
nodeId: string,
state: string,
req: Request,
legendumToken: string | null
): Promise<Response> {
// Update logic and notification dispatch
}
For testing, use an in-memory database option.
Recent changes
- Merged: namespaces.md → architecture.md
- Merged: configuration-file.md → architecture.md
- Merged: api-reference.md, graph-queries.md → architecture.md
- Merged: core-model.md → architecture.md
- Merged: api-reference.md → architecture.md