DEV Community

Alex Mammay
Alex Mammay

Posted on • Edited on

Firebase Emulator Suite: Running golang/python/java firestore event triggers locally with the emulator

Purpose

The firebase team has done a really awesome job at providing a rich and feature complete environment for running and testing out various products all locally (firestore emulator, real time database emulator, pub/sub emulator).

Most of the support/docs/guides out there mainly cover using the firebase-tools cli and using Node.Js.

This post is mainly to show that it's possible to recreate a nice local development setup with the other languages that GCP Cloud Functions supports, such as Java, Python, and Go.

for the purpose of this write up, we will be using golang

Setting up the emulator

Grab the latest firestore emulator with using firebase-tools cli by running firebase setup:emulators:firestore.

Invoke the help menu of the jar with java -jar ~/.cache/firebase/emulators/cloud-firestore-emulator-v1.11.7.jar --help, as of the time i am writing this the current version is v1.11.7 so thats what this sample will be with.

Run the emulator with java -jar ~/.cache/firebase/emulators/cloud-firestore-emulator-v1.11.7.jar --functions_emulator localhost:6000 this will set the callback url for functions events to localhost:6000 and thats where we will be running our local function at.

Register our function resource with the firestore emulator. We want to watch for all document writes to the root collection of skills/{id} and the name of our Function is called "WriteSkills" .

curl --location --request PUT 'http://localhost:8080/emulator/v1/projects/dummy/triggers/WriteSkills' \
--header 'Content-Type: application/json' \
--data-raw '{
   "eventTrigger": {
       "resource": "projects/dummy/databases/(default)/documents/skills/{id}",
       "eventType": "providers/cloud.firestore/eventTypes/document.write",
       "service": "firestore.googleapis.com"
   }
}'

As a quick schema reference you can use the following.

curl --location --request PUT 'http://[HOST:PORT]/emulator/v1/projects/[PROJECT_ID]/triggers/[FUNCTION_NAME]' \
--header 'Content-Type: application/json' \
--data-raw '{
    "eventTrigger": {
        "resource": "projects/[PROJECT_ID]/databases/(default)/documents/[RESOURCE_PATH]",
        "eventType": [EVENT_TYPE],
        "service": "firestore.googleapis.com"
    }
}'

the event types for firestore can be

"providers/cloud.firestore/eventTypes/document.create"
"providers/cloud.firestore/eventTypes/document.update"
"providers/cloud.firestore/eventTypes/document.delete"
"providers/cloud.firestore/eventTypes/document.write"

Our function code for golang will be


package main

import (
    "cloud.google.com/go/firestore"
    "cloud.google.com/go/functions/metadata"
    "context"
    "fmt"
    "github.com/GoogleCloudPlatform/functions-framework-go/funcframework"
    "log"
    "time"
)

type FirestoreEvent struct {
    OldValue   FirestoreValue `json:"oldValue"`
    Value      FirestoreValue `json:"value"`
    UpdateMask struct {
        FieldPaths []string `json:"fieldPaths"`
    } `json:"updateMask"`
}

type FirestoreValue struct {
    CreateTime time.Time   `json:"createTime"`
    Fields     interface{} `json:"fields"`
    Name       string      `json:"name"`
    UpdateTime time.Time   `json:"updateTime"`
}

func main() {
    // create a firestore write event in a go routine that will be ran by the time local dev server is spun up
    go func() {
        ctx := context.Background()
        firestoreClient, err := firestore.NewClient(ctx, "dummy")
        if err != nil {
            log.Fatalf("firestore.NewClient: %v", err)
        }
        defer firestoreClient.Close()

        if _, err := firestoreClient.Collection("skills").NewDoc().Create(ctx, map[string]interface{}{
            "skill":     "Running",
            "timestamp": firestore.ServerTimestamp,
        }); err != nil {
            log.Fatalf("firestoreClient.Collection.NewDoc().Set: %v", err)
        }
    }()
    funcframework.RegisterEventFunction("/functions/projects/dummy/triggers/WriteSkills", WriteSkills)
    if err := funcframework.Start("6000"); err != nil {
        panic(err)
    }
}

func WriteSkills(ctx context.Context, e FirestoreEvent) error {
    meta, err := metadata.FromContext(ctx)
    if err != nil {
        return fmt.Errorf("metadata.FromContext: %v", err)
    }
    log.Printf("Function triggered by change to: %v", meta.Resource)
    log.Printf("Old value: %+v", e.OldValue)
    log.Printf("New value: %+v", e.Value)
    return nil
}

now we will run it by invoking

export FIRESTORE_EMULATOR_HOST=localhost:8080 && go run .

and we shall see an output of

Serving function...
2020/09/08 16:16:58 Function triggered by change to: &{firestore.googleapis.com projects/dummy/databases/(default)/documents/skills/3wUTxvMAawFlJPQmxwYv  }
2020/09/08 16:16:58 Old value: {CreateTime:0001-01-01 00:00:00 +0000 UTC Fields:<nil> Name: UpdateTime:0001-01-01 00:00:00 +0000 UTC}
2020/09/08 16:16:58 New value: {CreateTime:2020-09-08 20:16:58.461011 +0000 UTC Fields:map[skill:map[stringValue:Running] timestamp:map[timestampValue:2020-09-08T20:16:58.459Z]] Name:projects/dummy/databases/(default)/documents/skills/3wUTxvMAawFlJPQmxwYv UpdateTime:2020-09-08 20:16:58.461011 +0000 UTC}

Util Time!

Finally i wrote a little util https://github.com/amammay/firebase-emu that will register your go function against the firestore emulator automatically and then add a entry into the funcframework for you.

package main

import (
    "cloud.google.com/go/firestore"
    "cloud.google.com/go/functions/metadata"
    "context"
    "fmt"
    "github.com/GoogleCloudPlatform/functions-framework-go/funcframework"
    "github.com/amammay/firebase-emu/fsemu"
    "log"
    "time"
)

func main() {

    go func() {
        ctx := context.Background()

        firestoreClient, err := firestore.NewClient(ctx, "dummy")
        if err != nil {
            log.Fatalf("firestore.NewClient: %v", err)
        }
        defer firestoreClient.Close()

        if _, err := firestoreClient.Collection("skills").NewDoc().Create(ctx, map[string]interface{}{
            "skill":     "Running",
            "timestamp": firestore.ServerTimestamp,
        }); err != nil {
            log.Fatalf("firestoreClient.Collection.NewDoc().Set: %v", err)
        }
    }()
    event := fsemu.EmuResource{ProjectId: "dummy", Address: "http://localhost:8080"}
    emuRegisters := []fsemu.EmuRegister{{
        TriggerFn:    WriteSkills,
        TriggerType:  fsemu.FirestoreOnWrite,
        ResourcePath: "skills/{id}",
    }}

        //register firestore emulator resources
    if err := event.RegisterToEmu(emuRegisters); err != nil {
        panic(err)
    }
    if err := funcframework.Start("6000"); err != nil {
        panic(err)
    }
}

type FirestoreEvent struct {
    OldValue   FirestoreValue `json:"oldValue"`
    Value      FirestoreValue `json:"value"`
    UpdateMask struct {
        FieldPaths []string `json:"fieldPaths"`
    } `json:"updateMask"`
}

type FirestoreValue struct {
    CreateTime time.Time   `json:"createTime"`
    Fields     interface{} `json:"fields"`
    Name       string      `json:"name"`
    UpdateTime time.Time   `json:"updateTime"`
}

func WriteSkills(ctx context.Context, e FirestoreEvent) error {

    meta, err := metadata.FromContext(ctx)
    if err != nil {
        return fmt.Errorf("metadata.FromContext: %v", err)
    }
    log.Printf("Function triggered by change to: %v", meta.Resource)
    log.Printf("Old value: %+v", e.OldValue)
    log.Printf("New value: %+v", e.Value)
    return nil
}


Top comments (0)