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 を構築する開発者向けの実践ガイドです。スキーマディレクティブ、認可、生成操作、OGM、v7 への移行を扱います。

主な機能

  1. 01GraphQL 型とリレーションディレクティブの設定
  2. 02クエリ、ミューテーション、サブスクリプションの生成
  3. 03JWT 認証と認可ルールの設定
  4. 04サーバー側アクセスでの OGM 利用
  5. 05移行時の変更点と一般的なエラーへの対応

活用シーン

  1. 01Neo4j グラフスキーマから 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 ディレクティブを使用して型定義を記述する場合
  • サーバー側でプログラムから 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 を明示する構文です
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 フィールドが自動生成されます。リレーションシップのプロパティには 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}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 で 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 foundv7 で削除されたconnect と create を個別に使用します
大規模なミューテーションでメモリエラーが発生するCypher の生成が複雑ミューテーションをバッチ処理し、server.memory.heap.max_size を増やします
@subscription が生成されないv7 では明示的な有効化が必要コンストラクターに 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 の実行をトリガーします)
  • 起動時に 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 に追加する

コメント