Get hands-on experience with 20+ free Google Cloud products and $300 in free credit for new customers.

Get updated values for nested fields in Firestore document

I am trying to implement a trigger (as a google cloud function) in golang that gets invoked when a firestore document is updated. I am following the documentation give here: https://cloud.google.com/functions/docs/calling/cloud-firestore
 
My firestore document is given below:
{
  "aaBool": true,
  "aaInt": 1111,
  "aaStr": "Lorem",
  "map1": {
    "m1book": true,
    "m1int": 3333,
    "m1str": "m1Lorem"
  },
  "map2": {
    "map3": {
      "m3int": 888,
      "m3str": "m3Lorem"
    }
  }
}
The code I have currently is:
package trigger_firestore

import (
    "cloud.google.com/go/firestore"
    _ "encoding/json"
    firebase "firebase.google.com/go/v4"
    "fmt"
    "github.com/GoogleCloudPlatform/functions-framework-go/functions"
    "github.com/cloudevents/sdk-go/v2/event"
    "github.com/googleapis/google-cloudevents-go/cloud/firestoredata"
    "golang.org/x/net/context"
    "google.golang.org/protobuf/proto"
    "log"
    "os"
    "reflect"
    "strings"
)

// 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("ProcessTrigger", ProcessTrigger)
}

func ProcessTrigger(ctx context.Context, event event.Event) error {

    var data firestoredata.DocumentEventData
    if err := proto.Unmarshal(event.Data(), &data); err != nil {
        return fmt.Errorf("proto.Unmarshal: %w", err)
    }

    fullPath := strings.Split(data.GetValue().GetName(), "/documents/")[1]
    pathParts := strings.Split(fullPath, "/")
    doc := strings.Join(pathParts[1:], "/")
    log.Printf("Doc: %+v\n", doc)

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

    processUpdate(&data)

    return nil

}

func processUpdate(data *firestoredata.DocumentEventData) {
    updateMask := data.GetUpdateMask()
    log.Printf("Update Mask: %+v\n", updateMask)

    for _, fieldName := range updateMask.GetFieldPaths() {
        log.Printf("=================================")
        log.Printf("FieldName: %v", fieldName)
        log.Printf("FieldNameType: %v", reflect.TypeOf(fieldName))

        fieldValue, ok := data.GetValue().GetFields()[fieldName]
        if ok {
            log.Printf("FieldValue: %v", fieldValue)
            stringSlice := strings.Split(fieldValue.String(), ":")
            log.Println(stringSlice)
        }
    }
}

The above code works fine for updates to the top-level non-nested-fields (prefix aa) and I am able to print the fieldValue.

However for updates to nested fields (inside map1 and map3), ok is false.

How do I get the updated fieldValue for nested fields?

The logs generated are:

2024/04/14 14:55:44 Doc: testdoc
2024/04/14 14:55:44 Function triggered by change to: //firestore.googleapis.com/projects/myproj/databases/(default)
2024/04/14 14:55:44 Old value: name:"projects/myproj/databases/(default)/documents/testcollection/testdoc" fields:{key:"aaBool" value:{boolean_value:true}} fields:{key:"aaInt" value:{integer_value:11111}} fields:{key:"aaStr" value:{string_value:"Lorem"}} fields:{key:"map1" value:{map_value:{fields:{key:"m1book" value:{boolean_value:true}} fields:{key:"m1int" value:{integer_value:3333}} fields:{key:"m1str" value:{string_value:"m1Lorem"}}}}} fields:{key:"map2" value:{map_value:{fields:{key:"map3" value:{map_value:{fields:{key:"m3int" value:{integer_value:888}} fields:{key:"m3str" value:{string_value:"m3Lorem"}}}}}}}} create_time:{seconds:1713104864 nanos:548214000} update_time:{seconds:1713106076 nanos:80031000}
2024/04/14 14:55:44 New value: name:"projects/myproj/databases/(default)/documents/testcollection/testdoc" fields:{key:"aaBool" value:{boolean_value:false}} fields:{key:"aaInt" value:{integer_value:222}} fields:{key:"aaStr" value:{string_value:"LoremIpsum"}} fields:{key:"map1" value:{map_value:{fields:{key:"m1book" value:{boolean_value:false}} fields:{key:"m1int" value:{integer_value:4444}} fields:{key:"m1str" value:{string_value:"m1LoremIpsum"}}}}} fields:{key:"map2" value:{map_value:{fields:{key:"map3" value:{map_value:{fields:{key:"m3int" value:{integer_value:999}} fields:{key:"m3str" value:{string_value:"m3LoremIpsum"}}}}}}}} create_time:{seconds:1713104864 nanos:548214000} update_time:{seconds:1713106543 nanos:26415000}
2024/04/14 14:55:44 Update Mask: field_paths:"map2.map3.m3int" field_paths:"map2.map3.m3str" field_paths:"map1.m1str" field_paths:"map1.m1int" field_paths:"map1.m1book" field_paths:"aaStr" field_paths:"aaBool" field_paths:"aaInt"
2024/04/14 14:55:44 =================================
2024/04/14 14:55:44 FieldName: map2.map3.m3int
2024/04/14 14:55:44 FieldNameType: string
2024/04/14 14:55:44 =================================
2024/04/14 14:55:44 FieldName: map2.map3.m3str
2024/04/14 14:55:44 FieldNameType: string
2024/04/14 14:55:44 =================================
2024/04/14 14:55:44 FieldName: map1.m1str
2024/04/14 14:55:44 FieldNameType: string
2024/04/14 14:55:44 =================================
2024/04/14 14:55:44 FieldName: map1.m1int
2024/04/14 14:55:44 FieldNameType: string
2024/04/14 14:55:44 =================================
2024/04/14 14:55:44 FieldName: map1.m1book
2024/04/14 14:55:44 FieldNameType: string
2024/04/14 14:55:44 =================================
2024/04/14 14:55:44 FieldName: aaStr
2024/04/14 14:55:44 FieldNameType: string
2024/04/14 14:55:44 FieldValue: string_value:"LoremIpsum"
2024/04/14 14:55:44 [string_value "LoremIpsum"]
2024/04/14 14:55:44 =================================
2024/04/14 14:55:44 FieldName: aaBool
2024/04/14 14:55:44 FieldNameType: string
2024/04/14 14:55:44 FieldValue: boolean_value:false
2024/04/14 14:55:44 [boolean_value false]
2024/04/14 14:55:44 =================================
2024/04/14 14:55:44 FieldName: aaInt
2024/04/14 14:55:44 FieldNameType: string
2024/04/14 14:55:44 FieldValue: integer_value:222
2024/04/14 14:55:44 [integer_value 222]

 In case required, this is how I deploy the trigger:

gcloud functions deploy test-trigger-1 \
    --gen2 \
    --runtime=go122 \
    --region=us-west1 \
    --trigger-location=us-west1 \
    --source=. \
    --entry-point=ProcessTrigger \
    --min-instances=1 \
    --memory=256Mi \
    --set-env-vars=[GOOGLE_CLOUD_PROJECT=myproj] \
    --trigger-event-filters=type=google.cloud.firestore.document.v1.written \
    --trigger-event-filters=database='(default)' \
    --trigger-event-filters-path-pattern=document='testcollection/{docID}'

 

 
 
 
 
5 1 494
1 REPLY 1

Hi @gcpfirestore,

Welcome to Google Cloud Community!

The reason your code wasn't working for nested fields was because you were trying to access them directly from a flat map. Firestore uses nested maps for nested data.

The solution is to use the GetNested method provided by the firestoredata.DocumentEventData struct. This method takes the path segments of the nested field (separated by ".") and returns its value.

The deployment configuration you provided looks good. Make sure to update the GOOGLE_CLOUD_PROJECT environment variable with your actual project ID.

This fix should allow you to access and process updates for nested fields within your Firestore documents triggered by your Cloud Function.