Pisząc niestandardowe moduły rozpoznawania, możesz rozszerzyć Firebase SQL Connect, aby obsługiwał inne źródła danych oprócz Cloud SQL. Możesz wtedy połączyć wiele źródeł danych (Cloud SQL i źródła danych udostępniane przez niestandardowe moduły rozpoznawania) w jedno zapytanie lub mutację.
Pojęcie „źródło danych” jest elastyczne. Obejmuje ono:
- bazy danych inne niż Cloud SQL, takie jak Cloud Firestore, MongoDB i inne;
- usługi przechowywania danych, takie jak Cloud Storage, AWS S3 i inne;
- dowolną integrację opartą na interfejsie API, taką jak Stripe, SendGrid, Salesforce i inne;
- niestandardową logikę biznesową.
Gdy napiszesz niestandardowe moduły rozpoznawania, aby obsługiwały dodatkowe źródła danych, Twoje SQL Connect zapytania i mutacje mogą je łączyć na wiele sposobów, co daje takie korzyści jak:
- ujednolicona warstwa autoryzacji dla źródeł danych; na przykład autoryzowanie dostępu do plików w Cloud Storage za pomocą danych przechowywanych w Cloud SQL;
- bezpieczne pod względem typów pakiety SDK klienta na potrzeby internetu, Androida i iOS;
- zapytania, które zwracają dane z wielu źródeł;
- wywołania funkcji ograniczone na podstawie stanu bazy danych.
Wymagania wstępne
Aby napisać własne niestandardowe moduły rozpoznawania, potrzebujesz tych elementów:
- wiersz poleceń Firebase w wersji 15.9.0 lub nowszej;
- pakiet SDK Funkcji Firebase w wersji 7.1.0 lub nowszej.
Ponadto musisz umieć pisać funkcje za pomocą Cloud Functions dla Firebase, ponieważ w ten sposób będziesz implementować logikę niestandardowych modułów rozpoznawania.
Zanim zaczniesz
Musisz mieć już skonfigurowany projekt do korzystania z SQL Connect.
Jeśli nie masz jeszcze skonfigurowanego projektu, możesz skorzystać z jednego z tych przewodników dla początkujących:
Pisanie niestandardowych modułów rozpoznawania
Pisanie niestandardowego modułu rozpoznawania składa się z 3 części: najpierw musisz zdefiniować a schemat niestandardowego modułu rozpoznawania, potem zaimplementować moduły rozpoznawania za pomocą Cloud Functions, a na koniec użyć pól niestandardowego modułu rozpoznawania w zapytaniach i mutacjach, być może w połączeniu z Cloud SQL lub innymi niestandardowymi modułami rozpoznawania.
Aby dowiedzieć się, jak to zrobić, wykonaj czynności opisane w kilku następnych sekcjach. Załóżmy, że informacje o profilu publicznym użytkowników są przechowywane poza Cloud SQL. W tych przykładach nie określono dokładnego magazynu danych, ale może to być np. Cloud Storage, instancja MongoDB lub cokolwiek innego.
W kolejnych sekcjach pokażemy szkieletową implementację niestandardowego modułu rozpoznawania, który może przenieść te zewnętrzne informacje o profilu do SQL Connect.
Definiowanie schematu niestandardowego modułu rozpoznawania
W katalogu projektu w Firebase uruchom to polecenie:
firebase init dataconnect:resolverWiersz poleceń Firebase poprosi Cię o nazwę niestandardowego modułu rozpoznawania i zapyta, czy chcesz wygenerować przykładowe implementacje modułów rozpoznawania w TypeScript lub JavaScript. Jeśli korzystasz z tego przewodnika, zaakceptuj domyślną nazwę i wygeneruj przykłady w TypeScript.
Narzędzie utworzy pusty plik
dataconnect/schema_resolver/schema.gqli doda konfigurację nowego modułu rozpoznawania do plikudataconnect.yaml.Zaktualizuj plik
schema.gqlza pomocą schematu GraphQL, który definiuje zapytania i mutacje, jakie będzie udostępniać niestandardowy moduł rozpoznawania. Oto na przykład schemat niestandardowego modułu rozpoznawania, który może pobierać i aktualizować profil publiczny użytkownika przechowywany w magazynie danych innym niż 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 }
Implementowanie logiki niestandardowego modułu rozpoznawania
Następnie zaimplementuj moduły rozpoznawania za pomocą Cloud Functions. W tle będziesz tworzyć serwer GraphQL, ale Cloud Functions ma metodę pomocniczą onGraphRequest, która obsługuje szczegóły tego procesu, więc musisz tylko napisać logikę modułu rozpoznawania, która uzyskuje dostęp do źródła danych.
Otwórz plik
functions/src/index.ts.Gdy powyżej uruchomisz polecenie
firebase init dataconnect:resolver, utworzy ono ten katalog kodu źródłowego Cloud Functions i zainicjuje go przykładowym kodem w plikuindex.ts.Dodaj te definicje:
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);
Te szkieletowe implementacje pokazują ogólny kształt, jaki musi mieć funkcja modułu rozpoznawania. Aby utworzyć w pełni działający niestandardowy moduł rozpoznawania, musisz wypełnić sekcje z komentarzami kodem, który odczytuje i zapisuje dane w źródle danych.
Używanie niestandardowych modułów rozpoznawania w zapytaniach i mutacjach
Teraz, gdy masz już zdefiniowany schemat niestandardowego modułu rozpoznawania i zaimplementowaną logikę, która go obsługuje, możesz używać niestandardowego modułu rozpoznawania w swoich SQL Connect zapytaniach i mutacjach. Później użyjesz tych operacji do automatycznego wygenerowania niestandardowego pakietu SDK klienta, za pomocą którego będziesz mieć dostęp do wszystkich danych, niezależnie od tego, czy są one przechowywane w Cloud SQL, niestandardowych modułach rozpoznawania czy w obu tych miejscach.
W pliku
dataconnect/example/queries.gqldodaj tę definicję:query GetPublicProfile($id: String!) @auth(level: PUBLIC, insecureReason: "Anyone can see a public profile.") { publicProfile(userId: $id) { name photoUrl bioLine } }To zapytanie pobiera profil publiczny użytkownika za pomocą niestandardowego modułu rozpoznawania.
W pliku
dataconnect/example/mutations.gqldodaj tę definicję: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 } }Ta mutacja zapisuje nowy zestaw danych profilu w magazynie danych, ponownie za pomocą niestandardowego modułu rozpoznawania. Pamiętaj, że schemat korzysta z SQL Connect's
@authdyrektywy, aby zapewnić, że użytkownicy mogą aktualizować tylko własne profile. Ponieważ uzyskujesz dostęp do magazynu danych za pomocą SQL Connect, możesz automatycznie korzystać z SQL Connect funkcji, takich jak ta.
W powyższych przykładach zdefiniowano operacje SQL Connect, które uzyskują dostęp do danych z magazynu danych za pomocą niestandardowych modułów rozpoznawania. Nie musisz jednak ograniczać się do uzyskiwania dostępu do danych z Cloud SQL lub z jednego niestandardowego źródła danych. Więcej zaawansowanych przypadków użycia, które łączą dane z wielu źródeł, znajdziesz w sekcji Przykłady.
Zanim to zrobisz, przejdź do następnej sekcji, aby zobaczyć, jak działają niestandardowe moduły rozpoznawania.
Wdrażanie niestandardowego modułu rozpoznawania i operacji
Podobnie jak w przypadku wprowadzania zmian w schematach SQL Connect, musisz je wdrożyć, aby zaczęły obowiązywać. Zanim to zrobisz, wdróż logikę niestandardowego modułu rozpoznawania, którą zaimplementowano za pomocą Cloud Functions:
firebase deploy --only functionsTeraz możesz wdrożyć zaktualizowane schematy i operacje:
firebase deploy --only dataconnectPo wprowadzeniu zmian w schematach SQL Connect musisz też wygenerować nowe pakiety SDK klienta:
firebase dataconnect:sdk:generatePrzykłady
Te przykłady pokazują, jak implementować bardziej zaawansowane przypadki użycia i jak unikać typowych pułapek.
Autoryzowanie dostępu do niestandardowego modułu rozpoznawania za pomocą danych z Cloud SQL
Jedną z korzyści integrowania źródeł danych z SQL Connect za pomocą niestandardowych modułów rozpoznawania jest to, że możesz pisać operacje, które łączą źródła danych.
Załóżmy, że tworzysz aplikację społecznościową i masz mutację zaimplementowaną jako niestandardowy moduł rozpoznawania, który wysyła e-maila z ponagleniem do znajomego użytkownika, jeśli nie miał z nim kontaktu od dłuższego czasu.
Aby zaimplementować funkcję przypomnienia, utwórz niestandardowy moduł rozpoznawania ze schematem takim jak ten:
# A GraphQL server must define a root query type per the spec.
type Query {
unused: String
}
type Mutation {
sendEmail(id: String!, content: String): Boolean
}
Ta definicja jest obsługiwana przez Cloud Function, np. taką jak ta:
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);
Wysyłanie e-maili jest kosztowne i może być wykorzystywane do nadużyć, dlatego przed użyciem niestandardowego modułu rozpoznawania sendEmail musisz się upewnić, że odbiorca jest już na liście znajomych użytkownika.
Załóżmy, że w Twojej aplikacji dane listy znajomych są przechowywane w 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!
}
Możesz napisać mutację, która najpierw wysyła zapytanie do Cloud SQL, aby sprawdzić, czy nadawca znajduje się na liście znajomych odbiorcy, a potem używa niestandardowego modułu rozpoznawania do wysłania e-maila:
# 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!")
}
Na marginesie warto dodać, że ten przykład pokazuje też, że źródło danych w kontekście niestandardowych modułów rozpoznawania może obejmować zasoby inne niż bazy danych i podobne systemy. W tym przykładzie źródłem danych jest usługa wysyłania e-maili w chmurze.
Zapewnianie sekwencyjnego wykonywania za pomocą mutacji
Podczas łączenia źródeł danych często trzeba się upewnić, że żądanie do jednego źródła danych zostanie zrealizowane, zanim zostanie wysłane żądanie do innego źródła danych. Załóżmy na przykład, że masz zapytanie, które dynamicznie transkrybuje film na żądanie za pomocą interfejsu API AI. Wywołanie interfejsu API może być kosztowne, dlatego musisz ograniczyć je do określonych kryteriów, np. użytkownik jest właścicielem filmu lub kupił w aplikacji kredyty premium.
Pierwsza próba osiągnięcia tego celu może wyglądać tak:
# 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)
}
}
To podejście nie zadziała, ponieważ kolejność wykonywania pól zapytania nie jest gwarantowana. Serwer GraphQL oczekuje, że będzie mógł rozpoznawać pola w dowolnej kolejności, aby zmaksymalizować współbieżność. Z drugiej strony pola mutacji są zawsze rozpoznawane w kolejności, ponieważ serwer GraphQL oczekuje, że niektóre pola mutacji mogą mieć skutki uboczne po rozpoznaniu.
Mimo że pierwszy krok przykładowej operacji nie ma skutków ubocznych, możesz zdefiniować operację jako mutację, aby skorzystać z faktu, że pola mutacji są rozpoznawane w kolejności:
# 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)
}
}
Ograniczenia
Funkcja niestandardowych modułów rozpoznawania jest udostępniana w ramach eksperymentalnej publicznej wersji przedpremierowej. Pamiętaj o tych aktualnych ograniczeniach:
Brak wyrażeń CEL w argumentach niestandardowego modułu rozpoznawania
Nie możesz używać wyrażeń CEL dynamicznie w argumentach niestandardowego modułu rozpoznawania. Na przykład nie jest to możliwe:
mutation UpdateMyProfile($newName: String!) @auth(level: USER) {
updateMongoDocument(
collection: "profiles"
# This isn't supported:
id_expr: "auth.uid"
update: { name: $newName }
)
}
Zamiast tego przekazuj standardowe zmienne (np. $authUid) i weryfikuj je na poziomie operacji za pomocą bezpiecznie ocenianej dyrektywy @auth(expr: ...).
mutation UpdateMyProfile(
$newName: String!, $authUid: String!
) @auth(expr: "vars.authUid == auth.uid") {
updateMongoDocument(
collection: "profiles"
id: $authUid
update: { name: $newName }
)
}
Innym obejściem jest przeniesienie całej logiki do niestandardowego modułu rozpoznawania i wykonanie wszystkich operacji na danych z Cloud Functions.
Rozważ na przykład ten przykład, który obecnie nie będzie działać:
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.
)
}
Zamiast tego przenieś zarówno zapytanie Cloud SQL, jak i wywołanie usługi e-mail do jednego pola mutacji obsługiwanego przez funkcję:
mutation ForwardToEmail($chatMessageId: UUID!) @auth(level: USER_EMAIL_VERIFIED) {
forwardChatToEmail(
chatMessageId: $chatMessageId
)
}
Wygeneruj pakiet Admin SDK dla bazy danych i użyj go w funkcji do wykonania zapytania 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);
Brak typów obiektów wejściowych w parametrach niestandardowego modułu rozpoznawania
Niestandardowe moduły rozpoznawania nie akceptują złożonych typów wejściowych GraphQL. Parametry muszą być podstawowymi typami skalarnymi (String, Int, Date, Any itp.) i typami 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
}
Niestandardowe moduły rozpoznawania nie mogą poprzedzać operacji SQL
Umieszczenie niestandardowego modułu rozpoznawania przed standardowymi operacjami SQL w mutacji powoduje błąd. Wszystkie operacje oparte na SQL muszą występować przed wywołaniami niestandardowych modułów rozpoznawania.
Brak transakcji (@transaction)
Niestandardowych modułów rozpoznawania nie można umieszczać w bloku @transaction ze standardowymi operacjami SQL. Jeśli funkcja w Cloud Functions obsługująca resolver ulegnie awarii po pomyślnym wstawieniu SQL, baza danych nie zostanie automatycznie wycofana.
Aby zapewnić bezpieczeństwo transakcyjne między SQL a innym źródłem danych, przenieś logikę operacji SQL do Cloud Function i obsługuj weryfikację oraz wycofywanie zmian za pomocą pakietu Admin SDK lub bezpośrednich połączeń SQL.