Mempersiapkan Firebase Extensions untuk migrasi ke Cloud Functions

Panduan ini menunjukkan cara memigrasikan ekstensi dari lingkungan Firebase Extensions yang tidak digunakan lagi ke fungsi yang diinstal dan di-deploy oleh pengguna di codebase Cloud Functions untuk Firebase (generasi ke-2) mereka sendiri.

Ini adalah jalur migrasi yang direkomendasikan. Firebase akan mempertahankan daftar ekstensi dengan npm resmi yang setara. Panduan ini akan memandu Anda membuat ekstensi Anda sendiri.

Di seluruh panduan ini, ekstensi Stream Firestore to BigQuery (firestore-bigquery-export) digunakan sebagai contoh. Setiap bagian diakhiri dengan Contoh yang berfungsi yang menunjukkan tampilan ekstensi tersebut sebelum migrasi, dan tampilannya setelah migrasi, sebagai paket @firebase/firestore-bigquery-export.

Mendaftar untuk mendapatkan informasi dan bantuan selengkapnya tentang migrasi ekstensi

Jika ada pertanyaan tentang cara bermigrasi dari Firebase Extensions, Anda dapat menghubungi kami di firebase-extensions-migrator-support-external@google.com. Kami juga akan mengirim email ke grup ini saat kami memperbarui panduan dengan informasi selengkapnya tentang pengemasan, pengujian, dan distribusi fungsi generasi ke-2 Anda.

Untuk bergabung dengan grup ini, kirim pesan ke firebase-extensions-migrator-support-external+subscribe@google.com, yang akan merespons dengan email permintaan keanggotaan. Anda harus membalas email tersebut, bukan mengklik tombol "Join This Group".

Sebelum memulai

Untuk menyelesaikan migrasi ini, Anda akan menggunakan fitur berikut dari Cloud Functions

  • Konfigurasi Berparameter. Setiap parameter yang Anda deklarasikan di extension.yaml akan menjadi parameter yang ditentukan dalam kode paket Anda.

  • Peran IAM deklaratif dan API yang diperlukan. Setiap peran yang Anda deklarasikan di extension.yaml akan menjadi panggilan requiresRole(...), dan setiap API akan menjadi panggilan requiresAPI(...) dalam kode fungsi Anda. Saat waktu deployment, Firebase CLI akan memberikan peran yang dideklarasikan ke akun layanan runtime terkelola dan mengaktifkan API yang dideklarasikan atas nama Anda.

  • Peristiwa siklus proses untuk codebase Cloud Functions. Cloud Functions Codebase kini mendukung peristiwa siklus proses yang analog dengan Firebase Extensions. Deklarasikan penyiapan waktu penginstalan dan waktu update dengan hook siklus proses afterFirstDeploy(...) dan afterRedeploy(...). Hook ini menggantikan lifecycleEvents yang Anda deklarasikan di extension.yaml.

Menginventarisasi Ekstensi

Mulailah dengan membuat inventaris ekstensi Anda: daftar lengkap semua yang dideklarasikan, dikirim, dan didokumentasikan oleh ekstensi, sehingga setiap perilaku memiliki tujuan yang ditentukan dalam fungsi generasi ke-2 dan tidak ada yang hilang dalam migrasi.

Tinjau setiap hal berikut, dan catat apa yang Anda temukan:

  • extension.yaml, yang mendeklarasikan parameter, fungsi, peristiwa, peran IAM, API yang diperlukan, secret, dan hook siklus proses Anda.

  • functions/, yang berisi kode fungsi, dependensi, konfigurasi build, pemicu, dan fungsi task queue Anda.

  • README.md, PREINSTALL.md, dan POSTINSTALL.md, yang berisi langkah-langkah penyiapan, peringatan, dan catatan penagihan.

  • scripts/, yang berisi utilitas impor, backfill, IAM, perbaikan, atau migrasi, dan alat lainnya yang Anda kirim bersama ekstensi.

Kemudian, untuk setiap item di extension.yaml, tentukan tempatnya di paket npm:

  • Mengonversi konfigurasi pengguna menjadi Cloud Functions parameter (bagian 5).

  • Mengonversi secret menjadi Cloud Functions secret (bagian 6).

  • Mengonversi peran IAM menjadi deklarasi requiresRole(...) (bagian 8).

  • Mengonversi Google API yang diperlukan menjadi deklarasi requiresAPI(...) jika sesuai (bagian 8).

  • Mengonversi hook penginstalan dan update menjadi deklarasi afterFirstDeploy(...) dan afterRedeploy(...) (bagian 8).

Contoh yang berfungsi: Stream Firestore to BigQuery

Membaca firestore-bigquery-export/extension.yaml dan functions/ akan menghasilkan inventaris ini:

Di extension.yaml Jumlah / nilai Tempatnya
params 25 (COLLECTION_PATH, DATASET_ID, TABLE_ID, DATASET_LOCATION, VIEW_TYPE, …) Cloud Functions parameter (bagian 5)
apis bigquery.googleapis.com requiresAPI(...) (bagian 7)
roles bigquery.dataEditor, datastore.user, bigquery.user requiresRole(...) (bagian 7)
resources 1 pemicu peristiwa (fsexportbigquery) + fungsi task queue (initBigQuerySync, setupBigQuerySync) Fungsi paket yang diekspor (bagian 3)
lifecycleEvents onInstall → initBigQuerySync; onUpdate / onConfigure → setupBigQuerySync afterFirstDeploy / afterRedeploy (bagian 9)
scripts/ import/ (backfill), gen-schema-view/ Dipertahankan sebagai skrip (di luar cakupan di sini)

Ekstensi tidak mendeklarasikan parameter type: secret, sehingga tidak ada yang perlu dimigrasikan di bagian 6 panduan ini. Pemicu peristiwa sudah generasi ke-2; hanya fungsi task queue yang masih generasi ke-1 (relevan di bagian 3).

Memperbarui package.json

Perbarui file package.json ekstensi Anda. Jika Anda memigrasikan satu ekstensi, ini dapat berupa package.json root. Jika Anda memigrasikan banyak ekstensi dalam satu repositori, berikan setiap ekstensi paketnya sendiri.

Versi SDK minimum. Deklarasikan firebase-functions >= 7.3 dan firebase-admin >= 14.2.0 sebagai dependensi. Deklarasikan versi firebase-functions Anda sebagai dependensi peer juga

{
  "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.3.0"
  },
  "dependencies": {
    "firebase-functions": "^7.3.0",
    "firebase-admin": "^14.2.0"
  }
}

Deklarasikan firebase-functions sebagai dependensi peer selain dependensi normal Anda, sehingga project Cloud Functions pengguna Anda memiliki versi SDK yang sama dengan yang digunakan untuk menulis library Anda.

Contoh yang berfungsi: Stream Firestore to BigQuery

Sebelum. functions/package.json ekstensi bersifat pribadi, memberi nama ID ekstensi, dan mendeklarasikan firebase-functions sebagai dependensi langsung:

{
  "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"
  }
}

Setelah. Paket yang dapat dipublikasikan: nama cakupan, peta ekspor, dan firebase-functions dipindahkan ke peerDependencies:

{
  "name": "@firebase/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.3.0" },
  "dependencies": {
      "@firebaseextensions/firestore-bigquery-change-tracker": "^2.0.4",
      "firebase-admin": "^14.2.0",
      "firebase-functions": "^7.3.0"
    }
}

Mengupgrade fungsi dari generasi ke-1 ke generasi ke-2

Jika ekstensi Anda masih mengekspor fungsi generasi ke-1, konversikan setiap fungsi ke fungsi generasi ke-2 yang setara. Impor dari modul firebase-functions/..., dan teruskan setelan runtime dalam opsi fungsi.

Anda dapat meminimalkan upaya penulisan ulang dengan pembongkaran peristiwa yang ditambal generasi ke-2 dan menghindari penulisan ulang logika fungsi karena SDK generasi ke-2 kini mengekspos parameter V1 sebagai kolom dalam objek peristiwa, sehingga Anda dapat menggunakan parameter yang dibongkar/diberi nama dan mempertahankan logika bisnis Anda tanpa perubahan.

Sebelum. Generasi ke-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);
  });

Setelah. Generasi ke-2:

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

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

Lihat Cloud Functions perbandingan versi untuk mengetahui daftar lengkap perbedaan antara fungsi generasi ke-1 dan generasi ke-2.

Mengonversi parameter dan secret ekstensi

Mengonversi parameter

Setiap parameter yang Anda deklarasikan di extension.yaml akan menjadi Cloud Functions parameter.

Mengonversi pembacaan lingkungan langsung:

const collectionPath = process.env.COLLECTION_PATH;

menjadi parameter 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);
  }
);

Gunakan collectionPath.value() untuk membaca string di dalam pengendali; gunakan collectionPath secara langsung jika placeholder diharapkan, seperti jalur pemicu fungsi.

The Firebase CLI menemukan parameter Anda dan membaca nilainya dari .env, .env.projectId, atau meminta pengguna Anda selama deployment. Pertahankan nama parameter yang sama, sehingga nilai dari penginstalan yang ada akan dipertahankan.

Anda tidak boleh mengubah nama parameter yang dideklarasikan dalam kode Anda sama sekali. Migrasi ekstensi akan mempertahankan nilai parameter pengguna akhir yang ada secara otomatis, tetapi hanya jika namanya tidak berubah.

Contoh yang berfungsi: Stream Firestore to BigQuery Sebelum. Parameter yang dideklarasikan di extension.yaml, dibaca sebagai variabel lingkungan mentah di config.ts:

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

Setelah. Satu defineString; CLI menemukannya dan membaca dari .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, analagous to "required: true" in extensions.yaml
  input: { text: { nonEmpty: true} }
}),

Nama parameter tidak berubah, sehingga .env yang ada akan terus berfungsi.

Mengonversi secret

Di extension.yaml, Anda mendeklarasikan secret dengan type: secret. Runtime Extensions menyimpan dan mengikatnya, sehingga kode ekstensi Anda dapat membaca process.env.PARAM_NAME secara langsung. Dalam codebase Cloud Functions yang umum, Anda mendeklarasikan dan mengikat setiap secret secara eksplisit:

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

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

Setelah ekstensi Anda dimigrasikan ke paket/kit npm, referensi secret akan dikelola dalam file .env pengguna akhir. Anda tidak boleh mengubah nama secret yang dideklarasikan dalam kode Anda sama sekali. Selama migrasi, secret pengguna akhir akan dimigrasikan sebagaimana mestinya.

Contoh yang berfungsi: Trigger Email From Cloud Firestore

Sebelum. MAIL_COLLECTION dan SMTP_PASSWORD dibaca sebagai variabel lingkungan mentah di 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,

Setelah. Satu defineString dan satu defineSecret; CLI menemukan keduanya dan membaca dari .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();
    // ...
  }
);

Memigrasikan panggilan task queue internal

Beberapa ekstensi mengantrekan pekerjaan ke task queue mereka sendiri dari dalam kode fungsi mereka, menggunakan Firebase Admin SDK. Hal ini berbeda dengan menerima tugas yang dikirim (dibahas di bagian Mengupgrade fungsi dan Mengonversi hook siklus proses). Di sini, kode Anda adalah produsen yang memanggil queue.enqueue(...).

Versi Admin SDK sebelumnya mengharuskan ekstensi untuk meneruskan ID instance ekstensi mereka sendiri sebagai parameter kedua untuk menargetkan fungsi Task Queue di ekstensi yang sama. Mulai `firebase-admin` 14.2.0, hal ini tidak diperlukan maupun direkomendasikan. Task Queue API kini akan menargetkan task queue dalam konteks yang sama (misalnya, ekstensi) secara default. Anda dapat dan sebaiknya menghapus parameter ini dalam kode Anda, baik sebagai ekstensi maupun sebagai fungsi mandiri. Menghapus parameter ini akan memastikan portabilitas dan kompatibilitas penerusan.

Semua hal lainnya tentang panggilan antrean — jalur resource lokasi/region/fungsi/name, payload tugas, dan logika percobaan ulang Anda — tetap sama.

Lihat /docs/functions/task-functions untuk mengetahui detail selengkapnya tentang fungsi antrean dengan Cloud Tasks.

Sebelum. Ekstensi generasi ke-1

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

Setelah. Ekstensi generasi ke-2

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

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

Jika panggilan antrean Anda menargetkan codebase dengan awalan, nama fungsi yang ditemukan juga akan diberi awalan (misalnya, orders-syncBigQuery).

Mendeklarasikan API dan peran IAM yang diperlukan

Pindahkan persyaratan IAM dan API ekstensi Anda dari extension.yaml ke dalam kode:

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

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

Dengan keamanan deklaratif, Firebase CLI akan membuat atau memperbarui akun layanan runtime terkelola untuk codebase, dan memberikannya gabungan semua peran yang dideklarasikan. Dokumentasikan untuk pengguna Anda bahwa semua fungsi dalam codebase berjalan dengan peran tersebut, kecuali jika API akhir mendukung model yang lebih sempit.

Contoh yang berfungsi: Stream Firestore to BigQuery

Sebelum. Dideklarasikan di extension.yaml; runtime Extensions mengaktifkan API dan memberikan peran ke akun terkelola:

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

Setelah. Dideklarasikan dalam kode dengan requiresAPI dan requiresRole:

import { requiresAPI, requiresRole } from "firebase-functions/";
requiresAPI("bigquery.googleapis.com",
  "Needed to write changelog rows and views");
requiresRole("roles/biguqery.dataEditor");
requiresRole("roles/datastore.user");
requiresRole("roles/bigquery.user");

Mengonversi hook siklus proses

Jika ekstensi Anda memanggil getExtensions().runtime(), misalnya setProcessingState atau setFatalError, hapus panggilan tersebut, karena panggilan tersebut akan menampilkan error jika dipanggil dari fungsi generasi ke-2 yang di-deploy secara normal. Status siklus proses kini didorong oleh afterFirstDeploy dan afterRedeploy jika pelacakan status ini tidak digunakan.

Firebase Extensions dapat menjalankan penyiapan saat pengguna menginstal, mengupdate, atau mengonfigurasi ulang ekstensi. Dalam paket npm Anda, deklarasikan tindakan siklus proses yang setara dalam kode.

Untuk penyiapan satu kali:

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: {}
  }
});

Untuk update konfigurasi atau kode:

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

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

Buat tindakan siklus proses Anda menjadi idempoten. Pengguna Anda mungkin perlu menjalankannya kembali secara manual jika pengiriman atau eksekusi gagal:

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

Contoh yang berfungsi: Stream Firestore to BigQuery

Sebelum. lifecycleEvents di extension.yaml, didorong oleh runtime Extensions:

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

Setelah. Dideklarasikan dalam kode; tugas menyediakan BigQuery pada deployment pertama:

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

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

Penyediaan bersifat idempoten, sehingga menjalankan ulang akan merekonsiliasi set data, tabel, dan tampilan. Pengguna dapat menjalankan ulang secara manual dengan firebase functions:lifecycle:run afterFirstDeploy CODEBASE_NAME.

Mendokumentasikan penyiapan untuk pengguna Anda

  • Tulis README paket yang menjelaskan, minimal:

  • Nilai .env yang diperlukan paket.

  • Secret yang diperlukan paket, dan cara memigrasikan nilai secret yang ada.

  • Peran IAM yang dideklarasikan paket dengan requiresRole(...).

  • Google API yang diaktifkan atau diperlukan paket.

  • Hook siklus proses yang dideklarasikan paket, dan cara menjalankannya kembali secara manual.

  • Catatan penagihan.

  • Perubahan dibandingkan dengan ekstensi asli.

Contoh yang berfungsi: Stream Firestore to BigQuery

README paket mengirimkan tabel "apa yang berubah" yang konkret:

Kepedulian Sebagai ekstensi Sebagai @firebase/firestore-bigquery-export
Konfigurasi Parameter ekstensi Parameter fungsi melalui .env
IAM Diberikan oleh Extensions requiresRole(...), diterapkan saat deployment
Penyediaan Tugas siklus proses oleh Extensions Tugas afterFirstDeploy / afterRedeploy
Nama fungsi ext-instanceId-fsexportbigquery fsexportbigquery (opsional dengan awalan)

Menguji fungsi generasi ke-2

Sekarang Anda akan memiliki fungsi generasi ke-2 yang, saat di-deploy, akan berperilaku sama dengan penginstalan baru ekstensi Anda. Langkah terakhir adalah memverifikasi dan memperbaiki masalah yang tidak sengaja muncul di sepanjang proses.

Pastikan Anda menggunakan firebase-tools >= 15.25.1, dan deploy fungsi generasi ke-2 yang dikonversi ke project pengujian dengan resource yang sesuai untuk menguji perilakunya. Jika Anda sudah menyiapkan project pengujian dari pengujian ekstensi, gunakan perintah:

firebase deploy --only functions

Setelah memasukkan perintah ini, isi wizard yang dihasilkan yang meminta nilai parameter dengan cara yang sama seperti saat Anda mengisi formulir penginstalan di konsol Firebase untuk ekstensi.

Contoh yang berfungsi: Stream Firestore to BigQuery

Kami memverifikasi sinkronisasi Cloud Firestore ke BigQuery secara menyeluruh:

  1. Di konsol Cloud Firestore, buat koleksi yang Anda tetapkan sebagai COLLECTION_PATH (pengguna) jika belum ada.
  2. Buat dokumen bernama bigquery-mirror-test yang berisi kolom apa pun dengan nilai apa pun.
  3. Di konsol BigQuery, kueri tabel log perubahan mentah. Tabel ini harus berisi satu baris yang mencatat pembuatan dokumen:
SELECT * FROM `PROJECT_ID.analytics.users_raw_changelog`
  1. Ajukan kueri untuk tampilan terakhir, yang harus menampilkan peristiwa perubahan terakhir untuk hanya dokumen yang ada: bigquery-mirror-test
SELECT * FROM `PROJECT_ID.analytics.users_raw_latest`
  1. Hapus dokumen bigquery-mirror-test di Cloud Firestore. Dokumen tersebut akan menghilang dari tampilan terakhir, dan peristiwa DELETE akan ditambahkan ke tabel log perubahan mentah.

Anda dapat memeriksa histori lengkap satu dokumen dengan:

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

Perbedaan dari pengujian ekstensi:

  • Pemicu di-deploy sebagai fsexportbigquery (opsional dengan awalan codebase), bukan ext-&lt;instanceId&gt;-fsexportbigquery. Cari nama tersebut di Cloud Functions dasbor dan log.
  • Kode Anda kini akan berjalan di Firebase Local Emulator Suite sebagai fungsi normal. Anda dapat menetapkan nilai parameter yang akan digunakan di emulator dengan .env.local. Anda juga dapat menguji unit kode menggunakan firebase-functions-test SDK seperti yang dijelaskan dalam Pengujian unit Cloud Functions
  • Penyediaan tidak lagi didorong oleh runtime Extensions. Jika tabel log perubahan tidak ada setelah deployment, jalankan kembali tugas penyiapan secara manual: firebase functions:lifecycle:run afterFirstDeploy CODEBASE_NAME. Tugas ini bersifat idempoten, sehingga menjalankannya kembali akan merekonsiliasi set data, tabel, dan tampilan.
  • Nilai parameter berasal dari .env, bukan formulir penginstalan, sehingga menjalankan ulang firebase deploy bersifat non-interaktif setelah .env selesai.