Skip to main content
Glama
component-graph.repository.ts11.8 kB
import { KuzuDBClient } from '../../db/kuzu'; import { Component, Context, Decision } from '../../types'; import { BaseComponentRepository } from '../base/base-component.repository'; import { RepositoryRepository } from '../repository.repository'; /** * Repository for Component graph traversal and relationship operations * Handles path finding, dependency queries, and relationship traversals */ export class ComponentGraphRepository extends BaseComponentRepository { /** * Find shortest path between two components */ async findShortestPath( repositoryName: string, startNodeId: string, startNodeBranch: string, endNodeId: string, params?: { relationshipTypes?: string[]; direction?: 'OUTGOING' | 'INCOMING' | 'BOTH'; algorithm?: string; projectedGraphName?: string; nodeTableNames?: string[]; relationshipTableNames?: string[]; }, ): Promise<{ path: Component[]; length: number; error?: string | null }> { let relationshipPattern: string; if (params?.relationshipTypes && params.relationshipTypes.length > 0) { const sanitizedTypes = params.relationshipTypes .map((rt) => rt.replace(/[^a-zA-Z0-9_]/g, '')) .filter((rt) => rt.length > 0); if (sanitizedTypes.length > 0) { relationshipPattern = `[e:${sanitizedTypes.join('|')}* SHORTEST]`; } else { relationshipPattern = `[* SHORTEST]`; } } else { relationshipPattern = `[* SHORTEST]`; } // Build direction arrows let arrowLeft = '-'; let arrowRight = '->'; if (params?.direction === 'INCOMING') { arrowLeft = '<-'; arrowRight = '-'; } else if (params?.direction === 'BOTH') { arrowLeft = '-'; arrowRight = '-'; } const startGraphUniqueId = this.createGraphUniqueId( repositoryName, startNodeBranch, startNodeId, ); const endGraphUniqueId = this.createGraphUniqueId(repositoryName, startNodeBranch, endNodeId); const query = ` MATCH p = (startNode:Component)${arrowLeft}${relationshipPattern}${arrowRight}(endNode:Component) WHERE startNode.graph_unique_id = $startGraphUniqueId AND endNode.graph_unique_id = $endGraphUniqueId AND startNode.branch = $startNodeBranch AND endNode.branch = $endNodeBranch RETURN p AS path, length(p) AS path_length ORDER BY path_length ASC LIMIT 1 `; try { const result = await this.executeQueryWithLogging( query, { startGraphUniqueId, endGraphUniqueId, startNodeBranch, endNodeBranch: startNodeBranch, }, 'findShortestPath', ); if (result.length === 0) { this.logger.debug(`No path found by query for ${startNodeId} -> ${endNodeId}`); return { path: [], length: 0, error: null }; } const row = result[0]; const kuzuPathObject = row.path; const pathLength = row.path_length || 0; // Extract nodes from the KuzuDB path structure let nodes: Component[] = []; if (kuzuPathObject?._NODES) { nodes = kuzuPathObject._NODES.map((node: any) => ({ ...node, id: node.id, graph_unique_id: undefined, })); } else if (kuzuPathObject?.nodes) { nodes = kuzuPathObject.nodes.map((node: any) => ({ ...node, id: node.id, graph_unique_id: undefined, })); } else if (Array.isArray(kuzuPathObject)) { nodes = kuzuPathObject.map((node: any) => ({ ...node, id: node.id, graph_unique_id: undefined, })); } return { path: nodes, length: pathLength, error: null }; } catch (error: any) { this.logger.error( `Error executing findShortestPath query from ${startNodeId} to ${endNodeId}:`, error, ); return { path: [], length: 0, error: error.message || 'Error executing shortest path query.', }; } } /** * Get all upstream dependencies for a component (transitive DEPENDS_ON) */ async getComponentDependencies( repositoryName: string, componentId: string, componentBranch: string, ): Promise<Component[]> { const startNodeGraphUniqueId = this.createGraphUniqueId( repositoryName, componentBranch, componentId, ); const query = ` MATCH (c:Component {graph_unique_id: $startNodeGraphUniqueId}) MATCH (c)-[:DEPENDS_ON]->(dep:Component) WHERE dep.branch = $componentBranch RETURN DISTINCT dep `; try { const result = await this.executeQueryWithLogging( query, { startNodeGraphUniqueId, componentBranch }, 'getComponentDependencies', ); if (result.length === 0) { return []; } return result.map((row: any) => { const depData = row.dep ?? row['dep'] ?? row; return { ...depData, id: depData.id, graph_unique_id: undefined, } as Component; }); } catch (error: any) { this.logger.error(`Error in getComponentDependencies for ${componentId}:`, error); throw error; } } /** * Get all downstream dependents for a component (transitive) */ async getComponentDependents( repositoryName: string, componentId: string, componentBranch: string, ): Promise<Component[]> { const targetNodeGraphUniqueId = this.createGraphUniqueId( repositoryName, componentBranch, componentId, ); const query = ` MATCH (targetComp:Component {graph_unique_id: $targetNodeGraphUniqueId}) MATCH (dependentComp:Component)-[:DEPENDS_ON]->(targetComp) WHERE dependentComp.branch = $componentBranch RETURN DISTINCT dependentComp `; try { const result = await this.executeQueryWithLogging( query, { targetNodeGraphUniqueId, componentBranch }, 'getComponentDependents', ); if (result.length === 0) { return []; } return result.map((row: any) => { const depData = row.dependentComp ?? row['dependentComp'] ?? row; return { ...depData, id: depData.id, graph_unique_id: undefined, } as Component; }); } catch (error: any) { this.logger.error(`Error in getComponentDependents for ${componentId}:`, error); throw error; } } /** * Get related items for a component based on specified relationship types, depth, and direction */ async getRelatedItems( repositoryName: string, componentId: string, componentBranch: string, relationshipTypes?: string[], depth?: number, direction?: 'INCOMING' | 'OUTGOING' | 'BOTH', ): Promise<Component[]> { const currentDepth = depth && depth > 0 && depth <= 10 ? depth : 1; const currentDirection = direction || 'OUTGOING'; let relTypeSpec = ''; if (relationshipTypes && relationshipTypes.length > 0) { const sanitizedTypes = relationshipTypes .map((rt) => rt.replace(/[^a-zA-Z0-9_]/g, '')) .filter((rt) => rt.length > 0); if (sanitizedTypes.length > 0) { relTypeSpec = ':' + sanitizedTypes.join('|'); } } let pathRelationship = `-[r${relTypeSpec}*1..${currentDepth}]-`; if (currentDirection === 'OUTGOING') { pathRelationship = `-[r${relTypeSpec}*1..${currentDepth}]->`; } else if (currentDirection === 'INCOMING') { pathRelationship = `<-[r${relTypeSpec}*1..${currentDepth}]-`; } const startNodeGraphUniqueId = this.createGraphUniqueId( repositoryName, componentBranch, componentId, ); const query = ` MATCH (startNode:Component {graph_unique_id: $startNodeGraphUniqueId}) MATCH (startNode)${pathRelationship}(relatedItem:Component) WHERE relatedItem.branch = $componentBranch RETURN DISTINCT relatedItem `; try { const result = await this.executeQueryWithLogging( query, { startNodeGraphUniqueId, componentBranch }, 'getRelatedItems', ); if (result.length === 0) { return []; } return result.map((row: any) => { const itemData = row.relatedItem ?? row['relatedItem']; return { ...itemData, id: itemData.id, graph_unique_id: undefined, } as Component; }); } catch (error: any) { this.logger.error(`Error executing getRelatedItems query for ${componentId}:`, error); throw error; } } /** * Retrieve contextual history (Context nodes) for a given component */ async getItemContextualHistory( repositoryName: string, itemId: string, itemBranch: string, itemType: 'Component' | 'Decision' | 'Rule', ): Promise<Context[]> { const itemGraphUniqueId = this.createGraphUniqueId(repositoryName, itemBranch, itemId); let itemMatchClause = ''; let relationshipMatchClause = ''; switch (itemType) { case 'Component': itemMatchClause = `(item:Component {graph_unique_id: $itemGraphUniqueId})`; relationshipMatchClause = `(ctx)-[:CONTEXT_OF]->(item)`; break; case 'Decision': itemMatchClause = `(item:Decision {graph_unique_id: $itemGraphUniqueId})`; relationshipMatchClause = `(ctx)-[:CONTEXT_OF_DECISION]->(item)`; break; case 'Rule': itemMatchClause = `(item:Rule {graph_unique_id: $itemGraphUniqueId})`; relationshipMatchClause = `(ctx)-[:CONTEXT_OF_RULE]->(item)`; break; default: { const exhaustiveCheck: never = itemType; this.logger.error(`Unsupported itemType for getItemContextualHistory: ${exhaustiveCheck}`); return []; } } const query = ` MATCH ${itemMatchClause} MATCH (ctx:Context) WHERE ctx.branch = $itemBranch AND ctx.repository = $repositoryName MATCH ${relationshipMatchClause} RETURN DISTINCT ctx ORDER BY ctx.created_at DESC LIMIT 100 `; try { const result = await this.executeQueryWithLogging( query, { itemGraphUniqueId, itemBranch, repositoryName }, 'getItemContextualHistory', ); if (result.length === 0) { return []; } return result.map((row: any) => { const ctxData = row.ctx ?? row['ctx']; return { ...ctxData, id: ctxData.id, graph_unique_id: undefined } as Context; }); } catch (error: any) { this.logger.error(`Error executing getItemContextualHistory for ${itemId}:`, error); throw error; } } /** * Get governing decisions for a component */ async getGoverningItemsForComponent( repositoryName: string, componentId: string, componentBranch: string, ): Promise<Decision[]> { const componentGraphUniqueId = this.createGraphUniqueId( repositoryName, componentBranch, componentId, ); const query = ` MATCH (comp:Component {graph_unique_id: $componentGraphUniqueId}) MATCH (dec:Decision {branch: $componentBranch})-[:DECISION_ON]->(comp) RETURN DISTINCT dec `; try { const result = await this.executeQueryWithLogging( query, { componentGraphUniqueId, componentBranch }, 'getGoverningItemsForComponent', ); if (result.length === 0) { return []; } return result.map((row: any) => { const decData = row.dec ?? row['dec']; return { ...decData, id: decData.id, graph_unique_id: undefined } as Decision; }); } catch (error: any) { this.logger.error(`Error executing getGoverningItemsForComponent for ${componentId}:`, error); throw error; } } }

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/Jakedismo/KuzuMem-MCP'

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