2018 年 2 月 6 日

GraphQL 基礎知識:解密 GraphQL 解析器中的 `info` 參數

GraphQL 伺服器的結構與實作(第三部分)

GraphQL Basics

如果您之前撰寫過 GraphQL 伺服器,很有可能已經接觸過傳遞到解析器中的 info 物件。幸運的是,在大多數情況下,您真的不需要理解它實際上做什麼,以及它在查詢解析期間的角色。

然而,在許多邊緣情況下,info 物件是造成許多混淆和誤解的原因。本文的目標是深入了解 info 物件的內部,並闡明其在 GraphQL 執行過程中的作用。

本文假設您已經熟悉 GraphQL 查詢和變異 (mutation) 如何解析的基本知識。如果您在這方面感到有點不確定,您絕對應該查看本系列的前幾篇文章:第一部分:GraphQL Schema(必讀)第二部分:網路層(可選)

info 物件的結構

回顧:GraphQL 解析器的簽名

快速回顧一下,當使用 GraphQL.js 建立 GraphQL 伺服器時,您有兩個主要任務

  • 定義您的 GraphQL schema(以 SDL 或純 JS 物件形式)
  • 對於 schema 中的每個欄位,實作一個 *resolver* 函數,該函數知道如何傳回該欄位的值

一個 resolver 函數接受四個參數(按順序排列)

  1. parent:先前 resolver 呼叫的結果(更多資訊)。
  2. args:resolver 欄位的參數。
  3. context:每個 resolver 都可以讀取/寫入的自訂物件。
  4. info:*這就是我們將在本文中討論的內容。*

以下是一個簡單 GraphQL 查詢的執行過程以及所屬解析器的調用概觀。由於 *第二層解析器* 的解析是微不足道的,因此實際上不需要實作這些解析器 — 它們的回傳值由 GraphQL.js 自動推斷。

GraphQL 解析器鏈中 parentargs 參數的概觀

info 包含查詢 AST 和更多執行資訊

那些對 info 物件的結構和角色感到好奇的人仍然感到困惑。官方 spec文件 都沒有提及它。過去有一個 GitHub issue 要求為其提供更好的文件,但該 issue 在沒有顯著行動的情況下被關閉。因此,除了深入研究程式碼之外,別無他法。

從非常高的層次來看,可以說 info 物件包含傳入 GraphQL 查詢的 AST。由於這個原因,解析器知道它們需要傳回哪些欄位。

若要了解更多關於查詢 AST 的外觀,請務必查看 Christian Joudrey 的精彩文章 Life of a GraphQL Query — Lexing/Parsing 以及 Eric Baer 的精彩演講 GraphQL Under the Hood

為了理解 info 的結構,讓我們看一下 它的 Flow 型別定義

以下是每個鍵的概觀和快速說明

  • fieldName:如前所述,您的 GraphQL schema 中的每個欄位都需要由 resolver 支援。fieldName 包含屬於目前 resolver 的欄位名稱。
  • fieldNodes:一個陣列,其中每個物件代表剩餘 *選擇集* 中的一個欄位。
  • returnType:對應欄位的 GraphQL 型別。
  • parentType:此欄位所屬的 GraphQL 型別。
  • path:追蹤已遍歷的欄位,直到到達目前欄位(即 resolver)。
  • schema:代表您 *可執行* schema 的 GraphQLSchema 實例。
  • fragments:查詢文件中 *fragment* 的對應表。
  • rootValue:傳遞給執行的 rootValue 參數。
  • operation:*整個* 查詢的 AST。
  • variableValues:與查詢一起提供的任何變數的對應表,對應於 variableValues 參數。

如果這仍然看起來很抽象,請別擔心,我們很快就會看到所有這些的範例。

欄位特定與全域

關於上面的鍵,有一個有趣的觀察。 info 物件上的鍵要么是 *欄位特定* 的,要么是 *全域* 的。

*欄位特定* 僅表示該鍵的值取決於傳遞 info 物件的欄位(及其支援的 resolver)。明顯的範例是 fieldNamerootTypeparentType。考慮以下 GraphQL 型別的 author 欄位

該欄位的 fieldName 只是 authorreturnTypeUser!,而 parentTypeQuery

現在,對於 feed,這些值當然會有所不同:fieldNamefeedreturnType[Post!]!,而 parentType 也是 Query

因此,這三個鍵的值是欄位特定的。進一步的欄位特定鍵是:fieldNodespath。實際上,上面 Flow 定義的前五個鍵是欄位特定的。

*全域*,另一方面,表示這些鍵的值不會改變 — 無論我們談論的是哪個 resolver。schemafragmentsrootValueoperationvariableValues 將始終為所有 resolver 攜帶相同的值。

一個簡單的範例

現在讓我們繼續看一個 info 物件內容的範例。為了設定情境,以下是我們將用於此範例的 schema 定義

假設該 schema 的 resolver 實作如下

請注意,Post.title resolver 實際上不是必需的,我們仍然在此處包含它,以查看調用 resolver 時 info 物件的外觀。

現在考慮以下查詢

為了簡潔起見,我們將僅討論 Query.author 欄位的 resolver,而不是 Post.title 的 resolver(當執行上述查詢時,它仍然會被調用)。

如果您想試玩這個範例,我們準備了一個 repository,其中包含上述 schema 的執行版本,因此您可以進行實驗!

接下來,讓我們看看 info 物件中的每個鍵,看看當調用 Query.author resolver 時它們的外觀(您可以在 這裡 找到 info 物件的完整記錄輸出)。

fieldName

fieldName 只是 author

fieldNodes

請記住,fieldNodes 是欄位特定的。它有效地包含查詢 AST 的 *摘錄*。此摘錄從目前欄位(即 author)開始,而不是從查詢的 *根* 開始。(從根開始的整個查詢 AST 儲存在 operation 中,請參閱下文)。

returnType & parentType

如前所述,returnTypeparentType 非常簡單

path

path 追蹤已遍歷的欄位,直到目前的欄位。對於 Query.author,它看起來很簡單:"path": { "key": "author" }

為了比較,在 Post.title resolver 中,path 看起來如下

其餘五個欄位屬於「全域」類別,因此對於 Post.title resolver 而言將是相同的。

schema

schema 是對可執行 schema 的參考。

fragments

fragments 包含 fragment 定義,由於查詢文件沒有任何 fragment,因此它只是一個空對應表:{}

rootValue

如前所述,rootValue 鍵的值對應於首先傳遞給 graphql 執行函數的 rootValue 參數。在範例中,它只是 null

operation

operation 包含傳入查詢的完整 查詢 AST。回想一下,除其他資訊外,這包含我們在上面看到的 fieldNodes 的相同值

variableValues

此鍵表示已為查詢傳遞的任何變數。由於我們的範例中沒有變數,因此該值再次只是一個空對應表:{}

如果查詢是用變數撰寫的

variableValues 鍵將只具有以下值

使用 GraphQL bindings 時 info 的角色

如本文開頭所述,在大多數情況下,您完全不需要擔心 info 物件。它只是恰好成為您的 resolver 簽名的一部分,但您實際上沒有將其用於任何用途。那麼,它何時變得相關?

info 傳遞給 binding 函數

如果您之前使用過 GraphQL bindings,您已經看到 info 物件是產生的 binding 函數的一部分。考慮以下 schema

使用 graphql-binding,您現在可以透過調用專用的 *binding 函數* 而不是傳送原始查詢和變異來傳送可用的查詢和變異。

例如,考慮以下原始查詢,檢索特定的 User

使用 binding 函數實現相同的功能將如下所示

透過在 binding 實例上調用 user 函數並傳遞相應的參數,我們傳達了與上述原始 GraphQL 查詢完全相同的資訊。

來自 graphql-binding 的 binding 函數接受三個參數

  1. args:包含欄位的參數(例如,上面 createUser 變異的 username)。
  2. context:在 resolver 鏈中向下傳遞的 context 物件。
  3. infoinfo 物件。請注意,您可以傳遞一個簡單定義選擇集的字串,而不是 GraphQLResolveInfo 的實例(info 的型別)。

使用 Prisma 將應用程式 schema 對應到資料庫 schema

info 物件可能引起混淆的另一個常見用例是基於 Prismaprisma-binding 實作 GraphQL 伺服器。

在這種情況下,想法是擁有兩個 GraphQL 層

  • 資料庫層 由 Prisma 自動產生,並提供通用且強大的 CRUD API
  • *應用程式層* 定義暴露給客戶端應用程式且根據您的應用程式需求量身定制的 GraphQL API

作為後端開發人員,您負責定義應用程式層的 *應用程式 schema* 並實作其 resolver。感謝 prisma-binding,resolver 的實作僅僅是一個 委派 傳入查詢到基礎資料庫 API 的過程,而沒有 major overhead。

讓我們考慮一個簡單的範例 — 假設您從以下 Prisma 資料庫服務的資料模型開始

Prisma 根據此資料模型產生的資料庫 schema 看起來與此類似

現在,假設您想要建立一個看起來與此類似的應用程式 schema

feed 查詢不僅傳回 Post 元素的列表,而且還能夠傳回列表的 count。請注意,它可選地採用 authorId,它會篩選 feed 以僅傳回由特定 User 撰寫的 Post 元素。

實作此應用程式 schema 的第一個直覺可能如下所示。

實作 1:此實作看起來正確,但有一個細微的缺陷

此實作看起來相當合理。在 feed resolver 內部,我們正在根據潛在的傳入 authorId 建構 authorFilterauthorFilter 然後用於執行 posts 查詢並檢索 Post 元素,以及 postsConnection 查詢,該查詢提供對列表 count 的存取。

也可以僅使用 *postsConnection* 查詢來檢索實際的 *Post* 元素。為了簡化起見,我們仍然使用 *posts* 查詢來實現這一點,並將另一種方法留給細心的讀者練習。

事實上,當使用此實作啟動 GraphQL 伺服器時,一開始看起來會很好。您會注意到簡單的查詢得到正確的服務,例如,以下查詢將成功

直到您嘗試檢索 Post 元素的 author 時,您才會遇到問題

好的!因此,由於某種原因,實作沒有傳回 author,這會觸發錯誤 *「Cannot return null for non-nullable Post.author.」*,因為 Post.author 欄位在 *應用程式 schema* 中被標記為必填。

讓我們再次看一下實作的相關部分

這是我們檢索 Post 元素的地方。但是,我們沒有將 *選擇集* 傳遞給 posts binding 函數。如果沒有將第二個參數傳遞給 Prisma binding 函數,則預設行為是查詢該型別的所有 *純量* 欄位。

這確實解釋了這種行為。對 ctx.db.query.posts 的呼叫傳回了正確的 Post 元素集,但僅傳回了它們的 idtitle 值 — 沒有關於 author 的關聯資料。

那麼,我們如何解決這個問題?顯然需要一種方法來告訴 posts binding 函數它需要傳回哪些欄位。但是,該資訊位於 feed resolver 的上下文中嗎?您能猜到嗎?

正確:info 物件內部!由於 Prisma binding 函數的第二個參數可以是字串 *或* info 物件,因此讓我們只將傳遞到 feed resolver 的 info 物件傳遞到 posts binding 函數。

此查詢失敗,並顯示實作 2:「型別為 'Post' 的欄位 'posts' 必須具有子選擇。」

但是,透過此實作,*沒有* 請求會得到正確的服務。例如,考慮以下查詢

錯誤訊息 *「型別為 'Post' 的欄位 'posts' 必須具有子選擇。」* 是由上述實作的 *第 8 行* 產生的。

那麼,這裡發生了什麼事?失敗的原因是因為 info 物件中的 *欄位特定* 鍵與 posts 查詢不符。

feed resolver 內部列印 info 物件可以更清楚地了解情況。讓我們僅考慮 fieldNodes 中的欄位特定資訊

此 JSON 物件也可以表示為字串選擇集

現在一切都說得通了!我們正在將上述選擇集傳送到 Prisma 資料庫 schema 的 posts 查詢,該查詢當然不知道 feedcount 欄位。誠然,產生的錯誤訊息不是超級有幫助,但至少我們現在了解了正在發生的事情。

那麼,這個問題的解決方案是什麼?解決此問題的一種方法是 *手動* 解析出 fieldNodes 選擇集的正確部分,並將其傳遞給 posts binding 函數(例如,作為字串)。

但是,有一個更優雅的解決方案來解決這個問題,那就是為應用程式 schema 中的 Feed 型別實作專用的 resolver。以下是正確的實作方式。

實作 3:此實作修正了上述問題

此實作修正了上面討論的所有問題。有幾件事需要注意

  • 在 *第 8 行* 中,我們現在傳遞一個字串選擇集({ id })作為第二個參數。這只是為了提高效率,因為否則將會擷取所有純量值(在我們的範例中這不會造成巨大的差異),而我們只需要 ID。
  • 我們從 Query.feed resolver 傳回 posts,而不是傳回 postIds,它只是一個 ID 陣列(表示為字串)。
  • Feed.posts resolver 中,我們現在可以存取由 *parent* resolver 傳回的 postIds。這次,我們可以利用傳入的 info 物件,並簡單地將其傳遞給 posts binding 函數。

如果您想試玩這個範例,您可以查看 這個 repository,其中包含上述範例的執行版本。隨時嘗試本文中提到的不同實作,並自行觀察其行為!

總結

在本文中,您深入了解了在使用 GraphQL.js 實作 GraphQL API 時使用的 info 物件。

info 物件沒有正式文件記錄 — 若要了解更多關於它的資訊,您需要深入研究程式碼。在本教學中,我們首先概述了其內部結構,並理解了其在 GraphQL resolver 函數中的作用。然後,我們介紹了一些邊緣情況和潛在的陷阱,在這些情況下,需要更深入地了解 info

本文中顯示的所有程式碼都可以在相應的 GitHub repository 中找到,因此您可以自行實驗並觀察 info 物件的行為。

不要錯過下一篇文章!

註冊 Prisma Newsletter