graphql
/apollo-server
/graphql-yoga
終点。このエンドポイントは、データベース(またはRESTエンドポイントまたはその他のサービス)から返されたデータを公開します。
データソースが正しいデータを返していることはわかっています。リゾルバー内のデータソースへの呼び出しの結果をログに記録すると、データが返されていることがわかります。ただし、私のGraphQLフィールドは常にnullに解決されます。
フィールドをnull以外にすると、レスポンスのerrors
配列内に次のエラーが表示されます。
Nullにできないフィールドに対してnullを返すことはできません
GraphQLがデータを返さないのはなぜですか?
1つまたは複数のフィールドがnullに解決される一般的な理由は2つあります。1)リゾルバー内で間違った形でデータを返す。 2)Promiseを正しく使用していない。
注:次のエラーが表示された場合:
Nullにできないフィールドに対してnullを返すことはできません
根本的な問題は、フィールドがnullを返すことです。以下に概説する手順に従って、このエラーを解決してください。
次の例では、この単純なスキーマを参照します。
type Query {
post(id: ID): Post
posts: [Post]
}
type Post {
id: ID
title: String
body: String
}
私たちのスキーマは、要求されたクエリとともに、エンドポイントから返された応答のdata
オブジェクトの「形状」を定義します。形状とは、オブジェクトが持つプロパティと、それらのプロパティの値がスカラー値か、他のオブジェクトか、オブジェクトまたはスカラーの配列かを意味します。
スキーマが応答全体の形状を定義するのと同じように、個々のフィールドのtypeはそのフィールドの値の形状を定義します。リゾルバーで返すデータの形状も、この予想される形状と一致している必要があります。そうでない場合、応答で予期しないnullが発生することがよくあります。
ただし、具体的な例に入る前に、GraphQLがフィールドを解決する方法を理解することが重要です。
スキーマ内のすべてのフィールドにリゾルバーを作成できることは確かですが、GraphQL.jsがデフォルトのリゾルバーを提供しない場合はデフォルトのリゾルバーを使用するため、多くの場合それは不要です。
高レベルでは、デフォルトのリゾルバーが行うことは単純です。それは、parentフィールドが解決される値を調べ、その値がJavaScriptオブジェクトの場合、解決されるフィールドとsame nameでそのオブジェクトのプロパティを検索します。そのプロパティが見つかると、そのプロパティの値に解決されます。それ以外の場合は、nullに解決されます。
post
フィールドのリゾルバーで、値{ title: 'My First Post', bod: 'Hello World!' }
を返すとします。 Post
型のフィールドのリゾルバーを作成しない場合でも、post
を要求できます。
query {
post {
id
title
body
}
}
そして私たちの応答は
{
"data": {
"post" {
"id": null,
"title": "My First Post",
"body": null,
}
}
}
title
フィールドは、デフォルトのリゾルバーが大掛かりな作業を行ったため、リゾルバーを提供していなくても解決されました。親フィールド(この場合はtitle
)に解決されるpost
という名前のプロパティが存在することがわかりました。それはそのプロパティの値に解決されました。 id
リゾルバーに返されたオブジェクトにpost
プロパティがなかったため、id
フィールドはnullに解決されました。タイプミスのため、body
フィールドもnullに解決されました。bod
ではなくbody
というプロパティがあります。
プロのヒント:
bod
がタイプミスではないがではないが、APIまたはデータベースが実際に返すものである場合、スキーマに一致するようにbody
フィールドのリゾルバーを常に記述します。例:(parent) => parent.bod
覚えておくべき重要なことの1つは、JavaScriptでは、ほとんどすべてがObjectであることです。したがって、post
フィールドが文字列または数値に解決される場合、Post
型の各フィールドのデフォルトリゾルバーは、親オブジェクトで適切に名前が付けられたプロパティを検索しようとしますが、失敗し、nullを返します。フィールドにオブジェクトタイプがあるが、リゾルバーでオブジェクト以外のもの(文字列や配列など)を返す場合、タイプの不一致に関するエラーは表示されませんが、そのフィールドの子フィールドは必然的にnullに解決されます。
post
クエリのリゾルバーを作成している場合、次のように他のエンドポイントからコードをフェッチする可能性があります。
function post (root, args) {
// axios
return axios.get(`http://SOME_URL/posts/${args.id}`)
.then(res => res.data);
// fetch
return fetch(`http://SOME_URL/posts/${args.id}`)
.then(res => res.json());
// request-promise-native
return request({
uri: `http://SOME_URL/posts/${args.id}`,
json: true
});
}
post
フィールドのタイプはPost
であるため、リゾルバーはid
、title
、body
などのプロパティを持つオブジェクトを返す必要があります。これがAPIから返されたものであれば、準備は完了です。 ただし、応答は実際には追加のメタデータを含むオブジェクトであることが一般的です。したがって、実際にエンドポイントから取得するオブジェクトは次のようになります。
{
"status": 200,
"result": {
"id": 1,
"title": "My First Post",
"body": "Hello world!"
},
}
この場合、返されるオブジェクトには必要なid
、title
、およびbody
プロパティがないため、レスポンスをそのまま返すことはできず、デフォルトのリゾルバーが正しく機能することを期待できません。リゾルバは次のようなことをする必要はありません:
function post (root, args) {
// axios
return axios.get(`http://SOME_URL/posts/${args.id}`)
.then(res => res.data.result);
// fetch
return fetch(`http://SOME_URL/posts/${args.id}`)
.then(res => res.json())
.then(data => data.result);
// request-promise-native
return request({
uri: `http://SOME_URL/posts/${args.id}`,
json: true
})
.then(res => res.result);
}
注:上記の例は、別のエンドポイントからデータをフェッチします。ただし、このようなラップされた応答は、(ORMを使用するのではなく)データベースドライバーを直接使用する場合にも非常に一般的です。たとえば、 node-postgres を使用している場合、Result
、rows
、fields
、rowCount
などのプロパティを含むcommand
オブジェクトを取得します。リゾルバー内に返す前に、この応答から適切なデータを抽出する必要があります。
データベースから投稿をフェッチすると、リゾルバは次のようになります。
function post(root, args, context) {
return context.Post.find({ where: { id: args.id } })
}
ここで、Post
は、コンテキストを通じて注入するモデルです。 sequelize
を使用している場合、findAll
を呼び出すことがあります。 mongoose
とtypeorm
にはfind
があります。これらのメソッドに共通するのは、WHERE
条件を指定できる一方で、それらが返す約束は、単一のオブジェクトではなく配列に解決される 。データベースには特定のIDを持つ投稿が1つしかない可能性がありますが、これらのメソッドのいずれかを呼び出すと、配列にラップされます。配列はまだオブジェクトであるため、GraphQLはpost
フィールドをnullとして解決しません。しかし、それははすべての子フィールドをnullとして解決します配列で適切に名前が付けられたプロパティを見つけることができないためです。
配列の最初の項目を取得してリゾルバーに返すだけで、このシナリオを簡単に修正できます。
function post(root, args, context) {
return context.Post.find({ where: { id: args.id } })
.then(posts => posts[0])
}
別のAPIからデータをフェッチする場合、これが多くの場合唯一のオプションです。一方、ORMを使用している場合、DBから1行のみを明示的に返す(またはfindOne
のように)別の方法を使用できることがよくあります(存在しない場合はnull)。
function post(root, args, context) {
return context.Post.findOne({ where: { id: args.id } })
}
INSERT
およびUPDATE
の呼び出しに関する特別な注意:行またはモデルインスタンスを挿入または更新するメソッドは、挿入または更新された行を返すことがよくあります。しばしばそうですが、いくつかのメソッドはそうしません。たとえば、sequelize
のupsert
メソッドは、ブール値、またはアップサートされたレコードとブール値のタプルに解決されます(returning
オプションがtrueに設定されている場合)。 mongoose
のfindOneAndUpdate
は、変更された行を含むvalue
プロパティを持つオブジェクトに解決されます。 ORMのドキュメントを参照し、リゾルバー内に返す前に結果を適切に解析してください。
私たちのスキーマでは、posts
フィールドの型はList
sのPost
です。つまり、そのリゾルバーはオブジェクトの配列(または1つに解決するPromise)を返す必要があります。次のように投稿を取得する場合があります。
function posts (root, args) {
return fetch('http://SOME_URL/posts')
.then(res => res.json())
}
ただし、APIからの実際の応答は、投稿の配列をラップするオブジェクトである可能性があります。
{
"count": 10,
"next": "http://SOME_URL/posts/?page=2",
"previous": null,
"results": [
{
"id": 1,
"title": "My First Post",
"body" "Hello World!"
},
...
]
}
GraphQLは配列を想定しているため、リゾルバーでこのオブジェクトを返すことはできません。その場合、フィールドはnullに解決され、次のようなエラーが応答に含まれます。
Iterableが必要ですが、Query.postsフィールドには見つかりませんでした。
上記の2つのシナリオとは異なり、この場合、GraphQLはリゾルバーで返す値のタイプを明示的にチェックでき、配列のように Iterable でない場合にスローします。
最初のシナリオで説明したように、このエラーを修正するには、応答を適切な形状に変換する必要があります。次に例を示します。
function posts (root, args) {
return fetch('http://SOME_URL/posts')
.then(res => res.json())
.then(data => data.results)
}
GraphQL.jsは、内部でPromise APIを使用します。そのため、リゾルバーは({ id: 1, title: 'Hello!' }
などの)値を返すことも、その値に解決するPromiseを返すこともできます。 List
タイプのフィールドの場合、Promiseの配列を返すこともできます。 Promiseが拒否した場合、そのフィールドはnullを返し、適切なエラーが応答のerrors
配列に追加されます。フィールドにオブジェクトタイプがある場合、Promiseが解決する値は、子フィールドのリゾルバに親値として渡されるものです。
Promise は、「オブジェクトは非同期操作の最終的な完了(または失敗)とその結果の値を表します。」次のいくつかのシナリオでは、リゾルバー内のPromiseを処理するときに遭遇するいくつかの一般的な落とし穴の概要を説明します。ただし、Promiseと新しいasync/await構文に慣れていない場合は、基本を理解することに時間をかけることを強くお勧めします。
注:次のいくつかの例は、getPost
関数を参照しています。この関数の実装の詳細は重要ではありません。これは、Promiseを返す関数にすぎません。Promiseは、postオブジェクトに解決されます。
post
フィールドの有効なリゾルバーは次のようになります。
function post(root, args) {
return getPost(args.id)
}
getPosts
はPromiseを返し、そのPromiseを返します。 Promiseが解決するものは何でも、私たちのフィールドが解決する値になります。いいね!
しかし、これを行うとどうなりますか?
function post(root, args) {
getPost(args.id)
}
投稿を解決するPromiseを作成しています。ただし、Promiseは返されないため、GraphQLはそれを認識せず、解決されるまで待機しません。明示的なreturn
ステートメントのないJavaScript関数では、暗黙的にundefined
を返します。したがって、この関数はPromiseを作成し、すぐにundefined
を返し、GraphQLがフィールドにnullを返すようにします。
getPost
から返されたPromiseが拒否された場合も、応答にエラーが表示されません。Promiseが返されなかったため、基になるコードは、解決するか拒否するかを気にしません。実際、Promiseが拒否した場合、サーバーコンソールにUnhandledPromiseRejectionWarning
が表示されます。
この問題の修正は簡単です。return
を追加するだけです。
getPost
への呼び出しの結果をログに記録することにしたので、リゾルバーを次のように変更します。
function post(root, args) {
return getPost(args.id)
.then(post => {
console.log(post)
})
}
クエリを実行すると、結果がコンソールに記録されますが、GraphQLはフィールドをnullに解決します。どうして?
Promiseでthen
を呼び出すと、Promiseが解決した値を効果的に取得し、新しいPromiseを返します。あなたはそれをプロミスを除いてArray.map
のようなものと考えることができます。 then
は、値または別のPromiseを返すことができます。どちらの場合でも、then
の内部で返されるものは、元のPromiseに「チェーン」されます。複数のthen
sを使用すると、このように複数のPromiseをチェーンできます。チェーン内の各Promiseは順番に解決され、最終的な値は元のPromiseの値として効果的に解決されるものです。
上記の例では、then
の内部には何も返されなかったため、Promiseはundefined
に解決され、GraphQLはnullに変換しました。これを修正するには、投稿を返す必要があります。
function post(root, args) {
return getPost(args.id)
.then(post => {
console.log(post)
return post // <----
})
}
リゾルバー内で解決する必要のある複数のPromiseがある場合、then
を使用して正しい値を返すことにより、それらを正しくチェーンする必要があります。たとえば、getFoo
を呼び出す前に、他の2つの非同期関数(getBar
とgetPost
)を呼び出す必要がある場合は、次のようにします。
function post(root, args) {
return getFoo()
.then(foo => {
// Do something with foo
return getBar() // return next Promise in the chain
})
.then(bar => {
// Do something with bar
return getPost(args.id) // return next Promise in the chain
})
プロのヒント:Promiseを正しくチェーンするのに苦労している場合は、非同期/待機の構文がより簡潔で扱いやすいことがあります。
Promiseの前は、非同期コードを処理する標準的な方法は、コールバック、または非同期の作業が完了すると呼び出される関数を使用することでした。たとえば、次のようにmongoose
のfindOne
メソッドを呼び出すことができます。
function post(root, args) {
return Post.findOne({ where: { id: args.id } }, function (err, post) {
return post
})
ここでの問題は2つあります。 1つは、コールバック内で返される値は何にも使用されない(つまり、基になるコードに渡されない)ことです。 2つ目は、コールバックを使用すると、Post.findOne
がPromiseを返さないことです。単にundefinedを返します。この例では、コールバックが呼び出され、post
の値をログに記録すると、データベースから返されたものがすべて表示されます。ただし、Promiseを使用しなかったため、GraphQLはこのコールバックが完了するのを待機しません。戻り値(未定義)を受け取り、それを使用します。
mongoose
のサポートを含む最も人気のあるライブラリは、そのままで約束します。この機能を追加する無料の「ラッパー」ライブラリがあまりないライブラリ。 GraphQLリゾルバーを使用する場合、コールバックを利用するメソッドの使用を避け、代わりにPromiseを返すメソッドを使用する必要があります。
プロのヒント:コールバックとプロミスの両方をサポートするライブラリは、コールバックが提供されない場合に関数がプロミスを返すように、頻繁に関数をオーバーロードします。詳細については、ライブラリのドキュメントを確認してください。
コールバックを使用する必要がある場合は、コールバックをPromiseでラップすることもできます。
function post(root, args) {
return new Promise((resolve, reject) => {
Post.findOne({ where: { id: args.id } }, function (err, post) {
if (err) {
reject(err)
} else {
resolve(post)
}
})
})
Nest.jsでも同じ問題が発生しました。
問題を解決したい場合。 @Queryデコレータに{nullable:true}オプションを追加できます。
ここに例があります。
@Resolver(of => Team)
export class TeamResolver {
constructor(
private readonly teamService: TeamService,
private readonly memberService: MemberService,
) {}
@Query(returns => Team, { name: 'team', nullable: true })
@UseGuards(GqlAuthGuard)
async get(@Args('id') id: string) {
return this.teamService.findOne(id);
}
}
次に、クエリに対してnullオブジェクトを返すことができます。
上記のいずれも役に立たず、たとえば「data」フィールド内にすべての応答を包括するグローバルインターセプターがある場合、graphqlに対してこれを無効にする必要があります。他の賢いgraphqlリゾルバーはnullに変換します。
これは私の場合にインターセプターにしたことです:
intercept(
context: ExecutionContext,
next: CallHandler,
): Observable<Response<T>> {
if (context['contextType'] === 'graphql') return next.handle();
return next
.handle()
.pipe(map(data => {
return {
data: isObject(data) ? this.transformResponse(data) : data
};
}));
}