Dengan menulis resolver kustom, Anda dapat memperluas Firebase SQL Connect untuk mendukung sumber data lain selain Cloud SQL. Kemudian, Anda dapat menggabungkan beberapa sumber data (Cloud SQL dan sumber data yang disediakan oleh resolver kustom Anda) ke dalam satu kueri atau mutasi.
Konsep "sumber data" bersifat fleksibel. Ini mencakup:
- Database selain Cloud SQL, seperti Cloud Firestore, MongoDB, dan lainnya.
- Layanan penyimpanan seperti Cloud Storage, AWS S3, dan lainnya.
- Integrasi berbasis API apa pun, seperti Stripe, SendGrid, Salesforce, dan lainnya.
- Logika bisnis kustom.
Setelah Anda menulis resolver kustom untuk mendukung sumber data tambahan, kueri dan mutasi SQL Connect dapat menggabungkannya dengan berbagai cara, sehingga memberikan manfaat seperti:
- Lapisan otorisasi terpadu untuk sumber data Anda. Misalnya, mengotorisasi akses ke file di Cloud Storage menggunakan data yang disimpan di Cloud SQL.
- SDK klien yang aman untuk web, Android, dan iOS.
- Kueri yang menampilkan data dari beberapa sumber.
- Pemanggilan fungsi yang dibatasi berdasarkan status database Anda.
Prasyarat
Untuk menulis resolver kustom Anda sendiri, Anda memerlukan hal berikut:
- Firebase CLI v15.9.0 atau yang lebih tinggi
- Firebase Functions SDK v7.1.0 atau yang lebih tinggi
Selain itu, Anda harus memahami cara menulis fungsi menggunakan Cloud Functions for Firebase, yang merupakan cara Anda menerapkan logika resolver kustom.
Sebelum memulai
Anda harus sudah menyiapkan project untuk menggunakan SQL Connect.
Anda dapat mengikuti salah satu panduan Memulai untuk menyiapkan jika belum:
Menulis resolver kustom
Secara umum, penulisan resolver kustom memiliki tiga bagian: pertama, menentukan skema untuk resolver kustom Anda; kedua, menerapkan resolver menggunakan Cloud Functions; dan terakhir, menggunakan kolom resolver kustom dalam kueri dan mutasi, mungkin bersamaan dengan Cloud SQL atau resolver kustom lainnya.
Ikuti langkah-langkah di beberapa bagian berikutnya untuk mempelajari cara melakukannya. Sebagai contoh motivasi, misalkan Anda memiliki informasi profil publik untuk pengguna yang disimpan di luar Cloud SQL. Datastore yang tepat tidak ditentukan dalam contoh ini, tetapi bisa berupa Cloud Storage, instance MongoDB, atau hal lainnya.
Bagian berikut akan menunjukkan penerapan kerangka resolver kustom yang dapat membawa informasi profil eksternal tersebut ke SQL Connect.
Menentukan skema untuk resolver kustom
Di direktori project Firebase, jalankan:
firebase init dataconnect:resolverFirebase CLI akan meminta nama untuk resolver kustom Anda, dan menanyakan apakah akan membuat contoh penerapan resolver di TypeScript atau JavaScript. Jika Anda mengikuti panduan ini, terima nama default dan buat contoh TypeScript.
Kemudian, alat ini akan membuat file
dataconnect/schema_resolver/schema.gqlkosong dan menambahkan konfigurasi resolver baru ke filedataconnect.yaml.Perbarui file
schema.gqlini dengan skema GraphQL yang menentukan kueri dan mutasi yang akan disediakan oleh resolver kustom Anda. Misalnya, berikut adalah skema untuk resolver kustom yang dapat mengambil dan memperbarui profil publik pengguna, yang disimpan di datastore selain 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 }
Menerapkan logika resolver kustom
Selanjutnya, terapkan resolver menggunakan Cloud Functions. Di balik layar, Anda akan membuat server GraphQL; namun, Cloud Functions memiliki metode helper, onGraphRequest, yang menangani detail melakukannya, sehingga Anda hanya perlu menulis logika resolver yang mengakses sumber data.
Buka file
functions/src/index.ts.Saat Anda menjalankan
firebase init dataconnect:resolverdi atas, perintah tersebut akan membuat direktori kode sumber Cloud Functions ini dan menginisialisasinya dengan kode contoh diindex.ts.Tambahkan definisi berikut:
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);
Implementasi kerangka ini menunjukkan bentuk umum yang harus diambil oleh fungsi resolver. Untuk membuat resolver kustom yang berfungsi penuh, Anda harus mengisi bagian yang dikomentari dengan kode yang membaca dan menulis ke sumber data.
Menggunakan resolver kustom dalam kueri dan mutasi
Setelah menentukan skema resolver kustom dan menerapkan logika yang mendukungnya, Anda dapat menggunakan resolver kustom dalam kueri dan mutasi SQL Connect. Nanti, Anda akan menggunakan operasi ini untuk otomatis membuat SDK klien kustom yang dapat digunakan untuk mengakses semua data, baik yang didukung oleh Cloud SQL, resolver kustom, atau kombinasi.
Di
dataconnect/example/queries.gql, tambahkan definisi berikut:query GetPublicProfile($id: String!) @auth(level: PUBLIC, insecureReason: "Anyone can see a public profile.") { publicProfile(userId: $id) { name photoUrl bioLine } }Kueri ini mengambil profil publik pengguna, menggunakan resolver kustom Anda.
Di
dataconnect/example/mutations.gql, tambahkan definisi berikut: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 } }Mutasi ini menulis kumpulan data profil baru ke datastore, sekali lagi menggunakan resolver kustom Anda. Perhatikan bahwa skema ini menggunakan SQL Connect's
@authdirektif untuk memastikan bahwa pengguna dapat hanya memperbarui profil mereka sendiri. Karena Anda mengakses datastore melalui SQL Connect, Anda otomatis dapat memanfaatkan fitur SQL Connect seperti ini.
Dalam contoh di atas, Anda telah menentukan operasi SQL Connect yang mengakses data dari datastore menggunakan resolver kustom. Namun, operasi Anda tidak terbatas untuk mengakses data dari Cloud SQL atau dari satu sumber data kustom. Lihat bagian Contoh untuk mengetahui beberapa kasus penggunaan yang lebih canggih yang menggabungkan data dari beberapa sumber.
Sebelum itu, lanjutkan ke bagian berikutnya untuk melihat resolver kustom Anda beraksi.
Men-deploy resolver dan operasi kustom
Seperti saat membuat perubahan pada skema SQL Connect, Anda harus men-deploy-nya agar perubahan tersebut diterapkan. Sebelum melakukannya, deploy terlebih dahulu logika resolver kustom yang Anda terapkan menggunakan Cloud Functions:
firebase deploy --only functionsSekarang Anda dapat men-deploy skema dan operasi yang diperbarui:
firebase deploy --only dataconnectSetelah membuat perubahan pada skema SQL Connect, Anda juga harus membuat SDK klien baru:
firebase dataconnect:sdk:generateContoh
Contoh ini menunjukkan cara menerapkan beberapa kasus penggunaan yang lebih canggih, dan cara menghindari masalah umum.
Mengotorisasi akses ke resolver kustom menggunakan data dari Cloud SQL
Salah satu manfaat mengintegrasikan sumber data ke SQL Connect menggunakan resolver kustom adalah Anda dapat menulis operasi yang menggabungkan data sumber.
Dalam contoh ini, misalkan Anda membuat aplikasi media sosial, dan Anda memiliki mutasi yang diterapkan sebagai resolver kustom, yang mengirim email pemberitahuan kepada teman pengguna jika mereka tidak berinteraksi dengan pengguna dalam beberapa waktu.
Untuk menerapkan fitur pemberitahuan, buat resolver kustom dengan skema seperti berikut:
# A GraphQL server must define a root query type per the spec.
type Query {
unused: String
}
type Mutation {
sendEmail(id: String!, content: String): Boolean
}
Definisi ini didukung oleh Cloud Function, seperti berikut:
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);
Karena pengiriman email mahal dan berpotensi disalahgunakan, Anda harus memastikan bahwa penerima yang dituju sudah ada dalam daftar teman pengguna sebelum menggunakan resolver kustom sendEmail.
Misalkan di aplikasi Anda, data daftar teman disimpan di 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!
}
Anda dapat menulis mutasi yang pertama-tama mengkueri Cloud SQL untuk memastikan bahwa pengirim ada dalam daftar teman penerima sebelum menggunakan resolver kustom untuk mengirim email:
# 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!")
}
Selain itu, contoh ini juga mengilustrasikan bahwa sumber data dalam konteks resolver kustom dapat mencakup resource selain database dan sistem serupa. Dalam contoh ini, sumber datanya adalah layanan pengiriman email cloud.
Memastikan eksekusi berurutan menggunakan mutasi
Saat menggabungkan sumber data, Anda sering kali perlu memastikan bahwa permintaan ke satu sumber data selesai sebelum membuat permintaan ke sumber data lain. Misalnya, misalkan Anda memiliki kueri yang secara dinamis mentranskripsikan video sesuai permintaan menggunakan AI API. Panggilan API seperti ini bisa mahal, jadi Anda ingin membatasi panggilan di balik beberapa kriteria, seperti pengguna memiliki video, atau pengguna telah membeli kredit premium di aplikasi Anda.
Upaya pertama untuk mencapai hal ini mungkin terlihat seperti ini:
# 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)
}
}
Pendekatan ini tidak akan berfungsi karena urutan eksekusi kolom kueri tidak dijamin; server GraphQL berharap dapat menyelesaikan kolom dalam urutan apa pun, untuk memaksimalkan konkurensi. Di sisi lain, kolom mutasi selalu diselesaikan secara berurutan, karena server GraphQL berharap beberapa kolom mutasi mungkin memiliki efek samping saat diselesaikan.
Meskipun langkah pertama operasi contoh tidak memiliki efek samping, Anda dapat menentukan operasi sebagai mutasi untuk memanfaatkan fakta bahwa kolom mutasi diselesaikan secara berurutan:
# 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)
}
}
Batasan
Fitur resolver kustom dirilis sebagai pratinjau publik eksperimental. Perhatikan batasan saat ini berikut:
Tidak ada ekspresi CEL dalam argumen resolver kustom
Anda tidak dapat menggunakan ekspresi CEL secara dinamis dalam argumen ke resolver kustom. Misalnya, hal berikut tidak mungkin dilakukan:
mutation UpdateMyProfile($newName: String!) @auth(level: USER) {
updateMongoDocument(
collection: "profiles"
# This isn't supported:
id_expr: "auth.uid"
update: { name: $newName }
)
}
Sebagai gantinya, teruskan variabel standar (misalnya, $authUid) dan validasi di tingkat operasi menggunakan direktif @auth(expr: ...) yang dievaluasi dengan aman.
mutation UpdateMyProfile(
$newName: String!, $authUid: String!
) @auth(expr: "vars.authUid == auth.uid") {
updateMongoDocument(
collection: "profiles"
id: $authUid
update: { name: $newName }
)
}
Solusi alternatif lainnya adalah memindahkan semua logika ke resolver kustom dan menyelesaikan semua operasi data dari Cloud Functions.
Misalnya, pertimbangkan contoh ini, yang saat ini tidak akan berfungsi:
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.
)
}
Sebagai gantinya, pindahkan kueri Cloud SQL dan panggilan ke layanan email ke dalam satu kolom mutasi, yang didukung oleh fungsi:
mutation ForwardToEmail($chatMessageId: UUID!) @auth(level: USER_EMAIL_VERIFIED) {
forwardChatToEmail(
chatMessageId: $chatMessageId
)
}
Buat SDK admin untuk database Anda dan gunakan dalam fungsi untuk menjalankan kueri 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);
Tidak ada jenis objek input dalam parameter resolver kustom
Resolver kustom tidak menerima jenis input GraphQL yang kompleks. Parameter harus berupa jenis skalar dasar (String, Int, Date, Any, dll.) dan 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
}
Resolver kustom tidak dapat mendahului operasi SQL
Dalam mutasi, menempatkan resolver kustom sebelum operasi SQL standar akan menghasilkan error. Semua operasi berbasis SQL harus muncul sebelum pemanggilan resolver kustom.
Tidak ada transaksi (@transaction)
Resolver kustom tidak dapat digabungkan di dalam blok @transaction dengan operasi SQL standar. Jika Cloud Function yang mendukung resolver gagal setelah penyisipan SQL berhasil, database tidak akan otomatis di-roll back.
Untuk mencapai keamanan transaksional antara SQL dan sumber data lain, pindahkan logika operasi SQL ke dalam Cloud Function, dan tangani validasi serta rollback menggunakan Admin SDK atau koneksi SQL langsung.