neo4j-graphql-skill について
neo4j-graphql-skill は、@neo4j/graphql で Neo4j GraphQL API を構築する開発者向けの実践ガイドです。スキーマディレクティブ、認可、生成操作、OGM、v7 への移行を扱います。
主な機能
- 01GraphQL 型とリレーションディレクティブの設定
- 02クエリ、ミューテーション、サブスクリプションの生成
- 03JWT 認証と認可ルールの設定
- 04サーバー側アクセスでの OGM 利用
- 05移行時の変更点と一般的なエラーへの対応
活用シーン
- 01Neo4j グラフスキーマから 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ディレクティブを使用して型定義を記述する場合- サーバー側でプログラムから Neo4j にアクセスするために OGM を使用する場合(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 フィールドが自動生成されます。リレーションシップのプロパティには actors ではなく、actorsConnection.edges.properties を通じてアクセスします。
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]。完全な構文、生成されるクエリの形式、プロバイダー設定については、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: 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 で CDC を FULL モードで有効にする必要があります。CDC のドキュメントを参照してください。
const neoSchema = new Neo4jGraphQL({
typeDefs,
driver,
features: { subscriptions: true },
});
サブスクリプションを認証するには、WebSocket コンテキストで検証済みの 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 がエラーをスローする | DB に @id 制約がない | { options: { create: true } } を追加するか、手動で CREATE CONSTRAINT を実行します |
| 認証が適用されない | コンテキストに JWT がない | コンテキスト関数で token: req.headers.authorization を渡します |
| 有効なデータがあるのに結果が 0 件 | v7 のフィルターに eq がない | { field: { eq: value } } を使用します({ field: value } ではありません) |
connectOrCreate not found | 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の実行をトリガーします) - 起動時に try/catch を使用して
assertIndexesAndConstraintsを呼び出す -
@relationshipの方向が正しいことを確認する:OUT= 矢印がこのノードから出る、IN= 矢印がこのノードに入る - リレーションシップの両側を逆方向で宣言する
-
@cypherのcolumnNameが RETURN のエイリアスと正確に一致している -
features.authorization.keyに JWT シークレットまたは JWKS URL を指定し、コンテキストでトークンを渡す -
@authorizationでは、filter と validate のどちらを使うかを意図的に選ぶ(黙って非表示にするか、エラーをスローするか) - v7:フィルターには明示的な
{ field: { eq: value } }構文を使用する - v7:
limit/sortはoptionsのラッパーではなく、クエリの直接の引数として渡す - OGM:
ogm.model()を使用する前にawait ogm.init()を呼び出す - サブスクリプション:
features.subscriptionsを有効にする前に、CDC を FULL モードで有効にする -
@neo4j/graphql≥ 7.6.2(v7)または ≥ 5.12.15(LTS)を使用する — これより前のバージョンには認証のバイパスがあります -
.envに認証情報を保存し、.envを.gitignoreに追加する