关于 neo4j-graphql-skill
neo4j-graphql-skill 为使用 @neo4j/graphql 构建 Neo4j GraphQL API 的开发者提供实践指南,涵盖 schema、认证授权及 v5/v6 到 v7 的迁移要点。
主要功能
- 01配置 GraphQL 类型与关系指令
- 02生成查询、变更和订阅
- 03设置 JWT 认证与授权规则
- 04使用 OGM 进行服务端访问
- 05排查版本迁移与常见错误
适用场景
- 01从 Neo4j 图 schema 创建 GraphQL API
- 02为字段和类型添加 JWT 授权
- 03将 @neo4j/graphql 项目迁移至 v7
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ neo4j-contrib/ neo4j-skills --skill neo4j-graphql-skill安装后在 Claude Code 中输入/neo4j-graphql-skill确认已加载。
附带文件README.mdreferences/search-directives.md
详细介绍
适用场景
- 使用
@neo4j/graphql根据 Neo4j 图模式创建 GraphQL API - 使用
@relationship、@cypher、@authorization指令编写类型定义 - 使用 OGM 在服务端以编程方式访问 Neo4j(绕过 GraphQL 身份验证)
- 配置自动生成的查询、变更和订阅
- 使用基于 JWT 或 JWKS 的
@authorization规则保护类型/字段 - 从 v5/v6 迁移到 v7(下文列出重大变更)
不适用场景
- 在 GraphQL 解析器之外执行原始 Cypher 查询 →
neo4j-cypher-skill - Spring Data Neo4j/Java 实体映射 →
neo4j-spring-data-skill - 不使用 Neo4j 的通用 GraphQL — 不在此范围内
版本矩阵
| 版本 | 状态 | 备注 |
|---|---|---|
| v7 | 当前版本 — 使用 ≥ 7.6.2 | 必须使用 @node;已移除 options;使用显式 eq 语法 |
| v5 | LTS — 使用 ≥ 5.12.15 | 旧版语法;仍支持 options: {limit, offset, sort} |
除非代码库使用 v5,否则默认使用 v7。发布身份验证功能前,请升级到高于以下安全补丁版本的版本:
| 修复内容 | 修复版本 |
|---|---|
订阅会接受来自 WebSocket connectionParams 的未经验证的 JWT(绕过身份验证/授权) | 7.5.6 / 5.12.14 |
当操作类型也有类型级 @authentication 时,根自定义解析器字段上的字段级 @authentication 会被忽略(权限提升) | 7.6.0 / 5.12.15 |
创建关系操作未应用 @authorization 规则 | 7.6.2 |
步骤 1 — 安装
npm install @neo4j/graphql neo4j-driver graphql @apollo/server
使用订阅(需要 CDC):
npm install ws graphql-ws express body-parser cors
步骤 2 — 最小化服务端设置
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { Neo4jGraphQL } from '@neo4j/graphql';
import neo4j from 'neo4j-driver';
const typeDefs = `#graphql
type Movie @node {
id: ID! @id
title: String!
actors: [Person!]! @relationship(type: "ACTED_IN", direction: IN)
}
type Person @node {
id: ID! @id
name: String!
movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT)
}
`;
const driver = neo4j.driver(
process.env.NEO4J_URI,
neo4j.auth.basic(process.env.NEO4J_USERNAME, process.env.NEO4J_PASSWORD)
);
const neoSchema = new Neo4jGraphQL({ typeDefs, driver });
// assertIndexesAndConstraints syncs @id → UNIQUE constraints; wrap in try/catch
await neoSchema.assertIndexesAndConstraints({ options: { create: true } });
const server = new ApolloServer({ schema: await neoSchema.getSchema() });
const { url } = await startStandaloneServer(server, {
context: async ({ req }) => ({ token: req.headers.authorization }),
listen: { port: 4000 },
});
关键指令
@node(v7 必需)
type Product @node {
id: ID! @id
name: String!
}
# Custom label (default = type name)
type Article @node(labels: ["Post", "Content"]) {
title: String!
}
@relationship — 完整语法
type Person @node {
# direction: OUT = (this)-[:KNOWS]->(other)
friends: [Person!]! @relationship(type: "KNOWS", direction: OUT)
# direction: IN = (other)-[:ACTED_IN]->(this)
actedIn: [Movie!]! @relationship(type: "ACTED_IN", direction: IN)
# direction: UNDIRECTED = matches both directions (use sparingly — double-counts)
colleagues: [Person!]! @relationship(type: "COLLEAGUE_OF", direction: UNDIRECTED)
# Relationship with properties — reference an @relationshipProperties interface
reviews: [Movie!]! @relationship(type: "REVIEWED", direction: OUT, properties: "ReviewedProps")
}
interface ReviewedProps @relationshipProperties {
rating: Int!
date: Date
}
查询关系属性 — Connection API
对于每个带有 properties: 的关系,都会自动生成一个 {field}Connection 字段。通过 actorsConnection.edges.properties 访问关系属性,而不是通过 actors 访问:
query {
movies(where: { title: { eq: "The Matrix" } }) {
title
actorsConnection {
edges {
properties { role } # maps to @relationshipProperties interface
node { name }
}
}
}
}
@cypher — 自定义解析器
type Person @node {
name: String!
# columnName must exactly match the RETURN alias — mismatch returns null silently
friendCount: Int
@cypher(
statement: "MATCH (this)-[:KNOWS]->(f:Person) RETURN count(f) AS friendCount"
columnName: "friendCount"
)
recommendedMovies: [Movie!]!
@cypher(
statement: """
MATCH (this)-[:WATCHED]->(m:Movie)<-[:WATCHED]-(o:Person)-[:WATCHED]->(rec:Movie)
WHERE NOT (this)-[:WATCHED]->(rec)
RETURN rec
"""
columnName: "rec"
)
}
# @cypher on Query field — custom top-level query
type Query {
topRatedMovies(limit: Int = 10): [Movie!]!
@cypher(
statement: "MATCH (m:Movie) WHERE m.rating IS NOT NULL RETURN m ORDER BY m.rating DESC LIMIT $limit"
columnName: "m"
)
}
在字段级 @cypher 中,this 指当前节点。参数以 $paramName 的形式传入。
@cypher — 字段参数和 extend type
# extend type adds computed fields without modifying the base type definition
extend type Movie @node {
avgRating: Float
@cypher(statement: "MATCH (this)<-[r:RATED]-(:User) RETURN avg(r.rating) AS result", columnName: "result")
# Field arguments passed as Cypher params; always provide default to avoid null
recommended(limit: Int = 3): [Movie!]!
@cypher(
statement: "MATCH (this)<-[:RATED]-(u:User)-[:RATED]->(rec:Movie) WITH rec, COUNT(u) AS score ORDER BY score DESC RETURN rec LIMIT $limit"
columnName: "rec"
)
}
@id 和 @timestamp
type Post @node {
id: ID! @id # auto-generates UUID; creates UNIQUE constraint
createdAt: DateTime! @timestamp(operations: [CREATE])
updatedAt: DateTime @timestamp(operations: [CREATE, UPDATE])
title: String!
}
@alias — 将 GraphQL 字段映射到 Neo4j 属性
type User @node {
id: ID! @id
email: String! @alias(property: "emailAddress") # GraphQL: email → DB: emailAddress
}
@fulltext 和 @vector
使用前,必须先在 Cypher 中创建索引。支持短语的 @vector 索引可接受 maxPhraseLength [7.6.0],以限制嵌入成本。未配置匹配的 features.vector 时使用 provider,会导致模式构建失败 [7.6.1]。完整语法、生成的查询结构和 provider 配置请参阅:references/search-directives.md。
安全 — @authentication 和 @authorization
步骤 1:在构造函数中配置 JWT
// Symmetric secret
const neoSchema = new Neo4jGraphQL({
typeDefs,
driver,
features: {
authorization: { key: process.env.JWT_SECRET },
},
});
// JWKS endpoint (production)
const neoSchema = new Neo4jGraphQL({
typeDefs,
driver,
features: {
authorization: {
key: { url: 'https://myapp.com/.well-known/jwks.json' },
},
},
});
步骤 2:在 context 中传递令牌
context: async ({ req }) => ({ token: req.headers.authorization }),
// Or pass pre-decoded JWT:
context: async ({ req }) => ({ jwt: myDecodeJwt(req.headers.authorization) }),
步骤 3:应用 @authentication 和 @authorization
# Require auth on all operations for a type
type Post @node
@authentication
@authorization(filter: [{ where: { node: { author: { id: { eq: "$jwt.sub" } } } } }]) {
title: String!
author: User! @relationship(type: "AUTHORED", direction: IN)
}
# requireAuthentication: false = allow public access without JWT
type Article @node
@authorization(filter: [
{ requireAuthentication: false, where: { node: { published: { eq: true } } } }
{ where: { node: { author: { id: { eq: "$jwt.sub" } } } } }
]) {
title: String!
published: Boolean!
}
# validate (throws error) vs filter (silently hides data)
type BankAccount @node
@authorization(validate: [{
when: [BEFORE],
where: { node: { owner: { id: { eq: "$jwt.sub" } } } }
}]) {
balance: Float!
}
# Role-based with custom JWT claims
type JWT @jwt {
roles: [String!]! @jwtClaim(path: "myApp.roles")
}
type AdminReport @node
@authentication(operations: [READ], jwt: { roles: { includes: "admin" } }) {
data: String!
}
BEFORE 与 AFTER:CREATE 仅支持 AFTER;READ 仅支持 BEFORE。
自动生成的操作
| 操作 | 生成的名称 | 示例 |
|---|---|---|
| 查询全部 | {plural} | movies(where, sort, limit, offset) |
| 游标分页 | {plural}Connection | moviesConnection(first, after, where, sort) |
| 创建 | create{Plural} | createMovies(input: [MovieCreateInput!]!) |
| 更新 | update{Plural} | updateMovies(where, update) |
| 删除 | delete{Plural} | deleteMovies(where, delete) |
v7 筛选语法(显式 eq)
# v7: explicit eq required
query {
movies(where: { title: { eq: "The Matrix" } }) {
title
actors { name }
}
}
# Sort and paginate (v7: direct args, not options wrapper)
query {
movies(sort: [{ title: ASC }], limit: 10, offset: 0) {
title
}
}
嵌套变更
mutation {
createMovies(input: [{
title: "Inception"
actors: {
create: [{ node: { name: "Leonardo DiCaprio" } }]
connect: { where: { node: { name: { eq: "Joseph Gordon-Levitt" } } } }
}
}]) {
movies { id title }
}
}
OGM — 编程式访问
OGM 会绕过 GraphQL 授权 — 仅在可信的服务端上下文中使用。
import { OGM } from '@neo4j/graphql-ogm';
const ogm = new OGM({ typeDefs, driver });
await ogm.init(); // must await before using models
const Movie = ogm.model('Movie');
// find
const movies = await Movie.find({
where: { title: { eq: 'The Matrix' } },
selectionSet: `{ id title actors { name } }`,
});
// create
const { movies: created } = await Movie.create({
input: [{ title: 'Dune', actors: { create: [{ node: { name: 'Timothée Chalamet' } }] } }],
});
// update
await Movie.update({
where: { id: { eq: movieId } },
update: { title: { set: 'Dune: Part One' } },
});
// delete
await Movie.delete({ where: { id: { eq: movieId } } });
需单独安装:npm install @neo4j/graphql-ogm
订阅(需要 CDC)
需要在 Neo4j 中启用 FULL 模式的 CDC。请参阅 CDC 文档。
const neoSchema = new Neo4jGraphQL({
typeDefs,
driver,
features: { subscriptions: true },
});
使用 WebSocket context 中经过验证的 token 对订阅进行身份验证;预解码的 jwt 仅在由服务端设置时才可信(7.5.6 / 5.12.14 起不再信任客户端提供的 connectionParams JWT)。
subscription {
movieCreated(where: { title: { eq: "The Matrix" } }) {
createdMovie { title }
}
}
# Also: movieUpdated, movieDeleted
模式控制指令
type ReadOnlyData @node @mutation(operations: []) { value: String! } # disable mutations
type HeavyDoc @node {
id: ID! @id
content: String! @filterable(byValue: false) @sortable(enabled: false) # perf guard
title: String!
}
type Series @node @plural(value: "seriesList") { title: String! } # irregular plural fix
常见错误
| 错误 | 原因 | 修复方法 |
|---|---|---|
Type 'X' not found | 类型缺少 @node(v7) | 为每种节点类型添加 @node |
@cypher 字段返回 null | columnName 与 RETURN 别名不匹配 | 让 columnName 与 RETURN 别名完全一致 |
| 关系方向不匹配 | 两侧声明了相同的方向 | 使用相反方向:如果 A 的 direction: OUT,B 必须是 direction: IN |
assertIndexesAndConstraints 抛出错误 | 数据库中没有 @id 约束 | 添加 { options: { create: true } },或手动运行 CREATE CONSTRAINT |
| 未应用身份验证 | 上下文中没有 JWT | 在上下文函数中传入 token: req.headers.authorization |
| 数据有效但结果为 0 | v7 筛选器缺少 eq | 使用 { field: { eq: value } },而非 { field: value } |
找不到 connectOrCreate | 已在 v7 中移除 | 分别使用 connect 和 create |
| 大型变更导致内存错误 | Cypher 生成过于复杂 | 分批执行变更;增大 server.memory.heap.max_size |
未生成 @subscription | v7 要求显式启用 | 在构造函数中添加 features: { subscriptions: true } |
v6 → v7 重大变更摘要
| v6 | v7 |
|---|---|
@node 可选 | 每种节点类型都必须添加 @node |
options: { limit, sort } | 将 limit、sort 作为直接参数传入 |
{ field: value } 筛选器 | { field: { eq: value } } |
connectOrCreate 嵌套变更 | 已移除 — 使用 connect 和 create |
查询中的 directed 参数 | @relationship 中的 queryDirection |
单个关系字段 actor: Person | 必须使用列表 actors: [Person!]! |
@private 指令 | 已移除 |
@unique 指令 | 已移除 |
参考资料
- Neo4j GraphQL 文档 — 完整的指令参考和迁移指南
- GraphAcademy:GraphQL 基础知识 — 实践课程
- GitHub:@neo4j/graphql — 更新日志、问题
- CDC 设置 — 订阅所需
- references/search-directives.md —
@fulltext、@vector、maxPhraseLength
检查清单
- 每个表示 Neo4j 节点的 GraphQL 类型都添加了
@node(v7 硬性要求) - 身份字段添加了
@id(通过assertIndexesAndConstraints触发CREATE CONSTRAINT) - 启动时调用了
assertIndexesAndConstraints,并使用 try/catch -
@relationship方向正确:OUT= 箭头从此节点指向外部,IN= 箭头指向此节点 - 关系两侧都声明了相反的方向
-
@cypher的columnName与 RETURN 别名完全一致 -
features.authorization.key中配置了 JWT 密钥或 JWKS URL;上下文中传入了令牌 - 有意选择了
@authorization筛选方式或验证方式(静默隐藏或抛出错误) - v7:筛选器使用显式的
{ field: { eq: value } }语法 - v7:将
limit/sort作为直接查询参数传入(不使用options包装) - OGM:在使用任何
ogm.model()之前调用了await ogm.init() - 订阅:启用
features.subscriptions前,已在 FULL 模式下启用 CDC -
@neo4j/graphql≥ 7.6.2(v7)或 ≥ 5.12.15(LTS)— 更早版本存在身份验证绕过漏洞 -
.env保存了凭据;.env已添加到.gitignore