カスタム リゾルバを作成することで、Firebase SQL Connect を拡張して、Cloud SQL 以外のデータソースもサポートできます。複数のデータソース(Cloud SQL とカスタム リゾルバによって提供されるデータソース)を 1 つのクエリまたはミューテーションに結合できます。
「データソース」の概念は柔軟です。It includes:
- Cloud SQL 以外のデータベース(Cloud Firestore、MongoDB など)。
- Cloud Storage、AWS S3 などのストレージ サービス。
- Stripe、SendGrid、Salesforce などの API ベースの統合。
- カスタム ビジネス ロジック。
追加のデータソースをサポートするカスタム リゾルバを作成すると、SQL Connect クエリとミューテーションでさまざまな方法でそれらを組み合わせることができます。これにより、次のようなメリットが得られます。
- データソースの統合認証レイヤ。たとえば、Cloud SQL に保存されているデータを使用して、Cloud Storage 内のファイルへのアクセスを承認します。
- ウェブ、Android、iOS 向けのタイプセーフなクライアント SDK。
- 複数のソースからデータを返すクエリ。
- データベースの状態に基づいて関数呼び出しを制限します。
前提条件
独自のカスタム リゾルバを作成するには、次のものが必要です。
- Firebase CLI v15.9.0 以降
- Firebase Functions SDK v7.1.0 以降
また、カスタム リゾルバのロジックを実装する方法である Cloud Functions for Firebase を使用した関数の作成にも精通している必要があります。
始める前に
SQL Connect を使用するように設定されたプロジェクトがすでに存在しているはずです。
まだ設定していない場合は、いずれかのクイックスタート ガイドに沿って設定できます。
カスタム リゾルバを作成する
カスタム リゾルバの作成は、大まかに次の 3 つの部分に分かれています。1 つ目はカスタム リゾルバのスキーマを定義すること、2 つ目は Cloud Functions を使用してリゾルバを実装すること、3 つ目はクエリとミューテーションでカスタム リゾルバ フィールドを使用することです。このとき、Cloud SQL や他のカスタム リゾルバと組み合わせて使用することもできます。
次のセクションの手順に沿って、この方法を学習します。動機付けの例として、ユーザーの公開プロフィール情報が Cloud SQL の外部に保存されているとします。これらの例では、正確なデータストアは指定されていませんが、Cloud Storage、MongoDB インスタンスなどを使用できます。
以降のセクションでは、外部プロファイル情報を SQL Connect に取り込むことができるカスタム リゾルバのスケルトン実装について説明します。
カスタム リゾルバのスキーマを定義する
Firebase プロジェクト ディレクトリで、次のコマンドを実行します。
firebase init dataconnect:resolverFirebase CLI は、カスタム リゾルバの名前の入力を求め、TypeScript または JavaScript のいずれかでリゾルバ実装の例を生成するかどうかを尋ねます。このガイドに沿って操作する場合は、デフォルトの名前を使用して TypeScript の例を生成します。
ツールは、空の
dataconnect/schema_resolver/schema.gqlファイルを作成し、新しいリゾルバ構成をdataconnect.yamlファイルに追加します。この
schema.gqlファイルを、カスタム リゾルバが提供するクエリとミューテーションを定義する GraphQL スキーマで更新します。たとえば、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);
これらのスケルトン実装は、リゾルバ関数が取るべき一般的な形状を示しています。完全に機能するカスタム リゾルバを作成するには、コメント付きのセクションに、データソースの読み取りと書き込みを行うコードを入力する必要があります。
クエリとミューテーションでカスタム リゾルバを使用する
カスタム リゾルバのスキーマを定義し、そのスキーマをサポートするロジックを実装したので、SQL Connect クエリとミューテーションでカスタム リゾルバを使用できます。これらのオペレーションは、Cloud SQL、カスタム リゾルバ、またはその組み合わせによってバックアップされているかどうかに関係なく、すべてのデータにアクセスするために使用できるカスタム クライアント SDK を自動的に生成するために使用されます。
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 } }このミューテーションは、カスタム リゾルバを使用して、新しいプロファイル データのセットをデータストアに書き込みます。このスキーマでは、SQL Connect の
@authディレクティブを使用して、ユーザーが自分のプロフィールのみを更新できるようにしています。SQL Connect を介してデータストアにアクセスしているため、このような SQL Connect 機能を利用できます。
上記の例では、カスタム リゾルバを使用してデータストアからデータにアクセスする SQL Connect オペレーションを定義しました。ただし、Cloud SQL または単一のカスタム データソースからデータにアクセスするオペレーションに制限はありません。複数のソースのデータを組み合わせる高度なユースケースについては、例のセクションをご覧ください。
その前に、次のセクションに進んで、カスタム リゾルバの動作を確認してください。
カスタム リゾルバとオペレーションをデプロイする
SQL Connect スキーマに変更を加える場合と同様に、変更を有効にするにはデプロイする必要があります。その前に、Cloud Functions を使用して実装したカスタム リゾルバ ロジックをデプロイします。
firebase deploy --only functions更新されたスキーマとオペレーションをデプロイできるようになりました。
firebase deploy --only dataconnectSQL Connect スキーマを変更した後は、新しいクライアント SDK も生成する必要があります。
firebase dataconnect:sdk:generate例
これらの例は、より高度なユースケースを実装する方法と、一般的な落とし穴を回避する方法を示しています。
Cloud SQL のデータを使用してカスタム リゾルバへのアクセスを承認する
カスタム リゾルバを使用してデータソースを SQL Connect に統合するメリットの 1 つは、データソースを結合するオペレーションを記述できることです。
この例では、ソーシャル メディア アプリを構築しているとします。ユーザーの友だちがしばらくユーザーと交流していない場合に、その友だちにナッジメールを送信するミューテーションがカスタム リゾルバとして実装されています。
ナッジ機能を実装するには、次のようなスキーマを持つカスタム リゾルバを作成します。
# 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 Functions によってサポートされています。
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!")
}
なお、この例は、カスタム リゾルバのコンテキストのデータソースに、データベースや類似のシステム以外のリソースを含めることができることも示しています。この例では、データソースはクラウド メール送信サービスです。
ミューテーションを使用して順次実行を保証する
データソースを組み合わせる場合、多くの場合、別のデータソースにリクエストを行う前に、1 つのデータソースに対するリクエストが完了していることを確認する必要があります。たとえば、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 クエリとメールサービスへの呼び出しの両方を、関数でサポートされる 1 つのミューテーション フィールドに移動します。
mutation ForwardToEmail($chatMessageId: UUID!) @auth(level: USER_EMAIL_VERIFIED) {
forwardChatToEmail(
chatMessageId: $chatMessageId
)
}
データベースの Admin 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)
カスタム リゾルバを標準 SQL オペレーションで @transaction ブロック内にラップすることはできません。SQL 挿入が成功した後にリゾルバをサポートする Cloud Functions が失敗した場合、データベースは自動的にロールバックされません。
SQL と別のデータソース間のトランザクションの安全性を確保するには、SQL オペレーション ロジックを Cloud Functions 内に移動し、Admin SDK または直接 SQL 接続を使用して検証とロールバックを処理します。