Создавая собственные резолверы, вы можете расширить функциональность Firebase SQL Connect , добавив поддержку других источников данных помимо Cloud SQL . Затем вы можете объединить несколько источников данных ( Cloud SQL и источники данных, предоставляемые вашими пользовательскими резолверами) в один запрос или мутацию.
Понятие «источник данных» является гибким. Оно включает в себя:
- Помимо Cloud SQL , можно использовать и другие базы данных, такие как Cloud Firestore, MongoDB и другие.
- Сервисы хранения данных, такие как Cloud Storage, AWS S3 и другие.
- Любая интеграция на основе API, например, Stripe, SendGrid, Salesforce и другие.
- Пользовательская бизнес-логика.
После того как вы напишете пользовательские обработчики для поддержки дополнительных источников данных, ваши запросы и мутации SQL Connect смогут комбинировать их различными способами, предоставляя такие преимущества, как:
- Единый уровень авторизации для ваших источников данных. Например, авторизуйте доступ к файлам в Cloud Storage, используя данные, хранящиеся в Cloud SQL .
- Типобезопасные клиентские SDK для веб-приложений, Android и iOS.
- Запросы, возвращающие данные из нескольких источников.
- Ограничение на вызовы функций в зависимости от состояния вашей базы данных.
Предварительные требования
Для написания собственных пользовательских обработчиков событий вам потребуется следующее:
- Firebase CLI версии 15.9.0 или выше.
- Firebase Functions SDK версии 7.1.0 или выше
Кроме того, вам следует ознакомиться с написанием функций с использованием Cloud Functions for Firebase , именно так вы будете реализовывать логику ваших пользовательских резолверов.
Прежде чем начать
У вас уже должен быть настроен проект для использования SQL Connect .
Если вы еще этого не сделали, вы можете воспользоваться одним из руководств по быстрой настройке:
Напишите собственные обработчики событий
В общих чертах, написание пользовательского резолвера состоит из трех частей: во-первых, определение схемы для вашего пользовательского резолвера; во-вторых, реализация ваших резолверов с использованием Cloud Functions; и, наконец, использование полей вашего пользовательского резолвера в запросах и мутациях, возможно, в сочетании с Cloud SQL или другими пользовательскими резолверами.
Следуйте инструкциям в следующих разделах, чтобы узнать, как это сделать. В качестве примера предположим, что у вас есть общедоступная информация профилей пользователей, хранящаяся вне Cloud SQL . Точное хранилище данных в этих примерах не указано, но это может быть что-то вроде Cloud Storage, экземпляр MongoDB или что-то еще.
В следующих разделах будет продемонстрирована базовая реализация пользовательского обработчика запросов, который может передавать информацию из внешнего профиля в SQL Connect .
Определите схему для вашего пользовательского обработчика запросов.
В каталоге вашего проекта Firebase выполните следующую команду:
firebase init dataconnect:resolverИнтерфейс командной строки Firebase запросит у вас имя для вашего пользовательского резолвера и спросит, следует ли генерировать примеры реализаций резолвера на 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 . Позже вы будете использовать эти операции для автоматической генерации пользовательского клиентского 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 } }Эта мутация записывает новый набор данных профиля в хранилище данных, снова используя ваш пользовательский обработчик запросов. Обратите внимание, что схема использует директиву
@authSQL Connect , чтобы гарантировать, что пользователи могут обновлять только свои собственные профили. Поскольку вы получаете доступ к своему хранилищу данных через SQL Connect , вы автоматически можете воспользоваться такими функциями SQL Connect , как эта.
В приведенных выше примерах вы определили операции SQL Connect , которые обращаются к данным из вашего хранилища данных с помощью ваших пользовательских резолверов. Однако ваши операции не ограничены доступом к данным только из Cloud SQL или из одного пользовательского источника данных. См. раздел «Примеры» для получения информации о более сложных вариантах использования, объединяющих данные из нескольких источников.
Прежде чем продолжить, перейдите к следующему разделу, чтобы увидеть ваши пользовательские обработчики событий в действии.
Разверните свой собственный механизм разрешения зависимостей и операции.
Как и при внесении любых изменений в схемы SQL Connect , для того чтобы они вступили в силу, необходимо их развернуть. Прежде чем это сделать, сначала разверните пользовательскую логику разрешения имен, которую вы реализовали с помощью Cloud Functions:
firebase deploy --only functionsТеперь вы можете развернуть обновленные схемы и операции:
firebase deploy --only dataconnectПосле внесения изменений в схемы SQL Connect необходимо также сгенерировать новые клиентские SDK:
firebase dataconnect:sdk:generateПримеры
Эти примеры демонстрируют, как реализовать некоторые более сложные сценарии использования и как избежать распространенных ошибок.
Авторизация доступа к пользовательскому обработчику запросов с использованием данных из Cloud SQL
Одним из преимуществ интеграции источников данных в SQL 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
}
Это определение подкрепляется облачной функцией, например, следующей:
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!")
}
В качестве дополнительного замечания, этот пример также иллюстрирует, что источник данных в контексте пользовательских резолверов может включать ресурсы, отличные от баз данных и подобных систем. В этом примере источником данных является облачный сервис отправки электронной почты.
Обеспечение последовательного выполнения с помощью мутаций.
При объединении источников данных часто необходимо убедиться, что запрос к одному источнику данных завершен, прежде чем отправлять запрос к другому. Например, предположим, у вас есть запрос, который динамически расшифровывает видео по запросу с помощью 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
)
}
Сгенерируйте 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-запроса, база данных не будет автоматически откатываться.
Для обеспечения транзакционной безопасности между SQL и другим источником данных перенесите логику операций SQL внутрь облачной функции, а проверку и откат обрабатывайте с помощью Admin SDK или прямых SQL-подключений.