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)