src / tools / neo4jTools.ts
src / tools / neo4jTools.ts
/**
* Neo4j Database Tools
* CRUD, path finding, multi-statement script execution.
* Driver preserves URI scheme (bolt://, neo4j://, neo4j+s://, neo4j+ssc://).
* Labels and relationship types are restricted to safe identifiers.
*/
import neo4j from 'neo4j-driver';
type Driver = ReturnType<typeof neo4j.driver>;
import { z } from 'zod';
// @ts-ignore
declare global {
var __neo4j_clients__: Map<string, any>;
}
if (!global.__neo4j_clients__) {
global.__neo4j_clients__ = new Map();
}
const IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
export function assertNeo4jIdentifier(value: string, kind: string): string {
if (!value || !IDENTIFIER_RE.test(value)) {
throw new Error(
`Invalid Neo4j ${kind}: "${value}". Use only letters, digits, and underscore (must start with a letter or underscore).`,
);
}
return value;
}
const neo4jQuerySchema = z.object({
connection_string: z.string().min(1),
cypher: z.string().min(1),
params: z.record(z.any()).optional(),
username: z.string().optional().default('neo4j'),
password: z.string().optional().default(''),
});
const createNodeSchema = z.object({
connection_string: z.string().min(1),
labels: z.array(z.string()),
properties: z.record(z.any()),
username: z.string().optional().default('neo4j'),
password: z.string().optional().default(''),
});
const deleteNodeSchema = z.object({
connection_string: z.string().min(1),
node_id: z.string(),
username: z.string().optional().default('neo4j'),
password: z.string().optional().default(''),
});
const updateNodeSchema = z.object({
connection_string: z.string().min(1),
node_id: z.string(),
properties: z.record(z.any()),
username: z.string().optional().default('neo4j'),
password: z.string().optional().default(''),
});
const createRelationshipSchema = z.object({
connection_string: z.string().min(1),
from_node_id: z.string(),
to_node_id: z.string(),
type: z.string(),
properties: z.record(z.any()).optional(),
username: z.string().optional().default('neo4j'),
password: z.string().optional().default(''),
});
const deleteRelationshipSchema = z.object({
connection_string: z.string().min(1),
relationship_id: z.string(),
username: z.string().optional().default('neo4j'),
password: z.string().optional().default(''),
});
const pathFindSchema = z.object({
connection_string: z.string().min(1),
start_node_id: z.string(),
end_node_id: z.string(),
max_depth: z.number().int().min(1).max(20).optional().default(5),
username: z.string().optional().default('neo4j'),
password: z.string().optional().default(''),
});
const graphAnalysisSchema = z.object({
connection_string: z.string().min(1),
node_id: z.string(),
depth: z.number().int().min(1).max(10).optional().default(2),
username: z.string().optional().default('neo4j'),
password: z.string().optional().default(''),
});
export function normalizeNeo4jUri(connection_string: string): {
uri: string;
username?: string;
password?: string;
} {
const trimmed = connection_string.trim();
const schemeMatch = trimmed.match(/^(bolt|neo4j|neo4j\+s|neo4j\+ssc):\/\//i);
const scheme = schemeMatch ? schemeMatch[1].toLowerCase() : 'bolt';
let rest = schemeMatch ? trimmed.slice(schemeMatch[0].length) : trimmed;
let username: string | undefined;
let password: string | undefined;
const atIndex = rest.lastIndexOf('@');
if (atIndex !== -1) {
const credentials = rest.substring(0, atIndex);
rest = rest.substring(atIndex + 1);
const colonIndex = credentials.lastIndexOf(':');
if (colonIndex !== -1) {
username = credentials.substring(0, colonIndex);
password = credentials.substring(colonIndex + 1);
} else {
username = credentials;
}
}
if (!rest) {
throw new Error(`Invalid Neo4j connection string: "${connection_string}"`);
}
return { uri: `${scheme}://${rest}`, username, password };
}
export function getNeo4jDriver(
connection_string: string,
username?: string,
password?: string,
): Driver {
const normalized = normalizeNeo4jUri(connection_string);
const finalUsername = username || normalized.username || 'neo4j';
const finalPassword = password ?? normalized.password ?? '';
const cacheKey = `${normalized.uri}|${finalUsername}`;
if (global.__neo4j_clients__.has(cacheKey)) {
const driver = global.__neo4j_clients__.get(cacheKey);
if (driver && !driver.closed) return driver;
global.__neo4j_clients__.delete(cacheKey);
}
const neoDriver = neo4j.driver(
normalized.uri,
neo4j.auth.basic(finalUsername, finalPassword),
{ maxConnectionLifetime: 3 * 60 * 1000, maxConnectionPoolSize: 1 },
);
global.__neo4j_clients__.set(cacheKey, neoDriver);
return neoDriver;
}
async function withDriver<T>(
connection_string: string,
username: string | undefined,
password: string | undefined,
fn: (session: ReturnType<Driver['session']>) => Promise<T>,
): Promise<T> {
let driver: Driver;
try {
driver = getNeo4jDriver(connection_string, username, password);
await driver.verifyConnectivity();
} catch (error) {
throw new Error(
`Failed to connect to Neo4j: ${error instanceof Error ? error.message : String(error)}`,
);
}
const session = driver.session();
try {
return await fn(session);
} finally {
await session.close();
}
}
/** Split Cypher on semicolons outside quotes/comments. */
export function splitCypherStatements(script: string): string[] {
const statements: string[] = [];
let current = '';
let inSingle = false;
let inDouble = false;
let inLineComment = false;
let inBlockComment = false;
for (let i = 0; i < script.length; i++) {
const ch = script[i];
const next = script[i + 1];
if (inLineComment) {
current += ch;
if (ch === '\n') inLineComment = false;
continue;
}
if (inBlockComment) {
current += ch;
if (ch === '*' && next === '/') {
current += next;
i++;
inBlockComment = false;
}
continue;
}
if (!inSingle && !inDouble) {
if (ch === '/' && next === '/') {
inLineComment = true;
current += ch;
continue;
}
if (ch === '/' && next === '*') {
inBlockComment = true;
current += ch;
continue;
}
}
if (ch === "'" && !inDouble) {
if (inSingle && next === "'") {
current += ch + next;
i++;
continue;
}
inSingle = !inSingle;
current += ch;
continue;
}
if (ch === '"' && !inSingle) {
inDouble = !inDouble;
current += ch;
continue;
}
if (ch === ';' && !inSingle && !inDouble) {
const trimmed = current.trim();
if (trimmed) statements.push(trimmed);
current = '';
continue;
}
current += ch;
}
const tail = current.trim();
if (tail) statements.push(tail);
return statements;
}
export async function neo4j_run_script(
connection_string: string,
cypher: string,
username?: string,
password?: string,
stop_on_error: boolean = true,
): Promise<{
ok: boolean;
statements_run: number;
statements_failed: number;
results: any[];
errors: Array<{ index: number; statement_preview: string; error: string }>;
}> {
const statements = splitCypherStatements(cypher);
if (statements.length === 0) {
return { ok: true, statements_run: 0, statements_failed: 0, results: [], errors: [] };
}
return withDriver(connection_string, username, password, async (session) => {
const results: any[] = [];
const errors: Array<{ index: number; statement_preview: string; error: string }> = [];
let ran = 0;
for (let i = 0; i < statements.length; i++) {
const stmt = statements[i];
try {
const result = await session.run(stmt, {});
ran++;
const objs = result.records.slice(0, 50).map((r: any) => r.toObject());
if (objs.length) results.push(...objs);
} catch (error) {
const msg = error instanceof Error ? error.message : String(error);
errors.push({ index: i, statement_preview: stmt.slice(0, 160), error: msg });
if (stop_on_error) break;
}
}
return {
ok: errors.length === 0,
statements_run: ran,
statements_failed: errors.length,
results,
errors,
};
});
}
export async function neo4j_query(
connection_string: string,
cypher: string,
params?: Record<string, any>,
username?: string,
password?: string,
): Promise<{ results: any[]; multi_statement?: boolean; statements_run?: number; statements_failed?: number }> {
const validated = neo4jQuerySchema.parse({
connection_string,
cypher,
params,
username,
password,
});
const statements = splitCypherStatements(validated.cypher);
if (statements.length > 1) {
const summary = await neo4j_run_script(
validated.connection_string,
validated.cypher,
validated.username,
validated.password,
true,
);
return {
results: summary.results,
multi_statement: true,
statements_run: summary.statements_run,
statements_failed: summary.statements_failed,
};
}
return withDriver(validated.connection_string, validated.username, validated.password, async (session) => {
try {
const result = await session.run(validated.cypher, validated.params || {});
return { results: result.records.map((r: any) => r.toObject()) };
} catch (error) {
throw new Error(
`Cypher execution failed: ${error instanceof Error ? error.message : String(error)}. ` +
`For large multi-statement scripts use neo4j_run_cypher_file or neo4j_run_script.`,
);
}
});
}
export async function neo4j_create_node(
connection_string: string,
labels: string[],
properties: Record<string, any>,
username?: string,
password?: string,
): Promise<{ node_id: string }> {
const validated = createNodeSchema.parse({ connection_string, labels, properties, username, password });
const safeLabels = validated.labels.map((l) => assertNeo4jIdentifier(l, 'label'));
const labelString = safeLabels.map((l) => `:${l}`).join('');
return withDriver(validated.connection_string, validated.username, validated.password, async (session) => {
try {
const cypher = `CREATE (n${labelString} $props) RETURN elementId(n) as node_id`;
const result = await session.run(cypher, { props: validated.properties });
return { node_id: String(result.records[0].get('node_id')) };
} catch (error) {
throw new Error(`Failed to create node: ${error instanceof Error ? error.message : String(error)}`);
}
});
}
export async function neo4j_delete_node(
connection_string: string,
node_id: string,
username?: string,
password?: string,
): Promise<{ success: boolean }> {
const validated = deleteNodeSchema.parse({ connection_string, node_id, username, password });
return withDriver(validated.connection_string, validated.username, validated.password, async (session) => {
try {
await session.run('MATCH (n) WHERE elementId(n) = $id DETACH DELETE n', { id: validated.node_id });
return { success: true };
} catch (error) {
throw new Error(`Failed to delete node: ${error instanceof Error ? error.message : String(error)}`);
}
});
}
export async function neo4j_update_node(
connection_string: string,
node_id: string,
properties: Record<string, any>,
username?: string,
password?: string,
): Promise<{ success: boolean }> {
const validated = updateNodeSchema.parse({ connection_string, node_id, properties, username, password });
if (Object.keys(validated.properties).length === 0) return { success: true };
return withDriver(validated.connection_string, validated.username, validated.password, async (session) => {
try {
await session.run('MATCH (n) WHERE elementId(n) = $id SET n += $props', {
id: validated.node_id,
props: validated.properties,
});
return { success: true };
} catch (error) {
throw new Error(`Failed to update node: ${error instanceof Error ? error.message : String(error)}`);
}
});
}
export async function neo4j_create_relationship(
connection_string: string,
from_node_id: string,
to_node_id: string,
type: string,
properties?: Record<string, any>,
username?: string,
password?: string,
): Promise<{ relationship_id: string }> {
const validated = createRelationshipSchema.parse({
connection_string, from_node_id, to_node_id, type, properties, username, password,
});
const relType = assertNeo4jIdentifier(validated.type, 'relationship type');
return withDriver(validated.connection_string, validated.username, validated.password, async (session) => {
try {
let cypher = `MATCH (a), (b) WHERE elementId(a) = $fromId AND elementId(b) = $toId CREATE (a)-[r:${relType}]->(b)`;
if (validated.properties && Object.keys(validated.properties).length > 0) cypher += ' SET r += $props';
cypher += ' RETURN elementId(r) as relationship_id';
const result = await session.run(cypher, {
fromId: validated.from_node_id,
toId: validated.to_node_id,
props: validated.properties || {},
});
return { relationship_id: String(result.records[0].get('relationship_id')) };
} catch (error) {
throw new Error(`Failed to create relationship: ${error instanceof Error ? error.message : String(error)}`);
}
});
}
export async function neo4j_delete_relationship(
connection_string: string,
relationship_id: string,
username?: string,
password?: string,
): Promise<{ success: boolean }> {
const validated = deleteRelationshipSchema.parse({ connection_string, relationship_id, username, password });
return withDriver(validated.connection_string, validated.username, validated.password, async (session) => {
try {
await session.run('MATCH ()-[r]->() WHERE elementId(r) = $id DELETE r', { id: validated.relationship_id });
return { success: true };
} catch (error) {
throw new Error(`Failed to delete relationship: ${error instanceof Error ? error.message : String(error)}`);
}
});
}
export async function neo4j_path_find(
connection_string: string,
start_node_id: string,
end_node_id: string,
max_depth?: number,
username?: string,
password?: string,
): Promise<{ paths: Array<{ nodes: any[]; relationships: any[] }> }> {
const validated = pathFindSchema.parse({
connection_string, start_node_id, end_node_id, max_depth, username, password,
});
return withDriver(validated.connection_string, validated.username, validated.password, async (session) => {
try {
const cypher = `MATCH path = shortestPath((start)-[*1..${validated.max_depth}]->(end))
WHERE elementId(start) = $fromId AND elementId(end) = $toId
RETURN path`;
const result = await session.run(cypher, {
fromId: validated.start_node_id,
toId: validated.end_node_id,
});
return {
paths: result.records.map((r: any) => {
const path = r.get('path');
const nodes = [];
const relationships = [];
for (const segment of path.segments) {
nodes.push(segment.start.properties);
relationships.push(segment.relationship.properties);
nodes.push(segment.end.properties);
}
return { nodes, relationships };
}),
};
} catch (error) {
throw new Error(`Path finding failed: ${error instanceof Error ? error.message : String(error)}`);
}
});
}
export async function neo4j_graph_analysis(
connection_string: string,
node_id: string,
depth?: number,
username?: string,
password?: string,
): Promise<{ nodes: any[]; relationships: any[] }> {
const validated = graphAnalysisSchema.parse({ connection_string, node_id, depth, username, password });
return withDriver(validated.connection_string, validated.username, validated.password, async (session) => {
try {
const cypher = `MATCH path = (n)-[*1..${validated.depth}]-()
WHERE elementId(n) = $nodeId
RETURN path`;
const result = await session.run(cypher, { nodeId: validated.node_id });
const nodesMap = new Map<string, any>();
const relsMap = new Map<string, any>();
for (const record of result.records) {
const path = record.get('path');
for (const segment of path.segments) {
if (!nodesMap.has(segment.start.elementId)) nodesMap.set(segment.start.elementId, segment.start.properties);
if (!nodesMap.has(segment.end.elementId)) nodesMap.set(segment.end.elementId, segment.end.properties);
if (!relsMap.has(segment.relationship.elementId)) relsMap.set(segment.relationship.elementId, segment.relationship.properties);
}
}
return { nodes: Array.from(nodesMap.values()), relationships: Array.from(relsMap.values()) };
} catch (error) {
throw new Error(`Graph analysis failed: ${error instanceof Error ? error.message : String(error)}`);
}
});
}
export async function neo4j_test_connection(
connection_string: string,
username?: string,
password?: string,
): Promise<{ ok: boolean; uri: string; username: string }> {
const normalized = normalizeNeo4jUri(connection_string);
const finalUsername = username || normalized.username || 'neo4j';
const driver = getNeo4jDriver(connection_string, username, password);
await driver.verifyConnectivity();
return { ok: true, uri: normalized.uri, username: finalUsername };
}
/**
* Neo4j Database Tools
* CRUD, path finding, multi-statement script execution.
* Driver preserves URI scheme (bolt://, neo4j://, neo4j+s://, neo4j+ssc://).
* Labels and relationship types are restricted to safe identifiers.
*/
import neo4j from 'neo4j-driver';
type Driver = ReturnType<typeof neo4j.driver>;
import { z } from 'zod';
// @ts-ignore
declare global {
var __neo4j_clients__: Map<string, any>;
}
if (!global.__neo4j_clients__) {
global.__neo4j_clients__ = new Map();
}
const IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
export function assertNeo4jIdentifier(value: string, kind: string): string {
if (!value || !IDENTIFIER_RE.test(value)) {
throw new Error(
`Invalid Neo4j ${kind}: "${value}". Use only letters, digits, and underscore (must start with a letter or underscore).`,
);
}
return value;
}
const neo4jQuerySchema = z.object({
connection_string: z.string().min(1),
cypher: z.string().min(1),
params: z.record(z.any()).optional(),
username: z.string().optional().default('neo4j'),
password: z.string().optional().default(''),
});
const createNodeSchema = z.object({
connection_string: z.string().min(1),
labels: z.array(z.string()),
properties: z.record(z.any()),
username: z.string().optional().default('neo4j'),
password: z.string().optional().default(''),
});
const deleteNodeSchema = z.object({
connection_string: z.string().min(1),
node_id: z.string(),
username: z.string().optional().default('neo4j'),
password: z.string().optional().default(''),
});
const updateNodeSchema = z.object({
connection_string: z.string().min(1),
node_id: z.string(),
properties: z.record(z.any()),
username: z.string().optional().default('neo4j'),
password: z.string().optional().default(''),
});
const createRelationshipSchema = z.object({
connection_string: z.string().min(1),
from_node_id: z.string(),
to_node_id: z.string(),
type: z.string(),
properties: z.record(z.any()).optional(),
username: z.string().optional().default('neo4j'),
password: z.string().optional().default(''),
});
const deleteRelationshipSchema = z.object({
connection_string: z.string().min(1),
relationship_id: z.string(),
username: z.string().optional().default('neo4j'),
password: z.string().optional().default(''),
});
const pathFindSchema = z.object({
connection_string: z.string().min(1),
start_node_id: z.string(),
end_node_id: z.string(),
max_depth: z.number().int().min(1).max(20).optional().default(5),
username: z.string().optional().default('neo4j'),
password: z.string().optional().default(''),
});
const graphAnalysisSchema = z.object({
connection_string: z.string().min(1),
node_id: z.string(),
depth: z.number().int().min(1).max(10).optional().default(2),
username: z.string().optional().default('neo4j'),
password: z.string().optional().default(''),
});
export function normalizeNeo4jUri(connection_string: string): {
uri: string;
username?: string;
password?: string;
} {
const trimmed = connection_string.trim();
const schemeMatch = trimmed.match(/^(bolt|neo4j|neo4j\+s|neo4j\+ssc):\/\//i);
const scheme = schemeMatch ? schemeMatch[1].toLowerCase() : 'bolt';
let rest = schemeMatch ? trimmed.slice(schemeMatch[0].length) : trimmed;
let username: string | undefined;
let password: string | undefined;
const atIndex = rest.lastIndexOf('@');
if (atIndex !== -1) {
const credentials = rest.substring(0, atIndex);
rest = rest.substring(atIndex + 1);
const colonIndex = credentials.lastIndexOf(':');
if (colonIndex !== -1) {
username = credentials.substring(0, colonIndex);
password = credentials.substring(colonIndex + 1);
} else {
username = credentials;
}
}
if (!rest) {
throw new Error(`Invalid Neo4j connection string: "${connection_string}"`);
}
return { uri: `${scheme}://${rest}`, username, password };
}
export function getNeo4jDriver(
connection_string: string,
username?: string,
password?: string,
): Driver {
const normalized = normalizeNeo4jUri(connection_string);
const finalUsername = username || normalized.username || 'neo4j';
const finalPassword = password ?? normalized.password ?? '';
const cacheKey = `${normalized.uri}|${finalUsername}`;
if (global.__neo4j_clients__.has(cacheKey)) {
const driver = global.__neo4j_clients__.get(cacheKey);
if (driver && !driver.closed) return driver;
global.__neo4j_clients__.delete(cacheKey);
}
const neoDriver = neo4j.driver(
normalized.uri,
neo4j.auth.basic(finalUsername, finalPassword),
{ maxConnectionLifetime: 3 * 60 * 1000, maxConnectionPoolSize: 1 },
);
global.__neo4j_clients__.set(cacheKey, neoDriver);
return neoDriver;
}
async function withDriver<T>(
connection_string: string,
username: string | undefined,
password: string | undefined,
fn: (session: ReturnType<Driver['session']>) => Promise<T>,
): Promise<T> {
let driver: Driver;
try {
driver = getNeo4jDriver(connection_string, username, password);
await driver.verifyConnectivity();
} catch (error) {
throw new Error(
`Failed to connect to Neo4j: ${error instanceof Error ? error.message : String(error)}`,
);
}
const session = driver.session();
try {
return await fn(session);
} finally {
await session.close();
}
}
/** Split Cypher on semicolons outside quotes/comments. */
export function splitCypherStatements(script: string): string[] {
const statements: string[] = [];
let current = '';
let inSingle = false;
let inDouble = false;
let inLineComment = false;
let inBlockComment = false;
for (let i = 0; i < script.length; i++) {
const ch = script[i];
const next = script[i + 1];
if (inLineComment) {
current += ch;
if (ch === '\n') inLineComment = false;
continue;
}
if (inBlockComment) {
current += ch;
if (ch === '*' && next === '/') {
current += next;
i++;
inBlockComment = false;
}
continue;
}
if (!inSingle && !inDouble) {
if (ch === '/' && next === '/') {
inLineComment = true;
current += ch;
continue;
}
if (ch === '/' && next === '*') {
inBlockComment = true;
current += ch;
continue;
}
}
if (ch === "'" && !inDouble) {
if (inSingle && next === "'") {
current += ch + next;
i++;
continue;
}
inSingle = !inSingle;
current += ch;
continue;
}
if (ch === '"' && !inSingle) {
inDouble = !inDouble;
current += ch;
continue;
}
if (ch === ';' && !inSingle && !inDouble) {
const trimmed = current.trim();
if (trimmed) statements.push(trimmed);
current = '';
continue;
}
current += ch;
}
const tail = current.trim();
if (tail) statements.push(tail);
return statements;
}
export async function neo4j_run_script(
connection_string: string,
cypher: string,
username?: string,
password?: string,
stop_on_error: boolean = true,
): Promise<{
ok: boolean;
statements_run: number;
statements_failed: number;
results: any[];
errors: Array<{ index: number; statement_preview: string; error: string }>;
}> {
const statements = splitCypherStatements(cypher);
if (statements.length === 0) {
return { ok: true, statements_run: 0, statements_failed: 0, results: [], errors: [] };
}
return withDriver(connection_string, username, password, async (session) => {
const results: any[] = [];
const errors: Array<{ index: number; statement_preview: string; error: string }> = [];
let ran = 0;
for (let i = 0; i < statements.length; i++) {
const stmt = statements[i];
try {
const result = await session.run(stmt, {});
ran++;
const objs = result.records.slice(0, 50).map((r: any) => r.toObject());
if (objs.length) results.push(...objs);
} catch (error) {
const msg = error instanceof Error ? error.message : String(error);
errors.push({ index: i, statement_preview: stmt.slice(0, 160), error: msg });
if (stop_on_error) break;
}
}
return {
ok: errors.length === 0,
statements_run: ran,
statements_failed: errors.length,
results,
errors,
};
});
}
export async function neo4j_query(
connection_string: string,
cypher: string,
params?: Record<string, any>,
username?: string,
password?: string,
): Promise<{ results: any[]; multi_statement?: boolean; statements_run?: number; statements_failed?: number }> {
const validated = neo4jQuerySchema.parse({
connection_string,
cypher,
params,
username,
password,
});
const statements = splitCypherStatements(validated.cypher);
if (statements.length > 1) {
const summary = await neo4j_run_script(
validated.connection_string,
validated.cypher,
validated.username,
validated.password,
true,
);
return {
results: summary.results,
multi_statement: true,
statements_run: summary.statements_run,
statements_failed: summary.statements_failed,
};
}
return withDriver(validated.connection_string, validated.username, validated.password, async (session) => {
try {
const result = await session.run(validated.cypher, validated.params || {});
return { results: result.records.map((r: any) => r.toObject()) };
} catch (error) {
throw new Error(
`Cypher execution failed: ${error instanceof Error ? error.message : String(error)}. ` +
`For large multi-statement scripts use neo4j_run_cypher_file or neo4j_run_script.`,
);
}
});
}
export async function neo4j_create_node(
connection_string: string,
labels: string[],
properties: Record<string, any>,
username?: string,
password?: string,
): Promise<{ node_id: string }> {
const validated = createNodeSchema.parse({ connection_string, labels, properties, username, password });
const safeLabels = validated.labels.map((l) => assertNeo4jIdentifier(l, 'label'));
const labelString = safeLabels.map((l) => `:${l}`).join('');
return withDriver(validated.connection_string, validated.username, validated.password, async (session) => {
try {
const cypher = `CREATE (n${labelString} $props) RETURN elementId(n) as node_id`;
const result = await session.run(cypher, { props: validated.properties });
return { node_id: String(result.records[0].get('node_id')) };
} catch (error) {
throw new Error(`Failed to create node: ${error instanceof Error ? error.message : String(error)}`);
}
});
}
export async function neo4j_delete_node(
connection_string: string,
node_id: string,
username?: string,
password?: string,
): Promise<{ success: boolean }> {
const validated = deleteNodeSchema.parse({ connection_string, node_id, username, password });
return withDriver(validated.connection_string, validated.username, validated.password, async (session) => {
try {
await session.run('MATCH (n) WHERE elementId(n) = $id DETACH DELETE n', { id: validated.node_id });
return { success: true };
} catch (error) {
throw new Error(`Failed to delete node: ${error instanceof Error ? error.message : String(error)}`);
}
});
}
export async function neo4j_update_node(
connection_string: string,
node_id: string,
properties: Record<string, any>,
username?: string,
password?: string,
): Promise<{ success: boolean }> {
const validated = updateNodeSchema.parse({ connection_string, node_id, properties, username, password });
if (Object.keys(validated.properties).length === 0) return { success: true };
return withDriver(validated.connection_string, validated.username, validated.password, async (session) => {
try {
await session.run('MATCH (n) WHERE elementId(n) = $id SET n += $props', {
id: validated.node_id,
props: validated.properties,
});
return { success: true };
} catch (error) {
throw new Error(`Failed to update node: ${error instanceof Error ? error.message : String(error)}`);
}
});
}
export async function neo4j_create_relationship(
connection_string: string,
from_node_id: string,
to_node_id: string,
type: string,
properties?: Record<string, any>,
username?: string,
password?: string,
): Promise<{ relationship_id: string }> {
const validated = createRelationshipSchema.parse({
connection_string, from_node_id, to_node_id, type, properties, username, password,
});
const relType = assertNeo4jIdentifier(validated.type, 'relationship type');
return withDriver(validated.connection_string, validated.username, validated.password, async (session) => {
try {
let cypher = `MATCH (a), (b) WHERE elementId(a) = $fromId AND elementId(b) = $toId CREATE (a)-[r:${relType}]->(b)`;
if (validated.properties && Object.keys(validated.properties).length > 0) cypher += ' SET r += $props';
cypher += ' RETURN elementId(r) as relationship_id';
const result = await session.run(cypher, {
fromId: validated.from_node_id,
toId: validated.to_node_id,
props: validated.properties || {},
});
return { relationship_id: String(result.records[0].get('relationship_id')) };
} catch (error) {
throw new Error(`Failed to create relationship: ${error instanceof Error ? error.message : String(error)}`);
}
});
}
export async function neo4j_delete_relationship(
connection_string: string,
relationship_id: string,
username?: string,
password?: string,
): Promise<{ success: boolean }> {
const validated = deleteRelationshipSchema.parse({ connection_string, relationship_id, username, password });
return withDriver(validated.connection_string, validated.username, validated.password, async (session) => {
try {
await session.run('MATCH ()-[r]->() WHERE elementId(r) = $id DELETE r', { id: validated.relationship_id });
return { success: true };
} catch (error) {
throw new Error(`Failed to delete relationship: ${error instanceof Error ? error.message : String(error)}`);
}
});
}
export async function neo4j_path_find(
connection_string: string,
start_node_id: string,
end_node_id: string,
max_depth?: number,
username?: string,
password?: string,
): Promise<{ paths: Array<{ nodes: any[]; relationships: any[] }> }> {
const validated = pathFindSchema.parse({
connection_string, start_node_id, end_node_id, max_depth, username, password,
});
return withDriver(validated.connection_string, validated.username, validated.password, async (session) => {
try {
const cypher = `MATCH path = shortestPath((start)-[*1..${validated.max_depth}]->(end))
WHERE elementId(start) = $fromId AND elementId(end) = $toId
RETURN path`;
const result = await session.run(cypher, {
fromId: validated.start_node_id,
toId: validated.end_node_id,
});
return {
paths: result.records.map((r: any) => {
const path = r.get('path');
const nodes = [];
const relationships = [];
for (const segment of path.segments) {
nodes.push(segment.start.properties);
relationships.push(segment.relationship.properties);
nodes.push(segment.end.properties);
}
return { nodes, relationships };
}),
};
} catch (error) {
throw new Error(`Path finding failed: ${error instanceof Error ? error.message : String(error)}`);
}
});
}
export async function neo4j_graph_analysis(
connection_string: string,
node_id: string,
depth?: number,
username?: string,
password?: string,
): Promise<{ nodes: any[]; relationships: any[] }> {
const validated = graphAnalysisSchema.parse({ connection_string, node_id, depth, username, password });
return withDriver(validated.connection_string, validated.username, validated.password, async (session) => {
try {
const cypher = `MATCH path = (n)-[*1..${validated.depth}]-()
WHERE elementId(n) = $nodeId
RETURN path`;
const result = await session.run(cypher, { nodeId: validated.node_id });
const nodesMap = new Map<string, any>();
const relsMap = new Map<string, any>();
for (const record of result.records) {
const path = record.get('path');
for (const segment of path.segments) {
if (!nodesMap.has(segment.start.elementId)) nodesMap.set(segment.start.elementId, segment.start.properties);
if (!nodesMap.has(segment.end.elementId)) nodesMap.set(segment.end.elementId, segment.end.properties);
if (!relsMap.has(segment.relationship.elementId)) relsMap.set(segment.relationship.elementId, segment.relationship.properties);
}
}
return { nodes: Array.from(nodesMap.values()), relationships: Array.from(relsMap.values()) };
} catch (error) {
throw new Error(`Graph analysis failed: ${error instanceof Error ? error.message : String(error)}`);
}
});
}
export async function neo4j_test_connection(
connection_string: string,
username?: string,
password?: string,
): Promise<{ ok: boolean; uri: string; username: string }> {
const normalized = normalizeNeo4jUri(connection_string);
const finalUsername = username || normalized.username || 'neo4j';
const driver = getNeo4jDriver(connection_string, username, password);
await driver.verifyConnectivity();
return { ok: true, uri: normalized.uri, username: finalUsername };
}