Defining model types

In this section you will learn how a user, messages and db are defined in the Forum Application.

User

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"`
}

Messages

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
}

Explanation of code

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.

DB

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"))
}

Explanation of code

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.

Decorative Orb