firevault

package module
v0.0.0-...-f7ccaef Latest Latest
Warning

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

Go to latest
Published: Apr 28, 2024 License: MIT Imports: 12 Imported by: 0

README

Firevault

Firevault is a Firestore object modeling tool to make life easier for Go devs. Inspired by Firevault.js.

Installation

Use go get to install Firevault.

go get github.com/bobch27/firevault-go

Importing

Import the package in your code.

import "github.com/bobch27/firevault-go"

Connection

You can connect to Firevault using the Firebase Admin SDK and the Connect method.

import (
	"log"

	firebase "firebase.google.com/go"
	"github.com/bobch27/firevault-go"
)

ctx := context.Background()
app, err := firebase.NewApp(ctx, nil)
if err != nil {
  log.Fatalln("Firebase initialisation failed:", err)
}

client, err := app.Firestore(ctx)
if err != nil {
  log.Fatalln("Firestore initialisation failed:", err)
}

defer client.Close()

connection := firevault.Connect(ctx, client)
import (
	"log"

	"cloud.google.com/go/firestore"
	"github.com/bobch27/firevault-go"
)

// Sets your Google Cloud Platform project ID.
projectId := "YOUR_PROJECT_ID"
ctx := context.Background()

client, err := firestore.NewClient(ctx, projectId)
if err != nil {
  log.Fatalln("Firestore initialisation failed:", err)
}

defer client.Close()

connection := firevault.Connect(ctx, client)

Models

Defining a model is as simple as creating a struct with Firevault tags.

type User struct {
	Name     string   `firevault:"name,required,omitempty"`
	Email    string   `firevault:"email,required,email,isUnique,omitempty"`
	Password string   `firevault:"password,required,min=6,transform=hashPass,omitempty"`
	Address  *Address `firevault:"address,omitempty"`
	Age      int      `firevault:"age,required,min=18,omitempty"`
}

type Address struct {
	Line1 string `firevault:",omitempty"`
	City  string `firevault:"-"`
}

Tags

When defining a new struct type with Firevault tags, note that the tags' order matters (apart from the omitempty and omitemptyupdate tags, which can be used anywhere).

The first tag is always the field name which will be used in Firestore. You can skip that by just using a comma, before adding further tags.

After that, each tag is a different validation rule, and they will be parsed in order.

Other than the validation tags, Firevault supports the following built-in tags:

  • omitempty - If the field is set to it’s default value (e.g. 0 for int, or "" for string), the field will be omitted from validation and Firestore.
  • omitemptyupdate - Works the same way as omitempty, but only for the Validate and UpdateById methods. Ignored during Create.
  • - - Ignores the field.

Validations

Firevault validates fields' values based on the defined rules. There are 4 built-in validations, whilst also supporting the ability to add custom ones.

Again, the order in which they are executed depends on the tag order.

Built-in validations:

  • required - Validates whether the field's value is not the default type value (i.e. nil for pointer, "" for string, 0 for int etc.). Fails when it is the default.
  • max - Validates whether the field's value, or length, is less than or equal to the param's value. Requires a param (e.g. max=20). For numbers, it checks the value, for strings, maps and slices, it checks the length.
  • min - Validates whether the field's value, or length, is greater than or equal to the param's value. Requires a param (e.g. min=20). For numbers, it checks the value, for strings, maps and slices, it checks the length.
  • email - Validates whether the field's string value is a valid email address.

Custom validations:

  • To define a custom validation, use the connection's RegisterValidation method.
    • Expects:
      • name: A string defining the validation name
      • func: A function of type ValidationFn. The passed in function accepts two parameters.
        • field: A reflect.Value of the field.
        • param: A string which will be validated against.
    • Returns:
      • result: A bool which returns true if check has passed, and false if it hasn't.
connection.RegisterValidation("isUpper", func(fieldValue reflect.Value, _ string) bool {
	if fieldValue.Kind() != reflect.String {
		return false
	}

	s := fieldValue.String()
	return s == strings.toUpper(s)
})

You can then chain the tag like a normal one.

type User struct {
	Name string `firevault:"name,required,isUpper,omitempty"`
}

Transformations

Firevault also supports rules that transform the field's value. To use them, it's as simple as registering a transformation and adding a prefix to the tag.

  • To define a transformation, use the connection's RegisterTransformation method.
    • Expects:
      • name: A string defining the validation name
      • func: A function of type TransformationFn. The passed in function accepts one parameter.
        • field: A reflect.Value of the field.
    • Returns:
      • newVal: An interface{} with the new value.
      • error: An error in case something goes wrong during the transformation.
connection.RegisterTransformation("toLower", func toUpper(fieldValue reflect.Value) (interface{}, error) {
	if fieldValue.Kind() != reflect.String {
		return fieldValue.Interface(), nil
	}

	if fieldValue.String() != "" {
		return strings.ToLower(fieldValue.String()), nil
	}

	return fieldValue.String(), nil
})

You can then chain the tag like a normal one, but don't forget to use the transform= prefix.

Again, the tag order matters. Defining a transformation at the end, means the value will be updated after the validations, whereas a definition at the start, means the field will be updated and then validated.

type User struct {
	Email string `firevault:"email,required,email,transform=toLower,omitempty"`
}

Collections

To create a collection instance, call the NewCollection method, using the struct type parameter, and passing in the connection instance, as well as a collection name.

collection, err := firevault.NewCollection[User](connection, "users")
if err != nil {
	fmt.Println(err)
}

Methods

The collection instance has 6 built-in methods to support interaction with Firestore.

  • Create - A method which validates passed in data and adds it as a document to Firestore.
    • Expects:
      • data: A pointer of a struct with populated fields which will be added to Firestore after validation.
      • options (optional): An instance of CreationOptions with two properties.
        • SkipRequired: A bool which when true, allows the validation to skip the required check. Default value is false.
        • ID: A string which will add a document to Firestore with the specified ID.
    • Returns:
      • id: A string with the new document's ID.
      • error: An error in case something goes wrong during validation or interaction with Firestore.
user := &User{
	Name: 	  "Bobby Donev",
	Email:    "hello@bobbydonev.com",
	Password: "12356",
	Age:      26,
	Address:  &Address{
		Line1: "1 High Street",
		City:  "London",
	},
}
id, err := collection.Create(user)
if err != nil {
	fmt.Println(err)
} 
fmt.Println(id) // "6QVHL46WCE680ZG2Xn3X"
id, err := collection.Create(user, CreationOptions{SkipRequired: true, ID: "custom-id"})
if err != nil {
	fmt.Println(err)
} 
fmt.Println(id) // "custom-id"
  • UpdateById - A method which validates passed in data and updates given Firestore document.
    • Expects:
      • id: A string with the document's ID.
      • data: A pointer of a struct with populated fields which will be used to update the document after validation.
      • options (optional): An instance of ValidationOptions with one property.
        • SkipRequired: A bool which when false, allows the validation to not skip the required check. Default value is true.
    • Returns:
      • error: An error in case something goes wrong during validation or interaction with Firestore.
    • Important:
      • If neither omitempty, nor omitemptyupdate tags have been used, non-specified field values in the passed in data will be set to Go's default values, thus updating all document fields. To prevent that behaviour, please use one of the two tags.
      • If a document with the specified ID does not exist, Firestore will create one with the specified fields, so it's worth checking whether the doc exists before using the method.
user := &User{
	Password: "123567",
}
err := collection.UpdateById("6QVHL46WCE680ZG2Xn3X", user)
if err != nil {
	fmt.Println(err)
} 
fmt.Println("Success")
err := collection.UpdateById("6QVHL46WCE680ZG2Xn3X", user, ValidationOptions{SkipRequired: false})
if err != nil {
	fmt.Println(err)
} 
fmt.Println("Success")
  • FindById - A method which gets the Firestore document with the specified ID.
    • Expects:
      • id: A string containing the specified ID.
    • Returns:
      • doc: Returns the document with type T (the type used when initiating the collection instance).
      • error: An error in case something goes wrong during interaction with Firestore.
user, err := collection.FindById("6QVHL46WCE680ZG2Xn3X")
if err != nil {
	fmt.Println(err)
} 
fmt.Println(user) // {Bobby Donev hello@bobbydonev.com asdasdkjahdks 26 0xc0001d05a0}}
  • DeleteById - A method which deletes the Firestore document with the specified ID.
    • Expects:
      • id: A string containing the specified ID.
    • Returns:
      • error: An error in case something goes wrong during interaction with Firestore.
    • If the document does not exist, it does nothing and error is nil.
err := collection.DeleteById("6QVHL46WCE680ZG2Xn3X")
if err != nil {
	fmt.Println(err)
} 
fmt.Println("Success")
  • Validate - A method which validates passed in data.
    • Expects:
      • data: A pointer of a struct with populated fields which will be validated.
      • options (optional): An instance of ValidationOptions with one property.
        • SkipRequired: A bool which when false, allows the validation to not skip the required check. Default value is true.
    • Returns:
      • error: An error in case something goes wrong during validation.
    • Important:
      • If neither omitempty, nor omitemptyupdate tags have been used, non-specified field values in the passed in data will be set to Go's default values.
user := &User{
	Email: "HELLO@BOBBYDONEV.COM",
}
err := collection.Validate(user)
if err != nil {
	fmt.Println(err)
} 
fmt.Println(user) // {hello@bobbydonev.com}
err := collection.Validate(user, ValidationOptions{SkipRequired: false})
if err != nil {
	fmt.Println(err)
} 
fmt.Println(user) // {hello@bobbydonev.com}
  • Find - A method which returns a Firevault query instance.
    • Returns:
      • query: A new query instance.
query := collection.Find()

Queries

A Firevault query instance allows querying Firestore, by chaining various methods. The query can have multiple filters.

  • Where - Returns a new query that filters the set of results.
    • Expects:
      • path: A string which can be a single field or a dot-separated sequence of fields.
      • operator: A string which must be one of ==, !=, <, <=, >, >=, array-contains, array-contains-any, in or not-in.
      • value: An interface{} used to filter out the results.
    • Returns:
      • A new query instance.
newQuery := collection.Find().Where("name", "==", "Bobby Donev")
  • OrderBy - Returns a new query that specifies the order in which results are returned.
    • Expects:
      • path: A string which can be a single field or a dot-separated sequence of fields. To order by document name, use the special field path firestore.DocumentID.
      • dir: An instance of firestore.Direction{} used to specify whether results are returned in ascending or descending order.
    • Returns:
      • A new query instance.
newQuery := collection.Find().Where("name", "==", "Bobby Donev").OrderBy("age", firestore.Asc)
  • Limit - Returns a new query that specifies the maximum number of first results to return.
    • Expects:
      • num: An int which indicates the max number of results to return.
    • Returns:
      • A new query instance.
newQuery := collection.Find().Where("name", "==", "Bobby Donev").Limit(1)
  • LimitToLast - Returns a new query that specifies the maximum number of last results to return.
    • Expects:
      • num: An int which indicates the max number of results to return.
    • Returns:
      • A new query instance.
newQuery := collection.Find().Where("name", "==", "Bobby Donev").LimitToLast(1)
  • Offset - Returns a new query that specifies the number of initial results to skip.
    • Expects:
      • num: An int which indicates the number of results to skip.
    • Returns:
      • A new query instance.
newQuery := collection.Find().Where("name", "==", "Bobby Donev").Offset(1)
  • StartAfter - Returns a new query that specifies that results should start just after the document with the given field values.
    • Expects:
      • path: A string which can be a single field or a dot-separated sequence of fields. To filter by document name, use the special field path firestore.DocumentID.
      • field: An interface{} value used to filter out results.
    • Returns:
      • A new query instance.
newQuery := collection.Find().Where("name", "==", "Bobby Donev").StartAfter(firestore.DocumentID, "6QVHL46WCE680ZG2Xn3X")
  • EndBefore - Returns a new query that specifies that results should end just before the document with the given field values.
    • Expects:
      • path: A string which can be a single field or a dot-separated sequence of fields. To filter by document name, use the special field path firestore.DocumentID.
      • field: An interface{} value used to filter out results.
    • Returns:
      • A new query instance.
newQuery := collection.Find().Where("name", "==", "Bobby Donev").EndBefore(firestore.DocumentID, "6QVHL46WCE680ZG2Xn3X")
  • Fetch - Returns results based on the chained methods before it.
    • Returns:
      • results: A slice containing the results of type Document[T] (where T is the type used when initiating the collection instance). Document[T] has two properties.
        • ID: A string which holds the document's ID.
        • Data: The document's data of type T.
      • error: An error in case something goes wrong.
users, err := collection.Find().Where("name", "==", "Bobby Donev").Fetch()
if err != nil {
	fmt.Println(err)
} else {
	fmt.Println(users) // []Document[User]
	fmt.Println(users[0].ID) // 6QVHL46WCE680ZG2Xn3X
}
  • Count - Returns the number of results based on the chained methods before it.
    • Returns:
      • count: An int64 representing the number of documents which meet the criteria.
      • error: An error in case something goes wrong.
count, err := collection.Find().Where("name", "==", "Bobby Donev").Count()
if err != nil {
	fmt.Println(err)
} else {
	fmt.Println(count) // 1
}

Custom Errors

During collection methods which require validation (i.e. Create, UpdateById and Validate), Firevault may return an error of a FieldError interface, which can aid in presenting custom error messages to users. All other errors are of the usual error type. Available methods for FieldError can be found in the field_error.go file.

Here is an example of parsing returned error.

func parseError(err firevault.FieldError) {
	if err.StructField() == "Password" { // or err.Field() == "password"
		if err.Tag() == "min=6" {
			fmt.Println("Password must be at least 6 characters long.")
		} else {
			fmt.Println(err.Error())
		}
	} else {
		fmt.Println(err.Error())
	}
}

id, err := collection.Create(&User{
	Name: "Bobby Donev",
	Email: "hello@bobbydonev.com",
	Password: "12345",
	Age: 26,
	Address: &Address{
		Line1: "1 High Street",
		City:  "London",
	},
})
if err != nil {
	var fErr firevault.FieldError
	if errors.As(err, &fErr) {
		parseError(fErr) // "Password must be at least 6 characters long."
	} else {
		fmt.Println(err.Error())
	}
} else {
	fmt.Println(id)
}

Contributing

Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.

License

MIT

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Collection

type Collection[T interface{}] struct {
	// contains filtered or unexported fields
}

A Firevault Collection holds a reference to a Firestore CollectionRef

func NewCollection

func NewCollection[T interface{}](connection *Connection, name string) (*Collection[T], error)

Create a new Collection instance

func (*Collection[T]) Create

func (c *Collection[T]) Create(data *T, opts ...CreationOptions) (string, error)

Create a Firestore document with provided data (after validation)

func (*Collection[T]) DeleteById

func (c *Collection[T]) DeleteById(id string) error

Delete a Firestore document with provided id

func (*Collection[T]) Find

func (c *Collection[T]) Find() *Query[T]

Create a new instance of a Firevault Query

func (*Collection[T]) FindById

func (c *Collection[T]) FindById(id string) (T, error)

Find a Firestore document with provided id

func (*Collection[T]) UpdateById

func (c *Collection[T]) UpdateById(id string, data *T, opts ...UpdatingOptions) error

Update a Firestore document with provided id and data (after validation)

func (*Collection[T]) Validate

func (c *Collection[T]) Validate(data *T, opts ...Options) error

Validate provided data

type Connection

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

A Firevault Connection contains the Firestore Client

func Connect

func Connect(ctx context.Context, client *firestore.Client) *Connection

Connect to Firevault

func (*Connection) RegisterTransformation

func (c *Connection) RegisterTransformation(name string, transformation TransformationFn) error

Register a new transformation rule

func (*Connection) RegisterValidation

func (c *Connection) RegisterValidation(name string, validation ValidationFn) error

Register a new validation rule

type CreationOptions

type CreationOptions struct {
	Options
	Id string
}

type Direction

type Direction int32

Direction is the sort direction for result ordering

const Asc Direction = Direction(1)

Asc sorts results from smallest to largest.

const Desc Direction = Direction(2)

Desc sorts results from largest to smallest.

type Document

type Document[T interface{}] struct {
	Id   string
	Data T
}

A Document holds the Id and data related to fetched document

type FieldError

type FieldError interface {
	// Code returns a reason for the error
	// (e.g. unknown-validation-rule)
	Code() string
	// Tag returns the validation tag that failed
	Tag() string
	// Field returns the field's name with the tag name taking
	// precedence over the field's actual name
	Field() string
	// StructField returns the field's actual name from the struct
	StructField() string
	// Value returns the actual field's value in case needed for
	// creating the error message
	Value() interface{}
	// Param returns the param value, in string form for comparison;
	// this will also help with generating an error message
	Param() string
	// Kind returns the Field's reflect Kind
	// (eg. time.Time's kind is a struct)
	Kind() reflect.Kind
	// Type returns the Field's reflect Type
	// (eg. time.Time's type is time.Time)
	Type() reflect.Type
	// Error returns the error message
	Error() string
}

FieldError contains all functions to get error details

type FieldPath

type FieldPath []string

FieldPath is a non-empty sequence of non-empty fields that reference a value.

For example,

[]string{"a", "b"}

is equivalent to the string form

"a.b"

but

[]string{"*"}

has no equivalent string form.

type Options

type Options struct {
	SkipValidation bool
	SkipRequired   bool
}

type Query

type Query[T interface{}] struct {
	// contains filtered or unexported fields
}

A Firevault Query represents a Firestore Query

func (*Query[T]) Count

func (q *Query[T]) Count() (int64, error)

Return document count for specified query criteria

func (*Query[T]) EndAt

func (q *Query[T]) EndAt(path string, field interface{}) *Query[T]

func (*Query[T]) EndBefore

func (q *Query[T]) EndBefore(path string, field interface{}) *Query[T]

func (*Query[T]) Fetch

func (q *Query[T]) Fetch() ([]Document[T], error)

Fetch documents based on query criteria

func (*Query[T]) Limit

func (q *Query[T]) Limit(num int) *Query[T]

func (*Query[T]) LimitToLast

func (q *Query[T]) LimitToLast(num int) *Query[T]

func (*Query[T]) Offset

func (q *Query[T]) Offset(num int) *Query[T]

func (*Query[T]) OrderBy

func (q *Query[T]) OrderBy(path string, direction Direction) *Query[T]

func (*Query[T]) StartAfter

func (q *Query[T]) StartAfter(path string, field interface{}) *Query[T]

func (*Query[T]) StartAt

func (q *Query[T]) StartAt(path string, field interface{}) *Query[T]

func (*Query[T]) Where

func (q *Query[T]) Where(path string, operator string, value interface{}) *Query[T]

type TransformationFn

type TransformationFn func(reflect.Value) (interface{}, error)

type UpdatingOptions

type UpdatingOptions struct {
	Options
	// Specify which field paths to be overwritten. Other fields
	// on the existing document will be untouched.
	//
	// It is an error if a provided field path does not refer to a value
	// in the data passed.
	//
	// If left empty, all the field paths given in the data argument
	// will be overwritten.
	MergeFields []FieldPath
}

type ValidationFn

type ValidationFn func(reflect.Value, string) bool

Jump to

Keyboard shortcuts

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