Skip to content

Commit

Permalink
Added Increment and Decrement methods for Number fields.
Browse files Browse the repository at this point in the history
  • Loading branch information
remychantenay committed Jun 24, 2020
1 parent ec0e776 commit 172d578
Show file tree
Hide file tree
Showing 7 changed files with 217 additions and 25 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ It does not do anything crazy or magic – the purpose is to reduce the amount o
## Context
While working on a project running on multiple services and serverless functions, I quickly realised that the amount of repetitive and boilerplate code related to Firestore increased exponentially.

I decided to extract this code in a thin and layer on top of the Firestore admin client. That's how fuego came to life.
I decided to extract this code in a thin layer on top of the Firestore admin client. That's how fuego came to life.

![Fuego](https://raw.githubusercontent.com/remychantenay/fuego/master/art/fuego.jpg)

## Features
### Documents
* CRUD operations
* Retrieving specific fields only
* Retrieving/Updating specific fields only
* Simple Exists check

### Collections
Expand Down
2 changes: 1 addition & 1 deletion document/array.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func (a *Array) Override(ctx context.Context, data []interface{}) error {

// Append will append the provided data to the existing data (if any) of an Array field.
//
// The update will be done inside a transaction as we need to read the value beforehand.
// The update will be executed inside a transaction.
// values, err := fuego.Document("users", "jsmith").Array("Address").Append(ctx, []interface{}{"More info"})
func (a *Array) Append(ctx context.Context, data []interface{}) error {

Expand Down
53 changes: 42 additions & 11 deletions document/document.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@ import (
// Document provides the necessary to interact with a Firestore document.
type Document interface {

// GetDocumentRef returns a Document Reference (DocumentRef).
GetDocumentRef() *firestore.DocumentRef

// Create a document.
Create(ctx context.Context, from interface{}) error

Expand Down Expand Up @@ -45,6 +42,15 @@ type Document interface {

// Timestamp returns a specific Timestamp field.
Timestamp(name string) *Timestamp

// GetDocumentRef returns a Document Reference (DocumentRef).
GetDocumentRef() *firestore.DocumentRef

// InBatch returns true if a WriteBatch has been started, false otherwise.
InBatch() bool

// Batch returns the pointer to the WriteBatch (if any), nil oherwise.
Batch() *firestore.WriteBatch
}

// FirestoreDocument provides features related to Firestore documents.
Expand All @@ -56,16 +62,20 @@ type FirestoreDocument struct {
// ID is the ID of the document
ID string

// writeBatch will be nil if not started with fuego.StartBatch() or cancelled with fuego.CancelBatch().
writeBatch *firestore.WriteBatch

firestore *firestore.Client
}

// New creates and returns a new FirestoreDocument.
func New(fs *firestore.Client, path, documentID string) *FirestoreDocument {
func New(fs *firestore.Client, path, documentID string, wb *firestore.WriteBatch) *FirestoreDocument {
r := fs.Collection(path)
return &FirestoreDocument{
ColRef: r,
ID: documentID,
firestore: fs,
ColRef: r,
ID: documentID,
firestore: fs,
writeBatch: wb,
}
}

Expand All @@ -81,7 +91,12 @@ func (d *FirestoreDocument) GetDocumentRef() *firestore.DocumentRef {

// Create a document in Firestore.
func (d *FirestoreDocument) Create(ctx context.Context, from interface{}) error {
_, err := d.GetDocumentRef().Set(ctx, from)
ref := d.GetDocumentRef()
if d.InBatch() {
d.Batch().Set(ref, from)
return nil
}
_, err := ref.Set(ctx, from)
return err
}

Expand Down Expand Up @@ -117,7 +132,12 @@ func (d *FirestoreDocument) Exists(ctx context.Context) bool {

// Delete removes a document from Firestore.
func (d *FirestoreDocument) Delete(ctx context.Context) error {
_, err := d.GetDocumentRef().Delete(ctx)
ref := d.GetDocumentRef()
if d.InBatch() {
d.Batch().Delete(ref)
return nil
}
_, err := ref.Delete(ctx)
if err != nil {
return err
}
Expand Down Expand Up @@ -145,8 +165,9 @@ func (d *FirestoreDocument) String(name string) *String {
// Number returns a new Number.
func (d *FirestoreDocument) Number(name string) *Number {
return &Number{
Document: d,
Name: name,
Document: d,
Name: name,
firestore: d.firestore,
}
}

Expand Down Expand Up @@ -174,3 +195,13 @@ func (d *FirestoreDocument) Timestamp(name string) *Timestamp {
Name: name,
}
}

// InBatch returns true if a WriteBatch has been started, false otherwise.
func (d *FirestoreDocument) InBatch() bool {
return d.writeBatch != nil
}

// Batch returns the pointer to the WriteBatch (if any), nil oherwise.
func (d *FirestoreDocument) Batch() *firestore.WriteBatch {
return d.writeBatch
}
80 changes: 77 additions & 3 deletions document/number.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,24 @@ type NumberField interface {
// Retrieve returns the value of a specific field containing a number (int64).
Retrieve(ctx context.Context) (int64, error)

// Update updates the value of a specific field containing a number (int64).
// Update the value of a specific field containing a number (int64).
Update(ctx context.Context, with int64) error

// Increment the value of a specific field containing a number (int64).
// If the field doesn't exist, it will be set to 1.
Increment(ctx context.Context) error

// Decrement the value of a specific field containing a number (int64).
// If the field doesn't exist, it will be set to 0.
Decrement(ctx context.Context) error
}

// Number represents a document field of type Number.
type Number struct {
Document Document
Name string

firestore *firestore.Client
}

// Retrieve returns the content of a specific field for a given document.
Expand All @@ -33,12 +43,76 @@ func (n *Number) Retrieve(ctx context.Context) (int64, error) {
return value.(int64), nil
}

// Update updates the value of a specific field of type Number.
// err := fuego.Document("users", "jsmith").Number("Age").Update(ctx, 42)
// Update the value of a specific field of type Number.
// err := fuego.Document("users", "jsmith").Number("Age").Update(ctx, 42).
func (n *Number) Update(ctx context.Context, with int64) error {

_, err := n.Document.GetDocumentRef().Set(ctx, map[string]int64{
n.Name: with,
}, firestore.MergeAll)
return err
}

// Increment the value of a specific field of type Number.
//
// The update will be executed inside a transaction.
// If the field doesn't exist, it will be set to 1.
// err := fuego.Document("users", "jsmith").Number("Age").Increment(ctx)
func (n *Number) Increment(ctx context.Context) error {

return n.firestore.RunTransaction(ctx, func(ctx context.Context, tx *firestore.Transaction) error {

ref := n.Document.GetDocumentRef()
document, err := tx.Get(ref)
if err != nil {
return err
}

newValue := int64(1)
value, err := document.DataAt(n.Name)
if err == nil {
newValue = value.(int64) + 1
}

err = tx.Set(ref, map[string]int64{
n.Name: newValue,
}, firestore.MergeAll)
if err != nil {
return err
}

return nil // Success, no errors
})
}

// Decrement the value of a specific field of type Number.
//
// The update will be executed inside a transaction.
// If the field doesn't exist, it will be set to 0.
// err := fuego.Document("users", "jsmith").Number("Age").Decrement(ctx)
func (n *Number) Decrement(ctx context.Context) error {

return n.firestore.RunTransaction(ctx, func(ctx context.Context, tx *firestore.Transaction) error {

ref := n.Document.GetDocumentRef()
document, err := tx.Get(ref)
if err != nil {
return err
}

newValue := int64(0)
value, err := document.DataAt(n.Name)
if err == nil {
newValue = value.(int64) - 1
}

err = tx.Set(ref, map[string]int64{
n.Name: newValue,
}, firestore.MergeAll)
if err != nil {
return err
}

return nil // Success, no errors
})
}
8 changes: 8 additions & 0 deletions errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package fuego

import "errors"

var (
// ErrBatchWriteNotStarted indicates that the a commit can't happen if the batch write hasn't been started.
ErrBatchWriteNotStarted = errors.New("fuego: no batch write started")
)
43 changes: 35 additions & 8 deletions fuego.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,56 @@ package fuego

import (
"cloud.google.com/go/firestore"
"context"
"github.com/remychantenay/fuego/collection"
"github.com/remychantenay/fuego/document"
"strings"
)

// Fuego is a wrapper for the Firestore client
// It contains the Firestore client
// Fuego is a wrapper for the Firestore client.
// Contains the Firestore client.
type Fuego struct {
firestore *firestore.Client

// FirestoreClient is a ptr to a firestore client.
FirestoreClient *firestore.Client

// WriteBatch is a ptr to a writebatch.
// will be nil if not started with StartBatch() or cancelled with CancelBatch().
WriteBatch *firestore.WriteBatch
}

// New creates and returns a Fuego wrapper
func New(fc *firestore.Client) *Fuego {
// New creates and returns a Fuego wrapper.
func New(fs *firestore.Client) *Fuego {
return &Fuego{
firestore: fc,
FirestoreClient: fs,
WriteBatch: nil,
}
}

// StartBatch starts a write batch.
// Write operations that follow will be added to the batch and processed when CommitBatch is called.
func (f *Fuego) StartBatch() {
f.WriteBatch = f.FirestoreClient.Batch()
}

// CommitBatch commits the write batch previously started with StartBatch().
func (f *Fuego) CommitBatch(ctx context.Context) ([]*firestore.WriteResult, error) {
if f.WriteBatch == nil {
return nil, ErrBatchWriteNotStarted
}
return f.WriteBatch.Commit(ctx)
}

// CancelBatch cancels an on-going write batch (if any).
func (f *Fuego) CancelBatch() {
f.WriteBatch = nil
}

// Document returns a new FirestoreDocument.
func (f *Fuego) Document(path, documentID string) *document.FirestoreDocument {
path = strings.TrimPrefix(path, "/")
path = strings.TrimSuffix(path, "/")
return document.New(f.firestore, path, documentID)
return document.New(f.FirestoreClient, path, documentID, f.WriteBatch)
}

// DocumentWithGeneratedID returns a new FirestoreDocument without ID.
Expand All @@ -36,5 +63,5 @@ func (f *Fuego) DocumentWithGeneratedID(path string) *document.FirestoreDocument
func (f *Fuego) Collection(path string) *collection.FirestoreCollection {
path = strings.TrimPrefix(path, "/")
path = strings.TrimSuffix(path, "/")
return collection.New(f.firestore, path)
return collection.New(f.FirestoreClient, path)
}
52 changes: 52 additions & 0 deletions integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,58 @@ func TestIntegration_Number_Update(t *testing.T) {
fmt.Println("New Age: ", value)
}

func TestIntegration_Number_Increment(t *testing.T) {
ctx := context.Background()

expectedNewValue := int64(32)

err := fuego.Document("users", "jsmith").
Number("Age").
Increment(ctx)
if err != nil {
t.Fatalf(err.Error())
}

value, err := fuego.Document("users", "jsmith").
Number("Age").
Retrieve(ctx)
if err != nil {
t.Fatalf(err.Error())
}

if value != expectedNewValue {
t.Fatalf("Got %d but expected %d", value, expectedNewValue)
}

fmt.Println("Incremented Age: ", value)
}

func TestIntegration_Number_Decrement(t *testing.T) {
ctx := context.Background()

expectedNewValue := int64(31)

err := fuego.Document("users", "jsmith").
Number("Age").
Decrement(ctx)
if err != nil {
t.Fatalf(err.Error())
}

value, err := fuego.Document("users", "jsmith").
Number("Age").
Retrieve(ctx)
if err != nil {
t.Fatalf(err.Error())
}

if value != expectedNewValue {
t.Fatalf("Got %d but expected %d", value, expectedNewValue)
}

fmt.Println("Decremented Age: ", value)
}

func TestIntegration_Timestamp_Retrieve(t *testing.T) {
ctx := context.Background()

Expand Down

0 comments on commit 172d578

Please sign in to comment.