Mempersiapkan Firebase Extensions untuk migrasi ke Cloud Functions

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

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

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

Daftar untuk mendapatkan informasi dan bantuan selengkapnya tentang memigrasikan 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 kepada grup ini saat kami memperbarui panduan dengan informasi selengkapnya tentang pengemasan, pengujian, dan pendistribusian 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 "Gabung ke Grup Ini".

Sebelum memulai

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

  • 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 menjadi panggilan requiresRole(...), dan setiap API menjadi panggilan requiresAPI(...) dalam kode fungsi Anda. Pada waktu deployment, CLI Firebase memberikan peran yang dideklarasikan ke akun layanan runtime terkelola dan mengaktifkan API yang dideklarasikan atas nama Anda.

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

Inventaris Ekstensi

Mulailah dengan membuat inventaris ekstensi Anda: daftar lengkap semua hal 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.

  • functions/, yang berisi kode fungsi, dependensi, konfigurasi build, pemicu, dan fungsi antrean tugas.

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

  • scripts/, yang berisi utilitas impor, pengisian ulang, IAM, perbaikan, atau migrasi, dan alat lainnya yang Anda kirimkan bersama ekstensi.

Kemudian, untuk setiap item di extension.yaml, tentukan tempat item tersebut dalam paket npm:

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

  • Konversi secret menjadi secret Cloud Functions (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 penggunaan: Streaming Firestore ke BigQuery

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

Di extension.yaml Jumlah / nilai Tujuan data
params 25 (COLLECTION_PATH, DATASET_ID, TABLE_ID, DATASET_LOCATION, VIEW_TYPE, …) Parameter Cloud Functions (bagian 5)
api bigquery.googleapis.com requiresAPI(...) (bagian 7)
roles bigquery.dataEditor, datastore.user, bigquery.user requiresRole(...) (bagian 7)
resource 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/ (pengisian ulang), gen-schema-view/ Dipertahankan sebagai skrip (di luar cakupan di sini)

Ekstensi tidak mendeklarasikan jenis: parameter rahasia, jadi tidak ada yang perlu dimigrasikan di bagian 6 panduan ini. Pemicu peristiwa sudah generasi ke-2; hanya fungsi antrean tugas yang masih generasi ke-1 (relevan di bagian 3).

Perbarui package.json

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

Versi SDK minimum. Nyatakan 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 penggunaan: Streaming Firestore ke 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 yang diberi 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, konversi setiap fungsi ke fungsi generasi ke-2 yang setara. Impor dari modul firebase-functions/..., dan teruskan setelan runtime di opsi fungsi.

Anda dapat meminimalkan upaya penulisan ulang dengan penghancuran peristiwa yang di-patch 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 dihancurkan/bernama 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 perbandingan versi Cloud Functions untuk mengetahui daftar lengkap perbedaan antara fungsi generasi ke-1 dan generasi ke-2.

Mengonversi parameter dan secret ekstensi

Parameter konversi

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

Mengonversi pembacaan lingkungan langsung:

const collectionPath = process.env.COLLECTION_PATH;

ke dalam 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 handler; gunakan collectionPath secara langsung di tempat placeholder diharapkan, seperti jalur pemicu fungsi.

CLI Firebase menemukan parameter Anda dan membaca nilainya dari .env, .env.projectId, atau meminta pengguna Anda selama deployment. Tetapkan 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 penggunaan: Streaming Firestore ke 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 tetap berfungsi.

Mengonversi secret

Di extension.yaml, Anda mendeklarasikan secret dengan type: secret. Runtime ekstensi 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 rahasia akan dikelola dalam file .env pengguna akhir. Anda tidak boleh mengubah nama rahasia yang dideklarasikan dalam kode Anda sama sekali. Selama migrasi, rahasia pengguna akhir akan dimigrasikan sebagaimana mestinya.

Contoh penggunaan: Memicu Email Dari 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 tugas ke antrean tugas sendiri dari dalam kode fungsinya, 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 produser yang memanggil queue.enqueue(...).

Versi Admin SDK sebelumnya mengharuskan ekstensi 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 antrean tugas dalam konteks yang sama (misalnya, ekstensi) secara default. Anda dapat menghapus parameter ini dengan aman dan sebaiknya dilakukan dalam kode Anda, baik sebagai ekstensi maupun sebagai fungsi mandiri. Menghapus parameter ini memastikan portabilitas dan kompatibilitas dengan versi baru.

Semua hal lain tentang panggilan antrean — jalur resource locations/region/functions/name, payload tugas, dan logika percobaan ulang — tetap sama.

Lihat /docs/functions/task-functions untuk mengetahui detail selengkapnya tentang mengantrekan fungsi 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";
import { region } from "firebase-functions/params";

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

Jika panggilan antrean Anda menargetkan codebase yang diberi awalan, nama fungsi yang ditemukan juga 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, CLI Firebase membuat atau memperbarui akun layanan runtime terkelola untuk codebase, dan memberikan 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 penggunaan: Streaming Firestore ke BigQuery

Sebelum. Dideklarasikan dalam extension.yaml; runtime Ekstensi 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 akan memunculkan error jika dipanggil dari fungsi gen 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, 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 bersifat 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 penggunaan: Streaming Firestore ke BigQuery

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

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 penayangan ulang akan merekonsiliasi set data, tabel, dan tampilan. Pengguna dapat menjalankan ulang secara manual dengan firebase functions:lifecycle:run afterFirstDeploy CODEBASE_NAME.

Menyiapkan dokumen untuk pengguna Anda

  • Tulis paket README yang menjelaskan, setidaknya:

  • 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 oleh paket.

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

  • Catatan penagihan.

  • Apa yang berubah dibandingkan dengan ekstensi asli.

Contoh penggunaan: Streaming Firestore ke 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 Ekstensi requiresRole(...), diterapkan saat men-deploy
Penyediaan Tugas siklus proses menurut Ekstensi Tugas afterFirstDeploy / afterRedeploy
Nama fungsi ext-instanceId-fsexportbigquery fsexportbigquery (dapat diawali dengan awalan)

Menguji fungsi generasi ke-2

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

Pastikan Anda menggunakan firebase-tools >= 15.24.0, dan men-deploy fungsi generasi ke-2 yang dikonversi ke dalam 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 penggunaan: Streaming Firestore ke BigQuery

Kami memverifikasi sinkronisasi Cloud Firestore ke BigQuery secara end-to-end:

  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. Log ini harus berisi satu baris yang mencatat pembuatan dokumen:
SELECT * FROM `PROJECT_ID.analytics.users_raw_changelog`
  1. Ajukan kueri untuk tampilan terbaru, yang akan menampilkan peristiwa perubahan terbaru hanya untuk dokumen yang ada: bigquery-mirror-test
SELECT * FROM `PROJECT_ID.analytics.users_raw_latest`
  1. Hapus dokumen bigquery-mirror-test di Cloud Firestore. Perubahan tersebut akan hilang dari tampilan terbaru, dan peristiwa DELETE 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 (dengan awalan codebase secara opsional), bukan ext-&lt;instanceId&gt;-fsexportbigquery. Cari nama tersebut di dasbor dan log Cloud Functions.
  • Kode Anda kini akan berjalan di fungsi Firebase Local Emulator Suite seperti biasa. Anda dapat menetapkan nilai parameter yang akan digunakan di emulator dengan .env.local. Anda juga dapat melakukan pengujian unit pada kode menggunakan firebase-functions-test SDK seperti yang dijelaskan dalam Pengujian unit Cloud Functions
  • Penyediaan tidak lagi didorong oleh runtime Ekstensi. Jika tabel log perubahan tidak ada setelah deployment, jalankan kembali tugas penyiapan secara manual: firebase functions:lifecycle:run afterFirstDeploy CODEBASE_NAME. Tugas ini bersifat idempotent, sehingga menjalankannya kembali akan merekonsiliasi set data, tabel, dan tampilan.
  • Nilai parameter berasal dari .env, bukan formulir penginstalan, sehingga firebase deploy yang dijalankan ulang tidak interaktif setelah .env selesai.