Cloud Firestore Triggers in Golang
- 4 minutes read - 695 wordsI was pleased to discover that Google provides a non-Node.JS mechanism to subscribe to and act upon Firestore triggers, Google Cloud Firestore Triggers. I’ve nothing against Node.JS but, for the project i’m developing, everything else is written in Golang. It’s good to keep it all in one language.
I’m perplexed that Cloud Functions still (!) only supports Go 1.13 (03-Sep-2019). Even Go 1.14 (25-Feb-2020) was released pre-pandemic and we’re now running on 1.16. Come on Google!
Golang
Google’s examples are ok but a couple of parts weren’t immediately obvious to me.
First, the use of {wildcard}
. The explanation makes sense, you can insert variables into wildcard document paths. What was confusing is, how to use the result. I was expecting wildcard
variable to surface in the event but it does not. What (appears to) happen is that {wildcard}
values are included in the event’s Value.Name
and can be parsed to extract the actual value.
So, for example, if you had a path /customers/{customerId}
and let’s suppose, you changed a customer
document property, the event would be triggered and e.Value.Name
would be something of the form /customers/12348378e41bf59b06d062f9c93ae0b246ad7e1c418c0557ab2dfcbb
. So, you could extract 1234...
from the Name
to determine the value of {customerId}
for this event.
Second, the explanation for the structure of the Firestore document object was confusing. It’s described as part of the Firebase Document object but it wasn’t obvious from Google’s example, how I’d map what I had to what I wanted (even after logging interface{}
).
In my very simple case, I have a Golang Customer
struct that I’m adding to Firestore:
type Customer struct {
Name string `firestore:"name"`
Domain string `firestore:"domain"`
Created time.Time `firestore:"created"`
Updated time.Time `firestore:"updated,omitempty"`
}
The equivalent struct for Fields
in my Cloud Functions trigger is:
type Customer struct {
Name struct {
StringValue string `json:"stringValue"`
} `json:"Name"`
Domain struct {
StringValue string `json:"stringValue"`
} `json:"Domain"`
}
Obviously, each property from the source is mapped to the destination struct. But, each scalar property becomes a struct and the property of the struct is the type per Value
. In my simple case of using on string
, I have properties called StringValue
of type string
with the json
annotation.
Here’s the source in its entirety:
package f
import (
"context"
stdlog "log"
"os"
"time"
"cloud.google.com/go/functions/metadata"
"github.com/go-logr/logr"
"github.com/go-logr/stdr"
)
// CustomerEvent is the payload of a Firestore event triggered by Customer change.
type CustomerEvent struct {
OldValue CustomerValue `json:"oldValue"`
Value CustomerValue `json:"value"`
UpdateMask struct {
FieldPaths []string `json:"fieldPaths"`
} `json:"updateMask"`
}
// CustomerValue holds Customer fields.
type CustomerValue struct {
Name string `json:"name"`
Fields CustomerFields `json:"fields"`
CreateTime time.Time `json:"createTime"`
UpdateTime time.Time `json:"updateTime"`
}
// CustomerFields is a mapping of the type of Firestore Customer record
type CustomerFields struct {
Name struct {
StringValue string `json:"stringValue"`
} `json:"Name"`
Domain struct {
StringValue string `json:"stringValue"`
} `json:"Domain"`
}
var (
log logr.Logger
)
func init() {
std := stdlog.New(os.Stderr, "", stdlog.LstdFlags)
opts := stdr.Options{LogCaller: stdr.All}
log = stdr.NewWithOptions(std, opts)
}
func CreateCustomerResource(ctx context.Context, e CustomerEvent) error {
log := log.WithValues("CreateCustomerResource")
meta, err := metadata.FromContext(ctx)
if err != nil {
msg := "failed to get metadata"
log.Error(err, msg)
}
log.Info("Trigger",
"event", meta.EventID,
"type", meta.EventType,
"resource", meta.Resource,
"name", meta.Resource.Name,
)
log.Info("Triggered",
"name", e.Value.Name,
)
log.Info("Triggered",
"old.name", e.OldValue.Fields.Name.StringValue,
"old.domain", e.OldValue.Fields.Domain.StringValue,
"new.name", e.Value.Fields.Name.StringValue,
"new.domain", e.Value.Fields.Domain.StringValue,
)
return nil
}
Deploy
gcloud functions deploy ${NAME} \
--source=${PWD} \
--entry-point="CreateCustomerResource" \
--runtime=go113 \
--trigger-event="providers/cloud.firestore/eventTypes/document.write" \
--trigger-resource="projects/${PROJECT}/databases/(default)/documents/customers/{customerId}" \
--region=${REGION} \
--project=${PROJECT}
The trigger-event
is well described in Event Types. I’m choosing to trigger the Fucntion on creates, updates and deletes for trigger-resource
.
The trigger-resource
as explained above has a fixed prefix projects/${PROJECT}/databases/(default)/documents
and then I want to focus on /customers
and I want to capture the {customerId}
.
NOTE I’m unsure why
default
is(default)
.
NOTE
{customerId}
is arbitrary here as a merely signals to the runtime to capture the value in this part of the path.
Trigger
I’ve been using the Firestore Console and editing a customers
document to trigger the Cloud Function.
And then using the Logs to see what’s happened (since my code merely logs that it’s been called).
Logs
FILTER="resource.type=\"cloud_function\" "\
"resource.labels.function_name=\"${NAME}\" "\
"resource.labels.region=\"${REGION}\""
gcloud logging read \
"${FILTER}" \
--project=${PROJECT} \
--format=json \
--freshness=5m \
| jq -r '.[]|.textPayload'