pelican/internal/board/board.go
Dan Anglin 5189ebe7bb
All checks were successful
/ test (pull_request) Successful in 40s
/ lint (pull_request) Successful in 42s
refactor: remove duplicate write function in db
Remove the original 'Write' function from the db package and rename the
'WriteMany' function to 'Write' as it can already write one or many Bolt
items to the database.

Also update the test suites and add more coverage in the board package.
2024-01-23 18:31:01 +00:00

413 lines
9.6 KiB
Go

package board
import (
"bytes"
"encoding/gob"
"fmt"
"sort"
"time"
"codeflow.dananglin.me.uk/apollo/pelican/internal/db"
bolt "go.etcd.io/bbolt"
)
type Board struct {
db *bolt.DB
}
// Open reads the board from the database.
// If no board exists then a new one will be created.
func Open(path string) (Board, error) {
database, err := db.OpenDatabase(path)
if err != nil {
return Board{}, fmt.Errorf("unable to open the db. %w", err)
}
board := Board{
db: database,
}
statusList, err := board.StatusList()
if err != nil {
return Board{}, err
}
if len(statusList) == 0 {
newStatusList := defaultStatusList()
boltItems := make([]db.BoltItem, len(newStatusList))
for i := range newStatusList {
boltItems[i] = &newStatusList[i]
}
if _, err := db.Write(database, db.WriteModeCreate, db.StatusBucket, boltItems); err != nil {
return Board{}, fmt.Errorf("unable to save the default status list to the db. %w", err)
}
}
return board, nil
}
// Close closes the project's Kanban board.
func (b *Board) Close() error {
if b.db == nil {
return nil
}
if err := b.db.Close(); err != nil {
return fmt.Errorf("error closing the db. %w", err)
}
return nil
}
// StatusList returns the ordered list of statuses from the db.
func (b *Board) StatusList() ([]Status, error) {
data, err := db.ReadAll(b.db, db.StatusBucket)
if err != nil {
return nil, fmt.Errorf("unable to read the status list, %w", err)
}
statuses := make([]Status, len(data))
for ind, d := range data {
buf := bytes.NewBuffer(d)
decoder := gob.NewDecoder(buf)
var status Status
if err := decoder.Decode(&status); err != nil {
return nil, fmt.Errorf("unable to decode data, %w", err)
}
statuses[ind] = status
}
sort.Sort(ByStatusPosition(statuses))
return statuses, nil
}
// Status returns a single status from the db.
// TODO: Add a test case that handles when a status does not exist.
// Or use in delete status case.
func (b *Board) Status(statusID int) (Status, error) {
data, err := db.Read(b.db, db.StatusBucket, statusID)
if err != nil {
return Status{}, fmt.Errorf("unable to read status [%d] from the database; %w", statusID, err)
}
if data == nil {
return Status{}, StatusNotExistError{ID: statusID}
}
var status Status
buf := bytes.NewBuffer(data)
decoder := gob.NewDecoder(buf)
if err := decoder.Decode(&status); err != nil {
return Status{}, fmt.Errorf("unable to decode data into a Status object; %w", err)
}
return status, nil
}
type StatusArgs struct {
Name string
Position int
}
// CreateStatus creates a status in the database.
func (b *Board) CreateStatus(args StatusArgs) error {
name := args.Name
pos := args.Position
if pos < 1 {
statuses, err := b.StatusList()
if err != nil {
return fmt.Errorf("unable to get the list of statuses; %w", err)
}
length := len(statuses)
if length == 0 {
pos = 1
} else {
pos = statuses[length-1].Position + 1
}
}
status := Status{
ID: -1,
Name: name,
Position: pos,
CardIds: nil,
}
if _, err := db.Write(b.db, db.WriteModeCreate, db.StatusBucket, []db.BoltItem{&status}); err != nil {
return fmt.Errorf("unable to write the status to the database; %w", err)
}
return nil
}
// UpdateStatusArgs is an argument type required for updating statuses.
type UpdateStatusArgs struct {
StatusID int
StatusArgs
}
// UpdateStatus modifies an existing status in the database.
func (b *Board) UpdateStatus(args UpdateStatusArgs) error {
status, err := b.Status(args.StatusID)
if err != nil {
return fmt.Errorf("unable to retrieve the status from the database. %w", err)
}
if len(args.Name) > 0 {
status.Name = args.Name
}
if args.Position > 0 {
status.Position = args.Position
}
if _, err := db.Write(b.db, db.WriteModeUpdate, db.StatusBucket, []db.BoltItem{&status}); err != nil {
return fmt.Errorf("unable to write the status to the db. %w", err)
}
return nil
}
// DeleteStatus deletes a status from the database.
// A status can only be deleted if it does not contain any cards.
func (b *Board) DeleteStatus(statusID int) error {
status, err := b.Status(statusID)
if err != nil {
return fmt.Errorf("unable to retrieve the status from the database; %w", err)
}
if len(status.CardIds) > 0 {
return StatusNotEmptyError{ID: statusID}
}
if err := db.Delete(b.db, db.StatusBucket, statusID); err != nil {
return fmt.Errorf("unable to delete the status from the database; %w", err)
}
if err := b.normaliseStatusesPositionValues(); err != nil {
return fmt.Errorf("unable to normalise the statuses position values; %w", err)
}
return nil
}
// normaliseStatusesPositionValues retrieves the ordered list of statuses from the database and sets
// each status' positional value based on its position in the list.
func (b *Board) normaliseStatusesPositionValues() error {
statuses, err := b.StatusList()
if err != nil {
return fmt.Errorf("unable to get the list of statuses; %w", err)
}
for i, status := range statuses {
updateArgs := UpdateStatusArgs{
StatusID: status.ID,
StatusArgs: StatusArgs{
Name: "",
Position: i + 1,
},
}
if err := b.UpdateStatus(updateArgs); err != nil {
return fmt.Errorf("unable to update the status %q; %w", status.Name, err)
}
}
return nil
}
type MoveToStatusArgs struct {
CardID int
CurrentStatusID int
NextStatusID int
}
// MoveToStatus moves a card between statuses.
func (b *Board) MoveToStatus(args MoveToStatusArgs) error {
currentStatus, err := b.Status(args.CurrentStatusID)
if err != nil {
return fmt.Errorf("unable to get the card's current status [%d], %w", args.CurrentStatusID, err)
}
nextStatus, err := b.Status(args.NextStatusID)
if err != nil {
return fmt.Errorf("unable to get the card's next status [%d], %w", args.NextStatusID, err)
}
nextStatus.AddCardID(args.CardID)
currentStatus.RemoveCardID(args.CardID)
boltItems := []db.BoltItem{&currentStatus, &nextStatus}
if _, err := db.Write(b.db, db.WriteModeUpdate, db.StatusBucket, boltItems); err != nil {
return fmt.Errorf("unable to update the statuses in the db. %w", err)
}
return nil
}
type CardArgs struct {
NewTitle string
NewDescription string
}
// CreateCard creates a card in the database.
func (b *Board) CreateCard(args CardArgs) (int, error) {
timestamp := time.Now().Format(time.DateTime)
statusList, err := b.StatusList()
if err != nil {
return 0, fmt.Errorf("unable to read the status list, %w", err)
}
if len(statusList) == 0 {
return 0, StatusListEmptyError{}
}
boltItems := []db.BoltItem{
&Card{
ID: -1,
Title: args.NewTitle,
Description: args.NewDescription,
Created: timestamp,
},
}
cardIDs, err := db.Write(b.db, db.WriteModeCreate, db.CardBucket, boltItems)
if err != nil {
return 0, fmt.Errorf("unable to write card to the db. %w", err)
}
if len(cardIDs) != 1 {
return 0, fmt.Errorf("unexpected number of card IDs returned after writing the card to the database; want: 1, got %d", len(cardIDs))
}
cardID := cardIDs[0]
statusInFirstPos := statusList[0]
statusInFirstPos.AddCardID(cardID)
_, err = db.Write(b.db, db.WriteModeUpdate, db.StatusBucket, []db.BoltItem{&statusInFirstPos})
if err != nil {
return 0, fmt.Errorf("unable to write the %s status to the db. %w", statusInFirstPos.Name, err)
}
return cardID, nil
}
// Card returns a Card value from the database.
func (b *Board) Card(cardID int) (Card, error) {
data, err := db.Read(b.db, db.CardBucket, cardID)
if err != nil {
return Card{}, fmt.Errorf("unable to read card [%d] from the db. %w", cardID, err)
}
if data == nil {
return Card{}, CardNotExistError{ID: cardID}
}
var card Card
buf := bytes.NewBuffer(data)
decoder := gob.NewDecoder(buf)
if err := decoder.Decode(&card); err != nil {
return Card{}, fmt.Errorf("unable to decode data, %w", err)
}
return card, nil
}
// CardList returns a list of Card values from the db.
func (b *Board) CardList(ids []int) ([]Card, error) {
data, err := db.ReadMany(b.db, db.CardBucket, ids)
if err != nil {
return nil, fmt.Errorf("unable to read card list from the db. %w", err)
}
cards := make([]Card, len(data))
for ind, d := range data {
buf := bytes.NewBuffer(d)
decoder := gob.NewDecoder(buf)
var card Card
if err := decoder.Decode(&card); err != nil {
return nil, fmt.Errorf("unable to decode data, %w", err)
}
cards[ind] = card
}
return cards, nil
}
type UpdateCardArgs struct {
CardID int
CardArgs
}
// UpdateCard modifies an existing card in the database.
func (b *Board) UpdateCard(args UpdateCardArgs) error {
card, err := b.Card(args.CardID)
if err != nil {
return err
}
if len(args.NewTitle) > 0 {
card.Title = args.NewTitle
}
if len(args.NewDescription) > 0 {
card.Description = args.NewDescription
}
if _, err := db.Write(b.db, db.WriteModeUpdate, db.CardBucket, []db.BoltItem{&card}); err != nil {
return fmt.Errorf("unable to write the card to the database; %w", err)
}
return nil
}
type DeleteCardArgs struct {
CardID int
StatusID int
}
// DeleteCard deletes a card from the database.
func (b *Board) DeleteCard(args DeleteCardArgs) error {
if err := db.Delete(b.db, db.CardBucket, args.CardID); err != nil {
return fmt.Errorf("unable to delete the card from the database; %w", err)
}
status, err := b.Status(args.StatusID)
if err != nil {
return fmt.Errorf("unable to read Status '%d' from the database; %w", args.StatusID, err)
}
status.RemoveCardID(args.CardID)
if _, err := db.Write(b.db, db.WriteModeUpdate, db.StatusBucket, []db.BoltItem{&status}); err != nil {
return fmt.Errorf("unable to update the status in the database; %w", err)
}
return nil
}