From d0a30ce3bf51abe65875f8e951156a2649164be5 Mon Sep 17 00:00:00 2001 From: jonathanedey Date: Thu, 4 Dec 2025 12:35:14 -0500 Subject: [PATCH] feat(firestore): Add Firestore Multi Database Support --- firebase.go | 9 ++ firebase_test.go | 16 +++ integration/firestore/firestore_test.go | 153 +++++++++++++++++++++++- 3 files changed, 172 insertions(+), 6 deletions(-) diff --git a/firebase.go b/firebase.go index 9373ae23..a4712f8d 100644 --- a/firebase.go +++ b/firebase.go @@ -111,6 +111,15 @@ func (a *App) Firestore(ctx context.Context) (*firestore.Client, error) { return firestore.NewClient(ctx, a.projectID, a.opts...) } +// FirestoreWithDatabase returns a new firestore.Client instance with the specified named database from the +// https://godoc.org/cloud.google.com/go/firestore package. +func (a *App) FirestoreWithDatabase(ctx context.Context, databaseID string) (*firestore.Client, error) { + if a.projectID == "" { + return nil, errors.New("project id is required to access Firestore") + } + return firestore.NewClientWithDatabase(ctx, a.projectID, databaseID, a.opts...) +} + // InstanceID returns an instance of iid.Client. func (a *App) InstanceID(ctx context.Context) (*iid.Client, error) { conf := &internal.InstanceIDConfig{ diff --git a/firebase_test.go b/firebase_test.go index 2699146a..8225cf5a 100644 --- a/firebase_test.go +++ b/firebase_test.go @@ -287,6 +287,18 @@ func TestFirestore(t *testing.T) { } } +func TestFirestoreWithDatabase(t *testing.T) { + ctx := context.Background() + app, err := NewApp(ctx, nil, option.WithCredentialsFile("testdata/service_account.json")) + if err != nil { + t.Fatal(err) + } + + if c, err := app.FirestoreWithDatabase(ctx, "other-db"); c == nil || err != nil { + t.Errorf("FirestoreWithDatabase() = (%v, %v); want (client, nil)", c, err) + } +} + func TestFirestoreWithProjectID(t *testing.T) { verify := func(varName string) { current := os.Getenv(varName) @@ -336,6 +348,10 @@ func TestFirestoreWithNoProjectID(t *testing.T) { if c, err := app.Firestore(ctx); c != nil || err == nil { t.Errorf("Firestore() = (%v, %v); want (nil, error)", c, err) } + + if c, err := app.FirestoreWithDatabase(ctx, "other-db"); c != nil || err == nil { + t.Errorf("FirestoreWithDatabase() = (%v, %v); want (nil, error)", c, err) + } } func TestInstanceID(t *testing.T) { diff --git a/integration/firestore/firestore_test.go b/integration/firestore/firestore_test.go index 3e6d2da9..0299bad3 100644 --- a/integration/firestore/firestore_test.go +++ b/integration/firestore/firestore_test.go @@ -20,9 +20,27 @@ import ( "reflect" "testing" + "time" + + "cloud.google.com/go/firestore" "firebase.google.com/go/v4/integration/internal" ) +var ( + cityData = map[string]interface{}{ + "name": "Mountain View", + "country": "USA", + "population": int64(77846), + "capital": false, + } + movieData = map[string]interface{}{ + "Name": "Interstellar", + "Year": int64(2014), + "Runtime": "2h 49m", + "Academy Award Winner": true, + } +) + func TestFirestore(t *testing.T) { if testing.Short() { log.Println("skipping Firestore integration tests in short mode.") @@ -40,11 +58,130 @@ func TestFirestore(t *testing.T) { } doc := client.Collection("cities").Doc("Mountain View") + if _, err := doc.Set(ctx, cityData); err != nil { + t.Fatal(err) + } + snap, err := doc.Get(ctx) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(snap.Data(), cityData) { + t.Errorf("Get() = %v; want %v", snap.Data(), cityData) + } + if _, err := doc.Delete(ctx); err != nil { + t.Fatal(err) + } +} + +func TestFirestoreWithDatabase(t *testing.T) { + if testing.Short() { + log.Println("skipping Firestore integration tests in short mode.") + return + } + ctx := context.Background() + app, err := internal.NewTestApp(ctx, nil) + if err != nil { + t.Fatal(err) + } + + // This test requires a non-default database to exist in the project. + // If it doesn't exist, this test will fail. + client, err := app.FirestoreWithDatabase(ctx, "testing-database") + if err != nil { + t.Fatal(err) + } + + doc := client.Collection("cities").NewDoc() + if _, err := doc.Set(ctx, cityData); err != nil { + t.Fatal(err) + } + snap, err := doc.Get(ctx) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(snap.Data(), cityData) { + t.Errorf("Get() = %v; want %v", snap.Data(), cityData) + } + if _, err := doc.Delete(ctx); err != nil { + t.Fatal(err) + } +} + +func TestFirestoreMultiDB(t *testing.T) { + if testing.Short() { + log.Println("skipping Firestore integration tests in short mode.") + return + } + ctx := context.Background() + app, err := internal.NewTestApp(ctx, nil) + if err != nil { + t.Fatal(err) + } + + cityClient, err := app.Firestore(ctx) + if err != nil { + t.Fatal(err) + } + // This test requires a non-default database to exist in the project. + movieClient, err := app.FirestoreWithDatabase(ctx, "testing-database") + if err != nil { + t.Fatal(err) + } + + cityDoc := cityClient.Collection("cities").NewDoc() + movieDoc := movieClient.Collection("movies").NewDoc() + + if _, err := cityDoc.Set(ctx, cityData); err != nil { + t.Fatal(err) + } + if _, err := movieDoc.Set(ctx, movieData); err != nil { + t.Fatal(err) + } + + citySnap, err := cityDoc.Get(ctx) + if err != nil { + t.Fatal(err) + } + movieSnap, err := movieDoc.Get(ctx) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(citySnap.Data(), cityData) { + t.Errorf("City Get() = %v; want %v", citySnap.Data(), cityData) + } + if !reflect.DeepEqual(movieSnap.Data(), movieData) { + t.Errorf("Movie Get() = %v; want %v", movieSnap.Data(), movieData) + } + + if _, err := cityDoc.Delete(ctx); err != nil { + t.Fatal(err) + } + if _, err := movieDoc.Delete(ctx); err != nil { + t.Fatal(err) + } +} + +func TestServerTimestamp(t *testing.T) { + if testing.Short() { + log.Println("skipping Firestore integration tests in short mode.") + return + } + ctx := context.Background() + app, err := internal.NewTestApp(ctx, nil) + if err != nil { + t.Fatal(err) + } + + client, err := app.Firestore(ctx) + if err != nil { + t.Fatal(err) + } + + doc := client.Collection("cities").NewDoc() data := map[string]interface{}{ - "name": "Mountain View", - "country": "USA", - "population": int64(77846), - "capital": false, + "name": "Mountain View", + "timestamp": firestore.ServerTimestamp, } if _, err := doc.Set(ctx, data); err != nil { t.Fatal(err) @@ -53,8 +190,12 @@ func TestFirestore(t *testing.T) { if err != nil { t.Fatal(err) } - if !reflect.DeepEqual(snap.Data(), data) { - t.Errorf("Get() = %v; want %v", snap.Data(), data) + got := snap.Data() + if got["name"] != "Mountain View" { + t.Errorf("Name = %v; want Mountain View", got["name"]) + } + if _, ok := got["timestamp"].(time.Time); !ok { + t.Errorf("Timestamp is not a time.Time: %v", got["timestamp"]) } if _, err := doc.Delete(ctx); err != nil { t.Fatal(err)