Neo4j Driver v6 Patterns
Migration guide and modern patterns for neo4j-driver v6 in the fXYZ codebase
Neo4j Driver v6 Patterns
Guide to the neo4j-driver v6 upgrade (from v5) and modern patterns used across the fXYZ codebase.
Breaking Changes Applied
1. TLS via URI Scheme (not encrypted)
The encrypted config option was removed in v6. TLS is now controlled entirely by the URI scheme.
// TLS is determined by the URI scheme
const driver = neo4j.driver(
'bolt+s://your-server:7687', // TLS enabled
neo4j.auth.basic(user, password),
{ connectionTimeout: 30000 }
);
// Plain connection (dev/local)
const driver = neo4j.driver(
'bolt://localhost:7687', // No TLS
neo4j.auth.basic(user, password)
);// ❌ REMOVED — 'encrypted' option no longer exists
const driver = neo4j.driver(uri, auth, {
encrypted: true, // This throws in v6
});URI scheme reference:
| Scheme | TLS | Routing |
|---|---|---|
bolt:// | No | No |
bolt+s:// | Yes | No |
bolt+ssc:// | Yes (self-signed) | No |
neo4j:// | No | Yes |
neo4j+s:// | Yes | Yes |
2. elementId Replaces identity
Node and relationship identifiers changed from Integer to string.
// elementId is a string like "4:abc123:42"
const nodeId = node.elementId;
// In our client, getNodeId() handles both for safety:
private getNodeId(node: Node): string {
if ('elementId' in node && (node as any).elementId) {
return String((node as any).elementId);
}
// Fallback for edge cases
const identity = (node as any).identity;
return identity != null ? String(identity) : '';
}// ❌ identity was an Integer object
const nodeId = node.identity.toNumber();
// or
const nodeId = node.identity.toString();In resolvers, this means replacing all identity access:
// ❌ Old pattern (v5)
const id = record.get('n').identity.toString();
// ✅ New pattern (v6)
const id = record.get('n').elementId;3. executeRead() / executeWrite() Replace beginTransaction()
Manual transaction management via session.beginTransaction() was removed. Use managed transactions instead.
// Read transaction — auto-commits on success, auto-rolls-back on error
const result = await session.executeRead(async (tx) => {
const res = await tx.run('MATCH (n:Member) RETURN n LIMIT 10');
return res.records;
});
// Write transaction — same auto-commit/rollback behavior
await session.executeWrite(async (tx) => {
await tx.run(
'CREATE (n:Member {did: $did, createdAt: datetime()})',
{ did: memberDid }
);
});// ❌ Manual transaction management (removed in v6)
const tx = session.beginTransaction();
try {
await tx.run('CREATE ...');
await tx.commit();
} catch (error) {
await tx.rollback();
throw error;
}4. Safe Integer Handling with toSafeNumber()
In v6, integers within JavaScript's safe range are returned as native number. Large integers may still return as Integer objects.
import { toSafeNumber } from '@repo/neo4j';
// Works with both native numbers AND Integer objects
const count = toSafeNumber(record.get('count'));
const timestamp = toSafeNumber(summary.resultAvailableAfter);Implementation (packages/neo4j/src/client.ts):
export function toSafeNumber(value: unknown): number {
if (typeof value === 'number') return value;
if (neo4j.isInt(value)) return (value as Integer).toNumber();
return Number(value) || 0;
}Where to use: Any place you read a count, sum, or numeric aggregate from a Neo4j query result.
Recommended Patterns
Session-Per-Query via Client Methods
Use Neo4jClient.run() or Neo4jClient.executeCypher() instead of manually managing sessions. The client handles session lifecycle automatically.
// ✅ Preferred — client manages session
const client = Neo4jClient.getInstance();
const results = await client.run<MemberResult>(
'MATCH (m:Member {did: $did}) RETURN m',
{ did: memberDid }
);
// ✅ Also good — when you need raw records
const result = await client.executeCypher(
'MATCH (m:Member {did: $did}) RETURN m',
{ did: memberDid }
);Managed Transactions for Multi-Statement Writes
When you need multiple statements in one atomic transaction, use executeWrite() directly on a session:
const session = client.getSession();
try {
await session.executeWrite(async (tx) => {
// Both statements are in the same transaction
await tx.run('CREATE (a:Node {id: $id})', { id: '1' });
await tx.run(
'MATCH (a:Node {id: $id}), (b:Node {id: $targetId}) CREATE (a)-[:LINKS]->(b)',
{ id: '1', targetId: '2' }
);
});
} finally {
await session.close();
}GQL Status Objects (v6 New Feature)
v6 introduces structured GQL status codes on query results. Use these for better error classification:
const result = await session.run('MATCH (n) RETURN n');
// Check for notifications/warnings
if (result.summary.notifications?.length) {
for (const notification of result.summary.notifications) {
logger.warn(`Neo4j: ${notification.code} — ${notification.description}`);
}
}Type-Safe Record Access
Always use explicit types when accessing record values:
// ✅ Type-safe with our client
const members = await client.run<{
id: string;
did: string;
email: string | null;
}>('MATCH (m:Member) RETURN m.id as id, m.did as did, m.email as email');
// Each member is typed as { id: string; did: string; email: string | null }
members.forEach(m => console.log(m.did));Anti-Patterns to Avoid
| Anti-Pattern | Fix |
|---|---|
Manual session.run() with manual session.close() | Use client.run() or client.executeCypher() |
node.identity.toNumber() | Use node.elementId (string) |
encrypted: true in driver config | Use bolt+s:// URI scheme |
session.beginTransaction() | Use session.executeRead() / session.executeWrite() |
neo4j.int(value).toNumber() | Use toSafeNumber(value) from @repo/neo4j |
Hardcoded Integer type checks | Use neo4j.isInt(value) guard |
Casting count results to Number() | Use toSafeNumber() for safety |
Key Files
| File | Purpose |
|---|---|
packages/neo4j/src/client.ts | Neo4j client singleton, toSafeNumber(), session management |
packages/neo4j/src/config.ts | Environment-based configuration (URI scheme for TLS) |
packages/neo4j/src/neo4j-types.d.ts | TypeScript declarations for v6 types |
packages/neo4j/src/migrate.ts | Migration runner using executeRead()/executeWrite() |
packages/neo4j/src/rdf-sync.ts | RDF sync using executeWrite() |
packages/neo4j/src/utils/formatters.ts | Value formatting with toSafeNumber() |
Migration Checklist
If you're adding new Neo4j code, verify:
- Using
client.run()orclient.executeCypher()(not manual sessions) - Using
elementIdfor node/relationship IDs (notidentity) - Using
toSafeNumber()for any numeric aggregates - URI scheme controls TLS (no
encryptedoption) - Multi-statement writes use
executeWrite()(notbeginTransaction()) - No
Integertype imports used unnecessarily (v6 returns native numbers for safe values)