This commit is contained in:
Colby McHenry
2026-01-18 16:25:00 -06:00
parent 08ccabb5a9
commit cc6e7a5c89
57 changed files with 23315 additions and 1 deletions
+8
View File
@@ -0,0 +1,8 @@
/**
* Graph Module
*
* Provides graph traversal and query functionality for the code knowledge graph.
*/
export { GraphTraverser } from './traversal';
export { GraphQueryManager } from './queries';
+416
View File
@@ -0,0 +1,416 @@
/**
* Graph Query Functions
*
* Higher-level query functions built on top of traversal algorithms.
*/
import { Node, Edge, Context, Subgraph, EdgeKind } from '../types';
import { QueryBuilder } from '../db/queries';
import { GraphTraverser } from './traversal';
/**
* Graph query manager for complex queries
*/
export class GraphQueryManager {
private queries: QueryBuilder;
private traverser: GraphTraverser;
constructor(queries: QueryBuilder) {
this.queries = queries;
this.traverser = new GraphTraverser(queries);
}
/**
* Get full context for a node
*
* Returns the focal node along with its ancestors, children,
* and both incoming and outgoing references.
*
* @param nodeId - ID of the focal node
* @returns Context object with all related information
*/
getContext(nodeId: string): Context {
const focal = this.queries.getNodeById(nodeId);
if (!focal) {
throw new Error(`Node not found: ${nodeId}`);
}
// Get ancestors (containment hierarchy)
const ancestors = this.traverser.getAncestors(nodeId);
// Get children
const children = this.traverser.getChildren(nodeId);
// Get incoming references (things that reference this node)
const incomingEdges = this.queries.getIncomingEdges(nodeId);
const incomingRefs: Array<{ node: Node; edge: Edge }> = [];
for (const edge of incomingEdges) {
// Skip containment edges (already in ancestors)
if (edge.kind === 'contains') {
continue;
}
const node = this.queries.getNodeById(edge.source);
if (node) {
incomingRefs.push({ node, edge });
}
}
// Get outgoing references (things this node references)
const outgoingEdges = this.queries.getOutgoingEdges(nodeId);
const outgoingRefs: Array<{ node: Node; edge: Edge }> = [];
for (const edge of outgoingEdges) {
// Skip containment edges (already in children)
if (edge.kind === 'contains') {
continue;
}
const node = this.queries.getNodeById(edge.target);
if (node) {
outgoingRefs.push({ node, edge });
}
}
// Get type information (type_of, returns edges)
const types: Node[] = [];
const typeEdgeKinds: EdgeKind[] = ['type_of', 'returns'];
for (const kind of typeEdgeKinds) {
const typeEdges = this.queries.getOutgoingEdges(nodeId, [kind]);
for (const edge of typeEdges) {
const typeNode = this.queries.getNodeById(edge.target);
if (typeNode && !types.some((t) => t.id === typeNode.id)) {
types.push(typeNode);
}
}
}
// Get relevant imports
const imports: Node[] = [];
const fileNode = ancestors.find((a) => a.kind === 'file');
if (fileNode) {
const importEdges = this.queries.getOutgoingEdges(fileNode.id, ['imports']);
for (const edge of importEdges) {
const importNode = this.queries.getNodeById(edge.target);
if (importNode) {
imports.push(importNode);
}
}
}
return {
focal,
ancestors,
children,
incomingRefs,
outgoingRefs,
types,
imports,
};
}
/**
* Get dependencies of a file
*
* Returns all files that this file imports from.
*
* @param filePath - Path to the file
* @returns Array of file paths this file depends on
*/
getFileDependencies(filePath: string): string[] {
const nodes = this.queries.getNodesByFile(filePath);
const fileNode = nodes.find((n) => n.kind === 'file');
if (!fileNode) {
return [];
}
const dependencies = new Set<string>();
const importEdges = this.queries.getOutgoingEdges(fileNode.id, ['imports']);
for (const edge of importEdges) {
const targetNode = this.queries.getNodeById(edge.target);
if (targetNode && targetNode.filePath !== filePath) {
dependencies.add(targetNode.filePath);
}
}
return Array.from(dependencies);
}
/**
* Get dependents of a file
*
* Returns all files that import from this file.
*
* @param filePath - Path to the file
* @returns Array of file paths that depend on this file
*/
getFileDependents(filePath: string): string[] {
const nodes = this.queries.getNodesByFile(filePath);
const dependents = new Set<string>();
// For each exported symbol in this file, find imports
for (const node of nodes) {
if (node.isExported) {
const incomingEdges = this.queries.getIncomingEdges(node.id, ['imports']);
for (const edge of incomingEdges) {
const sourceNode = this.queries.getNodeById(edge.source);
if (sourceNode && sourceNode.filePath !== filePath) {
dependents.add(sourceNode.filePath);
}
}
}
}
return Array.from(dependents);
}
/**
* Get all symbols exported by a file
*
* @param filePath - Path to the file
* @returns Array of exported nodes
*/
getExportedSymbols(filePath: string): Node[] {
const nodes = this.queries.getNodesByFile(filePath);
return nodes.filter((n) => n.isExported);
}
/**
* Find symbols by qualified name pattern
*
* @param pattern - Pattern to match (supports * wildcard)
* @returns Array of matching nodes
*/
findByQualifiedName(pattern: string): Node[] {
// Convert glob pattern to regex
const regexPattern = pattern
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
.replace(/\*/g, '.*')
.replace(/\?/g, '.');
const regex = new RegExp(`^${regexPattern}$`);
// This is inefficient for large graphs - would need FTS index on qualified_name
// For now, use kind-based filtering if possible
const allNodes: Node[] = [];
const kinds: Node['kind'][] = [
'class',
'function',
'method',
'interface',
'type_alias',
'variable',
'constant',
];
for (const kind of kinds) {
const nodes = this.queries.getNodesByKind(kind);
for (const node of nodes) {
if (regex.test(node.qualifiedName)) {
allNodes.push(node);
}
}
}
return allNodes;
}
/**
* Get the module/package structure
*
* Returns a tree structure of files organized by directory.
*
* @returns Map of directory paths to contained files
*/
getModuleStructure(): Map<string, string[]> {
const files = this.queries.getAllFiles();
const structure = new Map<string, string[]>();
for (const file of files) {
const parts = file.path.split('/');
const dir = parts.slice(0, -1).join('/') || '.';
if (!structure.has(dir)) {
structure.set(dir, []);
}
structure.get(dir)!.push(file.path);
}
return structure;
}
/**
* Find circular dependencies in the graph
*
* @returns Array of cycles, each cycle is an array of node IDs
*/
findCircularDependencies(): string[][] {
const files = this.queries.getAllFiles();
const cycles: string[][] = [];
const visited = new Set<string>();
const recursionStack = new Set<string>();
const dfs = (filePath: string, path: string[]): void => {
if (recursionStack.has(filePath)) {
// Found a cycle
const cycleStart = path.indexOf(filePath);
if (cycleStart !== -1) {
cycles.push(path.slice(cycleStart));
}
return;
}
if (visited.has(filePath)) {
return;
}
visited.add(filePath);
recursionStack.add(filePath);
const dependencies = this.getFileDependencies(filePath);
for (const dep of dependencies) {
dfs(dep, [...path, filePath]);
}
recursionStack.delete(filePath);
};
for (const file of files) {
if (!visited.has(file.path)) {
dfs(file.path, []);
}
}
return cycles;
}
/**
* Get complexity metrics for a node
*
* @param nodeId - ID of the node
* @returns Object containing various complexity metrics
*/
getNodeMetrics(nodeId: string): {
incomingEdgeCount: number;
outgoingEdgeCount: number;
callCount: number;
callerCount: number;
childCount: number;
depth: number;
} {
const incomingEdges = this.queries.getIncomingEdges(nodeId);
const outgoingEdges = this.queries.getOutgoingEdges(nodeId);
const callEdges = outgoingEdges.filter((e) => e.kind === 'calls');
const callerEdges = incomingEdges.filter((e) => e.kind === 'calls');
const containsEdges = outgoingEdges.filter((e) => e.kind === 'contains');
const ancestors = this.traverser.getAncestors(nodeId);
return {
incomingEdgeCount: incomingEdges.length,
outgoingEdgeCount: outgoingEdges.length,
callCount: callEdges.length,
callerCount: callerEdges.length,
childCount: containsEdges.length,
depth: ancestors.length,
};
}
/**
* Find dead code (nodes with no incoming references)
*
* @param kinds - Node kinds to check (default: functions, methods, classes)
* @returns Array of unreferenced nodes
*/
findDeadCode(kinds?: Node['kind'][]): Node[] {
const targetKinds = kinds || ['function', 'method', 'class'];
const deadCode: Node[] = [];
for (const kind of targetKinds) {
const nodes = this.queries.getNodesByKind(kind);
for (const node of nodes) {
// Skip exported symbols (they may be used externally)
if (node.isExported) {
continue;
}
const incomingEdges = this.queries.getIncomingEdges(node.id);
// Filter out containment edges
const references = incomingEdges.filter((e) => e.kind !== 'contains');
if (references.length === 0) {
deadCode.push(node);
}
}
}
return deadCode;
}
/**
* Get subgraph containing nodes matching a filter
*
* @param filter - Filter function to select nodes
* @param includeEdges - Whether to include edges between matching nodes
* @returns Subgraph containing matching nodes
*/
getFilteredSubgraph(
filter: (node: Node) => boolean,
includeEdges: boolean = true
): Subgraph {
const nodes = new Map<string, Node>();
const edges: Edge[] = [];
// Get all nodes of common kinds
const kinds: Node['kind'][] = [
'file',
'module',
'class',
'struct',
'interface',
'trait',
'function',
'method',
'variable',
'constant',
'enum',
'type_alias',
];
for (const kind of kinds) {
const kindNodes = this.queries.getNodesByKind(kind);
for (const node of kindNodes) {
if (filter(node)) {
nodes.set(node.id, node);
}
}
}
// Include edges between matching nodes
if (includeEdges) {
for (const nodeId of nodes.keys()) {
const outgoing = this.queries.getOutgoingEdges(nodeId);
for (const edge of outgoing) {
if (nodes.has(edge.target)) {
edges.push(edge);
}
}
}
}
return {
nodes,
edges,
roots: [],
};
}
/**
* Access the underlying traverser for direct traversal operations
*/
getTraverser(): GraphTraverser {
return this.traverser;
}
}
+612
View File
@@ -0,0 +1,612 @@
/**
* Graph Traversal Algorithms
*
* BFS and DFS traversal for the code knowledge graph.
*/
import { Node, Edge, Subgraph, TraversalOptions, EdgeKind } from '../types';
import { QueryBuilder } from '../db/queries';
/**
* Default traversal options
*/
const DEFAULT_OPTIONS: Required<TraversalOptions> = {
maxDepth: Infinity,
edgeKinds: [],
nodeKinds: [],
direction: 'outgoing',
limit: 1000,
includeStart: true,
};
/**
* Result of a single traversal step
*/
interface TraversalStep {
node: Node;
edge: Edge | null;
depth: number;
}
/**
* Graph traverser for BFS and DFS traversal
*/
export class GraphTraverser {
private queries: QueryBuilder;
constructor(queries: QueryBuilder) {
this.queries = queries;
}
/**
* Traverse the graph using breadth-first search
*
* @param startId - Starting node ID
* @param options - Traversal options
* @returns Subgraph containing traversed nodes and edges
*/
traverseBFS(startId: string, options: TraversalOptions = {}): Subgraph {
const opts = { ...DEFAULT_OPTIONS, ...options };
const startNode = this.queries.getNodeById(startId);
if (!startNode) {
return { nodes: new Map(), edges: [], roots: [] };
}
const nodes = new Map<string, Node>();
const edges: Edge[] = [];
const visited = new Set<string>();
const queue: TraversalStep[] = [{ node: startNode, edge: null, depth: 0 }];
if (opts.includeStart) {
nodes.set(startNode.id, startNode);
}
while (queue.length > 0 && nodes.size < opts.limit) {
const step = queue.shift()!;
const { node, edge, depth } = step;
if (visited.has(node.id)) {
continue;
}
visited.add(node.id);
// Add edge to result
if (edge) {
edges.push(edge);
}
// Check depth limit
if (depth >= opts.maxDepth) {
continue;
}
// Get adjacent edges
const adjacentEdges = this.getAdjacentEdges(node.id, opts.direction, opts.edgeKinds);
for (const adjEdge of adjacentEdges) {
const nextNodeId = opts.direction === 'incoming' ? adjEdge.source : adjEdge.target;
if (visited.has(nextNodeId)) {
continue;
}
const nextNode = this.queries.getNodeById(nextNodeId);
if (!nextNode) {
continue;
}
// Apply node kind filter
if (opts.nodeKinds && opts.nodeKinds.length > 0 && !opts.nodeKinds.includes(nextNode.kind)) {
continue;
}
// Add node to result
nodes.set(nextNode.id, nextNode);
// Queue for further traversal
queue.push({ node: nextNode, edge: adjEdge, depth: depth + 1 });
}
}
return {
nodes,
edges,
roots: [startId],
};
}
/**
* Traverse the graph using depth-first search
*
* @param startId - Starting node ID
* @param options - Traversal options
* @returns Subgraph containing traversed nodes and edges
*/
traverseDFS(startId: string, options: TraversalOptions = {}): Subgraph {
const opts = { ...DEFAULT_OPTIONS, ...options };
const startNode = this.queries.getNodeById(startId);
if (!startNode) {
return { nodes: new Map(), edges: [], roots: [] };
}
const nodes = new Map<string, Node>();
const edges: Edge[] = [];
const visited = new Set<string>();
if (opts.includeStart) {
nodes.set(startNode.id, startNode);
}
this.dfsRecursive(startNode, 0, opts, nodes, edges, visited);
return {
nodes,
edges,
roots: [startId],
};
}
/**
* Recursive DFS helper
*/
private dfsRecursive(
node: Node,
depth: number,
opts: Required<TraversalOptions>,
nodes: Map<string, Node>,
edges: Edge[],
visited: Set<string>
): void {
if (visited.has(node.id) || nodes.size >= opts.limit || depth >= opts.maxDepth) {
return;
}
visited.add(node.id);
// Get adjacent edges
const adjacentEdges = this.getAdjacentEdges(node.id, opts.direction, opts.edgeKinds);
for (const edge of adjacentEdges) {
const nextNodeId = opts.direction === 'incoming' ? edge.source : edge.target;
if (visited.has(nextNodeId)) {
continue;
}
const nextNode = this.queries.getNodeById(nextNodeId);
if (!nextNode) {
continue;
}
// Apply node kind filter
if (opts.nodeKinds && opts.nodeKinds.length > 0 && !opts.nodeKinds.includes(nextNode.kind)) {
continue;
}
// Add node and edge to result
nodes.set(nextNode.id, nextNode);
edges.push(edge);
// Recurse
this.dfsRecursive(nextNode, depth + 1, opts, nodes, edges, visited);
}
}
/**
* Get adjacent edges based on direction
*/
private getAdjacentEdges(
nodeId: string,
direction: 'outgoing' | 'incoming' | 'both',
edgeKinds?: EdgeKind[]
): Edge[] {
const kinds = edgeKinds && edgeKinds.length > 0 ? edgeKinds : undefined;
if (direction === 'outgoing') {
return this.queries.getOutgoingEdges(nodeId, kinds);
} else if (direction === 'incoming') {
return this.queries.getIncomingEdges(nodeId, kinds);
} else {
// Both directions
const outgoing = this.queries.getOutgoingEdges(nodeId, kinds);
const incoming = this.queries.getIncomingEdges(nodeId, kinds);
return [...outgoing, ...incoming];
}
}
/**
* Find all callers of a function/method
*
* @param nodeId - ID of the function/method node
* @param maxDepth - Maximum depth to traverse (default: 1)
* @returns Array of nodes that call this function
*/
getCallers(nodeId: string, maxDepth: number = 1): Array<{ node: Node; edge: Edge }> {
const result: Array<{ node: Node; edge: Edge }> = [];
const visited = new Set<string>();
this.getCallersRecursive(nodeId, maxDepth, 0, result, visited);
return result;
}
private getCallersRecursive(
nodeId: string,
maxDepth: number,
currentDepth: number,
result: Array<{ node: Node; edge: Edge }>,
visited: Set<string>
): void {
if (currentDepth >= maxDepth || visited.has(nodeId)) {
return;
}
visited.add(nodeId);
const incomingEdges = this.queries.getIncomingEdges(nodeId, ['calls']);
for (const edge of incomingEdges) {
const callerNode = this.queries.getNodeById(edge.source);
if (callerNode && !visited.has(callerNode.id)) {
result.push({ node: callerNode, edge });
this.getCallersRecursive(callerNode.id, maxDepth, currentDepth + 1, result, visited);
}
}
}
/**
* Find all functions/methods called by a function
*
* @param nodeId - ID of the function/method node
* @param maxDepth - Maximum depth to traverse (default: 1)
* @returns Array of nodes called by this function
*/
getCallees(nodeId: string, maxDepth: number = 1): Array<{ node: Node; edge: Edge }> {
const result: Array<{ node: Node; edge: Edge }> = [];
const visited = new Set<string>();
this.getCalleesRecursive(nodeId, maxDepth, 0, result, visited);
return result;
}
private getCalleesRecursive(
nodeId: string,
maxDepth: number,
currentDepth: number,
result: Array<{ node: Node; edge: Edge }>,
visited: Set<string>
): void {
if (currentDepth >= maxDepth || visited.has(nodeId)) {
return;
}
visited.add(nodeId);
const outgoingEdges = this.queries.getOutgoingEdges(nodeId, ['calls']);
for (const edge of outgoingEdges) {
const calleeNode = this.queries.getNodeById(edge.target);
if (calleeNode && !visited.has(calleeNode.id)) {
result.push({ node: calleeNode, edge });
this.getCalleesRecursive(calleeNode.id, maxDepth, currentDepth + 1, result, visited);
}
}
}
/**
* Get the call graph for a function (both callers and callees)
*
* @param nodeId - ID of the function/method node
* @param depth - Maximum depth in each direction (default: 2)
* @returns Subgraph containing the call graph
*/
getCallGraph(nodeId: string, depth: number = 2): Subgraph {
const focalNode = this.queries.getNodeById(nodeId);
if (!focalNode) {
return { nodes: new Map(), edges: [], roots: [] };
}
const nodes = new Map<string, Node>();
const edges: Edge[] = [];
// Add focal node
nodes.set(focalNode.id, focalNode);
// Get callers
const callers = this.getCallers(nodeId, depth);
for (const { node, edge } of callers) {
nodes.set(node.id, node);
edges.push(edge);
}
// Get callees
const callees = this.getCallees(nodeId, depth);
for (const { node, edge } of callees) {
nodes.set(node.id, node);
edges.push(edge);
}
return {
nodes,
edges,
roots: [nodeId],
};
}
/**
* Get the type hierarchy for a class/interface
*
* @param nodeId - ID of the class/interface node
* @returns Subgraph containing the type hierarchy
*/
getTypeHierarchy(nodeId: string): Subgraph {
const focalNode = this.queries.getNodeById(nodeId);
if (!focalNode) {
return { nodes: new Map(), edges: [], roots: [] };
}
const nodes = new Map<string, Node>();
const edges: Edge[] = [];
const visited = new Set<string>();
// Add focal node
nodes.set(focalNode.id, focalNode);
// Get ancestors (what this extends/implements)
this.getTypeAncestors(nodeId, nodes, edges, visited);
// Get descendants (what extends/implements this)
this.getTypeDescendants(nodeId, nodes, edges, visited);
return {
nodes,
edges,
roots: [nodeId],
};
}
private getTypeAncestors(
nodeId: string,
nodes: Map<string, Node>,
edges: Edge[],
visited: Set<string>
): void {
if (visited.has(nodeId)) {
return;
}
visited.add(nodeId);
const outgoingEdges = this.queries.getOutgoingEdges(nodeId, ['extends', 'implements']);
for (const edge of outgoingEdges) {
const parentNode = this.queries.getNodeById(edge.target);
if (parentNode && !nodes.has(parentNode.id)) {
nodes.set(parentNode.id, parentNode);
edges.push(edge);
this.getTypeAncestors(parentNode.id, nodes, edges, visited);
}
}
}
private getTypeDescendants(
nodeId: string,
nodes: Map<string, Node>,
edges: Edge[],
visited: Set<string>
): void {
if (visited.has(nodeId)) {
return;
}
visited.add(nodeId);
const incomingEdges = this.queries.getIncomingEdges(nodeId, ['extends', 'implements']);
for (const edge of incomingEdges) {
const childNode = this.queries.getNodeById(edge.source);
if (childNode && !nodes.has(childNode.id)) {
nodes.set(childNode.id, childNode);
edges.push(edge);
this.getTypeDescendants(childNode.id, nodes, edges, visited);
}
}
}
/**
* Find all usages of a symbol
*
* @param nodeId - ID of the symbol node
* @returns Array of nodes and edges that reference this symbol
*/
findUsages(nodeId: string): Array<{ node: Node; edge: Edge }> {
const result: Array<{ node: Node; edge: Edge }> = [];
// Get all incoming edges (references, calls, type_of, etc.)
const incomingEdges = this.queries.getIncomingEdges(nodeId);
for (const edge of incomingEdges) {
const sourceNode = this.queries.getNodeById(edge.source);
if (sourceNode) {
result.push({ node: sourceNode, edge });
}
}
return result;
}
/**
* Calculate the impact radius of a node
*
* Returns all nodes that could be affected by changes to this node.
*
* @param nodeId - ID of the node
* @param maxDepth - Maximum depth to traverse (default: 3)
* @returns Subgraph containing potentially impacted nodes
*/
getImpactRadius(nodeId: string, maxDepth: number = 3): Subgraph {
const focalNode = this.queries.getNodeById(nodeId);
if (!focalNode) {
return { nodes: new Map(), edges: [], roots: [] };
}
const nodes = new Map<string, Node>();
const edges: Edge[] = [];
const visited = new Set<string>();
// Add focal node
nodes.set(focalNode.id, focalNode);
// Traverse incoming edges to find all dependents
this.getImpactRecursive(nodeId, maxDepth, 0, nodes, edges, visited);
return {
nodes,
edges,
roots: [nodeId],
};
}
private getImpactRecursive(
nodeId: string,
maxDepth: number,
currentDepth: number,
nodes: Map<string, Node>,
edges: Edge[],
visited: Set<string>
): void {
if (currentDepth >= maxDepth || visited.has(nodeId)) {
return;
}
visited.add(nodeId);
// Get all incoming edges (things that depend on this node)
const incomingEdges = this.queries.getIncomingEdges(nodeId);
for (const edge of incomingEdges) {
const sourceNode = this.queries.getNodeById(edge.source);
if (sourceNode && !nodes.has(sourceNode.id)) {
nodes.set(sourceNode.id, sourceNode);
edges.push(edge);
this.getImpactRecursive(sourceNode.id, maxDepth, currentDepth + 1, nodes, edges, visited);
}
}
}
/**
* Find the shortest path between two nodes
*
* @param fromId - Starting node ID
* @param toId - Target node ID
* @param edgeKinds - Edge types to consider (all if empty)
* @returns Array of nodes and edges forming the path, or null if no path exists
*/
findPath(
fromId: string,
toId: string,
edgeKinds: EdgeKind[] = []
): Array<{ node: Node; edge: Edge | null }> | null {
const fromNode = this.queries.getNodeById(fromId);
const toNode = this.queries.getNodeById(toId);
if (!fromNode || !toNode) {
return null;
}
// BFS to find shortest path
const visited = new Set<string>();
const queue: Array<{ nodeId: string; path: Array<{ node: Node; edge: Edge | null }> }> = [
{ nodeId: fromId, path: [{ node: fromNode, edge: null }] },
];
while (queue.length > 0) {
const { nodeId, path } = queue.shift()!;
if (nodeId === toId) {
return path;
}
if (visited.has(nodeId)) {
continue;
}
visited.add(nodeId);
// Get outgoing edges
const outgoingEdges = this.queries.getOutgoingEdges(
nodeId,
edgeKinds.length > 0 ? edgeKinds : undefined
);
for (const edge of outgoingEdges) {
if (!visited.has(edge.target)) {
const nextNode = this.queries.getNodeById(edge.target);
if (nextNode) {
queue.push({
nodeId: edge.target,
path: [...path, { node: nextNode, edge }],
});
}
}
}
}
return null; // No path found
}
/**
* Get the containment hierarchy for a node (ancestors)
*
* @param nodeId - ID of the node
* @returns Array of ancestor nodes from immediate parent to root
*/
getAncestors(nodeId: string): Node[] {
const ancestors: Node[] = [];
const visited = new Set<string>();
let currentId = nodeId;
while (true) {
if (visited.has(currentId)) {
break;
}
visited.add(currentId);
// Look for 'contains' edges pointing to this node
const containingEdges = this.queries.getIncomingEdges(currentId, ['contains']);
const firstEdge = containingEdges[0];
if (!firstEdge) {
break;
}
// Typically there should be at most one containing parent
const parentNode = this.queries.getNodeById(firstEdge.source);
if (parentNode) {
ancestors.push(parentNode);
currentId = parentNode.id;
} else {
break;
}
}
return ancestors;
}
/**
* Get immediate children of a node
*
* @param nodeId - ID of the node
* @returns Array of child nodes
*/
getChildren(nodeId: string): Node[] {
const containsEdges = this.queries.getOutgoingEdges(nodeId, ['contains']);
const children: Node[] = [];
for (const edge of containsEdges) {
const childNode = this.queries.getNodeById(edge.target);
if (childNode) {
children.push(childNode);
}
}
return children;
}
}