Bingo is a Golang embedded database library that uses bbolt as a backend.
Through the use of generics, it includes capabilities for CRUD operations, validation, preprocessing, and more.
- Simple CRUD: Easily create, read, update, and delete documents in your
bboltdatabase. - Validation: Uses
go-playground/validatorfor struct validation. - Querying: Advanced querying capabilities using filters.
- Hooks: Execute functions before and after certain operations (Insert, Update, Delete). package main
Install the library using go get:
go get -u github.com/nokusukun/bingopackage main
import (
"fmt"
"github.com/nokusukun/bingo"
"os"
"strings"
)
type Platform string
const (
Excel = "excel"
Sheets = "sheets"
)
type Function struct {
bingo.Document
Name string `json:"name,omitempty"`
Category string `json:"category,omitempty"`
Args []string `json:"args,omitempty"`
Example string `json:"example,omitempty"`
Description string `json:"description,omitempty"`
URL string `json:"URL,omitempty"`
Platform Platform `json:"platform,omitempty"`
}
func main() {
driver, err := bingo.NewDriver(bingo.DriverConfiguration{
Filename: "clippy.db",
})
if err != nil {
panic(err)
}
defer func() {
os.Remove("clippy.db")
}()
functionDB := bingo.CollectionFrom[Function](driver, "functions")
// Registering a custom ID generator,
// this will be called when a new document is inserted
// and the if the document does not have a key/id set.
functionDB.OnNewId = func(_ int, doc *Function) []byte {
return []byte(strings.ToLower(doc.Name))
}
// Inserting
key, err := functionDB.Insert(Function{
Name: "SUM",
Category: "Math",
Args: []string{"a", "b"},
Example: "SUM(1, 2)",
Description: "Adds two numbers together",
URL: "https://support.google.com/docs/answer/3093669?hl=en",
Platform: Sheets,
})
if err != nil {
panic(err)
}
fmt.Println("Inserted document with key", string(key))
searchQuery := "sum"
platform := "sheets"
// Querying
query := functionDB.Query(bingo.Query[Function]{
Filter: func(doc Function) bool {
return doc.Platform == Platform(platform) && strings.Contains(strings.ToLower(doc.Name), strings.ToLower(searchQuery))
},
Count: 3,
})
if query.Error != nil {
panic(query.Error)
}
if !query.Any() {
panic("No documents found!")
}
fmt.Println("Found", query.Count(), "documents")
for _, function := range query.Items {
fmt.Printf("%s: %s\n", function.Name, function.Description)
}
sum, err := functionDB.FindByKey("sum")
if err != nil {
panic(err)
}
sum.Category = "Algebra"
err = functionDB.UpdateOne(sum)
if err != nil {
panic(err)
}
newSum, _ := functionDB.FindByBytesKey(sum.Key())
fmt.Println("Updated SUM category to", newSum.Category)
fmt.Println(newSum)
}Create a new driver instance:
config := bingo.DriverConfiguration{
DeleteNoVerify: false,
Filename: "mydb.db",
}
driver, err := bingo.NewDriver(config)You can specify an autoincrement ID by returning nil in the Key method.
type User struct {
bingo.Document
Username string `json:"username,omitempty" validate:"required,min=3,max=64"`
Email string `json:"email,omitempty" validate:"required,email"`
Password string `json:"password,omitempty" preprocessor:"password-prep" validate:"required,min=6,max=64"`
}
func (u *User) CheckPassword(password string) bool {
current := u.Password
if strings.HasPrefix(current, "hash:") {
current = strings.TrimPrefix(current, "hash:")
return bcrypt.CompareHashAndPassword([]byte(current), []byte(password)) == nil
}
return current == password
}
func (u *User) EnsureHashedPassword() error {
if strings.HasPrefix("hash:", u.Password) {
return nil
}
hashed, err := HashPassword(u.Password)
if err != nil {
return err
}
u.Password = fmt.Sprintf("hash:%s", hashed)
return nil
}Note: Your document type must have bingo.Document as it's first field or implement the bingo.DocumentSpec interface.
users := bingo.CollectionFrom[User](driver, "users")You can also add hooks:
users.BeforeUpdate(func(doc *User) error {
return doc.EnsureHashedPassword()
})Inserting documents:
id, err := users.Insert(User{
Username: "test",
Password: "test123",
Email: "random@rrege.com",
})
if err != nil {
panic(err)
}
fmt.Println("Inserted user with ID:", id)Inserting documents with a custom ID:
id, err := users.Insert(User{
Document: bingo.Document{
ID: []byte("custom-id"),
},
Username: "test",
Password: "test123",
Email: "random@rrege.com",
})
if err != nil {
panic(err)
}
fmt.Println("Inserted user with ID:", id)Querying documents:
result := users.Query(bingo.Query[User]{
Filter: func(doc User) bool {
return doc.Username == "test"
},
})
if !result.Any() {
panic("No user found")
}user := User{
Username: "john_doe",
Email: "john.doe@example.com",
Password: "password1234",
Active: true,
}
// Ensure password is hashed
_ = user.EnsureHashedPassword()
insertResult := userCollection.Insert(user)
if insertResult.Error() != nil {
log.Fatalf("Failed to insert user: %v", insertResult.Error())
}result, err := coll.FindOne(func(doc TestDocument) bool {
return doc.Name == "Apple"
})
if err != nil {
t.Fatalf("Failed to find document: %v", err)
}
result.Name = "Pineapple"
err = coll.UpdateOne(result)
if err != nil {
t.Fatalf("Failed to update document: %v", err)
}err := coll.UpdateIter(func(doc *TestDocument) *TestDocument {
if doc.Name == "Apple" {
doc.Name = "Pineapple"
return doc
}
return nil
})result, err := coll.FindOne(func(doc TestDocument) bool {
return doc.Name == "Apple"
})
if err != nil {
t.Fatalf("Failed to find document: %v", err)
}
err = coll.DeleteOne(result)
if err != nil {
t.Fatalf("Failed to delete document: %v", err)
}err := coll.DeleteIter(func(doc *TestDocument) bool {
return doc.Name == "Apple"
})result := userCollection.Query(bingo.Query[User]{
Filter: func(doc User) bool {
return doc.Username == "john_doe"
},
Count: 1, // Only get the first result
})
if result.Any() {
firstUser := result.First()
fmt.Println("Found user:", firstUser.Username, firstUser.Email)
} else {
fmt.Println("User not found")
}page1 := userCollection.Query(bingo.Query[User]{
Filter: func(doc User) bool {
return doc.Active
},
Count: 10, // Get 10 results
})
page2 := userCollection.Query(bingo.Query[User]{
Filter: func(doc User) bool {
return doc.Active
},
Count: 10, // Get 10 results
Skip: page1.Next, // Skip n Users, this value is returned by the previous query as QueryResult.Next
})package main
import (
"fmt"
"log"
"github.com/nokusukun/bingodb"
)
type User struct {
Username string `json:"username,omitempty" validate:"required,min=3,max=64"`
Email string `json:"email,omitempty" validate:"required,email"`
Password string `json:"password,omitempty" preprocessor:"password-prep" validate:"required,min=6,max=64"`
Active bool `json:"active,omitempty"`
}
func (u User) Key() []byte {
return []byte(u.Username)
}
func (u *User) CheckPassword(password string) bool {
current := u.Password
if strings.HasPrefix(current, "hash:") {
current = strings.TrimPrefix(current, "hash:")
return bcrypt.CompareHashAndPassword([]byte(current), []byte(password)) == nil
}
return current == password
}
func (u *User) EnsureHashedPassword() error {
if strings.HasPrefix("hash:", u.Password) {
return nil
}
hashed, err := HashPassword(u.Password)
if err != nil {
return err
}
u.Password = fmt.Sprintf("hash:%s", hashed)
return nil
}
func main() {
config := bingo.DriverConfiguration{
DeleteNoVerify: true,
Filename: "test.db",
}
driver, err := bingo.NewDriver(config)
if err != nil {
log.Fatalf("Failed to create new driver: %v", err)
}
userCollection := bingo.CollectionFrom[User](driver, "users")
//... your operations ...
}The Validate method allows you to validate the query result before performing any operations on it. This is useful for checking if a document exists before updating or deleting it.
newText := "Hello World!"
err := Posts.Query(bingo.Query[Post]{
KeysStr: []string{c.Param("id")}, // Get only the post with the given ID
}).Validate(func(qr *bingo.QueryResult[Post]) error {
if !qr.Any() {
return fmt.Errorf("404::post not found") // Return a premature error if no post was found
}
return nil
}).Iter(func(doc *Post) error {
if doc.Author != username {
return fmt.Errorf("401::you are not the author of this post")
}
doc.Content = newText
doc.Edited = time.Now().Unix()
return nil
}).Update()If for some reason you need to further filter the query result, you can use the Filter method:
newText := "The contents of this post has been deleted"
err := Posts.Query(bingo.Query[Post]{
Filter: func(doc Post) bool {
return doc.Author == username
},
}).Validate(func(qr *bingo.QueryResult[Post]) error {
if !qr.Any() {
return fmt.Errorf("404::posts not found") // Return a premature error if no post was found
}
return nil
}).Filter(func (doc Post) bool {
return doc.Edited == 0 // Only get posts that have not been edited
}).Iter(func(doc *Post) error {
doc.Content = newText
doc.Edited = time.Now().Unix()
return nil
}).Update()err := userCollection.Query(bingo.Query[User]{
Filter: func(doc User) bool {
return doc.Username == "john_doe"
},
Count: 1, // Only get the first result
}).Validate(func(qr *bingo.QueryResult[Post]) error {
if !qr.Any() {
return fmt.Errorf("404::user not found") // Return a premature error if no post was found
}
return nil
}).Iter(func(doc *User) error {
doc.Email = "new.email@example.com"
return nil
}).Update()
if err != nil {
log.Fatalf("Failed to update user: %v", err)
}err := userCollection.Query(bingo.Query[User]{
Filter: func(doc User) bool {
return doc.Username == "john_doe"
},
Count: 1, // Only get the first result
}).Validate(func(qr *bingo.QueryResult[Post]) error {
if !qr.Any() {
return fmt.Errorf("404::user not found") // Return a premature error if no post was found
}
return nil
}).Delete()
if err != nil {
log.Fatalf("Failed to delete user: %v", err)
}If you want to have custom operations before or after inserting, updating, or deleting users, you can define them as:
userCollection.BeforeInsert(func(u *User) error {
return u.EnsureHashedPassword()
})
userCollection.AfterInsert(func(u *User) error {
fmt.Println("User inserted:", u.Username)
return nil
})
userCollection.BeforeUpdate(func(u *User) error {
return u.EnsureHashedPassword()
})
userCollection.AfterUpdate(func(u *User) error {
fmt.Println("User updated:", u.Username)
return nil
})
userCollection.BeforeDelete(func(u *User) error {
fmt.Println("Deleting user:", u.Username)
return nil
})The library provides helper functions to check for specific errors:
err := users.Insert(newUser)
if bingo.IsErrDocumentExists(err) {
fmt.Println("Document already exists!")
} else if bingo.IsErrDocumentNotFound(err) {
fmt.Println("Document not found!")
}This method initializes a new connection to the database.
This is a factory method to create a new collection for a specific document type.
Inserts a document into the collection.
Query documents using a custom filter.
Special error types are provided for common error scenarios:
bingo.ErrDocumentNotFound: When a document is not found in the collection.bingo.ErrDocumentExists: When attempting to insert a document with an existing key.
Helper functions like IsErrDocumentNotFound and IsErrDocumentExists are available for easy error checking.
For destructive operations like Drop, safety checks are in place. By default, you need to set environment variables to permit such operations:
export BINGO_ALLOW_DROP_MY_COLLECTION_NAME=trueBingo relies on the following third-party packages:
If you'd like to contribute to the development of Bingo, please submit a pull request or open an issue.
This library is released under the MIT License.
Please make sure to adhere to the terms of use and licensing of the third-party dependencies if you decide to use this library.