Skip to main content
Glama
graph-traversal-processor.ts7.07 kB
/** * Graph Traversal Processor * Single responsibility: Navigate relationship networks with configurable depth and filtering * THE IMPLEMENTOR'S RULE: Graph exploration without the bloat */ import { getLimitsConfig } from '../../../config'; export interface GraphTraversalOptions { traverseFrom: string; traverseRelations?: string[]; maxDepth?: number; traverseDirection?: "outbound" | "inbound" | "both"; } export interface TraversalResult { id: string; name: string; type: string; distance: number; relation: string; strength?: number; source?: string; createdAt?: string; } export interface ProcessedTraversal { cypher: string; params: Record<string, any>; } export class GraphTraversalProcessor { /** * Process graph traversal into Cypher query * Zero-fallback: Invalid parameters throw errors immediately */ processTraversal(options: GraphTraversalOptions): ProcessedTraversal { this.validateTraversalOptions(options); const limits = getLimitsConfig(); const maxDepth = Math.min(options.maxDepth || 2, limits.maxTraversalDepth); const direction = options.traverseDirection || "both"; const relationTypes = options.traverseRelations; let cypher: string; const params: Record<string, any> = { startId: options.traverseFrom, maxDepth }; if (relationTypes && relationTypes.length > 0) { params.relationTypes = relationTypes; } switch (direction) { case "outbound": cypher = this.buildOutboundTraversal(maxDepth, relationTypes); break; case "inbound": cypher = this.buildInboundTraversal(maxDepth, relationTypes); break; case "both": default: cypher = this.buildBidirectionalTraversal(maxDepth, relationTypes); break; } return { cypher, params }; } /** * Build outbound traversal query (what this memory influences) */ private buildOutboundTraversal(maxDepth: number, relationTypes?: string[]): string { const relationFilter = relationTypes ? `ALL(rel IN relationships(path) WHERE rel.relationType IN $relationTypes) AND` : ''; return ` MATCH (start:Memory {id: $startId}) MATCH path = (start)-[r:RELATES_TO*1..${maxDepth}]->(end:Memory) WHERE ${relationFilter} end <> start AND end.id IS NOT NULL WITH end, length(path) as distance, relationships(path)[0] as firstRel RETURN DISTINCT end.id as id, end.name as name, end.memoryType as type, distance, firstRel.relationType as relation, firstRel.strength as strength, firstRel.source as source, firstRel.createdAt as createdAt ORDER BY distance ASC, end.name ASC LIMIT 50 `; } /** * Build inbound traversal query (what influences this memory) */ private buildInboundTraversal(maxDepth: number, relationTypes?: string[]): string { const relationFilter = relationTypes ? `ALL(rel IN relationships(path) WHERE rel.relationType IN $relationTypes) AND` : ''; return ` MATCH (target:Memory {id: $startId}) MATCH path = (start:Memory)-[r:RELATES_TO*1..${maxDepth}]->(target) WHERE ${relationFilter} start <> target AND start.id IS NOT NULL WITH start, length(path) as distance, relationships(path)[-1] as lastRel RETURN DISTINCT start.id as id, start.name as name, start.memoryType as type, distance, lastRel.relationType as relation, lastRel.strength as strength, lastRel.source as source, lastRel.createdAt as createdAt ORDER BY distance ASC, start.name ASC LIMIT 50 `; } /** * Build bidirectional traversal query (all connected memories) */ private buildBidirectionalTraversal(maxDepth: number, relationTypes?: string[]): string { const relationFilter = relationTypes ? `ALL(rel IN relationships(path) WHERE rel.relationType IN $relationTypes) AND` : ''; return ` MATCH (center:Memory {id: $startId}) MATCH path = (center)-[r:RELATES_TO*1..${maxDepth}]-(connected:Memory) WHERE ${relationFilter} connected <> center AND connected.id IS NOT NULL WITH connected, length(path) as distance, CASE WHEN startNode(relationships(path)[0]) = center THEN relationships(path)[0] ELSE relationships(path)[-1] END as relevantRel RETURN DISTINCT connected.id as id, connected.name as name, connected.memoryType as type, distance, relevantRel.relationType as relation, relevantRel.strength as strength, relevantRel.source as source, relevantRel.createdAt as createdAt ORDER BY distance ASC, connected.name ASC LIMIT 50 `; } /** * Validate traversal options */ private validateTraversalOptions(options: GraphTraversalOptions): void { if (!options.traverseFrom) { throw new Error('traverseFrom memory ID is required for graph traversal'); } const limits = getLimitsConfig(); if (options.maxDepth !== undefined && (options.maxDepth < 1 || options.maxDepth > limits.maxTraversalDepth)) { throw new Error(`maxDepth must be between 1 and ${limits.maxTraversalDepth}`); } const validDirections = ["outbound", "inbound", "both"]; if (options.traverseDirection && !validDirections.includes(options.traverseDirection)) { throw new Error(`Invalid traversal direction: ${options.traverseDirection}. Valid options: ${validDirections.join(', ')}`); } if (options.traverseRelations && options.traverseRelations.length === 0) { throw new Error('traverseRelations array cannot be empty if provided'); } } /** * Convert Neo4j integers in traversal results */ processTraversalResults(results: any[]): TraversalResult[] { return results.map(result => ({ id: result.id, name: result.name, type: result.type, distance: this.convertNeo4jInteger(result.distance), relation: result.relation, strength: result.strength, source: result.source, createdAt: result.createdAt })); } /** * Convert Neo4j Integer to JavaScript number */ private convertNeo4jInteger(value: any): number { if (typeof value === 'number') return value; if (value && typeof value.toNumber === 'function') return value.toNumber(); return 0; } /** * Get supported traversal directions for user guidance */ getSupportedDirections(): string[] { return [ 'outbound: What this memory influences', 'inbound: What influences this memory', 'both: All connected memories (default)' ]; } }

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/sylweriusz/mcp-neo4j-memory-server'

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