async-graphqlで独自の名前とフィールドを持つconnection/edgeを定義する
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に追加された独自フィールドを取得できている。
