نقل بيانات إضافات Firebase إلى Cloud Functions

يوضّح لك هذا الدليل كيفية نقل إضافاتك من بيئة Firebase Extensions المتوقّفة نهائيًا إلى دالة يثبّتها المستخدمون وينشرونها في Cloud Functions الخاص بهم من أجل قاعدة الرموز Firebase (الجيل الثاني).

هذا هو مسار نقل البيانات المقترَح. ستحتفظ Firebase بقائمة بالإضافات التي تتضمّن بدائل رسمية على npm، وسيرشدك هذا الدليل إلى كيفية إنشاء بديلك.

في هذا الدليل، يتم استخدام الإضافة Stream Cloud Firestore to BigQuery (firestore-bigquery-export) كمثال عملي. ينتهي كل قسم بمثال محلول يوضّح الشكل الذي كانت تظهر به هذه الإضافة قبل نقل البيانات والشكل الذي تظهر به بعد نقل البيانات، وذلك باستخدام حزمة @firebase-function-kits/firestore-bigquery-export.

الاشتراك للحصول على مزيد من المعلومات والمساعدة بشأن نقل الإضافات

إذا كانت لديك أسئلة حول كيفية نقل البيانات من Firebase Extensions، يمكنك التواصل معنا على firebase-extensions-migrator-support-external@google.com. سنرسل أيضًا رسالة إلكترونية إلى هذه المجموعة عند تعديل الدليل وإضافة المزيد من المعلومات حول تجميع وظائف الجيل الثاني واختبارها وتوزيعها.

للانضمام إلى هذه المجموعة، أرسِل رسالة إلى firebase-extensions-migrator-support-external+subscribe@google.com، وسيتم الردّ عليك برسالة إلكترونية تتضمّن طلبًا للانضمام. عليك الردّ على تلك الرسالة الإلكترونية، وليس النقر على الزر "الانضمام إلى هذه المجموعة".

قبل البدء

لإكمال عملية النقل هذه كما هو مكتوب، عليك استخدام الميزات التالية في Cloud Functions:

  • الإعدادات التي تتضمّن مَعلمات: تصبح كل مَعلمة تحدّدها في extension.yaml مَعلمة محدّدة في رمز الحزمة.

  • أدوار "إدارة الهوية وإمكانية الوصول" التعريفية وواجهات برمجة التطبيقات المطلوبة يصبح كل دور تحدّده في extension.yaml استدعاء requiresRole(...)، وتصبح كل واجهة برمجة تطبيقات استدعاء requiresAPI(...) في رمز الحزمة. عند النشر، تمنح واجهة سطر الأوامر (CLI) Firebase الأدوار المحدّدة لحساب خدمة وقت التشغيل المُدار وتفعّل واجهات برمجة التطبيقات المحدّدة نيابةً عنك.

  • أحداث مراحل النشاط لقواعد الرموز Cloud Functions تتيح قواعد الرموز Cloud Functions الآن أحداث مراحل النشاط المشابهة لأحداث Firebase Extensions. عليك الإفصاح عن عملية الإعداد أثناء التثبيت وأثناء التحديث باستخدام خطافَي مراحل النشاط afterFirstDeploy(...) وafterRedeploy(...). تحلّ هذه السمة محل lifecycleEvents التي تحدّدها في extension.yaml.

نقل مصدر Firebase Extensions إلى دالة من الجيل الثاني

(اختياري) نقل البيانات آليًا باستخدام Firebase Agent Skill

يمكنك تنفيذ الخطوات من 1 إلى 8 تلقائيًا (إعداد قائمة بالمراجع، وتفعيل عمليات الترقية، وتحويل المَعلمات والأسرار، وإدارة الهوية وإمكانية الوصول المستندة إلى التصريح، وخطوات دورة الحياة، وإنشاء ملف README للحزمة) باستخدام مهارة وكيل الذكاء الاصطناعي الرسمية extension-to-functions-codebase.

تثبيت المهارة

إذا لم تثبّت أنت أو مساعدك المستند إلى الذكاء الاصطناعي في الترميز (Gemini في Firebase أو Cursor أو Claude Code أو GitHub Copilot) المهارة بعد، نفِّذ الأمر التالي باستخدام واجهة سطر الأوامر الخاصة بالمهارات:

npx skills add firebase/agent-skills --skill extension-to-functions-codebase

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

"يُرجى نقل إضافة Firebase هذه إلى حزمة Function Kit من الجيل الثاني قابلة للنشر باتّباع التعليمات الواردة في extension-to-functions-codebase المهارة".

1. جرد الإضافة

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

راجِع كلّاً ممّا يلي، ودوِّن ما تجده:

  • extension.yaml، الذي يحدّد المَعلمات والدوال والأحداث وأدوار إدارة الهوية وإمكانية الوصول وواجهات برمجة التطبيقات المطلوبة والأسرار وخطافات مراحل النشاط.

  • functions/، الذي يحتوي على رمز الدالة والتبعيات وإعدادات الإنشاء والمشغّلات ودوال قائمة انتظار المهام.

  • ‫README.md وPREINSTALL.md وPOSTINSTALL.md، التي تتضمّن خطوات الإعداد والتحذيرات وملاحظات الفوترة

  • scripts/، الذي يحتوي على أي أدوات استيراد أو تعبئة أو إدارة هوية وإمكانية وصول أو إصلاح أو نقل، وأي أدوات أخرى يتم شحنها مع الإضافة

بعد ذلك، حدِّد مكان كل عنصر في حزمة npm ضمن extension.yaml:

  • حوِّل إعدادات المستخدم إلى مَعلمات Cloud Functions (الخطوة 4).

  • حوِّل الأسرار إلى أسرار Cloud Functions (الخطوة 4).

  • حوِّل أدوار إدارة الهوية وإمكانية الوصول إلى تعريفات requiresRole(...) (الخطوة 6).

  • حوِّل واجهات Google API المطلوبة إلى تعريفات requiresAPI(...) عند الاقتضاء (الخطوة 6).

  • تحويل خطافات التثبيت والتحديث إلى بيانات afterFirstDeploy(...) وafterRedeploy(...) (الخطوة 7)

  • تحويل أرقام تعريف المثيلات من EXT_INSTANCE_ID إلى FIREBASE_KIT_INSTANCE_ID (الخطوة 4)

مثال: نقل البيانات من Cloud Firestore إلى BigQuery

تؤدي قراءة firestore-bigquery-export/extension.yaml وfunctions/ إلى هذا المخزون:

في "extension.yaml" العدد / القيمة كيفية استخدام بياناتك
params ‫25 (COLLECTION_PATH، DATASET_ID، TABLE_ID، DATASET_LOCATION، VIEW_TYPE، …) Cloud Functions المَعلمات (الخطوة 4)
apis bigquery.googleapis.com requiresAPI(...) (الخطوة 6)
roles bigquery.dataEditor وdatastore.user وbigquery.user requiresRole(...) (الخطوة 6)
resources مشغّل حدث واحد (fsexportbigquery) + دوال قائمة انتظار المهام (initBigQuerySync وsetupBigQuerySync) وظائف الحزمة التي تم تصديرها (الخطوة 3)
lifecycleEvents ‫onInstall → initBigQuerySync؛ onUpdate / onConfigure → setupBigQuerySync ‫afterFirstDeploy / afterRedeploy (الخطوة 7)
رقم تعريف المثال لم يتم الاستخدام (لا توجد قراءات EXT_INSTANCE_ID) ما مِن بيانات لنقلها
scripts/ ‫import/ (ملء البيانات السابقة)، gen-schema-view/ يتم الاحتفاظ بها كنصوص برمجية (خارج نطاق هذا المستند)

التحليل: لا تحدّد الإضافة أيّ مَعلمات type: secret، لذا لا يمكن نقل أي أسرار في الخطوة 4. مشغّل الأحداث هو الجيل الثاني، بينما لا تزال دوال قائمة انتظار المهام هي الجيل الأول (مهم في الخطوة 3).

2. تعديل ملف package.json

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

إصدارات حزمة تطوير البرامج (SDK) الدنيا: عليك تعريف firebase-functions >= 7.4.0 وfirebase-admin >= 14.2.0 كعناصر تابعة. عليك أيضًا الإفصاح عن إصدار firebase-functions باعتباره تبعية متساوية، حتى يتضمّن مشروع Cloud Functions الخاص بالمستخدمين إصدار حزمة تطوير البرامج (SDK) نفسه الذي تم إنشاء مكتبتك به.

{
  "name": "<package-name>",
  "version": "1.0.0",
  "main": "lib/index.js",
  "types": "lib/index.d.ts",
  "exports": {
    ".": {
      "types": "./lib/index.d.ts",
      "default": "./lib/index.js"
    }
  },
  "engines": {
    "node": "22"
  },
  "peerDependencies": {
    "firebase-functions": "^7.4.0"
  },
  "dependencies": {
    "firebase-functions": "^7.4.0",
    "firebase-admin": "^14.2.0"
  }
}

مثال: نقل البيانات من Cloud Firestore إلى BigQuery

قبل: يكون functions/package.json الخاص بالإضافة خاصًا، ويسمّي معرّف الإضافة، ويحدّد firebase-functions كعنصر تابع مباشر:

{
  "name": "firestore-bigquery-export",
  "main": "lib/index.js",
  "private": true,
  "dependencies": {
    "@firebaseextensions/firestore-bigquery-change-tracker": "^2.0.4",
    "firebase-admin": "^14.2.0",
    "firebase-functions": "^6.3.2"
  }
}

بعد ذلك. حزمة قابلة للنشر: اسم ذو نطاق وخريطة exports وfirebase-functions تم نقلهما إلى peerDependencies:

{
  "name": "@firebase-function-kits/firestore-bigquery-export",
  "version": "0.1.0",
  "main": "lib/index.js",
  "types": "lib/index.d.ts",
  "exports": {
    ".": {
      "types": "./lib/index.d.ts",
      "default": "./lib/index.js"
    }
  },
  "engines": {
    "node": "22"
  },
  "peerDependencies": {
    "firebase-functions": "^7.4.0"
  },
  "dependencies": {
    "@firebaseextensions/firestore-bigquery-change-tracker": "^2.0.4",
    "firebase-admin": "^14.2.0",
    "firebase-functions": "^7.4.0"
  }
}

3- ترقية الدوال من الجيل الأول إلى الجيل الثاني

إذا كانت الإضافة لا تزال تصدّر دوال الجيل الأول، عليك تحويل كل مشغّل إلى ما يعادله من الجيل الثاني. استورِد من وحدات firebase-functions/...، ومرِّر إعدادات وقت التشغيل في خيارات المشغّل.

اطّلِع على Cloud Functions دليل الترقية إلى الجيل الثاني. والجدير بالذكر أنّه يمكنك تقليل جهود إعادة الكتابة باستخدام الجيل الثاني من تصحيح بنية الأحداث وتجنُّب إعادة كتابة منطق الدالة لأنّ الجيل الثاني من حزمة تطوير البرامج (SDK) يعرض مَعلمات الإصدار 1 كحقول في عنصر الحدث، ما يتيح لك استخدام مَعلمات مجزّأة/مسمّاة والحفاظ على منطق نشاطك التجاري بدون تغيير.

قبل: الجيل الأول:

import * as functions from "firebase-functions/v1";

export const sync = functions.firestore
  .document("{collectionId}/{documentId}")
  .onWrite(async (change, context) => {
    await handleWrite(change.before, change.after, context.params);
  });

بعد ذلك. الجيل الثاني:

import { onDocumentWritten } from "firebase-functions/firestore";

export const syncV2 = onDocumentWritten(
  { document: "{collectionId}/{documentId}" },
  async ({ change, context }) =>
    await handleWrite(change.before, change.after, context.params)
);

راجِع Cloud Functions مقارنة الإصدارات للحصول على قائمة شاملة بالاختلافات بين الجيل الأول والجيل الثاني من الدوال.

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

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

مثال: نقل البيانات من Cloud Firestore إلى BigQuery

defineString("DATABASE_REGION", {
  label: "Firestore Instance Location",
  description:
    "Where is the Firestore database located? You can check your current database location at https://console.cloud.google.com/firestore/databases. The functions in this kit deploy to the Cloud Run region closest to this location.",
  input: select({
    "Multi-region (Europe - Belgium and Netherlands)": "eur3",
    "Multi-region (United States)": "nam5",
    "Multi-region (Iowa, North Virginia, and Oklahoma)": "nam7",
    "Iowa (us-central1)": "us-central1",
    // More locations...
  })
});

// Firestore multi-region locations are not Cloud Run regions; deploying a
// function to one hard-fails, so they map to a region inside the multi-region.
const MULTI_REGION_TO_FUNCTION_REGION: Record<string, string> = {
  nam5: "us-central1",
  nam7: "us-central1",
  eur3: "europe-west1",
};

/**
 * Maps a Firestore database location to the Cloud Run region the functions
 * should deploy to. The lookup is case-insensitive and ignores surrounding
 * whitespace, as the CLI's own region handling is. Regional locations pass
 * through lowercased; an unset or blank location returns `undefined`, meaning
 * the functions declare no region.
 */
export function firestoreLocationToFunctionRegion(
  location: string | undefined
): string | undefined {
  const normalized = location?.trim().toLowerCase();
  if (!normalized) {
    return undefined;
  }
  return MULTI_REGION_TO_FUNCTION_REGION[normalized] ?? normalized;
}

const functionRegion = firestoreLocationToFunctionRegion(
  process.env.DATABASE_REGION
);

export const fsexportbigquery = onDocumentWritten(
  {
    region: functionRegion,
    // Other configuration
  },
  (event) => handleDocumentWrite(event, getHandlerContext())
);

عند نشر الحزمة لأول مرة، سيُطلب من المستخدمين تقديم DATABASE_REGION وسيتم نشر الدوال إلى functionRegion المقابل أعلاه، ما يؤدي إلى حل أي مشاكل في الموقع الجغرافي للدوال التي يتم تشغيلها عند وقوع حدث من الجيل الثاني.

4. تحويل مَعلمات الإضافة والأسرار

المَعلمات

يصبح كل معلَمة تحدّدها في extension.yaml معلَمة Cloud Functions.

تحويل قراءات البيئة المباشرة:

const collectionPath = process.env.COLLECTION_PATH;

إلى مَعلمات Cloud Functions:

import { defineString } from "firebase-functions/params";
import { onDocumentWritten } from "firebase-functions/firestore";

const collectionPath = defineString("COLLECTION_PATH");

// Pass the param directly when used as a placeholder (e.g. trigger path)
export const sync = onDocumentWritten(
  { document: collectionPath },
  async (event) => {
    // Call .value() to read the string inside a handler
    const path = collectionPath.value();
    await handleWrite(path, event);
  }
);

استخدِم collectionPath.value() لقراءة السلسلة داخل معالج، واستخدِم collectionPath مباشرةً في المواضع التي يُتوقّع فيها عنصر نائب، مثل مسار مشغّل دالة.

يكتشف Firebase CLI المَعلمات ويقرأ قيمها من .env أو .env.<projectId> أو يطلب من المستخدمين إدخالها أثناء عملية النشر. احتفِظ بأسماء المَعلمات نفسها، حتى يتم نقل القيم من عملية تثبيت حالية.

من المهم عدم تغيير أسماء المَعلمات المحدّدة في الرمز على الإطلاق. يحافظ نقل البيانات إلى الإضافة على قيم المَعلمات الحالية للمستخدم النهائي تلقائيًا، ولكن فقط عندما لا تتغير الأسماء.

مثال: نقل البيانات من Cloud Firestore إلى BigQuery

قبل: معلَمة تم تعريفها في extension.yaml، ويتم قراءتها كمتغيّر بيئة أولي في config.ts:

# extension.yaml
- param: COLLECTION_PATH
  label: Collection path
  type: string
  required: true
// functions/src/config.ts
collectionPath: process.env.COLLECTION_PATH,

بعد ذلك. جهاز واحد defineString، وتكتشفه واجهة سطر الأوامر وتقرأ منه .env:

// src/config.ts
import { defineString } from "firebase-functions/params";

collectionPath: defineString("COLLECTION_PATH", {
  label: "Collection path",
  // We now support "nonEmpty: true" to ensure a value other than the empty string
  // is entered, analogous to "required: true" in extension.yaml
  input: { text: { nonEmpty: true } }
}),

لم يتغيّر اسم المَعلمة، لذا سيظلّ .env الحالي يعمل.

رقم تعريف المثال

تقرأ الإضافات رقم تعريف المثيل من EXT_INSTANCE_ID، الذي يتم إدراجه بواسطة وقت تشغيل الإضافات. تقرأ حِزم الدوال رقم تعريف مثيلها من FIREBASE_KIT_INSTANCE_ID، والذي يضبطه Firebase CLI لكل مثيل حزمة إلى مفتاح المثيل في خريطة instances في firebase.json. توفّر واجهة سطر الأوامر هذه البيانات أثناء عملية البحث عند النشر وفي المحاكي، كما توفّرها للدوال التي تم نشرها.

معرّف المثيل ليس مَعلمة، لذا لا تُعرِّفه باستخدام defineString. في الواقع، FIREBASE_... هي بادئة محجوزة في ملفات .env، لذا لن يتمكّن المستخدمون من ضبطها أو تجاهلها هناك. لا تظهر القيم التي يتم إدخالها من خلال واجهة سطر الأوامر لنظام المَعلمات. يمكنك قراءتها مباشرةً من البيئة:

// Before
const instanceId = process.env.EXT_INSTANCE_ID;

// After
const instanceId = process.env.FIREBASE_KIT_INSTANCE_ID;

لن يتم ضبط المتغيّر إلا عند نشر الحزمة كحزمة تطوير برامج. إذا تم نشر الرمز أيضًا كقاعدة رموز مستقلة (راجِع الخطوة 9)، يمكنك التعامل معه كرمز اختياري أو إيقاف التطبيق سريعًا مع عرض رسالة واضحة عند عدم توفّره. إذا كان الإضافة تعرض رقم تعريف المثيل كمَعلمة ظاهرة للمستخدم، عليك إزالة هذه المَعلمة لأنّ واجهة سطر الأوامر Firebase هي التي تملك القيمة الآن.

مثال عملي: حذف بيانات المستخدم

(لا تقرأ إضافة Stream Cloud Firestore إلى BigQuery معرّف مثيلها، لذا لا يوجد أي بيانات لنقلها. تستخدم إضافة "حذف بيانات المستخدم" هذه السمة لتسمية مواضيع Pub/Sub.)

قبل: يمكنك قراءة متغيّر البيئة الأوّلي في config.ts باستخدام البادئة ext- التي استخدمتها "الإضافات" لمواردها:

// functions/src/config.ts
discoveryTopic: `ext-${process.env.EXT_INSTANCE_ID}-discovery`,
deletionTopic: `ext-${process.env.EXT_INSTANCE_ID}-deletion`,

بعد ذلك. عملية قراءة عادية process.env لـ FIREBASE_KIT_INSTANCE_ID مستخدَمة لمَعلمتَين عاديتَين ليتمكّن المستخدمون من تجاهل أسماء المواضيع:

// src/config.ts
const instanceId = process.env.FIREBASE_KIT_INSTANCE_ID;

// Non-empty defaults so Pub/Sub trigger bindings resolve during deploy
// discovery without freezing an empty topic name into the manifest.
discoveryTopicName: defineString("DISCOVERY_TOPIC_NAME", {
  default: `kit-${instanceId}-discovery`,
}),
deletionTopicName: defineString("DELETION_TOPIC_NAME", {
  default: `kit-${instanceId}-deletion`,
}),

يجب أن تكون القيم التلقائية غير فارغة لأنّه يتم تحديد روابط المشغّلات في وقت اكتشافها. سيتم إدراج قيمة تلقائية فارغة في بيان النشر كاسم الموضوع. توفّر الحزمة أيضًا دفاعًا ضد التشغيل خارج سياق الحزمة. في حال عدم توفّر المتغيّر، سيتم تقييم القيمة التلقائية على مستوى الوحدة إلى kit-undefined-discovery، وبالتالي سيتعذّر على أداة تحميل الإعدادات عرض خطأ توضيحي بدلاً من ذلك:

// ...
const instanceId = process.env.FIREBASE_KIT_INSTANCE_ID;
if (!instanceId) {
  throw new Error(
    "FIREBASE_KIT_INSTANCE_ID is not set. It is provided automatically to " +
      "kit instances by firebase-tools >= 15.32.0; deploy or emulate this " +
      "kit with a supported CLI version."
  );
}
// ...

يتم إجراء هذا التحقّق عندما يحلّل المعالج إعداداته لأول مرة، لذا يؤدي عدم توفّر متغيّر إلى حدوث خطأ واضح في وقت التشغيل بدلاً من ربط الدوال بشكل غير معلن بمواضيع kit-undefined-*. لرفض عملية النشر نفسها، عليك إجراء عملية التحقّق على مستوى الوحدة النمطية كي يتم تنفيذها أثناء عملية البحث. بما أنّ واجهة سطر الأوامر تستمدّ معرّف المثيل من firebase.json، لا يمكن ضبط INSTANCE_ID، ولا يوجد أي شيء يجب أن تتم مزامنته على مستوى مثيلات متعدّدة.

المفاتيح السرية

في extension.yaml، يمكنك تعريف الأسرار باستخدام type: secret. يخزّن وقت تشغيل الإضافات هذه البيانات ويربطها، ما يتيح لرمز الإضافة قراءة process.env.PARAM_NAME مباشرةً. في قاعدة الرموز البرمجية Cloud Functions النموذجية، عليك تعريف كل كلمة سر وربطها بشكل صريح:

import { defineSecret } from "firebase-functions/params";
import { onRequest } from "firebase-functions/https";

const apiKey = defineSecret("API_KEY");
export const fn = onRequest({ secrets: [apiKey] }, handler);

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

مثال عملي: إرسال رسالة إلكترونية مشغِّلة من Cloud Firestore

قبل: يتم قراءة MAIL_COLLECTION وSMTP_PASSWORD كمتغيرات بيئة أولية في config.ts:

# extension.yaml
- param: MAIL_COLLECTION
  label: Email documents collection
  type: string
  default: mail
  required: true

- param: SMTP_PASSWORD
  label: SMTP password
  type: secret
// functions/src/config.ts
mailCollection: process.env.MAIL_COLLECTION,
smtpPassword: process.env.SMTP_PASSWORD,

بعد ذلك. جهاز defineString وجهاز defineSecret، وتكتشف واجهة سطر الأوامر الجهازين معًا وتقرأ من .env:

import { defineString, defineSecret } from "firebase-functions/params";
import { onDocumentWritten } from "firebase-functions/firestore";

const mailCollection = defineString("MAIL_COLLECTION", {
  label: "Email documents collection",
  default: "mail"
});

const smtpPassword = defineSecret("SMTP_PASSWORD", { label: "SMTP password" });

export const processQueue = onDocumentWritten(
  { document: `${mailCollection}/{documentId}`, secrets: [smtpPassword] },
  async (event) => {
    const collection = mailCollection.value();
    const password = smtpPassword.value();
    // ...
  }
);

5- نقل طلبات قائمة انتظار المهام الداخلية

تضيف بعض الإضافات مهامًا إلى قوائم انتظار المهام الخاصة بها من داخل رمز الدالة، وذلك باستخدام Firebase Admin SDK. يختلف ذلك عن تلقّي مهمة تم إرسالها (يتم تناول ذلك في القسمَين وظائف الترقية وتحويل خطافات دورة الحياة). في هذا المثال، يكون الرمز البرمجي هو المنتج الذي يستدعي queue.enqueue(...).

كانت الإصدارات السابقة من Admin SDK تتطلّب أن تمرّر الإضافات رقم تعريف نسخة الإضافة الخاص بها كمَعلمة ثانية لاستهداف دالة "قائمة المهام" في الإضافة نفسها. اعتبارًا من الإصدار firebase-admin 14.2.0، لم يعُد ذلك مطلوبًا أو موصى به. تستهدف واجهة برمجة التطبيقات Task Queue API الآن قوائم المهام في السياق نفسه (على سبيل المثال، إضافة أو حزمة) تلقائيًا. ننصحك بإزالة هذه المَعلمة من الرمز البرمجي سواء كانت إضافة أو دالة مستقلة، لأنّ ذلك آمن. تضمن إزالة هذه المَعلمة إمكانية نقل البيانات والتوافق مع الإصدارات المستقبلية.

ستبقى جميع التفاصيل الأخرى المتعلقة بطلب إضافة إلى قائمة الانتظار كما هي، بما في ذلك مسار المورد locations/<region>/functions/<name> وحِزمة بيانات المهمة ومنطق إعادة المحاولة.

راجِع إضافة الدوال إلى قائمة الانتظار باستخدام Cloud Tasks للحصول على مزيد من التفاصيل حول إضافة الدوال إلى قائمة الانتظار باستخدام Cloud Tasks.

قبل: إضافة الجيل الأول:

import { getFunctions } from "firebase-admin/functions";

const queue = getFunctions().taskQueue(
  `locations/${config.location}/functions/syncBigQuery`,
  process.env.EXT_INSTANCE_ID, // extension instance ID, injected by the runtime
);
await queue.enqueue(taskData);

بعد ذلك. إضافة الجيل الثاني:

import { getFunctions } from "firebase-admin/functions";

const queue = getFunctions().taskQueue(
  `locations/${process.env.FUNCTION_REGION}/functions/syncBigQuery`
);
await queue.enqueue(taskData);

إذا كان طلب إضافة إلى قائمة الانتظار يستهدف قاعدة رموز برمجية ذات بادئة، سيتم أيضًا إضافة بادئة إلى اسم الدالة التي تم اكتشافها (على سبيل المثال، orders-syncBigQuery). راجِع مراجعة مثيل حزمة الدوال البديلة وتثبيته والاختبار كحزمة دوال.

6. تحديد واجهات برمجة التطبيقات وأدوار "إدارة الهوية وإمكانية الوصول" المطلوبة

نقل متطلبات إدارة الهوية وإمكانية الوصول (IAM) وواجهة برمجة التطبيقات الخاصة بالإضافة من extension.yaml إلى:

import { requiresAPI, requiresRole } from "firebase-functions";

requiresAPI("bigquery.googleapis.com", "Needed to write changelog rows");
requiresRole("roles/bigquery.dataEditor");
requiresRole("roles/bigquery.user");

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

مثال: نقل البيانات من Cloud Firestore إلى BigQuery

قبل: تم الإعلان عنه في extension.yaml، وقد فعّل وقت تشغيل الإضافات واجهة برمجة التطبيقات ومنح الأدوار إلى حساب مُدار:

apis:
  - apiName: bigquery.googleapis.com
roles:
  - role: bigquery.dataEditor
  - role: datastore.user
  - role: bigquery.user

بعد ذلك. يتم تعريفها في الرمز باستخدام requiresAPI وrequiresRole:

import { requiresAPI, requiresRole } from "firebase-functions";

requiresAPI(
  "bigquery.googleapis.com",
  "Needed to write changelog rows and views"
);
requiresRole("roles/bigquery.dataEditor");
requiresRole("roles/datastore.user");
requiresRole("roles/bigquery.user");

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

if (!!process.env.EVENTARC_CHANNEL) {
  requiresRole("roles/eventarc.publisher");
  requiresAPI(
    "eventarcpublishing.googleapis.com",
    "Publishes the extension's custom events to its Eventarc channel."
  );
}

7. تحويل عبارات ربط مراحل النشاط

إذا كانت الإضافة تستدعي getExtensions().runtime() (على سبيل المثال، setProcessingState أو setFatalError)، احذف عمليات الاستدعاء هذه لأنّها ستعرض خطأ إذا تم استدعاؤها من دالة من الجيل الثاني تم نشرها بشكل عادي. يتم الآن تحديد حالة مراحل النشاط من خلال afterFirstDeploy وafterRedeploy، حيث لا يتم استخدام تتبُّع الحالة هذا.

يمكن تشغيل Firebase Extensions عند تثبيت مستخدم لإضافة أو تحديثها أو إعادة ضبطها. في حزمة npm، حدِّد إجراءات مكافئة لدورة الحياة في الرمز البرمجي.

لإجراء عملية الإعداد لمرة واحدة، اتّبِع الخطوات التالية:

import { afterFirstDeploy } from "firebase-functions/lifecycle";
import { onTaskDispatched } from "firebase-functions/tasks";

export const runInitialSetup = onTaskDispatched(async (request) => {
  await initializeResources(request.data);
});

afterFirstDeploy({
  task: {
    function: "runInitialSetup",
    body: {}
  }
});

بالنسبة إلى تعديلات الإعداد أو الرمز:

import { afterRedeploy } from "firebase-functions/lifecycle";

afterRedeploy({
  task: {
    function: "runInitialSetup",
    body: { reconcile: true }
  }
});

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

firebase functions:lifecycle:run afterFirstDeploy CODEBASE_NAME
firebase functions:lifecycle:run afterRedeploy CODEBASE_NAME

مثال: نقل البيانات من Cloud Firestore إلى BigQuery

قبل: ‫lifecycleEvents في extension.yaml، يتم تشغيلها بواسطة وقت تشغيل الإضافات:

lifecycleEvents:
  onInstall:
    function: initBigQuerySync
    processingMessage: Configuring BigQuery Sync.
  onUpdate:
    function: setupBigQuerySync
    processingMessage: Configuring BigQuery Sync
  onConfigure:
    function: setupBigQuerySync
    processingMessage: Configuring BigQuery Sync

بعد ذلك. يتم تحديدها في الرمز، وتوفّر المهمة BigQuery عند النشر لأول مرة:

import { afterFirstDeploy, afterRedeploy } from "firebase-functions/lifecycle";

afterFirstDeploy({ task: { function: "initBigQuerySync" } });
afterRedeploy({ task: { function: "setupBigQuerySync" } });

توفير الموارد هو عملية متكررة، لذا فإنّ إعادة التشغيل تؤدي إلى تسوية مجموعة البيانات والجدول وطرق العرض. يمكن للمستخدمين إعادة تشغيلها يدويًا باستخدام firebase functions:lifecycle:run afterFirstDeploy CODEBASE_NAME.

8. إعداد المستندات للمستخدمين

اكتب حزمة README توضّح ما يلي على الأقل:

  • قيم .env التي تتطلّبها الحزمة
  • الأسرار التي تتطلّبها الحزمة وكيفية نقل قيم الأسرار الحالية
  • أدوار إدارة الهوية وإمكانية الوصول التي تحدّدها الحزمة باستخدام requiresRole(...)
  • واجهات Google APIs التي تتيحها الحزمة أو تتطلّبها
  • خطافات دورة الحياة التي تحدّدها الحزمة، وكيفية إعادة تشغيلها يدويًا
  • ملاحظات الفوترة
  • ما هي التغييرات التي تم إجراؤها مقارنةً بالإضافة الأصلية؟
  • كيفية حصول الحزمة على معرّف مثيلها (FIREBASE_KIT_INSTANCE_ID الذي يتم ضبطه بواسطة واجهة سطر الأوامر) وأنّه يتم نشر جميع دوال المثيل باستخدام البادئة kit-<instanceId>-.

مثال: نقل البيانات من Cloud Firestore إلى BigQuery

تحتوي الحزمة README على جدول "التغييرات" المحدّدة:

قلق كإضافة بصفتك @firebase-function-kits/firestore-bigquery-export
الإعداد مَعلمات الإضافة Cloud Functions مَعلمات من خلال .env
إدارة الهوية وإمكانية الوصول الامتيازات التي تمنحها الإضافات ‫requiresRole(...)، تم تطبيقه عند النشر
إدارة الحسابات مهمة مراحل النشاط حسب الإضافات afterFirstDeploy / afterRedeploy مهمة
أسماء الدوال ext-<instanceId>-fsexportbigquery fsexportbigquery (يمكن إضافة بادئة إليه)
رقم تعريف المثال EXT_INSTANCE_ID التي تم إدخالها من خلال الإضافات ‫FIREBASE_KIT_INSTANCE_ID، تم ضبطه من خلال واجهة سطر الأوامر من firebase.json

9. اختبار الدالة من الجيل الثاني

من المفترض أن يكون لديك الآن دالة من الجيل الثاني تتصرف عند نشرها بشكل مطابق لعملية تثبيت جديدة للإضافة. الخطوة التالية هي التحقّق من أي مشاكل تم إدخالها عن طريق الخطأ وإصلاحها.

لاستدعاء setGlobalOptions لضبط خيارات عامة، مثل منطقة تلقائية أو وحدة معالجة مركزية، يجب إجراء ذلك فقط عند نشر حزمة تطوير البرامج كدالة مستقلة من الجيل الثاني. عند تثبيت حِزمتك كحزمة npm، يستدعي المستخدمون setGlobalOptions في رمز التضمين لتحديد هذه المَعلمات، وسيتلقّون تحذيرات إذا حدث ذلك مرّتين. يمكنك حماية هذه المكالمة من خلال التحقّق من متغيّر البيئة FIREBASE_KIT_INSTANCE_ID:

import { setGlobalOptions } from "firebase-functions";

if (!process.env.FIREBASE_KIT_INSTANCE_ID) {
  setGlobalOptions({
    region: "us-east1",
    maxInstances: 10,
  });
}

تأكَّد من استخدام firebase-tools >= 15.32.0، ونشِّر الدالة المحوَّلة من الجيل الثاني في مشروع تجريبي يتضمّن الموارد المناسبة لاختبار سلوكها. إذا سبق لك إعداد مشروع اختبار من خلال اختبار إضافتك، نفِّذ الأمر التالي:

firebase deploy --only functions

املأ المعالج الناتج الذي يطلب منك قيم المَعلمات بالطريقة نفسها التي كنت ستملأ بها نموذج التثبيت في وحدة تحكّم Firebase للإضافة.

مثال: نقل البيانات من Cloud Firestore إلى BigQuery

نتأكّد من أنّ عملية المزامنة بين Cloud Firestore وBigQuery مشفّرة تمامًا بين الأطراف:

  1. في صفحة Cloud Firestore ضمن وحدة تحكّم Firebase، أنشئ المجموعة التي ضبطتها على COLLECTION_PATH (users) إذا لم تكن موجودة من قبل.
  2. أنشئ مستندًا باسم bigquery-mirror-test يحتوي على أي حقول بأي قيم.
  3. في صفحة BigQuery ضمن وحدة تحكّم Google Cloud، استعلم عن جدول سجلّ التغيير الأولي. يجب أن يحتوي على صف واحد يسجّل عملية إنشاء المستند:

    SELECT * FROM `PROJECT_ID.analytics.users_raw_changelog`
    
  4. استعلم عن آخر عرض، والذي من المفترض أن يعرض آخر حدث تغيير للمستند الوحيد المتوفّر (bigquery-mirror-test):

    SELECT * FROM `PROJECT_ID.analytics.users_raw_latest`
    
  5. احذف المستند bigquery-mirror-test في Cloud Firestore. ويختفي من العرض الأخير، ويتم إلحاق حدث DELETE بجدول سجلّ التغيير الأولي.

    يمكنك الاطّلاع على السجلّ الكامل لمستند واحد باستخدام:

    SELECT *
       FROM `PROJECT_ID.analytics.users_raw_changelog`
       WHERE document_name = "bigquery-mirror-test"
       ORDER BY timestamp ASC
    

الاختلافات عن اختبار الإضافة:

  • يتم نشر المشغّل على النحو fsexportbigquery (بدون بادئة عند النشر كدالة عادية من الجيل الثاني وليس كحزمة)، وليس ext-<instanceId>-fsexportbigquery. ابحث عن هذا الاسم في Cloud Functions لوحة البيانات والسجلات.
  • يعمل الرمز البرمجي الآن في Firebase Local Emulator Suite كدوال عادية. يمكنك ضبط قيمة المَعلمات التي سيتم استخدامها في المحاكي باستخدام .env.local. يمكنك أيضًا إجراء اختبارات الوحدة للرمز باستخدام حزمة تطوير البرامج (SDK) الخاصة بـ firebase-functions-test كما هو موضّح في اختبار الوحدة الخاص بـ Cloud Functions.
  • لم يعُد توفير الموارد يعتمد على وقت تشغيل الإضافات. إذا كان جدول سجلّ التغيير غير متوفّر بعد النشر، أعِد تنفيذ مهمة الإعداد يدويًا: firebase functions:lifecycle:run afterFirstDeploy CODEBASE_NAME. المهمة هي متكررة، لذا فإنّ إعادة تشغيلها تؤدي إلى تسوية مجموعة البيانات والجدول وطرق العرض.
  • تأتي قيم المَعلمات من .env بدلاً من نموذج التثبيت، لذا لن تكون عمليات إعادة تشغيل firebase deploy تفاعلية بعد اكتمال .env.

10. نشر مجموعة الدوال على npm

بعد التحقّق من صحة عملية نقل الإحالة الناجحة من إضافة إلى دالة من الجيل الثاني، يمكنك نشر إصدار تجريبي على npm لإجراء اختبار شامل باستخدام أحد الأدلة التالية:

عند نشر حزمة أدواتك على npm، يمكن تثبيتها باستخدام firebase functions:kits:install وإدراجها كبديل رسمي للإضافة.

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

قبل النشر:

  1. اختَر اسم حزمة. تعمل الأسماء ذات النطاق والأسماء غير ذات النطاق (راجِع الأدلة أعلاه). يُرجى العِلم أنّ الحِزم ذات النطاق تكون خاصة بشكلٍ تلقائي، لذا يجب تمرير --access public.
  2. إنشاء المحتوى والتحقّق من المحتوى الذي يتم شحنه يشير main وtypes إلى الناتج الذي تم تجميعه (lib/ في مثالنا العملي)، لذا يجب تضمين هذا الدليل في ملف tar المنشور. استخدِم .npmignore أو قائمة files المسموح بها، وافحص النتيجة باستخدام npm pack --dry-run. يمنع نص برمجي prepublishOnly يتم تشغيله أثناء عملية الإنشاء نشر ناتج قديم.

إضافات package.json التي تجعل النشر آمنًا بشكل تلقائي:

{
  "files": ["lib", "README.md", "CHANGELOG.md"],
  "publishConfig": { "access": "public", "tag": "next" },
  "scripts": {
    "build": "tsc -b",
    "prepublishOnly": "npm run build && npm test"
  }
}

يضمن الحقل "publishConfig": { "tag": "next" } ألا يحلّ npm publish العادي محلّ latest أبدًا.

إنشاء إصدار محتمل:

على سبيل المثال، لزيادة رقم الإصدار محليًا من 0.0.2-rc.3 إلى 0.0.2-rc.4 (يؤدي ذلك إلى إجراء عمليات الإيداع ووضع العلامات في Git إذا كان package.json في جذر المستودع):

npm version prerelease --preid rc

لنشر الإصدار المحتمل وتسجيل @your-org/your-kit@0.0.2-rc.4 على npm ضمن العلامة next، اتّبِع الخطوات التالية:

npm publish

قد يستغرق ظهور الإصدار الجديد على موقع npm الإلكتروني بضع دقائق، بينما يقرأ npm view السجلّ مباشرةً:

npm view @your-org/your-kit versions dist-tags

بعد إكمال الخطوة 11 والخطوة 12 من هذا الدليل، يمكنك ترقية الحزمة إلى إصدار ثابت باتّباع الخطوات التالية:

npm version 0.0.2
npm publish --tag latest
npm dist-tag add @your-org/your-kit@0.0.2 next

ملاحظة حول npm-shrinkwrap.json: ننصحك بشدة بتضمين ملف npm-shrinkwrap.json مع الحزمة، لأنّ واجهة سطر الأوامر تحذّر المستخدمين عند التثبيت في حال عدم تضمين هذا الملف. ويضمن استخدام المستخدمين للتبعيات التي تم اختبارها بالضبط، كما يساعد في الحماية من الهجمات على سلسلة الإمداد. ومع ذلك، يتم تطبيق shrinkwrap حرفيًا في مشاريع المستخدمين، بما في ذلك أثناء عملية إنشاء Cloud Functions (npm ci)، حيث يمكن أن تفشل الإدخالات المخصّصة للمطوّرين فقط بسبب EBADPLATFORM. قد تحتاج إلى إزالة إدخالات "dev": true وdevDependencies من نسخة shrinkwrap المنشورة.

مثال: نقل البيانات من Cloud Firestore إلى BigQuery

package.json للحزمة في وقت الإصدار المحتمل الرابع:

{
  "name": "@firebase-function-kits/firestore-bigquery-export",
  "version": "0.0.2-rc.4",
  "repository": {
    "type": "git",
    "url": "https://github.com/firebase/extensions.git",
    "directory": "kits/firestore-bigquery-export"
  },
  "main": "lib/index.js",
  "types": "lib/index.d.ts",
  "engines": { "node": "22" },
  "scripts": { "build": "tsc -b" }
}

يُرجى العِلم أنّه في هذه الحالة، تتوفّر الحزمة في مستودع أحادي، لذا من المهم تضمين repository.directory لكي يشير رابط سجلّ npm إلى المجلد الصحيح. يحتوي CHANGELOG.md على ملاحظات حول الإصدار المعلّق.

11. مجموعة أدوات الاختبار كدالة

بعد نشر حزمة أدواتك، ننصحك باختبارها باستخدام npm.

تأكَّد من استخدام الإصدار >= 15.32.0 من firebase-tools وتثبيت الحزمة:

firebase functions:kits:install --package <your-package-name>@<your-prerelease-version>

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

firebase deploy --only functions:<your-kit-instance-id>

بعد التثبيت، تطبع واجهة سطر الأوامر Firebase أمر نشر مشابهًا مع معرّف المثيل الذي اخترته أثناء التثبيت.

أعِد التحقّق من صحة المجموعة باستخدام التعليمات الواردة في الخطوة 9. اختبار الدالة من الجيل الثاني بعد أن أصبحت تنشر باستخدام الحِزم، يتم وضع بادئة لأسماء الدوال وتسميتها kit-<instance-id>-<method-name>. يتيح ذلك توفُّر عدة مثيلات للحِزم، ونشر الوظيفة نفسها عدة مرات في مشروع واحد، مع منح كل منها اسمًا فريدًا.

12. اختبار استبدال عملية نقل البيانات

يمكنك إعداد نسخة عاملة من الإضافة ثم استخدام دليل نقل البيانات للمستخدمين (باستخدام إما firebase ext:migrate --package أو أوامر واجهة سطر الأوامر الخاصة بحِزم الدوال) لإنهاء اختبار حزمة الدوال كبديل لعملية نقل البيانات.

13. إبلاغ المستخدمين وGoogle باستبدال الإضافة الرسمية

بعد أن يصبح بديل مجموعة الدوال جاهزًا ومتاحًا كحزمة npm يجب أن ينقل المستخدمون بياناتهم إليها، عليك إبلاغ كل من المستخدمين وGoogle بهذا البديل الرسمي. عدِّل ملف README.md في مستودع GitHub الذي يستضيف الإضافة بإضافة المعلومات التالية:

<!-- FIREBASE_EXTENSION_REPLACEMENT: extension="<your-extesion-id>" package="<your-npm-package-name>" -->
> [!WARNING]
> **Deprecation Notice:** The Firebase Extension `<your-extension>` is deprecated. Migrate to the [<your-npm-package-name>](<link-to-your-npm-package>) package.

تفحص Google ملفات README المعروفة الخاصة بالإضافات بحثًا عن تعليقات مثل <!-- FIREBASE_EXTENSION_REPLACEMENT: extension="firebase/firestore-bigquery-export" package="@firebase-function-kits/firestore-bigquery-export" -->، وتستخدم هذه التعليقات لملء سجلّ الاستبدالات الرسمي المخزّن في مستودع firebase-tools باسم replacements.json. يمكنك أيضًا التحقّق من replacements.json لمعرفة README.md التي سيتم فحصها لإضافتك. يتم تعديل القائمة الرسمية لعمليات الاستبدال أسبوعيًا.

مثال: نقل البيانات من Cloud Firestore إلى BigQuery

تتضمّن إضافة firestore-bigquery-export README.md ما يلي:

<!-- FIREBASE_EXTENSION_REPLACEMENT: extension="firebase/firestore-bigquery-export" package="@firebase-function-kits/firestore-bigquery-export" -->
> [!WARNING]
> **Deprecation Notice:** The Firebase Extension `firebase/firestore-bigquery-export` is deprecated. Please migrate to the [`@firebase-function-kits/firestore-bigquery-export`](https://www.npmjs.com/package/@firebase-function-kits/firestore-bigquery-export) package.