Preparare le estensioni Firebase per la migrazione a Cloud Functions

Questa guida mostra come eseguire la migrazione delle estensioni dall'ambiente deprecato Firebase Extensions a una funzione che gli utenti installano ed eseguono il deployment nel proprio Cloud Functions per Firebase (2ª gen.) codebase.

Questo è il percorso di migrazione consigliato. Firebase manterrà un elenco di estensioni con equivalenti npm ufficiali; questa guida ti illustrerà la procedura per crearne una.

In questa guida, l'estensione Stream Firestore to BigQuery (firestore-bigquery-export) viene utilizzata come esempio. Ogni sezione termina con un esempio pratico che mostra l'aspetto dell'estensione prima della migrazione e l'aspetto dopo, come pacchetto @firebase/firestore-bigquery-export.

Iscriviti per ricevere maggiori informazioni e assistenza sulla migrazione delle estensioni

Se hai domande su come eseguire la migrazione da Firebase Extensions, puoi contattarci all'indirizzo firebase-extensions-migrator-support-external@google.com. Invieremo un'email a questo gruppo anche quando aggiorneremo la guida con ulteriori informazioni su packaging, test e distribuzione delle funzioni di 2ª gen.

Per partecipare a questo gruppo, invia un messaggio a firebase-extensions-migrator-support-external+subscribe@google.com, che risponderà con un'email di richiesta di iscrizione. Devi rispondere a questa email, non fare clic sul pulsante "Partecipa a questo gruppo".

Prima di iniziare

Per completare questa migrazione, utilizzerai le seguenti funzionalità di Cloud Functions:

  • Configurazione con parametri. Ogni parametro dichiarato in extension.yaml diventa un parametro definito nel codice del pacchetto.

  • Ruoli IAM dichiarativi e API richieste. Ogni ruolo dichiarato in extension.yaml diventa una chiamata requiresRole(...) e ogni API diventa una chiamata requiresAPI(...) nel codice della funzione. Al momento del deployment, la Firebase CLI concede i ruoli dichiarati a un account di servizio di runtime gestito e abilita le API dichiarate per tuo conto.

  • Eventi del ciclo di vita per i codebase Cloud Functions. Cloud Functions i codebase ora supportano gli eventi del ciclo di vita analoghi a Firebase Extensions. Dichiara la configurazione al momento dell'installazione e dell'aggiornamento con gli hook del ciclo di vita afterFirstDeploy(...) e afterRedeploy(...). Questi sostituiscono lifecycleEvents dichiarati in extension.yaml.

Inventario dell'estensione

Inizia creando un inventario dell'estensione: un elenco completo di tutto ciò che l'estensione dichiara, distribuisce e documenta, in modo che ogni comportamento abbia una destinazione definita nella funzione di 2ª gen. e che non vada perso nulla durante la migrazione.

Esamina ciascuno dei seguenti elementi e prendi nota di ciò che trovi:

  • extension.yaml, che dichiara i parametri, le funzioni, gli eventi, i ruoli IAM, le API richieste, i secret e gli hook del ciclo di vita.

  • functions/, che contiene il codice della funzione, le dipendenze, la configurazione della build, i trigger e le funzioni della coda di attività.

  • README.md, PREINSTALL.md e POSTINSTALL.md, che contengono passaggi di configurazione, avvisi e note di fatturazione.

  • scripts/, che contiene tutte le utilità di importazione, backfill, IAM, riparazione o migrazione e qualsiasi altro strumento fornito insieme all'estensione.

Poi, per ogni elemento in extension.yaml, decidi dove inserirlo nel pacchetto npm:

  • Converti la configurazione utente in parametri Cloud Functions (sezione 5).

  • Converti i secret in Cloud Functions secret (sezione 6).

  • Converti i ruoli IAM in dichiarazioni requiresRole(...) (sezione 8).

  • Converti le API di Google richieste in dichiarazioni requiresAPI(...) ove appropriato (sezione 8).

  • Converti gli hook di installazione e aggiornamento in dichiarazioni afterFirstDeploy(...) e afterRedeploy(...) (sezione 8).

Esempio pratico: Stream Firestore to BigQuery

La lettura di firestore-bigquery-export/extension.yaml e functions/ produce questo inventario:

In extension.yaml Conteggio / valore Dove si trova
params 25 (COLLECTION_PATH, DATASET_ID, TABLE_ID, DATASET_LOCATION, VIEW_TYPE, …) Cloud Functions parametri (sezione 5)
apis bigquery.googleapis.com requiresAPI(...) (sezione 7)
roles bigquery.dataEditor, datastore.user, bigquery.user requiresRole(...) (sezione 7)
resources 1 trigger evento (fsexportbigquery) + funzioni della coda di attività (initBigQuerySync, setupBigQuerySync) Funzioni del pacchetto esportato (sezione 3)
lifecycleEvents onInstall → initBigQuerySync; onUpdate / onConfigure → setupBigQuerySync afterFirstDeploy / afterRedeploy (sezione 9)
scripts/ import/ (backfill), gen-schema-view/ Mantenuti come script (non trattati qui)

L'estensione non dichiara alcun tipo: parametri secret, quindi non è necessario eseguire la migrazione nella sezione 6 di questa guida. Il trigger evento è già di 2ª gen.; solo le funzioni della coda di attività sono ancora di 1ª gen. (rilevanti nella sezione 3).

Aggiorna package.json

Aggiorna il file package.json dell'estensione. Se stai eseguendo la migrazione di un'estensione, può essere la package.json principale. Se stai eseguendo la migrazione di molte estensioni in un repository, assegna a ogni estensione il proprio pacchetto.

Versioni SDK minime. Dichiara firebase-functions >= 7.3 e firebase-admin >= 14.2.0 come dipendenze. Dichiara anche la tua versione di firebase-functions come dipendenza peer.

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

Dichiara firebase-functions come dipendenza peer oltre alla dipendenza normale, in modo che il progetto Cloud Functions degli utenti abbia la stessa versione dell'SDK con cui è stata scritta la libreria.

Esempio pratico: Stream Firestore to BigQuery

Prima. Il file functions/package.json dell'estensione è privato, nomina l'ID dell'estensione e dichiara firebase-functions come dipendenza diretta:

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

Dopo. Un pacchetto pubblicabile: nome con ambito, una mappa di esportazioni e firebase-functions spostato in 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"
    }
}

Esegui l'upgrade delle funzioni da 1ª gen. a 2ª gen.

Se l'estensione esporta ancora funzioni di 1ª gen., converti ogni funzione nel suo equivalente di 2ª gen. Importa dai moduli firebase-functions/... e passa le impostazioni di runtime nelle opzioni della funzione.

Puoi ridurre al minimo gli sforzi di riscrittura con la destrutturazione degli eventi con patch di 2ª gen. ed evitare di riscrivere la logica della funzione perché l'SDK di 2ª gen. ora espone i parametri V1 come campi nell'oggetto evento, consentendoti di utilizzare parametri destrutturati/denominati e mantenere invariata la logica di business .

Prima. 1ª gen.:

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

Dopo. 2ª gen.:

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

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

Per un elenco completo delle differenze tra le funzioni di 1ª gen. e 2ª gen., consulta il confronto tra le versioniCloud Functions.

Converti i parametri e i secret dell'estensione

Converti i parametri

Ogni parametro dichiarato in extension.yaml diventa un Cloud Functions parametro.

Converti le letture dirette dell'ambiente:

const collectionPath = process.env.COLLECTION_PATH;

in parametri di 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);
  }
);

Utilizza collectionPath.value() per leggere la stringa all'interno di un gestore; utilizza collectionPath direttamente dove è previsto un segnaposto, ad esempio un percorso di trigger della funzione.

La Firebase CLI rileva i parametri e legge i relativi valori da .env, .env.projectId, o richiede agli utenti durante il deployment. Mantieni gli stessi nomi dei parametri, in modo che i valori di un'installazione esistente vengano trasferiti.

È importante non modificare in alcun modo i nomi dei parametri dichiarati nel codice. La migrazione dell'estensione manterrà automaticamente il valore del parametro dell'utente finale esistente, ma solo se i nomi rimangono invariati.

Esempio pratico: Stream Firestore to BigQuery Prima. Un parametro dichiarato in extension.yaml, letto come variabile di ambiente non elaborata in config.ts:

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

Dopo. Un defineString; la CLI lo rileva e legge da .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} }
}),

Il nome del parametro non è cambiato, quindi un file .env esistente continua a funzionare.

Converti i secret

In extension.yaml, dichiara i secret con type: secret. Il runtime di Extensions li archivia e li associa, in modo che il codice dell'estensione possa leggere direttamente process.env.PARAM_NAME. In un codebase Cloud Functions tipico, dichiari e associ ogni secret in modo esplicito:

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

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

Una volta eseguita la migrazione dell'estensione a un pacchetto/kit npm, i riferimenti ai secret verranno gestiti nel file .env dell'utente finale. È importante non modificare in alcun modo i nomi dei secret dichiarati nel codice. Durante la migrazione, i secret dell'utente finale verranno migrati di conseguenza.

Esempio pratico: Trigger Email From Cloud Firestore

Prima. MAIL_COLLECTION e SMTP_PASSWORD letti come variabili di ambiente non elaborate in 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,

Dopo. Un defineString e un defineSecret; la CLI li rileva entrambi e legge da .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();
    // ...
  }
);

Esegui la migrazione delle chiamate interne alla coda di attività

Alcune estensioni accodano il lavoro alle proprie code di attività dall'interno del loro codice funzione, utilizzando Firebase Admin SDK. Questo è diverso dalla ricezione di un'attività inviata (trattata nelle sezioni Esegui l'upgrade delle funzioni e Converti gli hook del ciclo di vita). Qui il codice è il produttore che chiama queue.enqueue(...).

Le versioni precedenti di Admin SDK richiedevano che le estensioni passassero il proprio ID istanza dell'estensione come secondo parametro per indirizzare una funzione della coda di attività in nella stessa estensione. A partire da `firebase-admin` 14.2.0, questo non è né richiesto né consigliato. L'API Task Queues ora indirizzerà le code di attività nello stesso contesto (ad es. estensione) per impostazione predefinita. È sicuro e consigliato rimuovere questo parametro nel codice sia come estensione sia come funzioni autonome. La rimozione di questo parametro garantisce la portabilità e la compatibilità con le versioni successive.

Tutto il resto della chiamata enqueue, il percorso della risorsa locations/region/functions/name, il payload dell'attività e la logica di ripetizione, rimangono invariati.

Per ulteriori dettagli sull'accodamento delle funzioni con Cloud Tasks, consulta /docs/functions/task-functions.

Prima. Estensione di 1ª gen.

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

Dopo. Estensione di 2ª gen.

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

Se la chiamata enqueue indirizza un codebase con prefisso, anche il nome della funzione rilevato ha il prefisso (ad esempio, orders-syncBigQuery).

Dichiara le API e i ruoli IAM richiesti

Sposta i requisiti IAM e API dell'estensione da extension.yaml al codice:

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

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

Con la sicurezza dichiarativa, la Firebase CLI crea o aggiorna un account di servizio di runtime gestito per il codebase e gli concede l'unione di tutti i ruoli dichiarati. Documenta per gli utenti che tutte le funzioni nel codebase vengono eseguite con questi ruoli, a meno che l'API finale non supporti un modello più ristretto.

Esempio pratico: Stream Firestore to BigQuery

Prima. Dichiarato in extension.yaml; il runtime di Extensions ha abilitato l'API e ha concesso i ruoli a un account gestito:

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

Dopo. Dichiarato nel codice con requiresAPI e 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");

Converti gli hook del ciclo di vita

Se l'estensione chiama getExtensions().runtime(), ad esempio setProcessingState o setFatalError, elimina queste chiamate, perché genereranno un errore se chiamate da una funzione di 2ª gen. con deployment normale. Lo stato del ciclo di vita è ora determinato da afterFirstDeploy e afterRedeploy dove non viene utilizzato questo monitoraggio dello stato.

Firebase Extensions può eseguire la configurazione quando un utente installa, aggiorna o riconfigura un'estensione. Nel pacchetto npm, dichiara le azioni del ciclo di vita equivalenti nel codice.

Per la configurazione una tantum:

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

Per gli aggiornamenti di configurazione o codice:

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

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

Rendi idempotenti le azioni del ciclo di vita. Gli utenti potrebbero doverle eseguire di nuovo manualmente se l'invio o l'esecuzione non riesce:

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

Esempio pratico: Stream Firestore to BigQuery

Prima. lifecycleEvents in extension.yaml, gestiti dal runtime di Extensions:

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

Dopo. Dichiarato nel codice; l'attività esegue il provisioning di BigQuery al primo deployment:

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

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

Il provisioning è idempotente, quindi una nuova esecuzione riconcilia il set di dati, la tabella e le viste. Gli utenti possono eseguire di nuovo manualmente con firebase functions:lifecycle:run afterFirstDeploy CODEBASE_NAME.

Documenta la configurazione per gli utenti

  • Scrivi un file README del pacchetto che spieghi, come minimo:

  • I valori .env richiesti dal pacchetto.

  • I secret richiesti dal pacchetto e come eseguire la migrazione dei valori dei secret esistenti.

  • I ruoli IAM dichiarati dal pacchetto con requiresRole(...).

  • Le API di Google abilitate o richieste dal pacchetto.

  • Gli hook del ciclo di vita dichiarati dal pacchetto e come eseguirli di nuovo manualmente.

  • Note di fatturazione.

  • Cosa è cambiato rispetto all'estensione originale.

Esempio pratico: Stream Firestore to BigQuery

Il file README del pacchetto include una tabella concreta "Cosa è cambiato":

Problema Come estensione Come @firebase/firestore-bigquery-export
Configurazione Parametri dell'estensione Parametri delle funzioni tramite .env
IAM Concesso da Extensions requiresRole(...), applicato al deployment
Provisioning Attività del ciclo di vita di Extensions Attività afterFirstDeploy / afterRedeploy
Nomi delle funzioni ext-instanceId-fsexportbigquery fsexportbigquery (con prefisso facoltativo)

Testa la funzione di 2ª gen.

Ora dovresti avere una funzione di 2ª gen. che, una volta eseguito il deployment, si comporterà in modo identico a una nuova installazione dell'estensione. L'ultimo passaggio consiste nel verificare e correggere eventuali problemi introdotti accidentalmente durante la procedura.

Assicurati di utilizzare firebase-tools >= 15.24.0 ed esegui il deployment della funzione di 2ª gen. convertita in un progetto di test con le risorse appropriate per testarne il comportamento. Se hai già configurato un progetto di test per testare l'estensione, utilizza il comando:

firebase deploy --only functions

Dopo aver inserito questo comando, compila la procedura guidata risultante che ti chiede i valori dei parametri nello stesso modo in cui avresti compilato il modulo di installazione nella Firebase console per l'estensione.

Esempio pratico: Stream Firestore to BigQuery

Verifichiamo la sincronizzazione end-to-end di Cloud Firestore con BigQuery:

  1. Nella console Cloud Firestore, crea la raccolta impostata come COLLECTION_PATH (users) se non esiste già.
  2. Crea un documento denominato bigquery-mirror-test contenente tutti i campi con tutti i valori.
  3. Nella console BigQuery, esegui una query della tabella del log delle modifiche non elaborato. Dovrebbe contenere una singola riga che registra la creazione del documento:
SELECT * FROM `PROJECT_ID.analytics.users_raw_changelog`
  1. Esegui una query della vista più recente, che dovrebbe restituire l'evento di modifica più recente per l'unico documento presente: bigquery-mirror-test
SELECT * FROM `PROJECT_ID.analytics.users_raw_latest`
  1. Elimina il documento bigquery-mirror-test in Cloud Firestore. Scompare dalla vista più recente e alla tabella del log delle modifiche non elaborato viene aggiunto un evento DELETE.

Puoi ispezionare la cronologia completa di un singolo documento con:

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

Differenze rispetto al test dell'estensione:

  • Il trigger viene eseguito il deployment come fsexportbigquery (con prefisso codebase facoltativo), non ext-&lt;instanceId&gt;-fsexportbigquery. Cerca questo nome nella Cloud Functions dashboard e nei log.
  • Il codice verrà ora eseguito in Firebase Local Emulator Suite come funzioni normali. Puoi impostare il valore dei parametri da utilizzare nell'emulatore con .env.local. Puoi anche eseguire test delle unità del codice utilizzando l' SDK firebase-functions-test come descritto in Test delle unità di Cloud Functions
  • Il provisioning non è più gestito dal runtime di Extensions. Se la tabella del log delle modifiche non è presente dopo il deployment, esegui di nuovo manualmente l'attività di configurazione: firebase functions:lifecycle:run afterFirstDeploy CODEBASE_NAME. L'attività è idempotente, quindi la sua esecuzione di nuovo riconcilia il set di dati, la tabella e le viste.
  • I valori dei parametri provengono da .env anziché dal modulo di installazione, quindi le esecuzioni di nuovo di firebase deploy non sono interattive una volta completato .env.