web-dev-qa-db-ja.com

GraphQLクエリがnullを返すのはなぜですか?

graphql/apollo-server/graphql-yoga 終点。このエンドポイントは、データベース(またはRESTエンドポイントまたはその他のサービス)から返されたデータを公開します。

データソースが正しいデータを返していることはわかっています。リゾルバー内のデータソースへの呼び出しの結果をログに記録すると、データが返されていることがわかります。ただし、私のGraphQLフィールドは常にnullに解決されます。

フィールドをnull以外にすると、レスポンスのerrors配列内に次のエラーが表示されます。

Nullにできないフィールドに対してnullを返すことはできません

GraphQLがデータを返さないのはなぜですか?

9
Daniel Rearden

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に解決されます。

一般的なシナリオ#1:ラップされた応答

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であるため、リゾルバーはidtitlebodyなどのプロパティを持つオブジェクトを返す必要があります。これがAPIから返されたものであれば、準備は完了です。 ただし、応答は実際には追加のメタデータを含むオブジェクトであることが一般的です。したがって、実際にエンドポイントから取得するオブジェクトは次のようになります。

{
  "status": 200,
  "result": {
    "id": 1,
    "title": "My First Post",
    "body": "Hello world!"
  },
}

この場合、返されるオブジェクトには必要なidtitle、および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 を使用している場合、ResultrowsfieldsrowCountなどのプロパティを含むcommandオブジェクトを取得します。リゾルバー内に返す前に、この応答から適切なデータを抽出する必要があります。

一般的なシナリオ#2:オブジェクトではなく配列

データベースから投稿をフェッチすると、リゾルバは次のようになります。

function post(root, args, context) {
  return context.Post.find({ where: { id: args.id } })
}

ここで、Postは、コンテキストを通じて注入するモデルです。 sequelizeを使用している場合、findAllを呼び出すことがあります。 mongoosetypeormには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の呼び出しに関する特別な注意:行またはモデルインスタンスを挿入または更新するメソッドは、挿入または更新された行を返すことがよくあります。しばしばそうですが、いくつかのメソッドはそうしません。たとえば、sequelizeupsertメソッドは、ブール値、またはアップサートされたレコードとブール値のタプルに解決されます(returningオプションがtrueに設定されている場合)。 mongoosefindOneAndUpdateは、変更された行を含むvalueプロパティを持つオブジェクトに解決されます。 ORMのドキュメントを参照し、リゾルバー内に返す前に結果を適切に解析してください。

一般的なシナリオ#3:配列ではなくオブジェクト

私たちのスキーマでは、postsフィールドの型はListsの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オブジェクトに解決されます。

一般的なシナリオ#4:値を返さない

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を追加するだけです。

一般的なシナリオ#5:Promiseを正しくチェーンしない

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に「チェーン」されます。複数のthensを使用すると、このように複数の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つの非同期関数(getBargetPost)を呼び出す必要がある場合は、次のようにします。

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を正しくチェーンするのに苦労している場合は、非同期/待機の構文がより簡潔で扱いやすいことがあります。

一般的なシナリオ#6

Promiseの前は、非同期コードを処理する標準的な方法は、コールバック、または非同期の作業が完了すると呼び出される関数を使用することでした。たとえば、次のようにmongoosefindOneメソッドを呼び出すことができます。

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)
      }
    })
  })
28
Daniel Rearden

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オブジェクトを返すことができます。

enter image description here

0

上記のいずれも役に立たず、たとえば「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
        };
      }));
  }
0
Dima Grossman