您可以撰寫自訂解析器,擴充 Firebase Data Connect 以支援 Cloud SQL 以外的資料來源。然後,您可以將多個資料來源 (Cloud SQL 和自訂解析器提供的資料來源) 合併為單一查詢或突變。
「資料來源」的概念相當彈性,It includes:
- Cloud SQL 以外的資料庫,例如 Cloud Firestore、MongoDB 等。
- Cloud Storage、AWS S3 等儲存空間服務。
- 任何以 API 為基礎的整合,例如 Stripe、SendGrid、Salesforce 等。
- 自訂商業邏輯。
編寫自訂解析器來支援其他資料來源後,Data Connect 查詢和突變就能以多種方式合併這些來源,帶來以下好處:
- 資料來源的統一授權層。舉例來說,您可以使用儲存在 Cloud SQL 中的資料,授權存取 Cloud Storage 中的檔案。
- 適用於網頁、Android 和 iOS 的型別安全用戶端 SDK。
- 查詢會傳回多個來源的資料。
- 根據資料庫狀態限制函式呼叫。
事前準備
如要編寫自己的自訂解析器,您需要:
- Firebase CLI 15.9.0 以上版本
- Firebase Functions SDK 7.1.0 以上版本
此外,您也應熟悉如何使用 Cloud Functions for Firebase 撰寫函式,因為您會使用這項服務實作自訂解析器的邏輯。
事前準備
您應該已設定專案來使用 Data Connect。
如果您尚未設定,可以按照任一快速入門指南進行:
撰寫自訂解析器
從高層次來看,編寫自訂解析器有三個部分:首先,定義自訂解析器的結構定義;其次,使用 Cloud Functions 實作解析器;最後,在查詢和突變中使用自訂解析器欄位,可能與 Cloud SQL 或其他自訂解析器搭配使用。
請按照接下來幾節的步驟操作,瞭解如何執行這項操作。舉例來說,假設您將使用者的公開個人資料資訊儲存在 Cloud SQL 以外的位置。這些範例未指定確切的資料儲存區,但可以是 Cloud Storage、MongoDB 執行個體或其他任何項目。
以下各節將示範自訂解析器的架構實作,可將外部設定檔資訊帶入 Data Connect。
定義自訂解析器的結構定義
在 Firebase 專案目錄中執行下列指令:
firebase init dataconnect:resolverFirebase CLI 會提示您輸入自訂解析器的名稱,並詢問是否要以 TypeScript 或 JavaScript 產生範例解析器實作。如果您是按照本指南操作,請接受預設名稱並產生 TypeScript 範例。
接著,工具會建立空白的
dataconnect/schema_resolver/schema.gql檔案,並將新的解析器設定新增至dataconnect.yaml檔案。使用 GraphQL 結構定義更新這個
schema.gql檔案,定義自訂解析器將提供的查詢和突變。舉例來說,以下是自訂解析器的結構定義,可擷取及更新儲存在 Cloud SQL 以外資料儲存區中的使用者公開設定檔:# dataconnect/schema_resolver/schema.gql type PublicProfile { name: String! photoUrl: String! bioLine: String! } type Query { # This field will be backed by your Cloud Function. publicProfile(userId: String!): PublicProfile } type Mutation { # This field will be backed by your Cloud Function. updatePublicProfile( userId: String!, name: String, photoUrl: String, bioLine: String ): PublicProfile }
實作自訂解析器邏輯
接著,使用 Cloud Functions 實作解析器。在幕後,您將建立 GraphQL 伺服器;不過,Cloud Functions 有一個輔助方法 onGraphRequest 可處理相關詳細資料,因此您只需要編寫存取資料來源的解析器邏輯。
開啟
functions/src/index.ts檔案。您在上方執行
firebase init dataconnect:resolver時,該指令會建立這個 Cloud Functions 原始碼目錄,並在index.ts中以程式碼範例初始化。新增下列定義:
import { FirebaseContext, onGraphRequest, } from "firebase-functions/dataconnect/graphql"; const opts = { // Points to the schema you defined earlier, relative to the root of your // Firebase project. schemaFilePath: "dataconnect/schema_resolver/schema.gql", resolvers: { query: { // This resolver function populates the data for the "publicProfile" field // defined in your GraphQL schema located at schemaFilePath. publicProfile( _parent: unknown, args: Record<string, unknown>, _contextValue: FirebaseContext, _info: unknown ) { const userId = args.userId; // Here you would use the user ID to retrieve the user profile from your data // store. In this example, we just return a hard-coded value. return { name: "Ulysses von Userberg", photoUrl: "https://example.com/profiles/12345/photo.jpg", bioLine: "Just a guy on a mountain. Ski fanatic.", }; }, }, mutation: { // This resolver function updates data for the "updatePublicProfile" field // defined in your GraphQL schema located at schemaFilePath. updatePublicProfile( _parent: unknown, args: Record<string, unknown>, _contextValue: FirebaseContext, _info: unknown ) { const { userId, name, photoUrl, bioLine } = args; // Here you would update in your datastore the user's profile using the // arguments that were passed. In this example, we just return the profile // as though the operation had been successful. return { name, photoUrl, bioLine }; }, }, }, }; export const resolver = onGraphRequest(opts);
這些骨架實作項目會顯示解析器函式必須採用的通用形狀。如要建立功能齊全的自訂解析器,您需要填入註解區段,並加入可讀取及寫入資料來源的程式碼。
在查詢和突變中使用自訂解析器
您已定義自訂解析器的結構定義,並實作支援該結構定義的邏輯,現在可以在 Data Connect 查詢和突變中使用自訂解析器。稍後,您將使用這些作業自動產生自訂用戶端 SDK,用來存取所有資料,無論資料是由 Cloud SQL、自訂解析器或兩者組合支援。
在
dataconnect/example/queries.gql中新增下列定義:query GetPublicProfile($id: String!) @auth(level: PUBLIC, insecureReason: "Anyone can see a public profile.") { publicProfile(userId: $id) { name photoUrl bioLine } }這項查詢會使用自訂解析器,擷取使用者的公開個人資料。
在
dataconnect/example/mutations.gql中新增下列定義:mutation SetPublicProfile( $id: String!, $name: String, $photoUrl: String, $bioLine: String ) @auth(expr: "vars.id == auth.uid") { updatePublicProfile(userId: $id, name: $name, photoUrl: $photoUrl, bioLine: $bioLine) { name photoUrl bioLine } }這項突變會將一組新的設定檔資料寫入資料存放區,同樣使用自訂解析器。請注意,結構定義會使用 Data Connect 的
@auth指令,確保使用者只能更新自己的個人資料。由於您是透過 Data Connect 存取資料儲存區,因此可以自動使用 Data Connect 功能,例如這個功能。
在上述範例中,您已定義 Data Connect 作業,這些作業會使用自訂解析器存取資料儲存庫中的資料。不過,您在作業中存取 Cloud SQL 或單一自訂資料來源的資料時,不會受到限制。如需結合多個來源資料的進階使用案例,請參閱「範例」一節。
在此之前,請繼續前往下一節,瞭解自訂解析器運作情形。
部署自訂解析器和作業
與變更Data Connect結構定義時相同,您必須部署結構定義,變更才會生效。請先使用 Cloud Functions 部署您實作的自訂解析器邏輯,再執行這項操作:
firebase deploy --only functions現在您可以部署更新後的結構定義和作業:
firebase deploy --only dataconnect變更 Data Connect 結構定義後,您也必須產生新的用戶端 SDK:
firebase dataconnect:sdk:generate範例
這些範例說明如何實作一些更進階的用途,以及如何避免常見的陷阱。
使用 Cloud SQL 中的資料授權存取自訂解析器
將資料來源整合至 Data Connect 的好處之一,就是您可以使用自訂解析器編寫作業,合併資料來源。
在這個範例中,假設您正在建構社群媒體應用程式,且您已將變動實作為自訂解析器,如果使用者一段時間未與好友互動,就會傳送提醒電子郵件給好友。
如要實作提醒功能,請建立自訂解析器,並使用下列結構定義:
# A GraphQL server must define a root query type per the spec.
type Query {
unused: String
}
type Mutation {
sendEmail(id: String!, content: String): Boolean
}
這項定義是由 Cloud Function 支援,例如:
import {
FirebaseContext,
onGraphRequest,
} from "firebase-functions/dataconnect/graphql";
const opts = {
schemaFilePath: "dataconnect/schema_resolver/schema.gql",
resolvers: {
mutation: {
sendEmail(
_parent: unknown,
args: Record<string, unknown>,
_contextValue: FirebaseContext,
_info: unknown
) {
const { id, content } = args;
// Look up the friend's email address and call the cloud service of your
// choice to send the friend an email with the given content.
return true;
},
},
},
};
export const resolver = onGraphRequest(opts);
由於傳送電子郵件的成本高昂,且可能成為濫用行為的途徑,因此使用sendEmail自訂解析器前,請務必確認預期收件者已在使用者好友名單中。
假設應用程式中的好友名單資料儲存在 Cloud SQL 中:
type User @table {
id: String! @default(expr: "auth.uid")
acceptNudges: Boolean! @default(value: false)
}
type UserFriend @table(key: ["user", "friend"]) {
user: User!
friend: User!
}
您可以先編寫變異,查詢 Cloud SQL 以確保傳送者位於接收者的好友清單中,再使用自訂解析器傳送電子郵件:
# Send a "nudge" to a friend as a reminder. This will only let the user send a
# nudge if $friendId is in the user's friends list.
mutation SendNudge($friendId: String!) @auth(level: USER_EMAIL_VERIFIED) {
# Step 1: Query and check
query @redact {
userFriend(
key: {userId_expr: "auth.uid", friendId: $friendId}
# This checks that $friendId is in the user's friends list.
) @check(expr: "this != null", message: "You must be friends to nudge") {
friend {
# This checks that the friend is accepting nudges.
acceptNudges @check(expr: "this == true", message: "Not accepting nudges")
}
}
}
# Step 2: Act
sendEmail(id: $friendId, content: "You've been nudged!")
}
此外,這個範例也說明,自訂解析器環境中的資料來源可包含資料庫和類似系統以外的資源。在本例中,資料來源是雲端電子郵件傳送服務。
使用變異確保依序執行
合併資料來源時,您通常需要確保對某個資料來源的要求完成後,才能對其他資料來源提出要求。舉例來說,假設您有一個查詢,可使用 AI API 動態轉錄隨選視訊。這類 API 呼叫的費用可能很高,因此您會希望根據某些條件限制呼叫,例如使用者擁有影片,或使用者已在應用程式中購買某種進階抵免額。
首次嘗試達成這項目標時,程式碼可能如下所示:
# This won't work as expected.
query BrokenTranscribeVideo($videoId: UUID!) @auth(level: USER_EMAIL_VERIFIED) {
# Step 1: Check quota using SQL.
# Verify the user owns the video and has "pro" status or credits.
checkQuota: query @redact {
video(id: $videoId)
{
user @check(expr: "this.id == auth.uid && this.hasCredits == true", message: "Unauthorized access") {
id
hasCredits
}
}
}
# Step 2: Trigger expensive compute
# Only triggers if Step 1 succeeds? No! This won't work because query field
# execution order is not guaranteed.
triggerTranscription: query {
# For example, might call Vertex AI or Transcoder API.
startVideoTranscription(videoId: $videoId)
}
}
這種做法行不通,因為查詢欄位的執行順序無法保證;GraphQL 伺服器應能以任何順序解析欄位,盡可能提高並行程度。另一方面,變異的欄位一律會依序解析,因為 GraphQL 伺服器預期變異的某些欄位在解析時可能會產生副作用。
即使範例作業的第一個步驟沒有副作用,您仍可將作業定義為變異,以便利用變異欄位依序解析的事實:
# By using a mutation, we guarantee the SQL check happens FIRST.
mutation TranscribeVideo($videoId: UUID!) @auth(level: USER_EMAIL_VERIFIED) {
# Step 1: Check quota using SQL.
# Verify the user owns the video and has "pro" status or credits.
checkQuota: query @redact {
video(id: $videoId)
{
user @check(expr: "this.id == auth.uid && this.hasCredits == true", message: "Unauthorized access") {
id
hasCredits
}
}
}
# Step 2: Trigger expensive compute
# This Cloud Function will ONLY trigger if Step 1 succeeds.
triggerTranscription: query {
# For example, might call Vertex AI or Transcoder API.
startVideoTranscription(videoId: $videoId)
}
}
限制
自訂解析器功能以實驗性公開測試版的形式發布。請注意下列目前限制:
自訂解析器引數中沒有 CEL 運算式
您無法在自訂解析器的引數中動態使用 CEL 運算式。舉例來說,您無法執行下列操作:
mutation UpdateMyProfile($newName: String!) @auth(level: USER) {
updateMongoDocument(
collection: "profiles"
# This isn't supported:
id_expr: "auth.uid"
update: { name: $newName }
)
}
請改為傳遞標準變數 (例如 $authUid),並使用安全評估的 @auth(expr: ...) 指令,在作業層級驗證這些變數。
mutation UpdateMyProfile(
$newName: String!, $authUid: String!
) @auth(expr: "vars.authUid == auth.uid") {
updateMongoDocument(
collection: "profiles"
id: $authUid
update: { name: $newName }
)
}
另一個解決方法是將所有邏輯移至自訂解析器,並從 Cloud Functions 完成所有資料作業。
舉例來說,請看以下範例,目前無法運作:
mutation BrokenForwardToEmail($chatMessageId: UUID!) @auth(level: USER_EMAIL_VERIFIED) {
query {
chatMessage(id: $chatMessageId) {
content
}
}
sendEmail(
title: "Forwarded Chat Message"
to_expr: "auth.token.email" # Not supported.
content_expr: "response.query.chatMessage.content" # Not supported.
)
}
請改為將 Cloud SQL 查詢和電子郵件服務呼叫作業移至一個突變欄位,並以函式做為後端:
mutation ForwardToEmail($chatMessageId: UUID!) @auth(level: USER_EMAIL_VERIFIED) {
forwardChatToEmail(
chatMessageId: $chatMessageId
)
}
為資料庫產生 Admin SDK,並在函式中使用該 SDK 執行 Cloud SQL 查詢:
const opts = {
schemaFilePath: "dataconnect/schema_resolver/schema.gql",
resolvers: {
query: {
async forwardToEmail(
_parent: unknown,
args: Record<string, unknown>,
_contextValue: FirebaseContext,
_info: unknown
) {
const chatMessageId = args.chatMessageId as string;
let decodedToken;
try {
decodedToken = await getAuth().verifyIdToken(_contextValue.auth.token ?? "");
} catch (error) {
return false;
}
const email = decodedToken.email;
if (!email) {
return false;
}
const response = await getChatMessage({chatMessageId});
const messageContent = response.data.chatMessage?.content;
// Here you call the cloud service of your choice to send the email with
// the message content.
return true;
}
},
},
};
export const resolver = onGraphRequest(opts);
自訂解析器參數中沒有輸入物件類型
自訂解析器不接受複雜的 GraphQL 輸入類型。參數必須是基本純量型別 (String、Int、Date、Any 等) 和 Enum。
input PublicProfileInput {
name: String!
photoUrl: String!
bioLine: String!
}
type Mutation {
# Not supported:
updatePublicProfile(userId: String!, profile: PublicProfileInput): PublicProfile
# OK:
updatePublicProfile(userId: String!, name: String, photoUrl: String, bioLine: String): PublicProfile
}
自訂解析器不得先於 SQL 作業
在變動中,將自訂解析器放在標準 SQL 作業之前會導致錯誤。所有以 SQL 為基礎的作業都必須出現在任何自訂解析器調用之前。
沒有交易 (@transaction)
自訂解析器無法包裝在 @transaction 區塊中,搭配標準 SQL 作業。如果 SQL 插入作業成功後,解析器支援的 Cloud Function 失敗,資料庫不會自動復原。
如要在 SQL 和其他資料來源之間確保交易安全,請將 SQL 作業邏輯移至 Cloud 函式內,並使用 Admin SDK 或直接 SQL 連線處理驗證和回溯。