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

Neste guia, mostramos como migrar suas extensões do ambiente Firebase Extensions descontinuado para uma função que seus usuários instalam e implantam no próprio Cloud Functions para a base de código do Firebase (2ª geração).

Esse é o caminho de migração recomendado. O Firebase vai manter uma lista de extensões com equivalentes oficiais do npm. Este guia vai mostrar como criar as suas.

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

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

Se tiver dúvidas sobre como migrar do Firebase Extensions, entre em contato com firebase-extensions-migrator-support-external@google.com. Também vamos enviar um e-mail para esse grupo quando atualizarmos o guia com mais informações sobre empacotamento, teste e distribuição das 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. Você precisa responder a esse e-mail, não clicar no botão "Participar deste grupo".

Antes de começar

Para concluir essa migração, você vai usar os seguintes recursos do 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 função declarada 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 CLI Firebase concede os papéis declarados a uma conta de serviço de ambiente de execução gerenciado e ativa as APIs declaradas em seu nome.

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

Inventário da extensão

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

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

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

  • functions/, que contém o código da função, dependências, configuração de build, gatilhos e funções de 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 qualquer utilitário de importação, backfill, IAM, reparo ou migração, além de outras ferramentas enviadas com a extensão.

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

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

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

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

  • Converta 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:transmitir o Firestore para 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, …) Parâmetros Cloud Functions (seção 5)
apis bigquery.googleapis.com requiresAPI(...) (seção 7)
papéis bigquery.dataEditor, datastore.user, bigquery.user requiresRole(...) (seção 7)
recursos 1 gatilho 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/ Mantidos como scripts (fora do escopo aqui)

A extensão não declara nenhum tipo: parâmetros secretos. Portanto, não há nada para migrar na seção 6 deste guia. O gatilho de evento já é da 2ª geração. Somente as funções da fila de tarefas ainda são da 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 pode ser o package.json raiz. Se você estiver migrando muitas extensões em um repositório, dê a cada uma delas 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 sua versão do firebase-functions como uma dependência de mesmo nível também.

{
  "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 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:transmitir o Firestore para BigQuery

Antes. O functions/package.json da extensão é particular, nomeia o ID da extensão e declara 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 das funções da 1ª para a 2ª geração

Se a extensão ainda exportar funções de 1ª geração, converta cada uma delas para o equivalente de 2ª geração. Importe dos módulos firebase-functions/... e transmita 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 da função, porque o SDK de 2ª geração agora expõe os parâmetros 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 comparação de versões do Cloud Functions para uma lista completa das diferenças entre as funções de 1ª geração e 2ª geração.

Converter parâmetros e secrets de extensão

Parâmetros de conversão

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

Converter leituras diretas do ambiente:

const collectionPath = process.env.COLLECTION_PATH;

em parâmetros 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);
  }
);

Use collectionPath.value() para ler a string dentro de um gerenciador; use collectionPath diretamente onde um marcador de posição é esperado, como um caminho de acionador de função.

A CLI Firebase descobre seus 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 mudar os nomes de parâmetros declarados no seu código de forma alguma. A migração da extensão preserva automaticamente o valor do parâmetro do usuário final, mas apenas quando os nomes permanecem inalterados.

Exemplo prático:transmitir o Firestore para 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 mudou, então um .env atual continua funcionando.

Converter secrets

Em extension.yaml, você declara secrets com type: secret. O ambiente de execução das extensões armazena e vincula esses parâmetros, para que o código da extensão possa ler process.env.PARAM_NAME diretamente. Em uma base de código Cloud Functions típica, você declara e vincula cada segredo 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 secretas serão gerenciadas no arquivo .env do usuário final. É importante não mudar os nomes de segredos declarados no seu código de forma alguma. Durante a migração, os segredos do usuário final serão migrados de acordo.

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

Antes. MAIL_COLLECTION e SMTP_PASSWORD são 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 despachada (abordado nas seções Fazer upgrade de funções e Converter hooks de ciclo de vida). Aqui, seu 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 da extensão como um segundo parâmetro para segmentar uma função da fila de tarefas na mesma extensão. A partir do `firebase-admin` 14.2.0, isso não é necessário nem recomendado. Por padrão, a API Task Queue agora vai segmentar filas de tarefas no mesmo contexto (por exemplo, extensão). É seguro e recomendável remover esse parâmetro no código, tanto como uma extensão quanto 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 locations/region/functions/name, o payload da tarefa e a lógica de nova tentativa) permanece o mesmo.

Consulte /docs/functions/task-functions para mais detalhes sobre como enfileirar funções 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";

const queue = getFunctions().taskQueue(
  `locations/${process.env.FUNCTION_REGION}/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 as APIs e os papéis do IAM necessários

Mova os requisitos de IAM e API da extensão de extension.yaml para 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 CLI Firebase cria ou atualiza uma conta de serviço de tempo de execução gerenciado para a base de código e concede a ela a união de todos os papéis declarados. Documente para seus usuários que todas as funções no codebase são executadas com essas funções, a menos que a API final seja compatível com um modelo mais restrito.

Exemplo prático:transmitir o Firestore para 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.

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

Para uma 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. Seus usuários talvez precisem executar novamente os comandos manualmente se o envio ou a execução falhar:

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

Exemplo prático:transmitir o Firestore para BigQuery

Antes. lifecycleEvents em extension.yaml, causado pelo tempo 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. Declarada 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. Portanto, uma nova execução reconcilia o conjunto de dados, a tabela e as visualizações. Os usuários podem executar novamente de forma manual com firebase functions:lifecycle:run afterFirstDeploy CODEBASE_NAME.

Configuração de documentos para seus usuários

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

  • Os valores de .env necessários para o pacote.

  • Os secrets que o pacote exige e como migrar valores de secrets existentes.

  • Os papéis do IAM que o pacote declara com requiresRole(...).

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

  • Os hooks de ciclo de vida que o pacote declara e como executá-los novamente manualmente.

  • Observações de faturamento.

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

Exemplo prático:transmitir o Firestore para BigQuery

O README do pacote envia uma tabela concreta de "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 (com prefixo opcional)

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 sua extensão. A última etapa é verificar e corrigir problemas introduzidos acidentalmente ao longo do processo.

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

firebase deploy --only functions

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

Exemplo prático:transmitir o Firestore para BigQuery

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

  1. No console 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 com campos e valores definidos por você.
  3. No console do BigQuery, consulte a tabela bruta de mudanças. Ele 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 vai retornar o último evento de mudança do único documento presente: 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 bruta do changelog.

É 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 em relação ao teste da extensão:

  • O gatilho é implantado como fsexportbigquery (opcionalmente com prefixo de base de código), não ext-&lt;instanceId&gt;-fsexportbigquery. Procure esse nome no painel e nos registros Cloud Functions.
  • Agora seu código 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 seu código usando o SDK firebase-functions-test, conforme descrito em Teste de unidade de Cloud Functions
  • O provisionamento não é mais feito pelo ambiente de execução das extensões. Se a tabela de changelog estiver faltando após a implantação, execute novamente a tarefa de configuração manualmente: firebase functions:lifecycle:run afterFirstDeploy CODEBASE_NAME. A tarefa é idempotente. Portanto, ao executá-la novamente, o conjunto de dados, a tabela e as visualizações são reconciliados.
  • 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 depois que .env é concluído.