gofire

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 19, 2026 License: Apache-2.0 Imports: 8 Imported by: 0

README

gofire

CI Go Reference

Typed repositories for Google Cloud Firestore. gofire binds a Go struct to a collection and gives you Create, Get, Set, Update, Delete, queries, and transactions returning Document[T] values instead of raw snapshots; one small package over the official client, no codegen, no reflection beyond the client's own decoding.

type User struct {
    Name  string `firestore:"name"`
    Email string `firestore:"email"`
}

users, err := gofire.NewRepository[User](client, "users")
if err != nil {
    return err
}

created, err := users.Create(ctx, User{Name: "Ada", Email: "ada@example.com"})
if err != nil {
    return err
}

doc, err := users.Get(ctx, created.ID)
// doc.ID    -> the Firestore document ID
// doc.Model -> a typed User, decoded for you

Install

Requires Go 1.26+:

go get github.com/kocieusz/gofire

API overview

Method Behaviour
Create(ctx, model) Insert under an auto-generated ID
CreateWithID(ctx, id, model) Insert under a chosen ID; ErrAlreadyExists if taken
Get(ctx, id) Load one document; ErrNotFound if missing
Set(ctx, doc) Create or fully overwrite by doc.ID
Update(ctx, id, updates) Partial field updates; ErrNotFound if missing
Delete(ctx, id) Remove; deleting a missing document is a no-op
Query() / Documents(ctx, q) Build a Firestore query, decode results typed
GetAll(ctx) Every document in the collection
RunTransaction(ctx, fn) / Tx(tx) Transactions, see below
Collection() Escape hatch to the raw *firestore.CollectionRef

Error handling

Failures wrap a gofire sentinel and the underlying Firestore error, so errors.Is works against the sentinel while status.Code still resolves the original gRPC status:

doc, err := users.Get(ctx, id)
switch {
case errors.Is(err, gofire.ErrNotFound):
    // 404 for your caller
case err != nil:
    // infrastructure problem; log and bail
}

Sentinels: ErrNotFound, ErrAlreadyExists, ErrEmptyID.

Queries

Query() returns the collection's base firestore.Query; refine it with the official client's Where, OrderBy, Limit, and friends, then decode the result set with Documents:

paying, err := users.Documents(ctx, users.Query().
    Where("plan", "==", "pro").
    OrderBy("name", firestore.Asc).
    Limit(50))

Transactions

Tx(tx) binds a repository to a running transaction. Transactions can span several repositories as long as they share the same client; Firestore requires all reads before the first write, and buffered write errors (for example a duplicate create) surface from RunTransaction at commit time:

err := users.RunTransaction(ctx, func(ctx context.Context, tx *firestore.Transaction) error {
    created, err := users.Tx(tx).Create(User{Name: "Grace"})
    if err != nil {
        return err
    }
    _, err = memberships.Tx(tx).CreateWithID(created.ID, Membership{Role: "owner"})
    return err
})

Returning an error from the function rolls the whole transaction back.

Testing

Unit tests run with plain go test ./.... The end-to-end suite runs the same command against the official Firestore emulator and skips itself when FIRESTORE_EMULATOR_HOST is not set.

With Task and Docker installed:

task test        # unit tests only
task test-e2e    # start emulator, run everything, tear it down

Or by hand:

docker compose up --wait firestore
FIRESTORE_EMULATOR_HOST=127.0.0.1:8080 go test ./...
docker compose down --volumes

Development

task            # list all tasks
task lint       # golangci-lint
task tidy       # go mod tidy

CI runs vet, unit tests, lint, and the emulator suite on every push and PR.

Contributing

gofire is open source but closed to external pull requests; see CONTRIBUTING.md. Bug reports and ideas are very welcome as issues.

License

Apache 2.0

Documentation

Overview

Package gofire is a small, typed repository layer over Google Cloud Firestore.

A Repository[T] binds one Go struct type to one Firestore collection and exposes the usual document operations: Create, Get, Set, Update, Delete, queries - returning typed Document[T] values instead of raw snapshots. Transactional variants of the same operations are available through Repository.Tx.

Errors carry both a gofire sentinel (ErrNotFound, ErrAlreadyExists, ErrEmptyID) and the underlying Firestore error, so callers can branch with errors.Is and still reach the gRPC status when they need it.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrNotFound reports that the requested document does not exist.
	ErrNotFound = errors.New("document not found")
	// ErrAlreadyExists reports a create targeting an ID that is taken.
	ErrAlreadyExists = errors.New("document already exists")
	// ErrEmptyID reports an operation that requires a document ID but
	// received an empty string.
	ErrEmptyID = errors.New("document id is empty")
)

Sentinel errors returned by repository operations. Sentinels are wrapped together with the underlying Firestore error, so errors.Is matches the sentinel while status.Code still resolves the original gRPC status.

Functions

This section is empty.

Types

type Document

type Document[T any] struct {
	ID    string
	Model T
}

Document pairs a decoded model with the ID of the Firestore document it was read from or written to.

type Repository

type Repository[T any] struct {
	// contains filtered or unexported fields
}

Repository is a typed wrapper around a single Firestore collection. The type parameter T is the struct that maps to the collection's documents via `firestore` struct tags. The zero value is not usable; construct one with NewRepository.

func NewRepository

func NewRepository[T any](client *firestore.Client, collectionPath string) (Repository[T], error)

NewRepository binds T to the collection at collectionPath. The path may name a top-level collection ("users") or a subcollection ("users/alice/orders"); Firestore requires an odd number of segments.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"cloud.google.com/go/firestore"
	"github.com/kocieusz/gofire"
)

type user struct {
	Name  string `firestore:"name"`
	Email string `firestore:"email"`
	Plan  string `firestore:"plan"`
}

func main() {
	ctx := context.Background()

	client, err := firestore.NewClient(ctx, "my-project")
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()

	users, err := gofire.NewRepository[user](client, "users")
	if err != nil {
		log.Fatal(err)
	}

	created, err := users.Create(ctx, user{Name: "Ada", Email: "ada@example.com", Plan: "free"})
	if err != nil {
		log.Fatal(err)
	}

	loaded, err := users.Get(ctx, created.ID)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(loaded.Model.Name)
}

func (Repository[T]) Collection

func (r Repository[T]) Collection() *firestore.CollectionRef

Collection exposes the underlying collection reference for needs the repository does not cover.

func (Repository[T]) Create

func (r Repository[T]) Create(ctx context.Context, model T) (Document[T], error)

Create inserts model as a new document with an auto-generated ID and returns that ID.

func (Repository[T]) CreateWithID

func (r Repository[T]) CreateWithID(ctx context.Context, id string, model T) (Document[T], error)

CreateWithID inserts model under the given ID and fails with ErrAlreadyExists if a document with that ID is already present.

func (Repository[T]) Delete

func (r Repository[T]) Delete(ctx context.Context, id string) error

Delete removes the document with the given ID. Deleting a document that does not exist is not an error, mirroring Firestore semantics.

func (Repository[T]) Documents

func (r Repository[T]) Documents(ctx context.Context, query firestore.Query) ([]Document[T], error)

Documents runs query and decodes every result into a Document[T].

Example
package main

import (
	"context"
	"fmt"
	"log"

	"cloud.google.com/go/firestore"
	"github.com/kocieusz/gofire"
)

type user struct {
	Name  string `firestore:"name"`
	Email string `firestore:"email"`
	Plan  string `firestore:"plan"`
}

func main() {
	ctx := context.Background()

	client, err := firestore.NewClient(ctx, "my-project")
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()

	users, err := gofire.NewRepository[user](client, "users")
	if err != nil {
		log.Fatal(err)
	}

	paying, err := users.Documents(ctx, users.Query().Where("plan", "==", "pro").Limit(10))
	if err != nil {
		log.Fatal(err)
	}
	for _, doc := range paying {
		fmt.Println(doc.ID, doc.Model.Email)
	}
}

func (Repository[T]) Get

func (r Repository[T]) Get(ctx context.Context, id string) (Document[T], error)

Get retrieves the document with the given ID.

func (Repository[T]) GetAll

func (r Repository[T]) GetAll(ctx context.Context) ([]Document[T], error)

GetAll returns every document in the collection. Use with care on large collections; prefer Documents with a limited query.

func (Repository[T]) Query

func (r Repository[T]) Query() firestore.Query

Query returns the collection's base query for callers to refine with Where, OrderBy, Limit, and friends. Decode the result with Documents.

func (Repository[T]) RunTransaction

func (r Repository[T]) RunTransaction(ctx context.Context, f func(ctx context.Context, tx *firestore.Transaction) error) error

RunTransaction executes f inside a Firestore transaction on the repository's client. Use Repository.Tx inside f to perform typed operations; transactions spanning several repositories work as long as all repositories share the same client. Firestore requires every read in a transaction to happen before the first write.

Example
package main

import (
	"context"
	"log"

	"cloud.google.com/go/firestore"
	"github.com/kocieusz/gofire"
)

type user struct {
	Name  string `firestore:"name"`
	Email string `firestore:"email"`
	Plan  string `firestore:"plan"`
}

func main() {
	ctx := context.Background()

	client, err := firestore.NewClient(ctx, "my-project")
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()

	users, err := gofire.NewRepository[user](client, "users")
	if err != nil {
		log.Fatal(err)
	}
	memberships, err := gofire.NewRepository[map[string]string](client, "memberships")
	if err != nil {
		log.Fatal(err)
	}

	// Create a user and their membership atomically: either both documents
	// land, or neither does.
	err = users.RunTransaction(ctx, func(ctx context.Context, tx *firestore.Transaction) error {
		created, err := users.Tx(tx).Create(user{Name: "Grace", Email: "grace@example.com"})
		if err != nil {
			return err
		}
		_, err = memberships.Tx(tx).CreateWithID(created.ID, map[string]string{"role": "owner"})
		return err
	})
	if err != nil {
		log.Fatal(err)
	}
}

func (Repository[T]) Set

func (r Repository[T]) Set(ctx context.Context, doc Document[T]) (Document[T], error)

Set creates or fully overwrites the document identified by doc.ID.

func (Repository[T]) Tx

func (r Repository[T]) Tx(tx *firestore.Transaction) TxRepository[T]

Tx binds the repository to a running transaction.

The firestore package buffers transactional writes: errors such as ErrAlreadyExists for a duplicate create surface from RunTransaction at commit time, not from the TxRepository call that queued the write.

func (Repository[T]) Update

func (r Repository[T]) Update(ctx context.Context, id string, updates []firestore.Update) error

Update applies partial field updates to an existing document. It fails with ErrNotFound if the document does not exist.

type TxRepository

type TxRepository[T any] struct {
	// contains filtered or unexported fields
}

TxRepository mirrors Repository's operations inside a transaction. The firestore.Transaction API carries the transaction's context, so methods do not take one.

func (TxRepository[T]) Create

func (t TxRepository[T]) Create(model T) (Document[T], error)

Create buffers an insert of model under an auto-generated ID and returns that ID immediately.

func (TxRepository[T]) CreateWithID

func (t TxRepository[T]) CreateWithID(id string, model T) (Document[T], error)

CreateWithID buffers an insert of model under the given ID. A duplicate ID surfaces as ErrAlreadyExists from RunTransaction at commit time.

func (TxRepository[T]) Delete

func (t TxRepository[T]) Delete(id string) error

Delete buffers removal of the document with the given ID.

func (TxRepository[T]) Get

func (t TxRepository[T]) Get(id string) (Document[T], error)

Get reads the document with the given ID inside the transaction.

func (TxRepository[T]) Set

func (t TxRepository[T]) Set(doc Document[T]) (Document[T], error)

Set buffers a create-or-overwrite of the document identified by doc.ID.

func (TxRepository[T]) Update

func (t TxRepository[T]) Update(id string, updates []firestore.Update) error

Update buffers partial field updates to an existing document. A missing document surfaces as ErrNotFound from RunTransaction at commit time.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL