firestore

package
v0.4.1 Latest Latest
Warning

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

Go to latest
Published: Jan 24, 2023 License: MIT Imports: 6 Imported by: 0

Documentation

Overview

Package firestore implements helper functions and utilities to make working with package "cloud.google.com/go/firestore" easier.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func CreateDocument

func CreateDocument(ctx context.Context, col *firestore.CollectionRef, id string, doc interface{}) error

Create a new document with the given id in the given collection.

func DeleteDocument

func DeleteDocument(ctx context.Context, col *firestore.CollectionRef, id string) error

func GetDocumentById

func GetDocumentById(ctx context.Context, col *firestore.CollectionRef, id string, result interface{}) error

Gets and unmarshals the document with the given id into result.

func GetDocumentsForQuery

func GetDocumentsForQuery(ctx context.Context, query firestore.Query) ([]*firestore.DocumentSnapshot, error)

func NewFirestoreError

func NewFirestoreError(inner error, code errors.ErrorCode) errors.Error

func NewFirestoreErrorInternal

func NewFirestoreErrorInternal(inner error) errors.Error

func ParseFirestoreError

func ParseFirestoreError(err error) errors.Error

Parses the given firestore error and returns an instance of Error from package "github.com/dkinzler/kit/errors" with an appropriate error code set.

func SetDocument

func SetDocument(ctx context.Context, col *firestore.CollectionRef, id string, data interface{}, opts ...firestore.SetOption) error

Without merge options, can provide most data types (struct, map, slice, ...), document will be completely overridden. With "MergeAll" option, can only use map as data argument. With "Merge" option, only the provided fields will be overridden, can use structs as data argument.

func UnmarshalDocSnapshot

func UnmarshalDocSnapshot(snap *firestore.DocumentSnapshot, result interface{}) error

Unmarshal the given snapshot into result, which should usually be a pointer to a struct or map.

func UpdateDocument

func UpdateDocument(ctx context.Context, col *firestore.CollectionRef, id string, updates []firestore.Update) error

func VerifyTransactionExpectations

func VerifyTransactionExpectations(t *firestore.Transaction, te TransactionExpectations) error

Verifies the transaction expectations in the given firestore transaction.

Types

type TransactionExpectation

type TransactionExpectation struct {
	DocRef     *firestore.DocumentRef
	Exists     bool
	UpdateTime time.Time
}

TransactionExpectation represents the state of a document, i.e. whether or not it exists and if it exists the last time it was updated/modified. Can be used to implement safe optimistic transactions.

func TransactionExpectationFromSnapshot

func TransactionExpectationFromSnapshot(snap *firestore.DocumentSnapshot) TransactionExpectation

func (TransactionExpectation) IsSatisfied

Returns nil if the given document snapshot satisfies the transaction expectation, i.e. they both refer to the same document and existence as well as latest update times are equal.

type TransactionExpectations

type TransactionExpectations map[string]TransactionExpectation

A set of transaction expectations, that can be used to implement optimistic concurrency/transactions. Functions that read data from firestore can return TransactionExpectations values, multiple of them can be combined. If we then try to update the data and want to make sure that it hasn't changed since we last read it, we can use a transaction that first gets the documents and then compares them to the TransactionExpectations value using the VerifyTransactionExpectations() function.

Optmistic concurrency works well if the probability of concurrent modifications is low. It has the advantage that it is easy to create code that guarantees consistency while not leaking any implementation details of the data store layer into business logic code.

Example
package main

import (
	"context"

	"cloud.google.com/go/firestore"
	"github.com/dkinzler/kit/errors"
)

// An example of how to integrate optimistic concurrency/transactions with application services and data stores based on firestore.

type Folder struct {
	FolderId string
	Name     string
	// Ids of the notes contained in this folder
	Notes []string
}

func (f *Folder) ContainsNote(noteId string) bool {
	for _, n := range f.Notes {
		if n == noteId {
			return true
		}
	}
	return false
}

func (f *Folder) AddNote(noteId string) {
	f.Notes = append(f.Notes, noteId)
}

type Note struct {
	NoteId string
	Text   string
}

// Any type implementing this interface can be used as the data store for the note taking application.
// To implement optimistic concurrency/transactions, methods that read data return a transaction expectation (interface type TE).
// Methods that write/update data take a transaction expectation and must guarantee that the write is only performed
// if the transaction expectations are still satisfied (i.e. the data they represent was not modified since the time the transaction expectation was created).
type NoteDatastore interface {
	Folder(ctx context.Context, folderId string) (Folder, TE, error)
	Note(ctx context.Context, id string) (Note, TE, error)
	UpdateFolder(ctx context.Context, folderId string, f Folder, te TE) error
}

// Transaction expectations, usually a timestamp representing the last time some piece of data was modified.
type TE interface {
	Combine(other TE) TE
}

func main() {
	// Imagine this is part of a method in a note taking application, where
	// we want to add a note to a folder. A folder can contain multiple notes.
	// To do this, we need to load the note and the folder from the data store.
	// Then we update the folder by adding the note to it and then persist
	// the changes by updating the folder in the data store.
	// We need to make sure that the data stays consistent, e.g. if a concurrent operation deleted the folder or the note,
	// the update operation on the data store should fail.
	// To this end we combine the transaction expectations for the folder and note
	// and pass them along to the UpdateFolder method of the data store.
	// The data store implementation can then make sure to only perform the update if the note and folder were not modified.
	//
	// firestoreNoteDatastore is an example implementation of NoteDatastore using firestore. It demonstrates how to use
	// transaction expectations to implement optimistic concurrency.

	ds := NewFirestoreNoteDatastore()

	folder, te1, _ := ds.Folder(context.Background(), "folder1234")
	note, te2, _ := ds.Note(context.Background(), "note1")

	if !folder.ContainsNote(note.NoteId) {
		folder.AddNote(note.NoteId)
	}

	te := te1.Combine(te2)

	ds.UpdateFolder(context.Background(), folder.FolderId, folder, te)
}

type firestoreNoteDatastore struct {
	client           *firestore.Client
	folderCollection *firestore.CollectionRef
	noteCollection   *firestore.CollectionRef
}

func NewFirestoreNoteDatastore() *firestoreNoteDatastore {
	// In an actual application we would have to provide a firestore client and initialize the collections.
	return &firestoreNoteDatastore{}
}

type firestoreTE TransactionExpectations

func (fte firestoreTE) Combine(other TE) TE {
	if ote, ok := other.(firestoreTE); ok {
		return fte.Combine(ote)
	}
	return fte
}

func (fds *firestoreNoteDatastore) Folder(ctx context.Context, folderId string) (Folder, TE, error) {
	var folder Folder
	te, err := GetDocumentByIdWithTE(ctx, fds.folderCollection, folderId, &folder)
	if err != nil {
		return Folder{}, nil, err
	}
	return folder, firestoreTE(te), nil
}

func (fds *firestoreNoteDatastore) Note(ctx context.Context, noteId string) (Note, TE, error) {
	var note Note
	te, err := GetDocumentByIdWithTE(ctx, fds.folderCollection, noteId, &note)
	if err != nil {
		return Note{}, nil, err
	}
	return note, firestoreTE(te), nil
}

func (fds *firestoreNoteDatastore) UpdateFolder(ctx context.Context, folderId string, f Folder, te TE) error {
	err := fds.client.RunTransaction(ctx, func(ctx context.Context, t *firestore.Transaction) error {
		if te != nil {
			// make sure the transaction expectation passed to the method has the correct type
			tmp, ok := te.(firestoreTE)
			if !ok {
				return NewFirestoreError(nil, errors.InvalidArgument)
			}
			fte := TransactionExpectations(tmp)

			// verify the transaction expectations, i.e. none of the documents have been modified
			if err := VerifyTransactionExpectations(t, fte); err != nil {
				return err
			}
		}

		err := t.Set(fds.folderCollection.Doc(folderId), f)
		if err != nil {
			return err
		}

		return nil
	}, firestore.MaxAttempts(1))
	return err
}
Output:

func GetDocumentByIdWithTE

func GetDocumentByIdWithTE(ctx context.Context, col *firestore.CollectionRef, id string, result interface{}) (TransactionExpectations, error)

Gets and unmarshals the document with the given id into result. Also returns a TransactionExpectations value that represents the last time the document was modified. Can be used to implement optimistic transactions.

func (TransactionExpectations) Add

func (TransactionExpectations) Combine

Combine two sets of transaction expectations. If both sets contain an expectation for the same document, the expectation with the more recent update time will be used.

func (TransactionExpectations) DocRefs

func (TransactionExpectations) Get

func (TransactionExpectations) Remove

func (tes TransactionExpectations) Remove(docRef *firestore.DocumentRef)

func (TransactionExpectations) Verify

Verifies that the given document snapshots are compatible/consistent with the set of transaction expectations, i.e. none of the documents were changed since the time the transaction expectations were created. Formally for each document snapshot it must be true that there is a transaction expectation for the same document and the document existence and latest update time of the snaphost and transaction expectation are equal.

Note that there must be a transaction expectation for every snapshot, but not the other way around.

Jump to

Keyboard shortcuts

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