mcpField
N
neo4j-graphql-skill

neo4j-graphql-skill

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

关于 neo4j-graphql-skill

neo4j-graphql-skill 为使用 @neo4j/graphql 构建 Neo4j GraphQL API 的开发者提供实践指南,涵盖 schema、认证授权及 v5/v6 到 v7 的迁移要点。

主要功能

  1. 01配置 GraphQL 类型与关系指令
  2. 02生成查询、变更和订阅
  3. 03设置 JWT 认证与授权规则
  4. 04使用 OGM 进行服务端访问
  5. 05排查版本迁移与常见错误

适用场景

  1. 01从 Neo4j 图 schema 创建 GraphQL API
  2. 02为字段和类型添加 JWT 授权
  3. 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 语法
v5LTS — 使用 ≥ 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}ConnectionmoviesConnection(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 字段返回 nullcolumnName 与 RETURN 别名不匹配让 columnName 与 RETURN 别名完全一致
关系方向不匹配两侧声明了相同的方向使用相反方向:如果 A 的 direction: OUT,B 必须是 direction: IN
assertIndexesAndConstraints 抛出错误数据库中没有 @id 约束添加 { options: { create: true } },或手动运行 CREATE CONSTRAINT
未应用身份验证上下文中没有 JWT在上下文函数中传入 token: req.headers.authorization
数据有效但结果为 0v7 筛选器缺少 eq使用 { field: { eq: value } },而非 { field: value }
找不到 connectOrCreate已在 v7 中移除分别使用 connect 和 create
大型变更导致内存错误Cypher 生成过于复杂分批执行变更;增大 server.memory.heap.max_size
未生成 @subscriptionv7 要求显式启用在构造函数中添加 features: { subscriptions: true }

v6 → v7 重大变更摘要

v6v7
@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 类型都添加了 @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

评论

支持 Markdown