lighter

package module
v0.2.5 Latest Latest
Warning

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

Go to latest
Published: Dec 13, 2019 License: Apache-2.0 Imports: 13 Imported by: 0

README

lighter

Cloud Firestore is a serverless data store with ACID transactions, multi-region replication, and powerful query engine. Its perfect for cloud-native solutions as it can autoscale with your application. Firestore has a very capable golang client (cloud.google.com/go/firestore) for reading and writing data to its database.

I created lighter as a wrapper to simplify many of the common (and sometimes verbose) tasks when using Firestore in my golang applications:

  • GCP Project ID required to create Firestore golang client is automatically derived from your GCP metadata server or any of the common environment variables in case of local execution
  • Common operations like Save, GetByID, and Delete are reduced to single line commands
  • Firestore ID requirements are encapsulated into simple GetNewID() and ToID(val) operations (see best practices)

Still, in case of more complex queries you will want to use the features of native golang Firestore client and lighter can still help.

  • Create Firestore client without specifying GCP project ID (NewClient method)
  • Simpler definition of your where and order by criteria (QueryCriteria struct, GetQueryByCriteria method)
  • Generic method to process Firestore query results (ResultHandler interface, HandleResults method)

I hope you find lighter helpful. Issues and PRs are welcomed.

Installation

To install lighter you will need Go version 1.11+ installed and execute the go get command:

go get -u github.com/mchmarny/lighter

Then in your code you can import lighter:

import "github.com/mchmarny/lighter"

Quick Start

Here is a simple application example assuming the following code is in your main.go file:

package main

import (
	"context"
	"time"

	"github.com/mchmarny/lighter"
)

type Product struct {
	ID     string    `firestore:"id"`
	SoldOn time.Time `firestore:"sold"`
	Name   string    `firestore:"name"`
	Cost   float64    `firestore:"cost"`
}

func main() {
	ctx := context.Background()
	store, err := lighter.NewStore(ctx)
	handleError(err)
	defer store.Close()

	p := &Product{
		ID:     "id-1234", // Firestore IDs must start with a letter, see IDs section below
		SoldOn: time.Now().UTC(),
		Name:   "Demo Product",
		Cost:   2.99,
	}

	err = store.Save(ctx, "product", p.ID, p)
	handleError(err)

	p2 := &Product{}
	err = store.GetByID(ctx, "product", p.ID, p2)
	handleError(err)

	err = store.DeleteByID(ctx, "product", p2.ID)
	handleError(err)
}

func handleError(err error) {
	if err != nil {
		panic(err)
	}
}

Get results sorted by struct property

Use the name and case of the property defined in the struct firestore attribute

To get all products sorted by descending cost you will need to create instance of the lighter.QueryCriteria object and then pass it to the store.GetByQuery method:

q := &lighter.QueryCriteria{
	Collection: "product",
	OrderBy: &lighter.Order{
		Property:   "cost",
		Descending: true,
	},
}

h := &ProductResultHandler{}
err = store.GetByQuery(ctx, q, h)

Get results filtered by struct property

Use the name and case of the property defined in the struct firestore attribute

To get all products where the cost is less than 10.0 you need to created instance of the lighter.QueryCriteria object and pass it to the store.GetByQuery method:

q := &lighter.QueryCriteria{
	Collection: "product",
	Criteria: []*lighter.Criterion{
		&lighter.Criterion{
			Property: "cost",
			Operator: ">=",
			Value:    10.0,
		},
	},
}

h := &ProductResultHandler{}
err = store.GetByQuery(ctx, q, h)

Process results using custom handler

lighter defines ResultHandler interface as a generic way to processing Firestore results:

type ResultHandler interface {
	// MakeNew makes new instance of the saved struct
	MakeNew() interface{}
	// Append adds newly loaded item to results on each result iteration
	Append(item interface{})
}

In your code you will then need to define an implementation of that interface (e.g. the ProductResultHandler):

type ProductResultHandler struct {
	Products []*Product
}

func (t *ProductResultHandler) MakeNew() interface{} {
	return &Product{}
}

func (t *ProductResultHandler) Append(item interface{}) {
	t.Products = append(t.Products, item.(*Product))
}

Once you have the struct that implements ResultHandler you can use it in either GetByQuery:

h := &ProductResultHandler{Product: make([]*Product, 0)}
err := store.GetByQuery(ctx, q, h)

Or to handle results of your own Firestore documents query where you pass resulting firestore.DocumentIterator along with your handler:

docs, err := client.Collection("products").Where("cost", ">-", 10.0)
handleError(err)

h := &ProductResultHandler{Product: make([]*Product, 0)}
err := store.HandleResults(ctx, docs, h)

IDs

Firestore IDs must start with a letter. lighter provides a couple helpers in this area. You can either create brand new ID using the v4 UUID provider like this:

id := lighter.GetNewID()
// results in something like this
// tid-7202525c-aa25-452c-a5c8-4d93fdc4074b

Or use existing value to create a valid ID. This is good approach to avoiding "hotspots" in your DB as these will eventually impact query latency.

id := lighter.ToID("my-1234-value")
// results in 32-bit hash with the `tid-` prefix

Disclaimer

This is my personal project and it does not represent my employer. I take no responsibility for issues caused by this code. I do my best to ensure that everything works, but if something goes wrong, my apologies is all you will get.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func GetNewID

func GetNewID() string

GetNewID parses Firestore valid IDs (can't start with digits)

func GetQueryByCriteria

func GetQueryByCriteria(c *firestore.Client, q *QueryCriteria) (query *firestore.Query, err error)

GetQueryByCriteria builds Firestore query

func HandleResults

func HandleResults(ctx context.Context, docs *firestore.DocumentIterator, h ResultHandler) error

HandleResults allows for filtered query using QueryHandler

func IsValidID

func IsValidID(val string) bool

IsValidID validates that passed value is a valid Firestore ID

func NewClient added in v0.2.5

func NewClient(ctx context.Context) (client *firestore.Client, err error)

NewClient creates new Firestore client with derived project ID

func ToID

func ToID(val string) string

ToID converts passed value to a valid Firestire ID

Types

type Criterion

type Criterion struct {
	// Property is the name of the property in where clause. Assumes index in Firestore
	Property string
	Operator string
	Value    interface{}
}

Criterion defines single Firestore where criteria

type MockedStoreObject

type MockedStoreObject struct {
	ID    string    `json:"id" firestore:"id"`
	On    time.Time `json:"on" firestore:"on"`
	Name  string    `json:"name" firestore:"name"`
	Count int       `json:"count" firestore:"count"`
	Value float64   `json:"value" firestore:"value"`
}

MockedStoreObject represents simple object

func NewTestObject

func NewTestObject(name string, count int, value float64) *MockedStoreObject

NewTestObject returns fully loaded Firestore object

type Order

type Order struct {
	Property   string
	Descending bool
}

Order defines a single Firestore property sort order

type QueryCriteria

type QueryCriteria struct {
	Collection string
	Criteria   []*Criterion
	OrderBy    *Order
}

QueryCriteria defines the Firestore query query

type ResultHandler

type ResultHandler interface {
	// MakeNew makes new item instance for loading from result iterator
	MakeNew() interface{}
	// Append adds newly loaded item to results
	Append(item interface{})
}

ResultHandler defines methods required to handle result items

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store represents simple FireStore helper

func NewStore

func NewStore(ctx context.Context) (db *Store, err error)

NewStore configures new client instance

func NewStoreWithCredentialsFile

func NewStoreWithCredentialsFile(ctx context.Context, path string) (db *Store, err error)

NewStoreWithCredentialsFile configures new client instance with credentials file

func (*Store) Close

func (d *Store) Close() error

Close closes client connection

func (*Store) DeleteAll

func (d *Store) DeleteAll(ctx context.Context, collection string, batchSize int) error

DeleteAll deletes all items in a collection

func (*Store) DeleteByID

func (d *Store) DeleteByID(ctx context.Context, collection, id string) error

DeleteByID deletes stored object for a given ID

func (*Store) GetByID

func (d *Store) GetByID(ctx context.Context, collection, id string, in interface{}) error

GetByID returns stored object for given ID

func (*Store) GetByQuery

func (d *Store) GetByQuery(ctx context.Context, q *QueryCriteria, h ResultHandler) error

GetByQuery allows for filtered query using QueryHandler

func (*Store) Save

func (d *Store) Save(ctx context.Context, collection string, id string, obj interface{}) error

Save inserts or updates by ID

type TestObjectHandler

type TestObjectHandler struct {
	Items []*MockedStoreObject
}

TestObjectHandler is a test implementation of the QueryHandler interface

func (*TestObjectHandler) Append

func (t *TestObjectHandler) Append(item interface{})

Append adds newly loaded item to results

func (*TestObjectHandler) MakeNew

func (t *TestObjectHandler) MakeNew() interface{}

MakeNew makes new item instance for loading from result iterator

Jump to

Keyboard shortcuts

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