من خلال كتابة أدوات حلّ مخصّصة، يمكنك توسيع نطاق Firebase SQL Connect ليتوافق مع مصادر بيانات أخرى بالإضافة إلى Cloud SQL. يمكنك بعد ذلك دمج مصادر بيانات متعددة (Cloud SQL ومصادر البيانات التي توفّرها أدوات الحلّ المخصّصة ) في طلب بحث أو تعديل واحد.
إنّ مفهوم "مصدر البيانات" مرن. ويشمل ما يلي:
- قواعد البيانات بخلاف Cloud SQL، مثل Cloud Firestore و MongoDB و غيرها
- خدمات التخزين مثل Cloud Storage وAWS S3 وغيرها
- أي عملية دمج مستندة إلى واجهة برمجة التطبيقات، مثل Stripe وSendGrid وSalesforce وغيرها
- المنطق التجاري المخصّص
بعد كتابة أدوات حلّ مخصّصة لدعم مصادر البيانات الإضافية، يمكن أن تجمع طلبات البحث والتعديلات في SQL Connect بينها بعدة طرق، ما يوفّر مزايا مثل:
- طبقة تفويض موحّدة لمصادر البيانات على سبيل المثال، يمكنك منح إذن الوصول إلى الملفات في Cloud Storage باستخدام البيانات المخزّنة في Cloud SQL.
- حِزم SDK للعملاء آمنة من ناحية النوع للويب وAndroid وiOS
- طلبات البحث التي تعرض بيانات من مصادر متعددة
- عمليات استدعاء الدوال المقيدة استنادًا إلى حالة قاعدة البيانات
المتطلبات الأساسية
لكتابة أدوات الحلّ المخصّصة، تحتاج إلى ما يلي:
- الإصدار 15.9.0 أو إصدار أحدث من Firebase CLI
- الإصدار 7.1.0 أو إصدار أحدث من حزمة تطوير البرامج (SDK) لوظائف Firebase
بالإضافة إلى ذلك، يجب أن تكون على دراية بكتابة الدوال باستخدام وظائف Firebase السحابية، وهي الطريقة التي ستنفّذ بها منطق أدوات الحلّ المخصّصة.
قبل البدء
يجب أن يكون لديك مشروع تم إعداده لاستخدام SQL Connect.
يمكنك اتّباع أحد أدلة البدء السريع لإعداد مشروعك إذا لم تكن قد فعلت ذلك بعد:
كتابة أدوات حلّ مخصّصة
على مستوى عالٍ، تتألف كتابة أداة حلّ مخصّصة من ثلاثة أجزاء: أولاً، تحديد مخطط لأداة الحلّ المخصّصة، وثانيًا، تنفيذ أدوات الحلّ باستخدام Cloud Functions، وأخيرًا، استخدام حقول أداة الحلّ المخصّصة في طلبات البحث والتعديلات، ربما بالتزامن مع Cloud SQL أو أدوات حلّ مخصّصة أخرى.
اتّبِع الخطوات الواردة في الأقسام القليلة التالية للتعرّف على كيفية إجراء ذلك. كمثال توضيحي ، لنفترض أنّ لديك معلومات ملف شخصي عام لمستخدميك مخزّنة خارج Cloud SQL. لم يتم تحديد مخزن البيانات الدقيق في هذه الأمثلة، ولكن يمكن أن يكون شيئًا مثل Cloud Storage أو مثيل MongoDB أو أي شيء آخر.
ستوضّح الأقسام التالية نموذجًا أوليًا لتنفيذ أداة حلّ مخصّصة يمكنها جلب معلومات الملف الشخصي الخارجية هذه إلى SQL Connect.
تحديد المخطط لأداة الحلّ المخصّصة
في دليل مشروع Firebase، شغِّل الأمر التالي:
firebase init dataconnect:resolverسيطلب منك Firebase 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 لاحقًا، ستستخدم هذه العمليات لإنشاء حزمة 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 } }تكتب عملية التعديل هذه مجموعة جديدة من بيانات الملف الشخصي في مخزن البيانات، باستخدام أداة الحلّ المخصّصة أيضًا. يُرجى العِلم أنّ المخطط يستخدم SQL Connect's
@authتوجيه لضمان ألا يتمكّن المستخدمون إلا من تعديل ملفاتهم الشخصية. بما أنّك تصل إلى مخزن البيانات من خلال 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
}
يستند هذا التعريف إلى Cloud Function، مثل ما يلي:
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!")
}
على سبيل المثال، يوضّح هذا المثال أيضًا أنّ مصدر البيانات في سياق أدوات الحلّ المخصّصة يمكن أن يشمل موارد أخرى غير قواعد البيانات والأنظمة المشابهة. في هذا المثال، مصدر البيانات هو خدمة إرسال الرسائل الإلكترونية في السحابة الإلكترونية.
ضمان التنفيذ المتسلسل باستخدام عمليات التعديل
عند دمج مصادر البيانات، ستحتاج غالبًا إلى التأكّد من اكتمال طلب إلى مصدر بيانات واحد قبل تقديم طلب إلى مصدر بيانات مختلف. على سبيل المثال، لنفترض أنّ لديك طلب بحث ينسخ فيديو حسب الطلب ديناميكيًا باستخدام واجهة برمجة تطبيقات للذكاء الاصطناعي. يمكن أن يكون طلب بيانات من واجهة برمجة التطبيقات من هذا النوع مكلفًا، لذا عليك تقييد الاستدعاء بمعايير معيّنة، مثل أن يكون المستخدم مالكًا للفيديو أو أن يكون قد اشترى نوعًا من أرصدة Premium في تطبيقك.
قد تبدو المحاولة الأولى لتحقيق ذلك على النحو التالي:
# 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 العادية. إذا تعذّر تنفيذ Cloud Function التي تدعم أداة الحلّ بعد نجاح عملية إدراج SQL، لن يتم تلقائيًا التراجع عن قاعدة البيانات.
لتحقيق الأمان على مستوى المعاملات بين SQL ومصدر بيانات آخر، يمكنك نقل منطق عملية SQL داخل Cloud Function، والتعامل مع عمليات التحقّق والتراجُع باستخدام Admin SDK أو اتصالات SQL المباشرة.