GraphQLで、適切にグラフ上のノード間の関係を示すために、connectionやedgeに独自の名前をつけて、独自のフィールドも追加したいことがある。
Explaining GraphQL Connections | Apollo GraphQL Blog
Instead think of them as the relationship between two nodes, and add fields that are appropriate for that relationship
async-graphqlでこれを実現するには、Connection
の型パラメータに独自の名前とフィールドを表す型を渡せばよい。
実装したいスキーマ
今回は次のスキーマを実装する。ユーザーと書籍がリソースとして存在して、ユーザーが書籍を「読了」したという関係性をUserReadBooksConnection
で表現する。UserReadBooksEdge
はreadAt
(読了日時)を持つ。
type Book { id: Int! title: String! author: String! } type Query { books: [Book!]! user: User! } type User { """ ユーザーが読了した書籍 """ readBooks: UserReadBooksConnection! } type UserReadBooksConnection { pageInfo: PageInfo! edges: [UserReadBooksEdge!]! nodes: [Book!]! } type UserReadBooksEdge { node: Book! cursor: String! """ 読了日時 """ readAt: String! }
async-graphqlのConnection
async-graphqlのConnection
の定義は次のようになっている[^1]。
pub struct Connection< Cursor, Node, ConnectionFields = EmptyFields, EdgeFields = EmptyFields, Name = DefaultConnectionName, EdgeName = DefaultEdgeName, NodesField = EnableNodesField > where Cursor: CursorType + Send + Sync, Node: OutputType, ConnectionFields: ObjectType, EdgeFields: ObjectType, Name: ConnectionNameType, EdgeName: EdgeNameType, NodesField: NodesFieldSwitcherSealed, { // ... }
型パラメータのうち、今回着目するものの意味は次のとおり。
ConnectionFields
, EdgeFields
connectionとedgeの独自フィールドの型。普通のフィールドと同じく、メソッドとしてリゾルバを持つObject
などを渡せばよい。指定しないときはconnection::EmptyFields
を渡す。
Name
, EdgeName
ConnectionNameType
とEdgeNameType
を実装した構造体を渡せばよい。指定しないときはconnection::DefaultConnectionName
を渡す。
独自のconnectionを定義
async-graphqlのConnection
に適切な型パラメータを渡して、独自の名前やフィールドを持つconnectionを定義する。
use async_graphql::{ connection::{Connection, Edge, EmptyFields}, types::connection::{ConnectionNameType, EdgeNameType}, Object, OutputType, Result, SimpleObject, }; // ... // 独自のconnectionを定義する // 型の記述が長くなるので別名をつける type UserReadBooksConnection = Connection< i32, Book, EmptyFields, UserReadBooksEdgeFields, UserReadBooksConnectionName, UserReadBooksEdgeName, >; // 普通のフィールドと同じく、メソッドとしてリゾルバを持つObjectを実装 struct UserReadBooksEdgeFields; #[Object] impl UserReadBooksEdgeFields { /// 読了日時 async fn read_at(&self) -> &'static str { "2024-01-01T12:34:56Z" } } // ConnectionNameTypeを実装した構造体で独自のconnection名を定義 struct UserReadBooksConnectionName; impl ConnectionNameType for UserReadBooksConnectionName { fn type_name<T: OutputType>() -> String { "UserReadBooksConnection".to_string() } } // EdgeNameTypeを実装した構造体で独自のedge名を定義 struct UserReadBooksEdgeName; impl EdgeNameType for UserReadBooksEdgeName { fn type_name<T: OutputType>() -> String { "UserReadBooksEdge".to_string() } }
このconnectionを使って、フィールドUser.readBooks
を次のように定義できる。
use async_graphql::{ connection::{Connection, Edge, EmptyFields}, types::connection::{ConnectionNameType, EdgeNameType}, Object, OutputType, Result, SimpleObject, }; // ... /// 書籍 #[derive(SimpleObject)] pub struct Book { id: i32, /// 書名 title: String, /// 著者 author: String, } // 書籍のダミーデータを作るファクトリ関数 fn get_books() -> Vec<Book> { let book1 = Book { id: 1, title: "Refactoring".to_string(), author: "Martin Fowler".to_string(), }; let book2 = Book { id: 2, title: "Extreme Programming Explained".to_string(), author: "Kent Beck".to_string(), }; vec![book1, book2] } struct User; #[Object] impl User { /// ユーザーが読了した書籍 async fn read_books(&self) -> Result<UserReadBooksConnection> { let mut connection = UserReadBooksConnection::new(false, false); connection.edges.extend( get_books() .into_iter() .map(|book| Edge::with_additional_fields(book.id, book, UserReadBooksEdgeFields)), ); Ok(connection) } }
read_books
の中で、Edge::with_additional_fields
を使ってUserReadBooksConnection
のインスタンスのedges
に独自フィールドを持つedgeを追加している。
結果
スキーマは独自に定義した名前のconnectionとedgeを提供しており、UserReadBooksEdge.readAt
が追加されている。
GraphiQLでクエリすると次のようになる。edgeに追加された独自フィールドを取得できている。