mcpField
N
neo4j-driver-javascript-skill

neo4j-driver-javascript-skill

github.com/neo4j-contrib开发118407 次安装1 周前更新
代码仓库

关于 neo4j-driver-javascript-skill

neo4j-driver-javascript-skill 为使用 JavaScript 或 TypeScript 连接 Neo4j 的开发者提供驱动开发指导。内容包括查询与事务、数据类型、安全参数化查询及资源生命周期管理。

主要功能

  1. 01设置驱动、会话和事务
  2. 02执行查询及管理结果流
  3. 03处理整数与记录类型
  4. 04管理会话生命周期和错误
  5. 05使用参数化 Cypher 查询

适用场景

  1. 01为 Node.js 应用配置 Neo4j 驱动
  2. 02排查事务重试和结果流问题
  3. 03避免整数序列化精度损失
  4. 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.md
references/
advanced-patterns.md
browser-usage.md
data-types.md
rxjs-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。


驱动程序生命周期

在启动时创建一个驱动程序实例,并在各处共享。切勿为每个请求单独创建。

// 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()大型结果集、流式处理、多查询事务✅惰性流
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 类型。

三种模式:

// 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
对 temporal 使用 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() }
在事务回调中执行副作用移到外部——回调可能会重试
每个请求都创建新的 driver在启动时创建一次
在浏览器中使用 bolt:// 或 neo4j://使用 neo4j+s://(WSS)
在 UNWIND 数组中使用 Integer先转换为普通 JavaScript Number
在无服务器环境中设置 maxConnectionPoolSize: 100每个函数实例使用 5–10

参考资料

按需加载:


检查清单

  • 在启动时创建一个 driver 实例,并在各处共享
  • 在每个查询/会话中指定 database
  • 在启动时调用 await driver.verifyConnectivity()
  • 在 finally 块中调用 session.close()
  • 使用 .get() 访问记录——不要使用点号/方括号表示法
  • 在 JSON 序列化之前调用 Integer 的 .toNumber() / .toString()
  • 在 JSON 序列化之前调用 temporal 的 .toString()
  • 在访问计数字段之前调用 summary.counters.updates()
  • 使用 $param 占位符——不要在 Cypher 中进行字符串拼接
  • 事务回调返回普通数据(而非流);在回调外执行副作用
  • 写入时对整数参数使用 neo4j.int()
  • 读取使用 executeRead;写入使用 executeWrite
  • 浏览器:使用 neo4j+s:// URI(WebSocket)

评论

支持 Markdown