@oneOf
ディレクティブとは
@oneOf
とは、 input 型が持つ field の中で、いずれか 1 つを必ず指定しなければならないことを表現するディレクティブです。
そりゃあカスタムディレクティブを頑張って実装すれば実現できるけど・・・と思ったそこのアナタ!
実はこれ、 GraphQL の公式仕様としてもうすぐマージされる予定の directive なんです。
もともと GraphQL には union
があり、出力については「いずれか 1 種類だけ返す」ことを API 上で表現できていましたが、入力については表現する方法がありませんでした。
@oneOf
ディレクティブによって、これが可能になります!
使用例
以下はユーザ検索 API に @oneOf
を適用した例です。
検索条件である FindUserCondition
を input で指定するとき、 id
フィールドか nameContaining
のどちらかに non-null な値が指定されている必要があることを表しています。
directive @oneOf on INPUT_OBJECT
input FindUserCondition @oneOf { # <--- ここで "oneOf" 指定
# ユーザを ID 指定で検索する場合に指定する
id: ID
# ユーザを、名前の部分一致で検索する場合に指定する
nameContaining: String
}
type Query {
# ユーザを検索する
findUser(by: FindUserCondition!): [User!]!
}
有効なクエリの例
query q1 {
findUser(by: {
id: "u1"
}) {
id
name
}
}
以下のような指定はエラーになります。
どういう指定が無効なのかも前述の PR の中にパターン表が載っています。
query error1 {
findUser(by: {
# NG: 2 つ以上のフィールドを同時に指定している
id: "1"
nameContaining: "foo"
}) {
id
name
}
}
query error2 {
findUser(by: {
# NG: 指定したフィールドは 1 つだが値が null
id: null
}) {
id
name
}
}
query error3 {
findUser(by: {
# NG: 一見 nameContaining が有効そうに見えるが、この指定方法はダメ
id: null
nameContaining: "foo"
}) {
id
name
}
}
もう使えるの?
今回私は spring-graphql のバージョンアップをしていて気づいたのですが、観測範囲では
少なくとも以下の実装に (モノによっては結構前から) @oneOf
が含まれていました。
spring-graphql で @oneOf
を使ってみる
せっかくなので spring-graphql で @oneOf
ディレクティブを使って API を実装してみます。
完全なコードは GitHub にあります。
まずは SDL から。
directive @oneOf on INPUT_OBJECT
type User {
id: ID!
name: String!
}
input FindUserCondition @oneOf {
id: ID
nameContaining: String
}
type Query {
findUser(by: FindUserCondition!): [User!]!
}
続いて Controller 側。
@Controller
public class UserController {
private final List<User> users;
public UserController() {
List<User> users = new ArrayList<>();
users.add(new User("u1", "taro"));
users.add(new User("u2", "jiro"));
users.add(new User("u3", "saburo"));
this.users = users;
}
@QueryMapping
public Flux<User> findUser(@Argument("by") FindUserCondition condition) {
Predicate<User> filter;
if (condition.id() != null) {
filter = u -> u.id().equals(condition.id());
} else if (condition.nameContaining() != null) {
filter = u -> u.name().contains(condition.nameContaining());
} else {
throw new IllegalStateException();
}
return Flux.fromStream(users.stream().filter(filter));
}
}
動かしてみる
正常系
- リクエスト
query findUser { findUser(by: { id: "u1" }) { id name } }
- レスポンス
{ "data": { "findUser": [ { "id": "u1", "name": "taro" } ] } }
異常系
- リクエスト
query findUser { findUser(by: { id: "u1" nameContaining: "x" }) { id name } }
- レスポンス
入力チェックは何も実装していませんが、ちゃんとエラーになってくれました!
{ "errors": [ { "message": "graphql.execution.OneOfTooManyKeysException: Exactly one key must be specified for OneOf type 'FindUserCondition'.", "locations": [ { "line": 2, "column": 3 } ], "path": [ "findUser" ], "extensions": { "classification": "INTERNAL_ERROR" } } ], "data": null }
あとがき
GraphQL の表現力が予約語や構文の追加ではなく directive で拡張されていくのは面白いですね。
こうなってくるとドキュメントにも directive の情報を出力したくなります。
私が GraphQL API のドキュメント生成で使っている SpectaQL では カスタムディレクティブの出力をサポートしていない のですが、こういう流れが加速していくのであれば是非対応してもらいところです。
(@oneOf
に限らず、認証のために @auth
みたいなカスタムディレクティブを作って設定してもドキュメントに出力されないのがイマイチなんですよねぇ)