Pisząc niestandardowe moduły rozpoznawania, możesz rozszerzyć Firebase Data Connect, aby obsługiwała inne źródła danych oprócz Cloud SQL. Możesz następnie 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 ona:
- bazy danych inne niż Cloud SQL, takie jak Cloud Firestore, MongoDB i inne.
- usługi pamięci masowej, takie jak Cloud Storage, AWS S3 i inne;
- Każda integracja oparta na interfejsie API, np. Stripe, SendGrid, Salesforce i inne.
- niestandardową logikę biznesową,
Po napisaniu niestandardowych funkcji rozpoznawania, które obsługują dodatkowe źródła danych, zapytania i mutacje Data Connect 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.
- Pakiety SDK po stronie klienta z bezpiecznym typowaniem na potrzeby internetu, Androida i iOS.
- Zapytania, które zwracają dane z wielu źródeł.
- Ograniczenie wywołań funkcji na podstawie stanu bazy danych.
Wymagania wstępne
Aby napisać własne niestandardowe funkcje rozpoznawania, musisz mieć:
- Wiersz poleceń Firebase w wersji 15.9.0 lub nowszej
- Pakiet SDK Funkcji Firebase w wersji 7.1.0 lub nowszej
Musisz też umieć pisać funkcje za pomocą Cloud Functions dla Firebase, czyli narzędzia, które umożliwia implementowanie logiki niestandardowych funkcji rozpoznawania.
Zanim zaczniesz
Aby korzystać z usługi Data Connect, musisz mieć skonfigurowany projekt.
Jeśli nie masz jeszcze konta, możesz skorzystać z jednego z tych krótkich przewodników:
Pisanie niestandardowych funkcji rozpoznawania
Ogólnie rzecz biorąc, pisanie niestandardowego rozwiązania ma 3 części: najpierw należy zdefiniować schemat niestandardowego rozwiązania, potem zaimplementować rozwiązania za pomocą Cloud Functions, a na koniec użyć pól niestandardowego rozwiązania w zapytaniach i mutacjach, być może w połączeniu z Cloud SQL lub innymi niestandardowymi rozwiązaniami.
W kolejnych sekcjach dowiesz się, jak to zrobić. Załóżmy, że masz publiczne informacje o profilach użytkowników 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 sekcjach poniżej znajdziesz szkieletową implementację niestandardowego modułu rozpoznawania, który może wprowadzać zewnętrzne informacje o profilu do Data Connect.
Określ schemat niestandardowego modułu rozpoznawania
W katalogu projektu w Firebase uruchom:
firebase init dataconnect:resolverWiersz poleceń Firebase poprosi Cię o podanie nazwy niestandardowego modułu rozpoznawania i zapyta, czy chcesz wygenerować przykładowe implementacje modułu rozpoznawania w języku 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 do niego nową konfigurację usługi rozpoznawania nazw.dataconnect.yamlZaktualizuj ten plik
schema.gqlza pomocą schematu GraphQL, który definiuje zapytania i mutacje, jakie będzie udostępniać Twój 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 }
Wdrażanie logiki niestandardowego narzędzia do rozpoznawania
Następnie zaimplementuj moduły rozpoznawania za pomocą Cloud Functions. W tle utworzysz serwer GraphQL, ale Cloud Functions ma metodę pomocniczą onGraphRequest, która obsługuje szczegóły tego procesu, więc musisz tylko napisać logikę funkcji rozpoznającej, 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 katalog kodu źródłowego Cloud Functions i zainicjuje go przykładowym kodem windex.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 przyjąć funkcja rozpoznawania. Aby utworzyć w pełni funkcjonalny niestandardowy resolver, musisz wypełnić sekcje z komentarzami kodem, który odczytuje dane ze źródła danych i zapisuje w nim informacje.
Używanie niestandardowych funkcji rozpoznawania w zapytaniach i mutacjach
Po zdefiniowaniu schematu niestandardowego modułu rozpoznawania i zaimplementowaniu logiki, która go obsługuje, możesz używać tego modułu w Data Connectzapytaniach i mutacjach. Później użyjesz tych operacji do automatycznego wygenerowania niestandardowego pakietu SDK klienta, za pomocą którego uzyskasz dostęp do wszystkich danych, niezależnie od tego, czy są one przechowywane w Cloud SQL, niestandardowych resolverach 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 publiczny profil 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 w DataStore nowy zestaw danych profilowych, ponownie używając niestandardowego modułu rozpoznawania. Pamiętaj, że schemat korzysta z dyrektywy Data Connect's
@auth, aby zapewnić, że użytkownicy mogą aktualizować tylko własne profile. Ponieważ uzyskujesz dostęp do magazynu danych za pomocą Data Connect, możesz automatycznie korzystać z funkcji Data Connect, takich jak ta.
W powyższych przykładach zdefiniowano operacje Data Connect, które uzyskują dostęp do danych z magazynu danych za pomocą niestandardowych funkcji rozpoznawania. Nie musisz jednak ograniczać się do uzyskiwania dostępu do danych z Cloud SQL ani 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ć działanie niestandardowych resolverów.
Wdrażanie niestandardowego narzędzia do rozpoznawania i operacji
Podobnie jak w przypadku innych zmian w Data Connect schematach, musisz je wdrożyć, aby zaczęły obowiązywać. Zanim to zrobisz, wdróż najpierw niestandardową logikę rozwiązywania nazw, 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 Data Connect musisz też wygenerować nowe pakiety SDK klienta:
firebase dataconnect:sdk:generatePrzykłady
Te przykłady pokazują, jak wdrożyć bardziej zaawansowane przypadki użycia i jak uniknąć typowych błędów.
Autoryzowanie dostępu do niestandardowego narzędzia do rozpoznawania nazw za pomocą danych z Cloud SQL
Jedną z zalet integrowania źródeł danych z Data Connectza pomocą niestandardowych funkcji rozpoznawania jest możliwość pisania operacji, które łączą źródła danych.
Załóżmy, że tworzysz aplikację społecznościową i masz mutację zaimplementowaną jako niestandardowy resolver, który wysyła e-maila z przypomnieniem do znajomego użytkownika, jeśli nie kontaktował się z nim od jakiegoś czasu.
Aby wdrożyć funkcję przypomnienia, utwórz niestandardowy moduł rozpoznawania z schematem podobnym do tego:
# 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 funkcję w Cloud Functions, np. taką:
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 sendEmailniestandardowego narzędzia do rozpoznawania musisz mieć pewność, że zamierzony odbiorca znajduje się 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 następnie 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!")
}
Przy okazji ten przykład pokazuje też, że źródło danych w kontekście niestandardowych funkcji 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 musisz mieć pewność, że żądanie wysłane do jednego źródła danych zostanie zrealizowane, zanim wyślesz żądanie do innego źródła. Załóżmy na przykład, że masz zapytanie, które dynamicznie transkrybuje film na żądanie za pomocą interfejsu AI API. Wywołanie interfejsu API, takie jak to, może być kosztowne, dlatego warto je ograniczyć za pomocą pewnych 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 możliwości rozwiązywania pól w dowolnej kolejności, aby zmaksymalizować współbieżność. Z drugiej strony pola mutacji są zawsze rozwiązywane w określonej kolejności, ponieważ serwer GraphQL oczekuje, że niektóre pola mutacji mogą mieć skutki uboczne podczas rozwiązywania.
Nawet jeśli pierwszy krok przykładowej operacji nie ma efektów ubocznych, możesz zdefiniować operację jako mutację, aby skorzystać z faktu, że pola mutacji są rozwiązywane w określonej 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 resolverów jest dostępna w eksperymentalnej publicznej wersji przedpremierowej. Pamiętaj o tych ograniczeniach:
Brak wyrażeń CEL w argumentach niestandardowego modułu rozpoznawania
Nie możesz używać wyrażeń CEL dynamicznie w argumentach niestandardowego narzędzia do rozwiązywania nazw. Na przykład nie można:
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 rozwiązaniem jest przeniesienie całej logiki do niestandardowego modułu rozpoznawania i wykonywanie wszystkich operacji na danych w Cloud Functions.
Weźmy na przykład ten kod, który obecnie nie 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ś zapytanie Cloud SQL 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 SDK administratora dla bazy danych i użyj go w funkcji, aby wykonać zapytanie SQL 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 funkcje rozpoznawania nie akceptują złożonych typów danych wejściowych GraphQL. Parametry muszą być podstawowymi typami skalarnymi (String, Int, Date, Any itp.) i 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
W mutacji umieszczenie niestandardowego modułu rozpoznawania przed standardowymi operacjami SQL powoduje błąd. Wszystkie operacje oparte na SQL muszą występować przed wywołaniami niestandardowych funkcji rozpoznawania.
Brak transakcji (@transaction)
Niestandardowych funkcji rozpoznawania nie można umieszczać w bloku @transaction ze standardowymi operacjami SQL. Jeśli funkcja Cloud Function obsługująca moduł rozpoznawania zakończy się niepowodzeniem po pomyślnym wstawieniu SQL, baza danych nie wycofa automatycznie zmian.
Aby zapewnić bezpieczeństwo transakcyjne między SQL a innym źródłem danych, przenieś logikę operacji SQL do funkcji w Cloud Functions i obsługuj weryfikację oraz wycofywanie zmian za pomocą pakietu Admin SDK lub bezpośrednich połączeń SQL.