使用 Cloud Run functions,透過事件觸發條件擴充 Cloud Run

透過 Cloud Run 函式,您可以部署程式碼來處理 Cloud Run 資料庫變更觸發的事件。這樣一來,您就能新增伺服器端功能,不必自行執行伺服器。

本指南說明如何從 Firestore 事件建立 Cloud Run 函式的觸發條件。

您可以透過 Firestore 資料庫中的事件觸發 Cloud Run 函式。觸發後,函式會透過 Firestore API 和用戶端程式庫,讀取及更新 Firestore 資料庫,以回應這些事件。

Firestore 事件觸發 Cloud Run 函式的程序包含下列步驟:

  1. 服務會等待特定文件的變更。

  2. 發生變更時,系統會觸發服務並執行工作。

  3. 服務會收到資料物件,其中包含受影響文件的快照。如果是 writeupdate 事件,資料物件會包含代表觸發事件前後文件狀態的快照。

事前準備

  1. 請確認您已按照設定頁面所述,為 Cloud Run 設定新專案。
  2. 啟用 Artifact Registry、Cloud Build、Cloud Run Admin API、Eventarc、Firestore Cloud Logging 和 Pub/Sub API:

    啟用 API

必要的角色

您或管理員必須授予部署者帳戶和觸發程序身分。(選用) 授予 Pub/Sub 服務代理下列 IAM 角色。

部署者帳戶的必要角色

如要取得從 Firestore 事件觸發函式所需的權限,請要求管理員為您授予專案的下列 IAM 角色:

如要進一步瞭解如何授予角色,請參閱「管理專案、資料夾和機構的存取權」。

您或許還可透過自訂角色或其他預先定義的角色取得必要權限。

請注意,根據預設,Cloud Build 權限包含上傳及下載 Artifact Registry 構件的權限

觸發身分所需的角色

  1. 記下 Compute Engine 預設服務帳戶,因為您會將其附加至 Eventarc 觸發程序,代表觸發程序的身分,以利進行測試。啟用或使用採用 Compute Engine 的服務後,系統會自動建立這個服務帳戶,電子郵件地址格式如下: Google Cloud

    PROJECT_NUMBER-compute@developer.gserviceaccount.com

    PROJECT_NUMBER 替換為專案編號。 Google Cloud您可以在 Google Cloud 控制台的「歡迎」頁面找到專案編號,也可以執行下列指令:

    gcloud projects describe PROJECT_ID --format='value(projectNumber)'

    在正式環境中,我們強烈建議建立新的服務帳戶,並授予一或多個包含最低必要權限的 IAM 角色,遵循最低權限原則。

  2. 根據預設,只有專案擁有者、專案編輯者,以及 Cloud Run 管理員和叫用者可以呼叫 Cloud Run 服務。您可以依據服務控管存取權,但為了進行測試,請在 Google Cloud 專案中將 Cloud Run 叫用者角色 (run.invoker) 授予 Compute Engine 服務帳戶。這會將角色授予專案中的所有 Cloud Run 服務和工作。
    gcloud projects add-iam-policy-binding PROJECT_ID \
        --member=serviceAccount:PROJECT_NUMBER-compute@developer.gserviceaccount.com \
        --role=roles/run.invoker

    請注意,如果您為經過驗證的 Cloud Run 服務建立觸發條件,但未授予 Cloud Run Invoker 角色,系統仍會成功建立並啟用觸發條件。不過,觸發條件不會正常運作,記錄中會顯示類似以下的訊息:

    The request was not authenticated. Either allow unauthenticated invocations or set the proper Authorization header.
  3. 將專案的 Eventarc 事件接收者角色 (roles/eventarc.eventReceiver) 授予 Compute Engine 預設服務帳戶,讓 Eventarc 觸發條件可以接收事件供應商的事件。
    gcloud projects add-iam-policy-binding PROJECT_ID \
        --member=serviceAccount:PROJECT_NUMBER-compute@developer.gserviceaccount.com \
        --role=roles/eventarc.eventReceiver

Pub/Sub 服務代理人的選用角色

  • 如果您是在 2021 年 4 月 8 日當天或之前啟用 Cloud Pub/Sub 服務代理,請將服務帳戶權杖建立者角色 (roles/iam.serviceAccountTokenCreator) 授予服務代理,以支援已驗證的 Pub/Sub 推送要求。否則,系統會預設授予這個角色:
    gcloud projects add-iam-policy-binding PROJECT_ID \
        --member=serviceAccount:service-PROJECT_NUMBER@gcp-sa-pubsub.iam.gserviceaccount.com \
        --role=roles/iam.serviceAccountTokenCreator

設定 Firestore 資料庫

部署服務前,請先建立 Firestore 資料庫:

  1. 前往 Firestore 頁面

  2. 選取「建立 Firestore 資料庫」

  3. 在「Name your database」(為資料庫命名) 欄位中,輸入資料庫 ID,例如 firestore-db

  4. 在「設定選項」部分,系統會預先選取「Firestore 原生模式」,並套用適用的安全性規則。

  5. 在「位置類型」下方選取「區域」,然後選擇資料庫所在的區域。選定後即無法變更。

  6. 按一下 [Create database] (建立資料庫)。

Firestore 資料模型包含內含文件的集合。文件包含一組鍵/值組合。

編寫 Firestore 觸發函式

如要編寫回應 Firestore 事件的函式,請準備在部署期間指定下列項目:

事件類型

Firestore 支援 createupdatedeletewrite 事件。write 事件涵蓋對文件進行的所有修改。

事件類型 觸發條件
google.cloud.firestore.document.v1.created (預設) 在第一次寫入文件時觸發。
google.cloud.firestore.document.v1.updated 在文件已經存在且已變更任何值時觸發。
google.cloud.firestore.document.v1.deleted 在刪除具有資料的文件時觸發。
google.cloud.firestore.document.v1.written 在建立、更新或刪除文件時觸發。
google.cloud.firestore.document.v1.created.withAuthContext created 相同,但會新增驗證資訊。
google.cloud.firestore.document.v1.updated.withAuthContext updated 相同,但會新增驗證資訊。
google.cloud.firestore.document.v1.deleted.withAuthContext deleted 相同,但會新增驗證資訊。
google.cloud.firestore.document.v1.written.withAuthContext written 相同,但會新增驗證資訊。

萬用字元會以大括號寫入觸發條件,例如: projects/YOUR_PROJECT_ID/databases/(default)/documents/collection/{document_wildcard}

觸發條件事件篩選器

如要觸發服務,請指定要監聽的文件路徑。文件路徑必須與服務位於同一 Google Cloud 專案。

以下列舉幾個有效的檔案路徑範例:

  • users/marie:監控單一文件 /users/marie

  • users/{username}:監控所有使用者文件。萬用字元用於監控集合中的所有文件。

  • users/{username}/addresses/home:監控所有使用者的住家地址文件。

  • users/{username}/addresses/{addressId}:監控所有地址文件。

  • users/{user=**}:監控所有使用者文件,以及每個使用者文件下子集合中的任何文件,例如 /users/userID/address/home/users/userID/phone/work

  • users/{username}/addresses無效的地址路徑。是指子集合 addresses,而非文件。

萬用字元和參數

如果您不知道要監控的特定文件,請使用 {wildcard},而非文件 ID:

  • users/{username} 會監聽所有使用者文件的變更。

在這個範例中,當 users 中任何文件的任何欄位發生變更時,系統會比對名為 {username} 的萬用字元。

如果 users 中的文件有子集合,且其中一個子集合文件的欄位有所變更,系統不會觸發 {username} 萬用字元。如要回應子集合中的事件,請使用多段萬用字元 {username=**}

系統會從文件路徑擷取萬用字元相符項目。您可以定義任意數量的萬用字元,取代明確的集合或文件 ID。您最多可以使用一個多區隔萬用字元,例如 {username=**}

函式程式碼

如要瞭解如何使用原生模式的 Firestore 事件觸發 Cloud Run 函式,請參閱範例

在來源中加入 proto 依附元件

您必須在函式的來源目錄中加入 Cloud Run data.proto 檔案。這個檔案會匯入下列必須一併納入來源目錄的 Proto:

依附元件使用相同的目錄結構。例如,將 struct.proto 放在 google/protobuf 內。

解碼事件資料時需要這些檔案。如果函式來源不含這些檔案,執行時會傳回錯誤。

活動屬性

每個事件都包含資料屬性,內含事件相關資訊,例如事件觸發時間。Cloud Run 會新增與事件相關的資料庫和文件其他資料。您可以透過下列方式存取這些屬性:

Java
logger.info("Function triggered by event on: " + event.getSource());
logger.info("Event type: " + event.getType());
logger.info("Event time " + event.getTime());
logger.info("Event project: " + event.getExtension("project"));
logger.info("Event location: " + event.getExtension("location"));
logger.info("Database name: " + event.getExtension("database"));
logger.info("Database document: " + event.getExtension("document"));
// For withAuthContext events
logger.info("Auth information: " + event.getExtension("authid"));
logger.info("Auth information: " + event.getExtension("authtype"));
Node.js
console.log(`Function triggered by event on: ${cloudEvent.source}`);
console.log(`Event type: ${cloudEvent.type}`);
console.log(`Event time: ${cloudEvent.time}`);
console.log(`Event project: ${cloudEvent.project}`);
console.log(`Event location: ${cloudEvent.location}`);
console.log(`Database name: ${cloudEvent.database}`);
console.log(`Document name: ${cloudEvent.document}`);
// For withAuthContext events
console.log(`Auth information: ${cloudEvent.authid}`);
console.log(`Auth information: ${cloudEvent.authtype}`);
Python
print(f"Function triggered by change to: {cloud_event['source']}")
print(f"Event type: {cloud_event['type']}")
print(f"Event time: {cloud_event['time']}")
print(f"Event project: {cloud_event['project']}")
print(f"Location: {cloud_event['location']}")
print(f"Database name: {cloud_event['database']}")
print(f"Document: {cloud_event['document']}")
// For withAuthContext events
print(f"Auth information: {cloud_event['authid']}")
print(f"Auth information: {cloud_event['authtype']}")

活動結構

這個觸發條件會使用類似下列的事件叫用服務:

{
    "oldValue": { // Update and Delete operations only
        A Document object containing a pre-operation document snapshot
    },
    "updateMask": { // Update operations only
        A DocumentMask object that lists changed fields.
    },
    "value": {
        // A Document object containing a post-operation document snapshot
    }
}

每個 Document 物件都包含一或多個 Value 物件。如需型別參照,請參閱 Value 說明文件

建立函式的觸發條件

按一下分頁標籤,瞭解如何使用自選工具。

主控台

使用 Google Cloud 控制台建立函式時,也可以為函式新增觸發條件。請按照下列步驟為函式建立觸發條件:

  1. 前往 Google Cloud 控制台的 Cloud Run:

    前往 Cloud Run

  2. 按一下「編寫函式」,然後輸入函式詳細資料。如要進一步瞭解如何在部署期間設定函式,請參閱「部署函式」。

  3. 在「觸發條件」部分中,按一下「新增觸發條件」

  4. 選取「Firestore 觸發條件」

  5. 在「Eventarc trigger」(Eventarc 觸發條件) 窗格中,按照下列步驟修改觸發條件詳細資料:

    1. 在「觸發條件名稱」欄位中輸入觸發條件名稱,或使用預設名稱。

    2. 從清單中選取「觸發條件類型」

      • Google 來源:指定 Pub/Sub、Cloud Storage、Firestore 和其他 Google 事件供應商的觸發條件。

      • 第三方:與提供 Eventarc 來源的非 Google 提供者整合。詳情請參閱「Eventarc 中的第三方事件」。

    3. 從「Event provider」(事件提供者) 清單中選取「Firestore」Firestore,即可選取提供事件類型的產品,觸發函式。如需事件提供者清單,請參閱事件提供者和目的地

    4. 從「Event type」(事件類型) 清單中選取「type=google.cloud.firestore.document.v1.created」。觸發條件設定會因支援的事件類型而異。詳情請參閱「事件類型」。

    5. 在「篩選器」部分,選取資料庫、作業和屬性值,或使用預設選取項目。

    6. 如果「區域」欄位已啟用,請選取 Eventarc 觸發程序的位置。一般來說,Eventarc 觸發條件的位置應與要監控事件的 Google Cloud 資源位置一致。在多數情況下,您也應該在相同區域中部署函式。如要進一步瞭解 Eventarc 觸發條件的所在位置,請參閱「瞭解 Eventarc 位置」。

    7. 在「服務帳戶」欄位中,選取服務帳戶。 Eventarc 觸發程序會連結至服務帳戶,在叫用函式時做為身分。Eventarc 觸發程序的服務帳戶必須具備叫用函式的權限。根據預設,Cloud Run 會使用 Compute Engine 預設服務帳戶

    8. 視需要指定服務網址路徑,將傳入要求傳送至該路徑。這是目的地服務上的相對路徑,觸發條件的事件應傳送至該路徑。例如://routerouteroute/subroute

  6. 填妥必填欄位後,按一下「儲存觸發條件」

gcloud

使用 gcloud CLI 建立函式時,您必須先部署函式,然後建立觸發條件。請按照下列步驟為函式建立觸發條件:

  1. 在包含程式碼範例的目錄中執行下列指令,即可部署函式:

    gcloud run deploy FUNCTION \
            --source . \
            --function FUNCTION_ENTRYPOINT \
            --base-image BASE_IMAGE_ID \
            --region REGION
    

    取代:

    • FUNCTION 改為您要部署的函式名稱。您可以完全省略這個參數,但這樣系統會提示您輸入名稱。

    • FUNCTION_ENTRYPOINT,並在原始碼中輸入函式的進入點。這是 Cloud Run 在函式執行時執行的程式碼。這個旗標的值必須是來源程式碼中存在的函式名稱或完整類別名稱。

    • BASE_IMAGE_ID 改為函式適用的基本映像檔環境。如要進一步瞭解基礎映像檔,以及每個映像檔中包含的套件,請參閱「執行階段基礎映像檔」。

    • REGION,其中 Google Cloud是您要部署函式的地區。例如:europe-west1

  2. 執行下列指令,建立用於篩選事件的觸發條件:

    gcloud eventarc triggers create TRIGGER_NAME  \
        --location=EVENTARC_TRIGGER_LOCATION \
        --destination-run-service=FUNCTION  \
        --destination-run-region=REGION \
        --event-filters="type=google.cloud.firestore.document.v1.created" \
        --service-account=PROJECT_NUMBER-compute@developer.gserviceaccount.com
    

    取代:

    • TRIGGER_NAME 改為觸發條件的名稱。

    • EVENTARC_TRIGGER_LOCATION,並提供 Eventarc 觸發條件的位置。一般來說,Eventarc 觸發條件的位置應與要監控事件的 Google Cloud 資源位置相符。在多數情況下,您也應該在相同區域中部署函式。詳情請參閱「Eventarc 區域」。

    • FUNCTION 替換成您要部署的函式名稱。

    • REGION,並使用函式的 Cloud Run 區域

    • PROJECT_NUMBER 改成您的 Google Cloud 專案編號。Eventarc 觸發程序會連結至服務帳戶,在叫用函式時做為身分。Eventarc 觸發程序的服務帳戶必須具備叫用函式的權限。根據預設,Cloud Run 會使用預設的 Compute 服務帳戶。

    每個 event-filters 標記都會指定事件類型,且只有在事件符合 event-filters 標記中指定的所有條件時,函式才會觸發。每個觸發條件都必須有 event-filters 旗標,指定支援的事件類型,例如寫入 Firestore 的新文件,或是上傳至 Cloud Storage 的檔案。建立事件篩選器後,就無法變更類型。 如要變更事件篩選器類型,請建立新觸發條件並刪除舊觸發條件。視需要重複使用 --event-filters 旗標,並以 ATTRIBUTE=VALUE 形式新增更多支援的篩選條件。

Terraform

如要為 Cloud Run 函式建立 Eventarc 觸發條件,請參閱「使用 Terraform 建立觸發條件」。

範例

下列範例說明如何使用原生模式的 Firestore 事件觸發 Cloud Run 函式。

範例 1:Hello Firestore 函式

以下範例會輸出觸發 Firestore 事件的欄位:

Node.js

/**
 * Cloud Event Function triggered by a change to a Firestore document.
 */
const functions = require('@google-cloud/functions-framework');
const protobuf = require('protobufjs');

functions.cloudEvent('helloFirestore', async cloudEvent => {
  console.log(`Function triggered by event on: ${cloudEvent.source}`);
  console.log(`Event type: ${cloudEvent.type}`);

  console.log('Loading protos...');
  const root = await protobuf.load('data.proto');
  const DocumentEventData = root.lookupType(
    'google.events.cloud.firestore.v1.DocumentEventData'
  );

  console.log('Decoding data...');
  const firestoreReceived = DocumentEventData.decode(cloudEvent.data);

  console.log('\nOld value:');
  console.log(JSON.stringify(firestoreReceived.oldValue, null, 2));

  console.log('\nNew value:');
  console.log(JSON.stringify(firestoreReceived.value, null, 2));
});

Python

from cloudevents.http import CloudEvent
import functions_framework
from google.events.cloud import firestore


@functions_framework.cloud_event
def hello_firestore(cloud_event: CloudEvent) -> None:
    """Triggers by a change to a Firestore document.

    Args:
        cloud_event: cloud event with information on the firestore event trigger
    """
    firestore_payload = firestore.DocumentEventData()
    firestore_payload._pb.ParseFromString(cloud_event.data)

    print(f"Function triggered by change to: {cloud_event['source']}")

    print("\nOld value:")
    print(firestore_payload.old_value)

    print("\nNew value:")
    print(firestore_payload.value)

Go


// Package hellofirestore contains a Cloud Event Function triggered by a Cloud Firestore event.
package hellofirestore

import (
	"context"
	"fmt"

	"github.com/GoogleCloudPlatform/functions-framework-go/functions"
	"github.com/cloudevents/sdk-go/v2/event"
	"github.com/googleapis/google-cloudevents-go/cloud/firestoredata"
	"google.golang.org/protobuf/proto"
)

func init() {
	functions.CloudEvent("helloFirestore", HelloFirestore)
}

// HelloFirestore is triggered by a change to a Firestore document.
func HelloFirestore(ctx context.Context, event event.Event) error {
	var data firestoredata.DocumentEventData

	// If you omit `DiscardUnknown`, protojson.Unmarshal returns an error
	// when encountering a new or unknown field.
	options := proto.UnmarshalOptions{
		DiscardUnknown: true,
	}
	err := options.Unmarshal(event.Data(), &data)

	if err != nil {
		return fmt.Errorf("proto.Unmarshal: %w", err)
	}

	fmt.Printf("Function triggered by change to: %v\n", event.Source())
	fmt.Printf("Old value: %+v\n", data.GetOldValue())
	fmt.Printf("New value: %+v\n", data.GetValue())
	return nil
}

Java

import com.google.cloud.functions.CloudEventsFunction;
import com.google.events.cloud.firestore.v1.DocumentEventData;
import com.google.protobuf.InvalidProtocolBufferException;
import io.cloudevents.CloudEvent;
import java.util.logging.Logger;

public class FirebaseFirestore implements CloudEventsFunction {
  private static final Logger logger = Logger.getLogger(FirebaseFirestore.class.getName());

  @Override
  public void accept(CloudEvent event) throws InvalidProtocolBufferException {
    DocumentEventData firestoreEventData = DocumentEventData
        .parseFrom(event.getData().toBytes());

    logger.info("Function triggered by event on: " + event.getSource());
    logger.info("Event type: " + event.getType());

    logger.info("Old value:");
    logger.info(firestoreEventData.getOldValue().toString());

    logger.info("New value:");
    logger.info(firestoreEventData.getValue().toString());
  }
}

C#

using CloudNative.CloudEvents;
using Google.Cloud.Functions.Framework;
using Google.Events.Protobuf.Cloud.Firestore.V1;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace FirebaseFirestore;

public class Function : ICloudEventFunction<DocumentEventData>
{
    private readonly ILogger _logger;

    public Function(ILogger<Function> logger) =>
        _logger = logger;

    public Task HandleAsync(CloudEvent cloudEvent, DocumentEventData data, CancellationToken cancellationToken)
    {
        _logger.LogInformation("Function triggered by event on {subject}", cloudEvent.Subject);
        _logger.LogInformation("Event type: {type}", cloudEvent.Type);
        MaybeLogDocument("Old value", data.OldValue);
        MaybeLogDocument("New value", data.Value);

        // In this example, we don't need to perform any asynchronous operations, so the
        // method doesn't need to be declared async.
        return Task.CompletedTask;
    }

    /// <summary>
    /// Logs the names and values of the fields in a document in a very simplistic way.
    /// </summary>
    private void MaybeLogDocument(string message, Document document)
    {
        if (document is null)
        {
            return;
        }

        // ConvertFields converts the Firestore representation into a .NET-friendly
        // representation.
        IReadOnlyDictionary<string, object> fields = document.ConvertFields();
        var fieldNamesAndTypes = fields
            .OrderBy(pair => pair.Key)
            .Select(pair => $"{pair.Key}: {pair.Value}");
        _logger.LogInformation(message + ": {fields}", string.Join(", ", fieldNamesAndTypes));
    }
}

部署函式

如要部署 Hello Firestore 函式,請執行下列指令:

如果尚未設定,請先設定 Firestore 資料庫

如要部署函式,請參閱「為函式建立觸發條件」。

測試函式

如要測試 Hello Firestore 函式,請在 Firestore 資料庫中設定名為 users 的集合:

  1. 前往 Google Cloud 控制台的 Firestore 資料庫頁面:

    前往 Firestore

  2. 按一下「啟動集合」

  3. users 指定為集合 ID。

  4. 如要開始新增集合的第一份文件,請在「新增第一份文件」下方,接受系統自動產生的「文件 ID」

  5. 為文件新增至少一個欄位,並指定名稱和值。 舉例來說,在「Field name」(欄位名稱) 中輸入 username,在「Field value」(欄位值) 中輸入 rowan

  6. 完成時,請按一下 [Save] (儲存)

    這項操作會建立新文件,進而觸發函式。

  7. 如要確認函式已觸發,請在 Google Cloud 控制台的「Cloud Run Overview」(Cloud Run 總覽) 頁面中,按一下函式的連結名稱,開啟「Service details」(服務詳細資料) 頁面。

  8. 選取「記錄」分頁標籤,然後尋找以下字串:

Function triggered by change to: //firestore.googleapis.com/projects/your-project-id/databases/(default)'

範例 2:轉換為大寫函式

以下範例會擷取使用者新增的值、將該位置的字串轉換為大寫,並以大寫字串取代該值:

Node.js

使用 protobufjs 解碼事件資料。在來源中加入 google.events.cloud.firestore.v1 data.proto

const functions = require('@google-cloud/functions-framework');
const Firestore = require('@google-cloud/firestore');
const protobuf = require('protobufjs');

const firestore = new Firestore({
  projectId: process.env.GOOGLE_CLOUD_PROJECT,
});

// Converts strings added to /messages/{pushId}/original to uppercase
functions.cloudEvent('makeUpperCase', async cloudEvent => {
  console.log('Loading protos...');
  const root = await protobuf.load('data.proto');
  const DocumentEventData = root.lookupType(
    'google.events.cloud.firestore.v1.DocumentEventData'
  );

  console.log('Decoding data...');
  const firestoreReceived = DocumentEventData.decode(cloudEvent.data);

  const resource = firestoreReceived.value.name;
  const affectedDoc = firestore.doc(resource.split('/documents/')[1]);

  const curValue = firestoreReceived.value.fields.original.stringValue;
  const newValue = curValue.toUpperCase();

  if (curValue === newValue) {
    // Value is already upper-case
    // Don't perform a(nother) write to avoid infinite loops
    console.log('Value is already upper-case.');
    return;
  }

  console.log(`Replacing value: ${curValue} --> ${newValue}`);
  affectedDoc.set({
    original: newValue,
  });
});

Python

from cloudevents.http import CloudEvent
import functions_framework
from google.cloud import firestore
from google.events.cloud import firestore as firestoredata

client = firestore.Client()


# Converts strings added to /messages/{pushId}/original to uppercase
@functions_framework.cloud_event
def make_upper_case(cloud_event: CloudEvent) -> None:
    firestore_payload = firestoredata.DocumentEventData()
    firestore_payload._pb.ParseFromString(cloud_event.data)

    path_parts = firestore_payload.value.name.split("/")
    separator_idx = path_parts.index("documents")
    collection_path = path_parts[separator_idx + 1]
    document_path = "/".join(path_parts[(separator_idx + 2) :])

    print(f"Collection path: {collection_path}")
    print(f"Document path: {document_path}")

    affected_doc = client.collection(collection_path).document(document_path)

    cur_value = firestore_payload.value.fields["original"].string_value
    new_value = cur_value.upper()

    if cur_value != new_value:
        print(f"Replacing value: {cur_value} --> {new_value}")
        affected_doc.set({"original": new_value})
    else:
        # Value is already upper-case
        # Don't perform a second write (which can trigger an infinite loop)
        print("Value is already upper-case.")

Go


// Package upper contains a Firestore Cloud Function.
package upper

import (
	"context"
	"errors"
	"fmt"
	"log"
	"os"
	"strings"

	"cloud.google.com/go/firestore"
	firebase "firebase.google.com/go/v4"
	"github.com/GoogleCloudPlatform/functions-framework-go/functions"
	"github.com/cloudevents/sdk-go/v2/event"
	"github.com/googleapis/google-cloudevents-go/cloud/firestoredata"
	"google.golang.org/protobuf/proto"
)

// set the GOOGLE_CLOUD_PROJECT environment variable when deploying.
var projectID = os.Getenv("GOOGLE_CLOUD_PROJECT")

// client is a Firestore client, reused between function invocations.
var client *firestore.Client

func init() {
	// Use the application default credentials.
	conf := &firebase.Config{ProjectID: projectID}

	// Use context.Background() because the app/client should persist across
	// invocations.
	ctx := context.Background()

	app, err := firebase.NewApp(ctx, conf)
	if err != nil {
		log.Fatalf("firebase.NewApp: %v", err)
	}

	client, err = app.Firestore(ctx)
	if err != nil {
		log.Fatalf("app.Firestore: %v", err)
	}

	// Register cloud event function
	functions.CloudEvent("MakeUpperCase", MakeUpperCase)
}

// MakeUpperCase is triggered by a change to a Firestore document. It updates
// the `original` value of the document to upper case.
func MakeUpperCase(ctx context.Context, e event.Event) error {
	var data firestoredata.DocumentEventData

	// If you omit `DiscardUnknown`, protojson.Unmarshal returns an error
	// when encountering a new or unknown field.
	options := proto.UnmarshalOptions{
		DiscardUnknown: true,
	}
	err := options.Unmarshal(e.Data(), &data)

	if err != nil {
		return fmt.Errorf("proto.Unmarshal: %w", err)
	}

	if data.GetValue() == nil {
		return errors.New("Invalid message: 'Value' not present")
	}

	fullPath := strings.Split(data.GetValue().GetName(), "/documents/")[1]
	pathParts := strings.Split(fullPath, "/")
	collection := pathParts[0]
	doc := strings.Join(pathParts[1:], "/")

	var originalStringValue string
	if v, ok := data.GetValue().GetFields()["original"]; ok {
		originalStringValue = v.GetStringValue()
	} else {
		return errors.New("Document did not contain field \"original\"")
	}

	newValue := strings.ToUpper(originalStringValue)
	if originalStringValue == newValue {
		log.Printf("%q is already upper case: skipping", originalStringValue)
		return nil
	}
	log.Printf("Replacing value: %q -> %q", originalStringValue, newValue)

	newDocumentEntry := map[string]string{"original": newValue}
	_, err = client.Collection(collection).Doc(doc).Set(ctx, newDocumentEntry)
	if err != nil {
		return fmt.Errorf("Set: %w", err)
	}
	return nil
}

Java

import com.google.cloud.firestore.Firestore;
import com.google.cloud.firestore.FirestoreOptions;
import com.google.cloud.firestore.SetOptions;
import com.google.cloud.functions.CloudEventsFunction;
import com.google.events.cloud.firestore.v1.DocumentEventData;
import com.google.events.cloud.firestore.v1.Value;
import com.google.protobuf.InvalidProtocolBufferException;
import io.cloudevents.CloudEvent;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.logging.Logger;

public class FirebaseFirestoreReactive implements CloudEventsFunction {
  private static final Logger logger = Logger.getLogger(FirebaseFirestoreReactive.class.getName());
  private final Firestore firestore;

  private static final String FIELD_KEY = "original";
  private static final String APPLICATION_PROTOBUF = "application/protobuf";

  public FirebaseFirestoreReactive() {
    this(FirestoreOptions.getDefaultInstance().getService());
  }

  public FirebaseFirestoreReactive(Firestore firestore) {
    this.firestore = firestore;
  }

  @Override
  public void accept(CloudEvent event)
      throws InvalidProtocolBufferException, InterruptedException, ExecutionException {
    if (event.getData() == null) {
      logger.warning("No data found in event!");
      return;
    }

    if (!event.getDataContentType().equals(APPLICATION_PROTOBUF)) {
      logger.warning(String.format("Found unexpected content type %s, expected %s",
          event.getDataContentType(),
          APPLICATION_PROTOBUF));
      return;
    }

    DocumentEventData firestoreEventData = DocumentEventData
        .parseFrom(event.getData().toBytes());

    // Get the fields from the post-operation document snapshot
    // https://firebase.google.com/docs/firestore/reference/rest/v1/projects.databases.documents#Document
    Map<String, Value> fields = firestoreEventData.getValue().getFieldsMap();
    if (!fields.containsKey(FIELD_KEY)) {
      logger.warning("Document does not contain original field");
      return;
    }
    String currValue = fields.get(FIELD_KEY).getStringValue();
    String newValue = currValue.toUpperCase();

    if (currValue.equals(newValue)) {
      logger.info("Value is already upper-case");
      return;
    }

    // Retrieve the document name from the resource path:
    // projects/{project_id}/databases/{database_id}/documents/{document_path}
    String affectedDoc = firestoreEventData.getValue()
        .getName()
        .split("/documents/")[1]
        .replace("\"", "");

    logger.info(String.format("Replacing values: %s --> %s", currValue, newValue));

    // Wait for the async call to complete
    this.firestore
        .document(affectedDoc)
        .set(Map.of(FIELD_KEY, newValue), SetOptions.merge())
        .get();
  }
}

C#

using CloudNative.CloudEvents;
using Google.Cloud.Firestore;
using Google.Cloud.Functions.Framework;
using Google.Cloud.Functions.Hosting;
using Google.Events.Protobuf.Cloud.Firestore.V1;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace FirestoreReactive;

public class Startup : FunctionsStartup
{
    public override void ConfigureServices(WebHostBuilderContext context, IServiceCollection services) =>
        services.AddSingleton(FirestoreDb.Create());
}

// Register the startup class to provide the Firestore dependency.
[FunctionsStartup(typeof(Startup))]
public class Function : ICloudEventFunction<DocumentEventData>
{
    private readonly ILogger _logger;
    private readonly FirestoreDb _firestoreDb;

    public Function(ILogger<Function> logger, FirestoreDb firestoreDb) =>
        (_logger, _firestoreDb) = (logger, firestoreDb);

    public async Task HandleAsync(CloudEvent cloudEvent, DocumentEventData data, CancellationToken cancellationToken)
    {
        // Get the recently-written value. This expression will result in a null value
        // if any of the following is true:
        // - The event doesn't contain a "new" document
        // - The value doesn't contain a field called "original"
        // - The "original" field isn't a string
        string currentValue = data.Value?.ConvertFields().GetValueOrDefault("original") as string;
        if (currentValue is null)
        {
            _logger.LogWarning($"Event did not contain a suitable document");
            return;
        }

        string newValue = currentValue.ToUpperInvariant();
        if (newValue == currentValue)
        {
            _logger.LogInformation("Value is already upper-cased; no replacement necessary");
            return;
        }

        // The CloudEvent subject is "documents/x/y/...".
        // The Firestore SDK FirestoreDb.Document method expects a reference relative to
        // "documents" (so just the "x/y/..." part). This may be simplified over time.
        if (cloudEvent.Subject is null || !cloudEvent.Subject.StartsWith("documents/"))
        {
            _logger.LogWarning("CloudEvent subject is not a document reference.");
            return;
        }
        string documentPath = cloudEvent.Subject.Substring("documents/".Length);

        _logger.LogInformation("Replacing '{current}' with '{new}' in '{path}'", currentValue, newValue, documentPath);
        await _firestoreDb.Document(documentPath).UpdateAsync("original", newValue, cancellationToken: cancellationToken);
    }
}

部署函式

如要部署 Convert to Uppercase 函式,請執行下列指令:

如果尚未設定,請先設定 Firestore 資料庫

如要部署函式,請參閱「為函式建立觸發條件」。

測試函式

如要測試剛部署的 Convert to Uppercase 函式,請在 Firestore 資料庫中設定名為 messages 的集合:

  1. 前往 Google Cloud 控制台的 Firestore 資料庫頁面:

    前往 Firestore

  2. 按一下「啟動集合」

  3. messages 指定為集合 ID。

  4. 如要開始新增集合的第一份文件,請在「新增第一份文件」下方,接受系統自動產生的「文件 ID」

  5. 如要觸發已部署的函式,請新增文件,其中「欄位名稱」original,「欄位值」minka

  6. 儲存文件時,您會發現值欄位中的小寫字會轉換為大寫。

    如果後續編輯欄位值,使其包含小寫字母,系統會再次觸發函式,將所有小寫字母轉換為大寫。

函式限制

  • 我們不保證排序。快速變更可能會以非預期的順序觸發函式呼叫。
  • 系統至少會遞送一次事件,但單一事件可能會導致多次函式叫用。請避免依附於「只執行一次」機制,並編寫等冪函式
  • 觸發條件會與單一資料庫建立關聯。您無法建立與多個資料庫相符的觸發條件。
  • 刪除資料庫時,系統不會自動刪除該資料庫的任何觸發程序。觸發程序會停止傳送事件,但會繼續存在,直到您刪除觸發程序為止。