Skip to main content
Glama
relationships.ts11.2 kB
// HUMMBL Relationships API Routes // CRUD endpoints for model relationship management import { Hono } from "hono"; import type { AppContext } from "../api.js"; import { createD1Client } from "../storage/d1-client.js"; import type { D1Database, KVNamespace } from "@cloudflare/workers-types"; import type { CreateRelationshipRequest, UpdateRelationshipRequest, RelationshipQuery, ModelRelationshipsResponse, GraphExport, GraphNode, GraphEdge, CytoscapeGraph, CytoscapeElement, Confidence, ReviewStatus, } from "../types/relationships.js"; import { isRelationshipType, isDirection, isConfidence, isReviewStatus, } from "../types/relationships.js"; const router = new Hono<{ Bindings: { DB: D1Database; API_KEYS: KVNamespace; SESSIONS: KVNamespace }; }>(); /** * GET /v1/relationships * List relationships with optional filtering */ router.get("/relationships", async (c: AppContext) => { try { const db = createD1Client(c.env.DB); const typeParam = c.req.query("type"); const confidenceParam = c.req.query("confidence"); const statusParam = c.req.query("status"); const query: RelationshipQuery = { model: c.req.query("model"), type: typeParam && isRelationshipType(typeParam) ? typeParam : undefined, confidence: confidenceParam && isConfidence(confidenceParam) ? confidenceParam : undefined, status: statusParam && isReviewStatus(statusParam) ? statusParam : undefined, limit: c.req.query("limit") ? parseInt(c.req.query("limit")!) : undefined, offset: c.req.query("offset") ? parseInt(c.req.query("offset")!) : undefined, }; const relationships = await db.getRelationships(query); const total = relationships.length; return c.json({ relationships, total, limit: query.limit || 50, offset: query.offset || 0, }); } catch { return c.json({ error: "Internal server error" }, 500); } }); /** * GET /v1/relationships/:id * Get single relationship by ID */ router.get("/relationships/:id", async (c: AppContext) => { try { const db = createD1Client(c.env.DB); const id = c.req.param("id"); const result = await db.getRelationship(id); if (!result) { return c.json({ error: "Relationship not found" }, 404); } return c.json(result); } catch { return c.json({ error: "Internal server error" }, 500); } }); /** * GET /v1/models/:code/relationships * Get relationships for a specific model */ router.get("/models/:code/relationships", async (c: AppContext) => { try { const db = createD1Client(c.env.DB); const code = c.req.param("code").toUpperCase(); const relationshipsData = await db.getRelationships({ model: code }); const relationships: ModelRelationshipsResponse["relationships"] = relationshipsData.map( (rel) => { const isModelA = rel.model_a === code; return { related_model: isModelA ? rel.model_b : rel.model_a, type: rel.relationship_type, direction: isModelA ? "outgoing" : "incoming", confidence: rel.confidence, logical_derivation: rel.logical_derivation, relationship_id: rel.id, }; } ); return c.json({ model: code, relationships, }); } catch { return c.json({ error: "Internal server error" }, 500); } }); /** * POST /v1/relationships * Create new relationship (admin only) */ router.post("/relationships", async (c: AppContext) => { try { // Check if user has admin permissions const user = c.get("user"); if (!user?.permissions.includes("admin:*")) { return c.json({ error: "Admin permissions required" }, 403); } const db = createD1Client(c.env.DB); const body: CreateRelationshipRequest = await c.req.json(); // Validate required fields if ( !body.model_a || !body.model_b || !body.relationship_type || !body.direction || !body.logical_derivation ) { return c.json({ error: "Missing required fields" }, 400); } // Validate relationship type if (!isRelationshipType(body.relationship_type)) { return c.json({ error: "Invalid relationship type" }, 400); } // Validate direction if (!isDirection(body.direction)) { return c.json({ error: "Invalid direction" }, 400); } // Validate confidence if provided if (body.confidence && !isConfidence(body.confidence)) { return c.json({ error: "Invalid confidence level" }, 400); } // Ensure confidence is valid for RelationshipInput (A, B, or C) // Default to "C" if not provided, or convert "U" to "C" const validConfidence = body.confidence && body.confidence !== "U" ? (body.confidence as "A" | "B" | "C") : "C"; // Generate ID const id = `R${Date.now().toString().slice(-6)}`; const relationshipData = { id, model_a: body.model_a.toUpperCase(), model_b: body.model_b.toUpperCase(), relationship_type: body.relationship_type, direction: body.direction, confidence: body.confidence || "U", // Keep "U" for relationshipData (full ModelRelationship) logical_derivation: body.logical_derivation, has_literature_support: body.literature_support?.has_support ? 1 : 0, literature_citation: body.literature_support?.citation, literature_url: body.literature_support?.url, empirical_observation: body.empirical_observation, validated_by: user.name, validated_at: new Date().toISOString(), review_status: "draft" as const, notes: body.notes, }; // Convert to RelationshipInput format const relationshipInput = { source_code: relationshipData.model_a, target_code: relationshipData.model_b, relationship_type: relationshipData.relationship_type, confidence: validConfidence, evidence: relationshipData.logical_derivation, }; const result = await db.createRelationship(relationshipInput); return c.json(result, 201); } catch { return c.json({ error: "Internal server error" }, 500); } }); /** * PATCH /v1/relationships/:id * Update relationship (admin only) */ router.patch("/relationships/:id", async (c: AppContext) => { try { // Check if user has admin permissions const user = c.get("user"); if (!user?.permissions.includes("admin:*")) { return c.json({ error: "Admin permissions required" }, 403); } const db = createD1Client(c.env.DB); const id = c.req.param("id"); const body: UpdateRelationshipRequest = await c.req.json(); // Validate fields if provided if (body.relationship_type && !isRelationshipType(body.relationship_type)) { return c.json({ error: "Invalid relationship type" }, 400); } if (body.direction && !isDirection(body.direction)) { return c.json({ error: "Invalid direction" }, 400); } if (body.confidence && !isConfidence(body.confidence)) { return c.json({ error: "Invalid confidence level" }, 400); } if (body.review_status && !isReviewStatus(body.review_status)) { return c.json({ error: "Invalid review status" }, 400); } const updates: Partial<Parameters<typeof db.updateRelationship>[1]> = {}; if (body.relationship_type) updates.relationship_type = body.relationship_type; if (body.confidence) updates.confidence = body.confidence; if (body.logical_derivation) updates.logical_derivation = body.logical_derivation; const result = await db.updateRelationship(id, updates); return c.json({ success: true, relationship: result }); } catch { return c.json({ error: "Internal server error" }, 500); } }); /** * DELETE /v1/relationships/:id * Delete relationship (admin only) */ router.delete("/relationships/:id", async (c: AppContext) => { try { // Check if user has admin permissions const user = c.get("user"); if (!user?.permissions.includes("admin:*")) { return c.json({ error: "Admin permissions required" }, 403); } const db = createD1Client(c.env.DB); const id = c.req.param("id"); // Check if relationship exists const existing = await db.getRelationship(id); if (!existing) { return c.json({ error: "Relationship not found" }, 404); } // TODO: Implement deleteRelationship method in D1Client // For now, return error return c.json({ error: "Delete not yet implemented" }, 501); } catch { return c.json({ error: "Internal server error" }, 500); } }); /** * GET /v1/graph * Export graph data for visualization */ router.get("/graph", async (c: AppContext) => { try { const db = createD1Client(c.env.DB); const format = c.req.query("format") || "json"; const confidenceMin = c.req.query("confidence_min") || "C"; const status = c.req.query("status") || "confirmed"; // Get all relationships const relationships = await db.getRelationships({ confidence: confidenceMin, status, limit: 1000, }); // Build nodes and edges from relationships const nodeSet = new Set<string>(); const nodes: GraphNode[] = []; const edges: GraphEdge[] = []; relationships.forEach((rel) => { nodeSet.add(rel.model_a); nodeSet.add(rel.model_b); edges.push({ source: rel.model_a, target: rel.model_b, type: rel.relationship_type, confidence: rel.confidence, direction: rel.direction || "a→b", logical_derivation: rel.logical_derivation, }); }); // Create nodes (simplified - would need model lookup for full data) nodeSet.forEach((modelCode) => { // Extract transformation from model code (e.g., "DE1" -> "DE") const transformation = modelCode.match(/^([A-Z]+)/)?.[1] || ""; nodes.push({ id: modelCode, name: modelCode, transformation }); }); if (format === "cytoscape") { const elements: CytoscapeElement[] = [ ...nodes.map((node) => ({ data: { id: node.id, label: node.name, type: "node", transformation: node.transformation, }, })), ...edges.map((edge) => ({ data: { id: `${edge.source}-${edge.target}`, source: edge.source, target: edge.target, type: edge.type, confidence: edge.confidence, direction: edge.direction, }, })), ]; return c.json({ elements } as CytoscapeGraph); } // Default JSON format const graphExport: GraphExport = { nodes, edges, metadata: { total_nodes: nodes.length, total_edges: edges.length, confidence_filter: (confidenceMin && isConfidence(confidenceMin) ? confidenceMin : "C") as Confidence, status_filter: (status && isReviewStatus(status) ? status : "confirmed") as ReviewStatus, generated_at: new Date().toISOString(), }, }; return c.json(graphExport); } catch { return c.json({ error: "Internal server error" }, 500); } }); export default router;

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/hummbl-dev/mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server