neo4j-driver-javascript-skill について
neo4j-driver-javascript-skill は、JavaScript または TypeScript で Neo4j に接続する開発者向けのドライバーガイドです。クエリとトランザクション、データ型、パラメーター化クエリ、安全なリソース管理を扱います。
主な機能
- 01ドライバー、セッション、トランザクションの設定
- 02クエリ実行と結果ストリームの管理
- 03整数とレコード型の処理
- 04セッションのライフサイクルとエラー管理
- 05パラメーター化 Cypher クエリの利用
活用シーン
- 01Node.js アプリ向け Neo4j ドライバーの設定
- 02トランザクション再試行や結果ストリームの調査
- 03シリアライズ時の整数精度損失の回避
- 04セッションリークやクエリインジェクションリスクの修正
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ neo4j-contrib/ neo4j-skills --skill neo4j-driver-javascript-skillインストール後、Claude Code で/neo4j-driver-javascript-skillを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
README.mdreferences/advanced-patterns.mdbrowser-usage.mddata-types.mdrxjs-session.md詳細
使用する場面
- Neo4j に接続する JS/TS コードを書く場合(Node.js またはブラウザー)
- ドライバー、セッション、トランザクション、クエリ実行を設定する場合
- Integer の処理、結果の消費、セッションリーク、非同期エラーをデバッグする場合
- ドライバーオブジェクトに TypeScript の型注釈を付ける場合
使用しない場面
- Cypher の作成/最適化 →
neo4j-cypher-skill - ドライバーのバージョンアップ →
neo4j-migration-skill - RxJS セッション API → references/rxjs-session.md
インストール
npm install neo4j-driver # or: yarn add neo4j-driver
環境変数
環境変数から接続設定を読み込みます。認証情報をハードコードしないでください。
# .env file (add to .gitignore)
NEO4J_URI=neo4j+s://xxx.databases.neo4j.io
NEO4J_USERNAME=neo4j
NEO4J_PASSWORD=secret
NEO4J_DATABASE=neo4j
// npm install dotenv (for Node.js < 20 or when .env auto-load is off)
import 'dotenv/config' // or: require('dotenv').config()
const URI = process.env.NEO4J_URI
const USER = process.env.NEO4J_USERNAME
const PASSWORD = process.env.NEO4J_PASSWORD
const DATABASE = process.env.NEO4J_DATABASE ?? 'neo4j'
Node 20 以降では、--env-file .env を使って .env をネイティブに読み込めます。Next.js/Vite は .env を自動で読み込むため、dotenv のインポートは不要です。
ドライバーのライフサイクル
起動時にドライバーインスタンスを 1 つだけ作成します。どこからでも共有してください。リクエストごとに作成しないでください。
// CommonJS
const neo4j = require('neo4j-driver')
// ESM / TypeScript
import neo4j from 'neo4j-driver'
const driver = neo4j.driver(
process.env.NEO4J_URI, // 'neo4j+s://xxx.databases.neo4j.io'
neo4j.auth.basic(process.env.NEO4J_USER, process.env.NEO4J_PASSWORD)
)
await driver.verifyConnectivity() // fail fast on startup if unreachable
// On shutdown:
await driver.close()
URI スキーム:
| スキーム | トランスポート | 用途 |
|---|---|---|
neo4j+s:// | TLS + クラスターのルーティング | Aura、本番クラスター |
neo4j:// | 平文 + クラスターのルーティング | ローカル開発クラスター |
bolt+s:// | TLS、単一インスタンス | TLS を使用する単一 Neo4j インスタンス |
bolt:// | 平文、単一インスタンス | ローカルの単一インスタンス |
認証オプション:
neo4j.auth.basic(user, password) // username/password
neo4j.auth.bearer(token) // SSO / JWT
neo4j.auth.kerberos(base64Ticket) // Kerberos
neo4j.auth.none() // unauthenticated (dev only)
Web フレームワークでのシングルトン — 一度作成し、どこからでもインポートします:
// db.js
let _driver = null
export function getDriver() {
if (!_driver) _driver = neo4j.driver(process.env.NEO4J_URI,
neo4j.auth.basic(process.env.NEO4J_USER, process.env.NEO4J_PASSWORD))
return _driver
}
export async function closeDriver() {
if (_driver) { await _driver.close(); _driver = null }
}
サーバーレス(Lambda/Vercel/Workers):maxConnectionPoolSize: 5 を維持してください。SIGTERM は保証されません。
適切な API の選択
| API | 使用する場面 | 自動再試行 | 結果 |
|---|---|---|---|
driver.executeQuery() | ほとんどのクエリでのデフォルト | ✅ | 即時取得(全レコード) |
session.executeRead/Write() | 大きな結果、ストリーミング、複数クエリの tx | ✅ | 遅延ストリーム |
session.run() | LOAD CSV、CALL IN TRANSACTIONS、スクリプト | ❌ | 遅延ストリーム |
executeQuery — デフォルト
const { records, summary, keys } = await driver.executeQuery(
'MATCH (p:Person {name: $name})-[:KNOWS]->(f) RETURN f.name AS name',
{ name: 'Alice' },
{ database: 'neo4j', routing: neo4j.routing.READ }
)
for (const record of records) {
console.log(record.get('name')) // use .get() — records are NOT plain objects
}
// Write and count results
const { summary: s } = await driver.executeQuery(
'CREATE (p:Person {name: $name, age: $age})',
{ name: 'Bob', age: neo4j.int(30) },
{ database: 'neo4j' }
)
console.log(s.counters.updates().nodesCreated) // ✅ must call .updates()
必ず database を指定してください。省略すると余分なラウンドトリップが発生します。
❌ Cypher をテンプレートリテラルにしないでください:
// ❌ injection risk + disables plan caching
await driver.executeQuery(`MATCH (p:Person {name: '${name}'}) RETURN p`)
// ✅ parameterised
await driver.executeQuery('MATCH (p:Person {name: $name}) RETURN p', { name })
管理対象トランザクション(executeRead / executeWrite)
一時的な障害が発生した場合は自動で再試行されます。コールバック内でレコードを消費してください。コールバックが戻るとストリームは利用できなくなります。
const session = driver.session({ database: 'neo4j' })
try {
const names = await session.executeRead(async tx => {
const result = await tx.run(
'MATCH (p:Person) WHERE p.name STARTS WITH $prefix RETURN p.name AS name',
{ prefix: 'Al' }
)
// ✅ collect() while tx is open; return plain data
return (await result.collect()).map(r => r.get('name'))
})
await session.executeWrite(async tx => {
await tx.run('MERGE (p:Person {name: $name})', { name: 'Carol' })
})
} finally {
await session.close() // always in finally
}
重要:await tx.run() が返すのはストリームハンドルであり、レコードではありません。
// ❌ returns stream; tx closes; records = []
return await tx.run('MATCH (p:Person) RETURN p.name AS name')
// ✅ collect fully inside callback
const result = await tx.run('MATCH (p:Person) RETURN p.name AS name')
return (await result.collect()).map(r => r.get('name'))
// ✅ or stream with for-await
for await (const record of result) { names.push(record.get('name')) }
コールバックは複数回実行されることがあります(一時的な障害時に再試行されるため)。コールバック内で副作用を発生させないでください:
// ❌ fetch() called on every retry
await session.executeWrite(async tx => {
await fetch('https://api.example.com/notify')
await tx.run('CREATE ...')
})
// ✅ side effects after confirmed commit
await session.executeWrite(async tx => { await tx.run('MERGE ...') })
await fetch('https://api.example.com/notify')
session.run — 暗黙的なトランザクション
自動再試行されません。LOAD CSV/CALL IN TRANSACTIONS/スクリプトの場合にのみ使用してください。
const session = driver.session({ database: 'neo4j' })
try {
const result = await session.run('CREATE (p:Person {name: $name}) RETURN p', { name: 'Alice' })
console.log(result.summary.counters.updates().nodesCreated)
} finally {
await session.close()
}
セッションのクローズ — 常に finally で実行
// ❌ session leaks if executeRead rejects
session.executeRead(async tx => { ... })
.then(result => doSomething(result))
.then(() => session.close())
// ✅ guaranteed close
try {
const result = await session.executeRead(async tx => { ... })
doSomething(result)
} finally {
await session.close()
}
// ✅ promise-chain equivalent
session.executeRead(async tx => { ... })
.then(doSomething)
.catch(handleError)
.finally(() => session.close())
Integer の処理
Neo4j の整数は 64 ビットです。JS の Number は IEEE 754 形式で、安全に扱える範囲は 2^53−1 までです。ドライバーはデフォルトで独自の Integer を返します。
3 つのモード:
// Mode 1 (default): Integer class — safe for all values, requires conversion
const driver1 = neo4j.driver(URI, auth)
// record.get('count') → Integer { low: 42, high: 0 }
// Mode 2: native JS number — only safe within Number.MAX_SAFE_INTEGER
const driver2 = neo4j.driver(URI, auth, { disableLosslessIntegers: true })
// record.get('count') → 42
// Mode 3: BigInt — precise but breaks JSON.stringify
const driver3 = neo4j.driver(URI, auth, { useBigInt: true })
// record.get('count') → 42n
Integer クラスの操作:
const count = record.get('count') // Integer { low: 42, high: 0 }
neo4j.isInt(count) // true
neo4j.integer.inSafeRange(count) // check before toNumber()
count.toNumber() // 42 (only safe within MAX_SAFE_INTEGER)
count.toString() // '42' (always safe)
count.toBigInt() // 42n
// Send integer parameter — plain JS number sends as FLOAT
await driver.executeQuery('CREATE (p:Person {age: $age})', { age: neo4j.int(30) })
JSON シリアライズ時の注意点:
// ❌ Integer → {"low":42,"high":0}
JSON.stringify({ age: record.get('age') })
// ❌ BigInt → TypeError: Do not know how to serialize a BigInt
// ✅ convert first
JSON.stringify({ age: record.get('age').toNumber() })
// ✅ or use disableLosslessIntegers: true
// ❌ temporal types → {} silently
JSON.stringify({ dt: record.get('created') })
// ✅
JSON.stringify({ dt: record.get('created').toString() })
レコードへのアクセス
レコードはプレーンオブジェクトではありません。.get() を使用してください:
const record = records[0]
record.get('name') // ✅ by key
record.get(0) // ✅ by index
record.keys // ['name', 'age']
record.has('name') // true
record.name // ❌ undefined
record['name'] // ❌ undefined
record.toObject() はプレーンな JS キーを返しますが、値はドライバーの型のまま(Integer、時間型など)です。変換しないと JSON として安全に扱えません。
エラー処理
import { Neo4jError, SERVICE_UNAVAILABLE, SESSION_EXPIRED } from 'neo4j-driver'
try {
await driver.executeQuery('...', {}, { database: 'neo4j' })
} catch (err) {
if (err instanceof Neo4jError) {
if (err.code === 'Neo.ClientError.Schema.ConstraintValidationFailed') { /* unique constraint */ }
if (err.code === SERVICE_UNAVAILABLE) { /* unreachable */ }
if (err.code === SESSION_EXPIRED) { /* open a new session */ }
if (err.retriable) { /* transient — executeQuery already retried to exhaustion */ }
}
}
executeQuery と executeRead/Write は再試行可能なエラーを自動で再試行します。session.run は再試行しません。
TypeScript
import neo4j, { Driver, Session, ManagedTransaction, Record, Node, Integer } from 'neo4j-driver'
const driver: Driver = neo4j.driver(URI, neo4j.auth.basic(USER, PASSWORD))
const session: Session = driver.session({ database: 'neo4j' })
const names: string[] = await session.executeRead(
async (tx: ManagedTransaction): Promise<string[]> => {
const result = await tx.run('MATCH (p:Person) RETURN p.name AS name')
return (await result.collect()).map((r: Record) => r.get('name') as string)
}
)
// Typed node — Integer generic changes with disableLosslessIntegers
const node = record.get('p') as Node<Integer>
const age: number = node.properties.age.toNumber()
// With disableLosslessIntegers: true → Node<number>; age is already number
UNWIND による一括書き込み
// ❌ one transaction per item
for (const p of people) { await driver.executeQuery('CREATE ...', p) }
// ✅ single transaction
await driver.executeQuery(
`UNWIND $people AS person
MERGE (p:Person {name: person.name})
SET p.age = person.age`,
{ people }, // array of plain objects; numeric fields must be JS numbers, not neo4j.int()
{ database: 'neo4j' }
)
よくある間違い
| 間違い | 修正方法 |
|---|---|
| テンプレートリテラルの Cypher | $param プレースホルダーを使用します |
record.name / record['name'] | record.get('name') |
Integer に対する JSON.stringify | .toNumber() または disableLosslessIntegers |
時間型に対する JSON.stringify | 先に .toString() を呼び出します |
summary.counters.nodesCreated | summary.counters.updates().nodesCreated |
database を省略する | 常に { database: 'neo4j' } を指定します |
トランザクションコールバックから result を返す | await result.collect() またはマッピングしたデータを返します |
.then(() => session.close()) | try/finally { await session.close() } |
| トランザクションコールバック内で副作用を発生させる | 外に移します — コールバックは再試行される場合があります |
| リクエストごとに新しいドライバーを作成する | 起動時に一度だけ作成します |
ブラウザーで bolt:// または neo4j:// を使用する | neo4j+s://(WSS)を使用します |
| UNWIND 配列内の Integer | 先に通常の JS Number に変換します |
サーバーレス環境で maxConnectionPoolSize: 100 を指定する | 関数インスタンスごとに 5~10 を使用します |
リファレンス
必要に応じて読み込みます:
- references/data-types.md — 型マッピング表の全容、時間型、グラフ型(Node/Relationship/Path)、空間型(Point/WGS-84/Cartesian)、
neo4j.UUID[6.2+]、toNative()変換ヘルパー - references/advanced-patterns.md — 明示的なトランザクション、因果整合性/ブックマーク、接続プールの調整、結果トランスフォーマー、遅延ストリーミング、リポジトリパターン
- references/browser-usage.md — WebSocket URI、CORS、バンドラー設定、セキュリティガイダンス
- references/rxjs-session.md — RxJS セッション API(
rxSession.run()、Observable パターン)
チェックリスト
- 起動時にドライバーインスタンスを 1 つ作成し、すべての場所で共有する
- すべてのクエリ/セッションで
databaseを指定する - 起動時に
await driver.verifyConnectivity()を呼び出す -
finallyブロックでsession.close()を呼び出す - レコードにはドット/ブラケット記法ではなく
.get()でアクセスする - JSON シリアライズの前に Integer に対して
.toNumber()/.toString()を呼び出す - JSON シリアライズの前に時間型に対して
.toString()を呼び出す - カウンターフィールドにアクセスする前に
summary.counters.updates()を呼び出す -
$paramプレースホルダーを使用し、Cypher で文字列連結をしない - トランザクションコールバックからプレーンデータを返す(ストリームは返さない)。副作用はコールバックの外で発生させる
- 書き込みでは整数パラメーターに
neo4j.int()を使用する - 読み取りには
executeRead、書き込みにはexecuteWriteを使用する - ブラウザーでは
neo4j+s://URI(WebSocket)を使用する