In this section you will learn how a user, messages and db are defined in the Forum Application.
This is how a User
is defined in the Forum Application.
package model
type User struct {
Name string `json:"name"`
Moderator bool `json:"moderator"`
Banned bool `json:"banned"`
NumMessages int64 `json:"numMessages"`
Version uint64 `json:"version"`
SchemaVersion int `json:"schemaVersion"`
}
This is a Message
is defined in the Forum Application. It also allows you to perform various operations on a message
package model
import (
"errors"
"fmt"
"strings"
"github.com/dgraph-io/badger/v4"
)
type BanTx struct {
UserName string `json:"username"`
}
// Message represents a message sent by a user.
type Message struct {
Sender string `json:"sender"`
Message string `json:"message"`
}
type MsgHistory struct {
Msg string `json:"history"`
}
func AppendToChat(db *DB, message Message) (string, error) {
historyBytes, err := db.Get([]byte("history"))
if err != nil {
return "", fmt.Errorf("error fetching history: %w", err)
}
msgBytes := string(historyBytes)
msgBytes = msgBytes + "{sender:" + message.Sender + ",message:" + message.Message + "}"
return msgBytes, nil
}
func FetchHistory(db *DB) (string, error) {
historyBytes, err := db.Get([]byte("history"))
if err != nil {
return "", fmt.Errorf("error fetching history: %w", err)
}
msgHistory := string(historyBytes)
return msgHistory, nil
}
func AppendToExistingMessages(db *DB, message Message) (string, error) {
existingMessages, err := GetMessagesBySender(db, message.Sender)
if err != nil && !errors.Is(err, badger.ErrKeyNotFound) {
return "", err
}
if errors.Is(err, badger.ErrKeyNotFound) {
return message.Message, nil
}
return existingMessages + ";" + message.Message, nil
}
// GetMessagesBySender retrieves all messages sent by a specific sender
// Get Message using String.
func GetMessagesBySender(db *DB, sender string) (string, error) {
var messages string
err := db.db.View(func(txn *badger.Txn) error {
item, err := txn.Get([]byte(sender + "msg"))
if err != nil {
return err
}
value, err := item.ValueCopy(nil)
if err != nil {
return err
}
messages = string(value)
return nil
})
if err != nil {
return "", err
}
return messages, nil
}
// ParseMessage parse messages.
func ParseMessage(tx []byte) (*Message, error) {
msg := &Message{}
// Parse the message into key-value pairs
pairs := strings.Split(string(tx), ",")
if len(pairs) != 2 {
return nil, errors.New("invalid number of key-value pairs in message")
}
for _, pair := range pairs {
kv := strings.Split(pair, ":")
if len(kv) != 2 {
return nil, fmt.Errorf("invalid key-value pair in message: %s", pair)
}
key := kv[0]
value := kv[1]
switch strings.ToLower(key) {
case "sender":
msg.Sender = value
case "message":
msg.Message = value
case "history":
return nil, fmt.Errorf("reserved key name: %s", key)
default:
return nil, fmt.Errorf("unknown key in message: %s", key)
}
}
// Check if the message contains a sender and message
if msg.Sender == "" {
return nil, errors.New("message is missing sender")
}
if msg.Message == "" {
return nil, errors.New("message is missing message")
}
return msg, nil
}
AppendToChat
AppendToChat
takes a pointer to a DB
object and a Message
object as parameters. It appends the message to the
chat history stored in the DB object, and returns the updated chat history as a string. If there is an error retrieving
the chat history, it returns an empty string and the error.
FetchHistory
FetchHistory
takes a pointer to a DB
struct as an argument. It attempts to retrieve a value from the database using
the ViewDB
function, passing in the DB
underlying database and a key called "history"
.
If an error occurs during the retrieval, it prints an error message and returns an empty string and the error. The retrieved value is then converted to a string and returned. If an error occurs during the conversion, it prints an error message but still returns the converted value and the error.
AppendToExistingMsgs
AppendToExistingMsgs
takes a pointer to a DB
object and a Message
object as input. It retrieves existing messages
from the database by the sender of the input message and appends the input message to the existing messages.
If no existing messages are found, it returns the input message as is. The function returns the combined messages or an error.
GetMessagesBySender
GetMessagesBySender
retrieves all messages sent by a specific sender from a database. It takes a pointer to a DB
object
and a string representing the sender
as input. It returns a string containing the messages and an error if any occurred.
It uses the badger package to interact with the database and retrieves the messages by concatenating the sender with the string “msg” and performing a database lookup.
ParseMessage
ParseMessage
takes a byte array tx as input and returns a pointer to a Message
struct and an error.
The function first initializes an empty Message
struct. It then splits the input byte array into key-value pairs using
a comma as the separator. If the number of pairs is not equal to 2, it returns an error indicating an invalid number of
key-value pairs.
Next, it iterates over each pair, splitting it into key and value using a colon as the separator. If the number of elements in a pair is not equal to 2, it returns an error indicating an invalid key-value pair.
For each key-value pair, it checks the key and assigns the corresponding value to the appropriate field in the Message
struct.
Finally, it checks if the Sender
and Message
fields in the Message
struct are empty. If either of them is empty,
it returns an error indicating that the message is missing the sender or the message itself.
If all checks pass, it returns the populated Message struct and a nil
error.
These are the storage operation in the Forum Application. It also allows you to perform various operations related to storage in the underline database.
package model
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"github.com/dgraph-io/badger/v4"
"github.com/cometbft/cometbft/abci/types"
)
type DB struct {
db *badger.DB
}
func (db *DB) Init(database *badger.DB) {
db.db = database
}
func (db *DB) Commit() error {
return db.db.Update(func(txn *badger.Txn) error {
return txn.Commit()
})
}
func NewDB(dbPath string) (*DB, error) {
// Open badger DB
opts := badger.DefaultOptions(dbPath)
db, err := badger.Open(opts)
if err != nil {
return nil, err
}
// Create a new DB instance and initialize with badger DB
dbInstance := &DB{}
dbInstance.Init(db)
return dbInstance, nil
}
func (db *DB) GetDB() *badger.DB {
return db.db
}
func (db *DB) Size() int64 {
lsm, vlog := db.GetDB().Size()
return lsm + vlog
}
func (db *DB) CreateUser(user *User) error {
// Check if the user already exists
err := db.db.View(func(txn *badger.Txn) error {
_, err := txn.Get([]byte(user.Name))
return err
})
if err == nil {
return errors.New("user already exists")
}
// Save the user to the database
err = db.db.Update(func(txn *badger.Txn) error {
userBytes, err := json.Marshal(user)
if err != nil {
return fmt.Errorf("failed to marshal user to JSON: %w", err)
}
err = txn.Set([]byte(user.Name), userBytes)
if err != nil {
return err
}
return nil
})
return err
}
func (db *DB) FindUserByName(name string) (*User, error) {
// Read the user from the database
var user *User
err := db.db.View(func(txn *badger.Txn) error {
item, err := txn.Get([]byte(name))
if err != nil {
return err
}
err = item.Value(func(val []byte) error {
return json.Unmarshal(val, &user)
})
return err
})
if err != nil {
return nil, fmt.Errorf("error in retrieving user: %w", err)
}
return user, nil
}
func (db *DB) UpdateOrSetUser(uname string, toBan bool, txn *badger.Txn) error {
user, err := db.FindUserByName(uname)
// If user is not in the db, then add it
if errors.Is(err, badger.ErrKeyNotFound) {
u := new(User)
u.Name = uname
u.Banned = toBan
user = u
} else {
if err != nil {
return errors.New("not able to process user")
}
user.Banned = toBan
}
userBytes, err := json.Marshal(user)
if err != nil {
return fmt.Errorf("error marshaling user: %w", err)
}
return txn.Set([]byte(user.Name), userBytes)
}
func (db *DB) Set(key, value []byte) error {
return db.db.Update(func(txn *badger.Txn) error {
return txn.Set(key, value)
})
}
func ViewDB(db *badger.DB, key []byte) ([]byte, error) {
var value []byte
err := db.View(func(txn *badger.Txn) error {
item, err := txn.Get(key)
if err != nil {
if !errors.Is(err, badger.ErrKeyNotFound) {
return err
}
return nil
}
value, err = item.ValueCopy(nil)
return err
})
if err != nil {
return nil, err
}
return value, nil
}
func (db *DB) Close() error {
return db.db.Close()
}
func (db *DB) Get(key []byte) ([]byte, error) {
return ViewDB(db.db, key)
}
func (db *DB) GetValidators() (validators []types.ValidatorUpdate, err error) {
err = db.db.View(func(txn *badger.Txn) error {
opts := badger.DefaultIteratorOptions
opts.PrefetchSize = 10
it := txn.NewIterator(opts)
defer it.Close()
for it.Rewind(); it.Valid(); it.Next() {
var err error
item := it.Item()
k := item.Key()
if isValidatorTx(k) {
err := item.Value(func(v []byte) error {
validator := new(types.ValidatorUpdate)
err = types.ReadMessage(bytes.NewBuffer(v), validator)
if err == nil {
validators = append(validators, *validator)
}
return err
})
if err != nil {
return err
}
}
}
return nil
})
if err != nil {
return nil, err
}
return validators, nil
}
func isValidatorTx(tx []byte) bool {
return bytes.HasPrefix(tx, []byte("val"))
}
Commit
Commit
calls the Update method on the db object and passes a function as an argument. Inside this function, it calls the Commit method on a Txn object and returns its result.
NewDB
NewDB
creates a new database instance. It uses the badger package to open a BadgerDB database at the specified dbPath
and returns a pointer to the newly created database instance. If there is an error during the database creation, it returns the error.
GetDB
GetDB()
returns a pointer to a badger.DB
object.
Size
This code defines a method Size()
returns the sum of two values obtained from another method Size() of a DB instance: lsm and vlog.
CreateUser
CreateUser
creates a new user in the database using Badger as the key-value store. The method checks if the user already
exists by performing a read operation on the database. If the user already exists, it returns an error. If the user does
not exist, it saves the user to the database by performing a write operation. The method returns any errors encountered
during the process.
FindUserByName
FindUserByName
takes a name string as input and returns a pointer to a User struct and an error. The method reads a
user from the database using the db.db.View method provided by the badger package. It retrieves the user by the provided
name, un-marshals the JSON data into the user variable, and returns it along with any error that occurred during the process.
Set
Set
takes in a key and a value as byte slices. It uses the badger database library to update the database with the given key and value.
ViewDB
ViewDB
takes a pointer to a badger.DB
object and a byte slice called key as arguments. The function reads a value
from the database using the provided key. If the key is not found in the database, it returns nil. Otherwise, it returns
the value associated with the key.
The function uses the View method of the badger.DB object to perform a read-only transaction on the database. Inside the View method, it retrieves the item corresponding to the key using the Get method of the transaction object. If the key is not found, it handles the badger.ErrKeyNotFound error and returns nil. Otherwise, it copies the value associated with the item using the ValueCopy method and assigns it to the value variable. Finally, it returns the value variable and any error that occurred during the transaction.
Overall, this code snippet provides a concise way to read data from a BadgerDB database using a specified key.
Close
Close
takes a pointer receiver db of type *DB
and returns an error. The method calls the Close method of the db
field of the DB struct.
Get
Get
on a type DB
. The method takes a key of type []byte
as input and returns a []byte
and an error.
Inside the method, it calls a function ViewDB with the db.db and key as arguments and returns the result.
GetValidators
GetValidators
retrieves a list of validator updates from a database using the Badger library. It iterates over the
key-value pairs in the database, checks if a key corresponds to a validator transaction, and if so, reads the value and
appends it to the validators slice.
Finally, it returns the validators slice and any potential error.
isValidatorTx
isValidatorTx
takes a byte slice as input and returns a boolean value. It checks if the string representation of the
byte slice starts with the prefix “val” and returns true if it does, otherwise it returns false.
In the next session, you will learn about the main method responsible for running the Forum Application blockchain.