Adding nested struct to Firestore - firebase

I am trying to add a nested struct to Firestore and for some reason the contents added are all non-structs, which look something like:
The structs look something like this:
type Status struct {
Title string `json:"title,omitempty" firestore:"title,omitempty"`
Message string `json:"message,omitempty" firestore:"title,omitempty"`
}
type Config struct {
Name string `json:"name,omitempty" firestore:"name,omitempty"`
Status Status `json:"status,omitempty" firestore:"status,omitempty"`
}
And the code looks something like this:
import (
"context"
firebase "firebase.google.com/go/v4"
"google.golang.org/api/option"
)
func main() {
configuration := Config{
Name: "Test",
Status: Status{
Title: "hello",
Message: "hi",
},
}
ctx := context.Background()
config := firebase.Config{
ProjectID: "",
StorageBucket: "",
}
opt := option.WithCredentialsFile("firebase_config.json")
app, err := firebase.NewApp(ctx, &config, opt)
if err != nil {
panic(err)
}
// Get an auth client from the firebase.App
client, err := app.Firestore(ctx)
if err != nil {
panic(err)
}
_, _, err = client.Collection("forecast").Add(ctx, configuration)
if err != nil {
panic(err)
}
}
The above code only works for elements that are not in the nested structure.
Any help on this would be appreciated
Update 1
Status is not a sub collection but an object, something like:
{
"name": "Test",
"status": {
"title": "hello",
"message": "hi"
}
}

Firestore is optimized as a hash entry and retrieval datastore. As a consequence, it's better to create maps out of your structs. Structs are good for Go data modeling but when it's time to submit to the database, convert it to a map.
I usually just use Fatih's struct to map converter
It makes it easy to reason about your data on the Go side and still be able to submit it for storage.

Posting this as Community Wiki answer, based in the discussion of the comments.
The solution for this case seems to be adding values manually in a field of type Map. The steps to achieve that are the following: Access the Firebase console -> Firestore -> Create a document -> Add field of type Map. Following this order it's possible to create a field of type Map, that has the format needed to add data as described in the description of the question.
More information about this type, how it works, sort options, etc., can be found in the official documentation here: Supported data types.

Related

How to get any data from my real-time database in firebase using Go

I have simple test database with one row (key - value) and I can't get any data from database, although I use docs for Go (admin sdk - https://firebase.google.com/docs/database/admin/start?authuser=0#go). I tried search but info how to use by Go is very small
Here my code.
Here my json-file.
In playground don't work, need use json-file and execute from code editor/IDE.
All I found was a couple of video tutorials from the firebase guys themselves where they show how to connect, but the same thing does not work for me. All other information is about how to use firebase via android, iphone and web (js).
package main
import (
"context"
"fmt"
"log"
firebase "firebase.google.com/go"
"google.golang.org/api/option"
)
type Data struct {
TypeClient string `json:"typeClient,omitempty"`
}
var responseData structs.Data
func main() {
ctx := context.Background()
// Initialize the app with a custom auth variable, limiting the server's
access
ao := map[string]interface{}{"uid": "my-service-worker"}
conf := &firebase.Config{
DatabaseURL: "https://test-v06f06-default-rtdb.firebaseio.com",
AuthOverride: &ao,
}
// Fetch the service account key JSON file contents
opt := option.WithCredentialsFile("./test-v06f06-firebase-adminsdk-1ze0m-
bbf3b57ef3.json")
app, err := firebase.NewApp(ctx, conf, opt)
if err != nil {
log.Fatalln("Error initializing app:", err)
}
// fmt.Printf("%T\n", app)
client, err := app.Database(ctx)
if err != nil {
log.Fatalln("Error initializing database client:", err)
}
// fmt.Printf("%T\n", client)
// The app only has access as defined in the Security Rules
ref := client.NewRef("/admin_permission")
// fmt.Printf("%T\n", ref)
// Get data
if err := ref.Get(ctx, &responseData); err != nil {
log.Fatalln("Error reading from database:", err)
}
fmt.Println("Client Type -", responseData)}
In last row where print I get empty variable. The data is simply not written to the variable.
Help if you can. Thanks in advance.
screenshot tiny db

How can we update a record by a http post method in GoLang?

Problem description:
I am learning Golang to implement the REST API for a small project.
I was following this small example to get the some idea how to connect things.
However, it looks like there are some bugs in the sample example, that i could not get the expected response in postman after hitting the endpoints.
I have fixed it by adding the missing functions (HandleFunc functions) to make it work.
Problem Description:
However, I still have an issue with CreateEvent section.
The expectation is that after using POST method with a given sample Event (json format) like below, event list is updated.
{
"id": "23",
"title": "This is simple Go lang title for test!",
"Description":"In this course you will learn REST api implementation in Go lang"
}
But after reaching the "http://localhost:8080/events" endpoint in which I defined to return all the events (1 defined inside code, the other should be added by calling CreateEvent function) i get only one of the event (hard coded one inside code only) in response.
Here is the complete code.
I appreciate for any suggestions/comments.
package main
import (
"fmt"
"log"
"net/http"
"io/ioutil"
"encoding/json"
"github.com/gorilla/mux"
)
func homeLink(w http.ResponseWriter, r *http.Request) {
fmt.Println("test started!")
fmt.Fprintf(w, "Welcome home!")
}
func main() {
router := mux.NewRouter().StrictSlash(true)
router.HandleFunc("/", homeLink)
/*i have added the next 3 lines, missing in the sample code*/
router.HandleFunc("/event", createEvent)
router.HandleFunc("/events/{id}", getOneEvent)
router.HandleFunc("/events", getAllEvents)
log.Fatal(http.ListenAndServe(":8080", router))
}
type event struct {
ID string `json:"ID"`
Title string `json:"Title"`
Description string `json:"Description"`
}
type allEvents []event
var events = allEvents{
{
ID: "1",
Title: "Introduction to Golang",
Description: "Come join us for a chance to learn how golang works and get to eventually try it out",
},
}
func createEvent(w http.ResponseWriter, r *http.Request) {
var newEvent event
reqBody, err := ioutil.ReadAll(r.Body)
if err != nil {
fmt.Fprintf(w, "Kindly enter data with the event title and description only in order to update")
}
fmt.Println("Create Event is called!")
json.Unmarshal(reqBody, &newEvent)
events = append(events, newEvent)
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(newEvent)
}
func getOneEvent(w http.ResponseWriter, r *http.Request) {
eventID := mux.Vars(r)["id"]
fmt.Println("get one event is called!")
fmt.Println(eventID)
for _, singleEvent := range events {
if singleEvent.ID == eventID {
json.NewEncoder(w).Encode(singleEvent)
}
}
}
func getAllEvents(w http.ResponseWriter, r *http.Request) {
fmt.Println("Get all events is called!")
json.NewEncoder(w).Encode(events)
}
Your code is working fine. I have tested it (just copied above code and ran in my local machine and tested with Postman).
Btw, i have added few recommendations for a better code below.
If there is not nil error, handle it and return.
reqBody, err := ioutil.ReadAll(r.Body)
if err != nil {
fmt.Fprintf(w, "Kindly enter data with the event title and description only in order to update")
return //add this return, otherwise continue the function with the error
}
Put this json error handling for createEvent Handler function
err = json.Unmarshal(reqBody, &newEvent)
if err != nil {
fmt.Fprintf(w, "json format invalid")
return
}
Add http methods to your endpoints.
router.HandleFunc("/", homeLink).Methods(http.MethodGet)
/*i have added the next 3 lines, missing in the sample code*/
router.HandleFunc("/event", createEvent).Methods(http.MethodPost)
router.HandleFunc("/events/{id}", getOneEvent).Methods(http.MethodGet)
router.HandleFunc("/events", getAllEvents).Methods(http.MethodGet)

Firestore Golang run query with filters in transaction

I can't figure out how to run a firestore.Query in a transaction in the Golang Admin SDK.
The firestore.Transaction has a GetAll() method that takes an array of *firestore.DocumentRef, which I assume is how one would query multiple documents in a transaction. This works if I want to query an entire collection, since I can use tx.DocumentRefs to convert a *firestore.CollectionRef into document refs but there doesn't seem to be an equivalent method for queries (for example if I want to filter the collection).
In the NodeJS Admin SDK I could do something like:
admin.firestore().runTransaction(async (t) => {
const fooCollectionRef = admin.firestore().collection('foos').where('bar', '==', true);
const foosSnapshot = await t.get(fooCollectionRef);
// do stuff with the foos
})
How can I accomplish the same in Golang?
Use the Transaction.Documents method. It accepts a Queryier which can be a firestore.Query. See this method in the docs https://pkg.go.dev/cloud.google.com/go/firestore#Transaction.Documents
Following is a simple example:
err := client.RunTransaction(ctx, func(ctx context.Context, tx *firestore.Transaction) error {
col := client.Collection("myCollection")
query := col.Where("myCondition", "==", 1)
docs := tx.Documents(query)
for {
d, err := docs.Next()
if err != nil {
if err == iterator.Done {
break
}
//handle error
}
//do something with the document
}
return nil
})
if err != nil {
// handle error.
}

Create new client for every operation to firestore?

I see examples like these in firestore Go Docs. Should we have to create a client like below for every operation to firestore or can we create a client during application startup and use the same client to perform an operation? Please let me know.
ctx := context.Background()
client, err := firestore.NewClient(ctx, "project-id")
if err != nil {
// TODO: Handle error.
}
defer client.Close()
type State struct {
Capital string `firestore:"capital"`
Population float64 `firestore:"pop"` // in millions
}
wr, err := client.Doc("States/Colorado").Create(ctx, State{
Capital: "Denver",
Population: 5.5,
})
if err != nil {
// TODO: Handle error.
}
fmt.Println(wr.UpdateTime)
No.Create the client once and re-use the same client. More details on this post - Should a Firestore client be created per a request with Google App Engine?

Asynchronous Testing With Stream Processing

I'm very new to Go, so I may be misunderstanding something foundational about Go's async/stream handling, but here goes...
I'm trying to write some tests using ginkgo on a function I wrote that processes streams.
The processing side reads in newline-delimited text from a File until it encounters a special delimiter line at which point it tries to parse the text as JSON. The code looks like this:
func ParseConfig(inStream *os.File) (Config, error){
var header string
var stdin = bufio.NewScanner(inStream)
for stdin.Scan() {
line := stdin.Text()
if line == "|||" {
break;
}
header += line
}
// parse JSON here and return
}
My test looks something like this
Describe("ParseConfig()", func() {
It("should pass for a valid header", func(){
_, err := io.WriteString(stream, "{\"Key\": \"key\", \"File\": \"file\"}\n|||\n")
Expect(err).NotTo(HaveOccurred())
conf, err := parser.ParseConfig(stream)
Expect(err).NotTo(HaveOccurred())
Expect(conf.Key).To(Equal("key"))
})
})
Unfortunately, this yields a JSON parsing error, as it's trying to parse an empty string. I'm assuming that my problem is that I'm sending the string on the stream before I've told the ParseConfig() function to listen on that string for data? But I'm not entirely clear how I could refactor this to use proper go routines to first listen for data then send it.
Some of the potential solutions I saw were around the use of "channels" (with which I'm unfamiliar) but I was worried that this one need might not be worth a major refactor to introduce a whole new paradigm of concurrency.
Thanks!
Not sure if I understood correctly, but your ParseConfig should probably take an io.Reader instead of a *os.File. That way you can test it directly without worrying about concurrency.
file t_test.go:
package main
import (
"strings"
"testing"
"github.com/onsi/ginkgo"
"github.com/onsi/gomega"
)
var _ = ginkgo.Describe("ParseConfig()", func() {
ginkgo.It("should pass for a valid header", func() {
// really don't know what you were doing with your 'stream' variable
// This is a test, you should forge a test scenario and pass it to your config function
stream := strings.NewReader(`{"Key": "key", "File": "file"}` + "\n|||\n")
conf, err := ParseConfig(stream)
gomega.Expect(err).NotTo(gomega.HaveOccurred())
gomega.Expect(conf.Key).To(gomega.Equal("key"))
})
})
func TestParseConfig(t *testing.T) {
ginkgo.RunSpecs(t, "Parse Config")
}
file main.go
package main
import (
"bufio"
"encoding/json"
"io"
"log"
"os"
)
type Config struct {
Key string
File string
}
func ParseConfig(inStream io.Reader) (*Config, error) {
var header string
var stdin = bufio.NewScanner(inStream)
for stdin.Scan() {
line := stdin.Text()
if line == "|||" {
break
}
header += line
}
c := &Config{}
// parse JSON here and return
if err := json.Unmarshal([]byte(header), c); err != nil {
return nil, err
}
return c, nil
}
func main() {
f, err := os.Open("config.json")
if err != nil {
log.Fatal(err)
}
ParseConfig(f)
}

Resources