Preparar as Extensões do Firebase para migração para o Cloud Functions

Este guia mostra como migrar suas extensões do ambiente Firebase Extensions descontinuado para uma função que os usuários instalam e implantam na própria base de código do Cloud Functions para Firebase (2ª geração).

Esse é o caminho de migração recomendado. Firebase vai manter uma lista de extensões com equivalentes oficiais do npm. Este guia vai orientar você na criação da sua.

Neste guia, a extensão Stream Firestore to BigQuery (firestore-bigquery-export) é usada como exemplo. Cada seção termina com um exemplo prático que mostra como era a extensão antes da migração e como ela fica depois, como o pacote @firebase/firestore-bigquery-export.

Inscreva-se para receber mais informações e ajuda na migração de extensões

Se você tiver dúvidas sobre como migrar de Firebase Extensions, entre em contato pelo e-mail firebase-extensions-migrator-support-external@google.com. Também vamos enviar e-mails para esse grupo à medida que atualizarmos o guia com mais informações sobre como empacotar, testar e distribuir suas funções de 2ª geração.

Para participar desse grupo, envie uma mensagem para firebase-extensions-migrator-support-external+subscribe@google.com, que vai responder com um e-mail de solicitação de participação. Responda a esse e-mail, não clique no botão "Participar deste grupo".

Antes de começar

Para concluir essa migração, use os seguintes recursos de Cloud Functions:

  • Configuração parametrizada. Cada parâmetro declarado em extension.yaml se torna um parâmetro definido no código do pacote.

  • Papéis declarativos do IAM e APIs necessárias. Cada papel declarado em extension.yaml se torna uma chamada requiresRole(...), e cada API se torna uma chamada requiresAPI(...) no código da função. No momento da implantação, a Firebase CLI concede os papéis declarados a uma conta de serviço de ambiente de execução gerenciada e ativa as APIs declaradas em seu nome.

  • Eventos de ciclo de vida para bases de código Cloud Functions. Cloud Functions bases de código agora oferecem suporte a eventos de ciclo de vida análogos a Firebase Extensions. Declare a configuração de instalação e atualização com os hooks de ciclo de vida afterFirstDeploy(...) e afterRedeploy(...). Eles substituem os lifecycleEvents declarados em extension.yaml.

Inventário da extensão

Comece fazendo um inventário da extensão: uma lista completa de tudo o que a extensão declara, envia e documenta, para que cada comportamento tenha um destino definido na função de 2ª geração e nada seja perdido na migração.

Analise cada um dos itens a seguir e anote o que encontrar:

  • extension.yaml, que declara parâmetros, funções, eventos, papéis do IAM, APIs necessárias, secrets e hooks de ciclo de vida.

  • functions/, que contém o código da função, dependências, configuração de build, acionadores e funções da fila de tarefas.

  • README.md, PREINSTALL.md e POSTINSTALL.md, que contêm etapas de configuração, avisos e observações de faturamento.

  • scripts/, que contém utilitários de importação, preenchimento, IAM, reparo ou migração e outras ferramentas enviadas com a extensão.

Em seguida, para cada item em extension.yaml, decida para onde ele vai no pacote npm:

  • Converter a configuração do usuário em Cloud Functions parâmetros (seção 5).

  • Converter secrets em Cloud Functions secrets (seção 6).

  • Converter papéis do IAM em declarações requiresRole(...) (seção 8).

  • Converter as APIs do Google necessárias em declarações requiresAPI(...), quando apropriado (seção 8).

  • Converter hooks de instalação e atualização em declarações afterFirstDeploy(...) e afterRedeploy(...) (seção 8).

Exemplo prático: Stream Firestore to BigQuery

A leitura de firestore-bigquery-export/extension.yaml e functions/ produz este inventário:

Em extension.yaml Contagem / valor Para onde ele vai
params 25 (COLLECTION_PATH, DATASET_ID, TABLE_ID, DATASET_LOCATION, VIEW_TYPE, …) Cloud Functions parâmetros (seção 5)
apis bigquery.googleapis.com requiresAPI(...) (seção 7)
roles bigquery.dataEditor, datastore.user, bigquery.user requiresRole(...) (seção 7)
resources 1 acionador de evento (fsexportbigquery) + funções da fila de tarefas (initBigQuerySync, setupBigQuerySync) Funções de pacote exportadas (seção 3)
lifecycleEvents onInstall → initBigQuerySync; onUpdate / onConfigure → setupBigQuerySync afterFirstDeploy / afterRedeploy (seção 9)
scripts/ import/ (preenchimento), gen-schema-view/ Mantido como scripts (fora do escopo aqui)

A extensão não declara nenhum parâmetro de tipo: secret. Portanto, não há nada para migrar na seção 6 deste guia. O acionador de eventos já é de 2ª geração. Somente as funções da fila de tarefas ainda são de 1ª geração (relevante na seção 3).

Atualizar package.json

Atualize o arquivo package.json da extensão. Se você estiver migrando uma extensão, esse poderá ser o package.json raiz. Se você estiver migrando muitas extensões em um repositório, atribua a cada extensão um pacote próprio.

Versões mínimas do SDK. Declare firebase-functions >= 7.3 e firebase-admin >= 14.2.0 como dependências. Declare também sua versão do firebase-functions como uma dependência de mesmo nível.

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

Declare o firebase-functions como uma dependência de mesmo nível, além da dependência normal , para que o projeto Cloud Functions dos usuários tenha a mesma versão do SDK com que sua biblioteca foi escrita.

Exemplo prático: Stream Firestore to BigQuery

Antes. O functions/package.json da extensão é particular, nomeia o ID da extensão e declara o firebase-functions como uma dependência direta:

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

Depois. Um pacote publicável: nome com escopo, um mapa de exportações e firebase-functions movido para 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"
    }
}

Fazer upgrade de funções da 1ª para a 2ª geração

Se a extensão ainda exportar funções de 1ª geração, converta cada função para o equivalente de 2ª geração. Importe dos módulos firebase-functions/... e transmita as configurações de ambiente de execução nas opções de função.

É possível minimizar os esforços de reescrita com a desestruturação de eventos corrigidos de 2ª geração e evitar a reescrita da lógica de função porque o SDK de 2ª geração agora expõe os parâmetros da V1 como campos no objeto de evento, permitindo que você use parâmetros desestruturados/nomeados e mantenha a lógica de negócios inalterada.

Antes. 1ª geração:

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

Depois. 2ª geração:

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

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

Consulte a Cloud Functions comparação de versões para conferir uma lista abrangente de diferenças entre as funções de 1ª e 2ª geração.

Converter parâmetros e secrets de extensão

Converter parâmetros

Cada parâmetro declarado em extension.yaml se torna um Cloud Functions parâmetro.

Converter leituras diretas do ambiente:

const collectionPath = process.env.COLLECTION_PATH;

em Cloud Functions parâmetros:

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

Use collectionPath.value() para ler a string dentro de um gerenciador. Use collectionPath diretamente quando um marcador for esperado, como um caminho de acionador de função .

A Firebase CLI descobre os parâmetros e lê os valores deles em .env, .env.projectId, ou solicita aos usuários durante a implantação. Mantenha os mesmos nomes de parâmetros para que os valores de uma instalação atual sejam transferidos.

É importante não alterar os nomes de parâmetros declarados no código de forma alguma. A migração de extensão vai preservar o valor do parâmetro do usuário final automaticamente, mas apenas quando os nomes estiverem inalterados.

Exemplo prático: Stream Firestore to BigQuery Antes. Um parâmetro declarado em extension.yaml, lido como uma variável de ambiente bruta em config.ts:

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

Depois. Um defineString; a CLI o descobre e lê de .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} }
}),

O nome do parâmetro não foi alterado, então um .env atual continua funcionando.

Converter secrets

Em extension.yaml, declare secrets com type: secret. O ambiente de execução das extensões os armazena e vincula, para que o código da extensão possa ler process.env.PARAM_NAME diretamente. Em uma base de código típica do Cloud Functions você declara e vincula cada secret explicitamente:

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

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

Depois que a extensão for migrada para um pacote/kit npm, as referências de secret serão gerenciadas no arquivo .env do usuário final. É importante não alterar os nomes de secret declarados no código de forma alguma. Durante a migração, os secrets do usuário final serão migrados de acordo.

Exemplo prático: acionar e-mail de Cloud Firestore

Antes. MAIL_COLLECTION e SMTP_PASSWORD lidos como variáveis de ambiente brutas em 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,

Depois. Um defineString e um defineSecret; a CLI descobre os dois e lê de .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();
    // ...
  }
);

Migrar chamadas internas da fila de tarefas

Algumas extensões enfileiram o trabalho nas próprias filas de tarefas de dentro do código da função, usando o Firebase Admin SDK. Isso é diferente de receber uma tarefa enviada (abordada nas seções Fazer upgrade de funções e Converter hooks de ciclo de vida). Aqui, o código é o produtor que chama queue.enqueue(...).

As versões anteriores do Admin SDK exigiam que as extensões transmitissem o próprio ID da instância de extensão como um segundo parâmetro para segmentar uma função da fila de tarefas em a mesma extensão. A partir do `firebase-admin` 14.2.0, isso não é necessário nem recomendado. A API Task Queue agora vai segmentar filas de tarefas no mesmo contexto (por exemplo, extensão) por padrão. É seguro e recomendado remover esse parâmetro no código como uma extensão e como funções independentes. A remoção desse parâmetro garante a portabilidade e a compatibilidade com versões futuras.

Todo o resto sobre a chamada de enfileiramento (o caminho do recurso de locations/region/functions/name, o payload da tarefa e a lógica de repetição) permanece o mesmo.

Consulte /docs/functions/task-functions para mais detalhes sobre funções de enfileiramento com Cloud Tasks.

Antes. Extensão de 1ª geração

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

Depois. Extensão de 2ª geração

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 a chamada de enfileiramento segmentar uma base de código prefixada, o nome da função descoberta também será prefixado (por exemplo, orders-syncBigQuery).

Declarar APIs e papéis do IAM necessários

Mova os requisitos de IAM e API da extensão de extension.yaml para o código:

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

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

Com a segurança declarativa, a Firebase CLI cria ou atualiza uma conta de serviço de ambiente de execução gerenciada para a base de código e concede a ela a união de todos os papéis declarados. Documente para os usuários que todas as funções na base de código são executadas com esses papéis, a menos que a API final ofereça suporte a um modelo mais restrito.

Exemplo prático: Stream Firestore to BigQuery

Antes. Declarado em extension.yaml; o ambiente de execução das extensões ativou a API e concedeu os papéis a uma conta gerenciada:

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

Depois. Declarado no código com 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");

Converter hooks de ciclo de vida

Se a extensão chamar getExtensions().runtime(), por exemplo setProcessingState ou setFatalError, exclua essas chamadas, porque elas vão gerar um erro se forem chamadas de uma função de 2ª geração implantada normalmente. O estado do ciclo de vida agora é controlado por afterFirstDeploy e afterRedeploy, em que esse rastreamento de estado não é usado.

Firebase Extensions podem executar a configuração quando um usuário instala, atualiza ou reconfigura uma extensão. No pacote npm, declare ações de ciclo de vida equivalentes no código.

Para configuração única:

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

Para atualizações de configuração ou código:

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

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

Torne as ações de ciclo de vida idempotentes. Os usuários podem precisar executá-las novamente manualmente se o envio ou a execução falharem:

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

Exemplo prático: Stream Firestore to BigQuery

Antes. lifecycleEvents em extension.yaml, controlado pelo ambiente de execução das extensões:

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

Depois. Declarado no código; a tarefa provisiona BigQuery na primeira implantação:

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

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

O provisionamento é idempotente, então uma nova execução reconcilia o conjunto de dados, a tabela e as visualizações. Os usuários podem executar novamente manualmente com firebase functions:lifecycle:run afterFirstDeploy CODEBASE_NAME.

Documentar a configuração para os usuários

  • Escreva um README do pacote que explique, no mínimo:

  • Os valores .env exigidos pelo pacote.

  • Os secrets exigidos pelo pacote e como migrar os valores de secret atuais.

  • Os papéis do IAM declarados pelo pacote com requiresRole(...).

  • As APIs do Google que o pacote ativa ou exige.

  • Os hooks de ciclo de vida declarados pelo pacote e como executá-los novamente manualmente.

  • Observações de faturamento.

  • O que mudou em comparação com a extensão original.

Exemplo prático: Stream Firestore to BigQuery

O README do pacote envia uma tabela concreta "o que mudou":

Problema Como a extensão Como @firebase/firestore-bigquery-export
Configuração Parâmetros de extensão Parâmetros de funções via .env
IAM Concedido por extensões requiresRole(...), aplicado na implantação
Provisionamento Tarefa de ciclo de vida por extensões Tarefa afterFirstDeploy / afterRedeploy
Nomes de funções ext-instanceId-fsexportbigquery fsexportbigquery (prefixado opcionalmente)

Testar a função de 2ª geração

Agora você tem uma função de 2ª geração que, quando implantada, se comporta de maneira idêntica a uma nova instalação da extensão. A última etapa é verificar e corrigir problemas introduzidos acidentalmente ao longo do caminho.

Verifique se você está usando firebase-tools >= 15.24.0 e implante a função de 2ª geração convertida em um projeto de teste com os recursos adequados para testar o comportamento dela. Se você já tiver uma configuração de projeto de teste para testar a extensão, use o comando:

firebase deploy --only functions

Depois de inserir esse comando, preencha o assistente resultante solicitando valores de parâmetro da mesma forma que você teria preenchido o formulário de instalação no Firebase console para a extensão.

Exemplo prático: Stream Firestore to BigQuery

Verificamos a sincronização do Cloud Firestore com o BigQuery de ponta a ponta:

  1. No console do Cloud Firestore, crie a coleção definida como COLLECTION_PATH (usuários) se ela ainda não existir.
  2. Crie um documento chamado bigquery-mirror-test que contenha campos com valores definidos por você.
  3. No console do BigQuery, consulte a tabela de registro de alterações bruta. Ela precisa conter uma única linha registrando a criação do documento:
SELECT * FROM `PROJECT_ID.analytics.users_raw_changelog`
  1. Consulte a visualização mais recente, que retornará o evento de alteração mais recente para o único documento atual: bigquery-mirror-test
SELECT * FROM `PROJECT_ID.analytics.users_raw_latest`
  1. Exclua o documento bigquery-mirror-test em Cloud Firestore. Ele desaparece da visualização mais recente, e um evento DELETE é anexado à tabela de registro de alterações bruta.

É possível inspecionar o histórico completo de um único documento com:

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

Diferenças do teste da extensão:

  • O acionador é implantado como fsexportbigquery (prefixado opcionalmente pela base de código), não ext-&lt;instanceId&gt;-fsexportbigquery. Procure esse nome no Cloud Functions painel e nos registros.
  • O código agora será executado no Firebase Local Emulator Suite como funções normais. É possível definir o valor dos parâmetros a serem usados no emulador com .env.local. Também é possível testar o código da unidade usando o SDK firebase-functions-test, conforme descrito em Teste de unidade do Cloud Functions
  • O provisionamento não é mais controlado pelo ambiente de execução das extensões. Se a tabela de registro de alterações estiver ausente após a implantação, execute a tarefa de configuração manualmente: firebase functions:lifecycle:run afterFirstDeploy CODEBASE_NAME. A tarefa é idempotente, então a execução reconcilia o conjunto de dados, a tabela e as visualizações.
  • Os valores de parâmetro vêm de .env em vez do formulário de instalação. Portanto, as novas execuções de firebase deploy não são interativas quando .env está concluído.