Skip to content

Instantly share code, notes, and snippets.

@icchan
Created January 8, 2026 14:57
Show Gist options
  • Select an option

  • Save icchan/ceb4eac748def5433b3313ce3754def7 to your computer and use it in GitHub Desktop.

Select an option

Save icchan/ceb4eac748def5433b3313ce3754def7 to your computer and use it in GitHub Desktop.
package service
import (
"context"
"errors"
"log"
"reflect"
"cloud.google.com/go/firestore"
)
// FirestoreRepository defines the basic CRUD operations for Firestore collections.
// It provides a generic way to perform firebase operations in a typesafe way
// Instead of return objects and casting to required object you pass in the required interface and it will populate
type FirestoreRepository interface {
AddDocument(ctx context.Context, collection string, data interface{}) (string, error)
GetDocument(ctx context.Context, collection string, docID string, dest interface{}) error
UpdateDocument(ctx context.Context, collection string, docID string, updates map[string]interface{}) error
DeleteDocument(ctx context.Context, collection string, docID string) error
GetDocumentsByField(ctx context.Context, collection string, field string, value interface{}, dest interface{}) error
GetCollection(ctx context.Context, collection string, dest interface{}) error
Close() error
}
func NewFirestoreService(client *firestore.Client) FirestoreRepository {
return &firestoreService{client: client}
}
// FirestoreService implements FirestoreRepository using the Firestore Go client.
type firestoreService struct {
client *firestore.Client
}
func (fs *firestoreService) AddDocument(ctx context.Context, collection string, data interface{}) (string, error) {
doc, _, err := fs.client.Collection(collection).Add(ctx, data)
if err != nil {
return "", err
}
return doc.ID, nil
}
func (fs *firestoreService) GetDocument(ctx context.Context, collection string, docID string, dest interface{}) error {
dsnap, err := fs.client.Collection(collection).Doc(docID).Get(ctx)
if err != nil {
return err
}
return dsnap.DataTo(dest)
}
func (fs *firestoreService) UpdateDocument(ctx context.Context, collection string, docID string, updates map[string]interface{}) error {
_, err := fs.client.Collection(collection).Doc(docID).Set(ctx, updates, firestore.MergeAll)
return err
}
func (fs *firestoreService) DeleteDocument(ctx context.Context, collection string, docID string) error {
_, err := fs.client.Collection(collection).Doc(docID).Delete(ctx)
return err
}
func (fs *firestoreService) GetDocumentsByField(ctx context.Context, collection, field string, value interface{}, dest interface{}) error {
q := fs.client.Collection(collection).Where(field, "==", value)
docs, err := q.Documents(ctx).GetAll()
if err != nil {
return err
}
return populateSliceFromDocs(docs, dest)
}
func (fs *firestoreService) GetCollection(ctx context.Context, collection string, dest interface{}) error {
docs, err := fs.client.Collection(collection).Documents(ctx).GetAll()
if err != nil {
log.Printf("[firestoreService] Error getting collection %s: %v", collection, err)
return err
}
log.Printf("[firestoreService] Retrieved %d documents from collection %s", len(docs), collection)
return populateSliceFromDocs(docs, dest)
}
// Internal helper to populate a slice from Firestore documents
func populateSliceFromDocs(docs []*firestore.DocumentSnapshot, dest interface{}) error {
slicePtr := reflect.ValueOf(dest)
if slicePtr.Kind() != reflect.Ptr || slicePtr.Elem().Kind() != reflect.Slice {
return errors.New("dest must be a pointer to a slice")
}
sliceVal := slicePtr.Elem()
elemType := sliceVal.Type().Elem()
for _, doc := range docs {
elemPtr := reflect.New(elemType)
if err := doc.DataTo(elemPtr.Interface()); err != nil {
log.Printf("[populateSliceFromDocs] Error populating element from doc %s: %v", doc.Ref.ID, err)
continue
}
sliceVal.Set(reflect.Append(sliceVal, elemPtr.Elem()))
}
return nil
}
func (fs *firestoreService) Close() error {
return fs.client.Close()
}
@icchan
Copy link
Author

icchan commented Jan 8, 2026

note to self: GetCollection does not return the docID... there may be cases where its required, so it needs to be updated

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment