توسيع نطاق Data Connect باستخدام أدوات تحليل مخصّصة

من خلال كتابة أدوات تحليل مخصّصة، يمكنك توسيع نطاق Firebase Data Connect ليشمل مصادر بيانات أخرى بالإضافة إلى Cloud SQL. يمكنك بعد ذلك دمج مصادر بيانات متعددة (Cloud SQL ومصادر البيانات التي توفّرها أدوات تحليل الطلبات المخصّصة) في طلب بحث أو تعديل واحد.

مفهوم "مصدر البيانات" مرن. ويشمل ذلك:

  • قواعد البيانات غير Cloud SQL، مثل Cloud Firestore وMongoDB وغيرها
  • خدمات التخزين، مثل Cloud Storage وAWS S3 وغيرها
  • أي عملية دمج مستندة إلى واجهة برمجة التطبيقات، مثل Stripe وSendGrid وSalesforce وغيرها
  • منطق مخصّص للنشاط التجاري

بعد كتابة أدوات تحليل مخصّصة لدعم مصادر البيانات الإضافية، يمكن لطلبات البحث وعمليات التعديل في Data Connect دمجها بعدة طرق، ما يوفّر مزايا مثل:

  • طبقة تفويض موحَّدة لمصادر البيانات على سبيل المثال، يمكنك منح إذن الوصول إلى الملفات في Cloud Storage باستخدام البيانات المخزَّنة في Cloud SQL.
  • حِزم تطوير البرامج (SDK) المتوافقة مع أنواع البيانات للويب وAndroid وiOS
  • الاستعلامات التي تعرض بيانات من مصادر متعددة
  • استدعاءات الدوال المقيدة استنادًا إلى حالة قاعدة البيانات

المتطلبات الأساسية

لكتابة أدوات تحليل مخصّصة، يجب توفُّر ما يلي:

  • الإصدار 15.9.0 من Firebase CLI أو إصدار أحدث
  • الإصدار 7.1.0 أو إصدار أحدث من حزمة تطوير البرامج (SDK) لوظائف Firebase

بالإضافة إلى ذلك، يجب أن تكون على دراية بكتابة الدوال باستخدام وظائف Firebase السحابية، وهي الطريقة التي ستنفّذ بها منطق أدوات تحليل الاسم المخصّصة.

قبل البدء

يجب أن يكون لديك مشروع تم إعداده مسبقًا لاستخدام Data Connect.

يمكنك اتّباع أحد أدلة البدء السريع لإعداد حسابك إذا لم يسبق لك ذلك:

كتابة أدوات تحليل مخصّصة

بشكل عام، يتضمّن إنشاء برنامج تعيين مخصّص ثلاثة أجزاء: أولاً، تحديد مخطط لبرنامج التعيين المخصّص، وثانيًا، تنفيذ برامج التعيين باستخدام "وظائف السحابة"، وأخيرًا، استخدام حقول برنامج التعيين المخصّص في طلبات البحث وعمليات التعديل، ربما بالتزامن مع Cloud SQL أو برامج تعيين مخصّصة أخرى.

اتّبِع الخطوات الواردة في الأقسام القليلة التالية لمعرفة كيفية إجراء ذلك. لنفترض مثلاً أنّ لديك معلومات ملف شخصي عام للمستخدمين مخزّنة خارج Cloud SQL. لم يتم تحديد مخزن البيانات الدقيق في هذه الأمثلة، ولكن يمكن أن يكون شيئًا مثل Cloud Storage أو مثيل MongoDB أو أي شيء آخر.

ستوضّح الأقسام التالية عملية تنفيذ أساسية لبرنامج تعيين مخصّص يمكنه نقل معلومات الملف الشخصي الخارجية إلى Data Connect.

تحديد المخطط الخاص بأداة تحليل الاسم المخصّصة

  1. في دليل مشروع Firebase الخاص بك، نفِّذ الأمر التالي:

    firebase init dataconnect:resolver

    سيطلب منك Firebase CLI إدخال اسم للمحلّل المخصّص، وسيسألك عمّا إذا كنت تريد إنشاء أمثلة على عمليات تنفيذ المحلّل في TypeScript أو JavaScript. إذا كنت تتّبع هذا الدليل، اقبل الاسم التلقائي وأنشئ أمثلة TypeScript.

    ستنشئ الأداة بعد ذلك ملف dataconnect/schema_resolver/schema.gql فارغًا وتضيف إعدادات برنامج التعيين الجديدة إلى الملف dataconnect.yaml.

  2. عدِّل ملف 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، تتولّى تفاصيل ذلك، لذا عليك فقط كتابة منطق محلّل الاسم الذي يصل إلى مصدر البيانات.

  1. افتح الملف functions/src/index.ts.

    عند تنفيذ الأمر firebase init dataconnect:resolver أعلاه، أنشأ هذا الأمر دليل الرمز المصدر الخاص بـ Cloud Functions هذا، وتمت تهيئته باستخدام رمز نموذجي في index.ts.

  2. أضِف التعريفات التالية:

    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);
    

تعرض عمليات التنفيذ الأساسية هذه الشكل العام الذي يجب أن تتخذه دالة محلِّل. لإنشاء أداة تحليل مخصّصة تعمل بكامل طاقتها، عليك ملء الأقسام التي تم التعليق عليها برمز يقرأ ويكتب إلى مصدر البيانات.

استخدام أدوات تحليل مخصّصة في طلبات البحث وعمليات التعديل

بعد تحديد مخطط أداة الحلّ المخصّصة وتنفيذ المنطق الذي يستند إليه، يمكنك استخدام أداة الحلّ المخصّصة في طلبات البحث وعمليات التعديل التي تتضمّن Data Connect. في وقت لاحق، ستستخدم هذه العمليات لإنشاء حزمة SDK مخصّصة للعميل تلقائيًا، ويمكنك استخدامها للوصول إلى جميع بياناتك، سواء كانت مخزّنة في Cloud SQL أو في أدوات تحليل مخصّصة أو في مزيج من الاثنين.

  1. في dataconnect/example/queries.gql، أضِف التعريف التالي:

    query GetPublicProfile($id: String!)
        @auth(level: PUBLIC, insecureReason: "Anyone can see a public profile.") {
      publicProfile(userId: $id) {
        name
        photoUrl
        bioLine
      }
    }
    

    يستردّ طلب البحث هذا الملف الشخصي العلني للمستخدم باستخدام أداة تحليل الطلبات المخصّصة.

  2. في 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
      }
    }
    

    تكتب عملية التغيير هذه مجموعة جديدة من بيانات الملف الشخصي إلى مخزن البيانات، وذلك باستخدام أداة تحليل مخصّصة. يُرجى العِلم أنّ المخطط يستخدِم توجيه @auth الخاص بـ Data Connect لضمان أنّه بإمكان المستخدمين تعديل ملفاتهم الشخصية فقط. بما أنّك تصل إلى مستودع البيانات من خلال Data Connect، يمكنك تلقائيًا الاستفادة من ميزات Data Connect، مثل هذه الميزة.

في الأمثلة أعلاه، حدّدت عمليات Data Connect تصل إلى البيانات من مخزن البيانات باستخدام برامج التعيين المخصّصة. ومع ذلك، لا تقتصر عملياتك على الوصول إلى البيانات من Cloud SQL أو من مصدر بيانات مخصّص واحد. اطّلِع على قسم الأمثلة للاطّلاع على بعض حالات الاستخدام الأكثر تقدّمًا التي تجمع البيانات من مصادر متعددة.

قبل ذلك، انتقِل إلى القسم التالي للاطّلاع على كيفية عمل أدوات تحليل الاسم المخصّصة.

تفعيل أداة تحليل الاسم المخصّصة والعمليات

كما هو الحال عند إجراء أي تغييرات على مخططات Data Connect، يجب نشرها لتطبيقها. قبل إجراء ذلك، عليك أولاً نشر منطق برنامج التعيين المخصّص الذي نفّذته باستخدام Cloud Functions:

firebase deploy --only functions

يمكنك الآن نشر المخططات والعمليات المعدَّلة:

firebase deploy --only dataconnect

بعد إجراء تغييرات على مخططات Data Connect، عليك أيضًا إنشاء حِزم SDK جديدة للعملاء:

firebase dataconnect:sdk:generate

أمثلة

توضّح هذه الأمثلة كيفية تنفيذ بعض حالات الاستخدام الأكثر تقدّمًا، وكيفية تجنُّب الأخطاء الشائعة.

منح إذن الوصول إلى أداة تحليل مخصّصة باستخدام بيانات من Cloud SQL

من مزايا دمج مصادر البيانات في Data 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، مثل ما يلي:

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!")
}

بالإضافة إلى ذلك، يوضّح هذا المثال أيضًا أنّ مصدر البيانات في سياق أدوات تحليل مخصّصة يمكن أن يتضمّن موارد أخرى غير قواعد البيانات والأنظمة المشابهة. في هذا المثال، مصدر البيانات هو خدمة إرسال البريد الإلكتروني على السحابة الإلكترونية.

ضمان التنفيذ التسلسلي باستخدام عمليات التغيير

عند دمج مصادر البيانات، عليك غالبًا التأكّد من اكتمال طلب إلى أحد مصادر البيانات قبل تقديم طلب إلى مصدر بيانات مختلف. على سبيل المثال، لنفترض أنّ لديك طلب بحث ينسخ بشكل ديناميكي فيديو عند الطلب باستخدام واجهة برمجة تطبيقات مستندة إلى الذكاء الاصطناعي. يمكن أن تكون طلبات البيانات من واجهة برمجة التطبيقات من هذا النوع مكلفة، لذا عليك وضع بعض المعايير قبل السماح بتنفيذ الطلب، مثل أن يملك المستخدم الفيديو أو أن يكون قد اشترى بعضًا من رصيد المحتوى المميز في تطبيقك.

قد تبدو المحاولة الأولى لتحقيق ذلك على النحو التالي:

# 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 المباشرة.