ƒxyzƒxyz Network
Developer

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:

SchemeTLSRouting
bolt://NoNo
bolt+s://YesNo
bolt+ssc://Yes (self-signed)No
neo4j://NoYes
neo4j+s://YesYes

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.


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-PatternFix
Manual session.run() with manual session.close()Use client.run() or client.executeCypher()
node.identity.toNumber()Use node.elementId (string)
encrypted: true in driver configUse bolt+s:// URI scheme
session.beginTransaction()Use session.executeRead() / session.executeWrite()
neo4j.int(value).toNumber()Use toSafeNumber(value) from @repo/neo4j
Hardcoded Integer type checksUse neo4j.isInt(value) guard
Casting count results to Number()Use toSafeNumber() for safety

Key Files

FilePurpose
packages/neo4j/src/client.tsNeo4j client singleton, toSafeNumber(), session management
packages/neo4j/src/config.tsEnvironment-based configuration (URI scheme for TLS)
packages/neo4j/src/neo4j-types.d.tsTypeScript declarations for v6 types
packages/neo4j/src/migrate.tsMigration runner using executeRead()/executeWrite()
packages/neo4j/src/rdf-sync.tsRDF sync using executeWrite()
packages/neo4j/src/utils/formatters.tsValue formatting with toSafeNumber()

Migration Checklist

If you're adding new Neo4j code, verify:

  • Using client.run() or client.executeCypher() (not manual sessions)
  • Using elementId for node/relationship IDs (not identity)
  • Using toSafeNumber() for any numeric aggregates
  • URI scheme controls TLS (no encrypted option)
  • Multi-statement writes use executeWrite() (not beginTransaction())
  • No Integer type imports used unnecessarily (v6 returns native numbers for safe values)

On this page