feat: move a card between statuses #3
8 changed files with 224 additions and 111 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1 +1,2 @@
|
||||||
/test/databases/*.db
|
/test/databases/*.db
|
||||||
|
/pelican
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
package board
|
package board
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/gob"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
"forge.dananglin.me.uk/code/dananglin/pelican/internal/card"
|
|
||||||
"forge.dananglin.me.uk/code/dananglin/pelican/internal/database"
|
"forge.dananglin.me.uk/code/dananglin/pelican/internal/database"
|
||||||
"forge.dananglin.me.uk/code/dananglin/pelican/internal/status"
|
|
||||||
bolt "go.etcd.io/bbolt"
|
bolt "go.etcd.io/bbolt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -23,8 +23,15 @@ func LoadBoard(path string) (*bolt.DB, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(statusList) == 0 {
|
if len(statusList) == 0 {
|
||||||
newStatusList := status.NewDefaultStatusList()
|
newStatusList := defaultStatusList()
|
||||||
if err := database.WriteStatusList(db, newStatusList); err != nil {
|
|
||||||
|
boltItems := make([]database.BoltItem, len(newStatusList))
|
||||||
|
|
||||||
|
for i := range newStatusList {
|
||||||
|
boltItems[i] = &newStatusList[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := database.WriteMany(db, database.StatusBucket, boltItems); err != nil {
|
||||||
return nil, fmt.Errorf("unable to save the default status list to the database, %w", err)
|
return nil, fmt.Errorf("unable to save the default status list to the database, %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,21 +39,37 @@ func LoadBoard(path string) (*bolt.DB, error) {
|
||||||
return db, nil
|
return db, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadStatusList returns an ordered list of statuses from the database.
|
// ReadStatusList returns the ordered list of statuses from the database.
|
||||||
func ReadStatusList(db *bolt.DB) ([]status.Status, error) {
|
func ReadStatusList(db *bolt.DB) ([]Status, error) {
|
||||||
statuses, err := database.ReadStatusList(db)
|
data, err := database.ReadAll(db, database.StatusBucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return statuses, fmt.Errorf("unable to read the status list, %w", err)
|
return []Status{}, fmt.Errorf("unable to read the status list, %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Sort(status.ByStatusOrder(statuses))
|
statuses := make([]Status, len(data))
|
||||||
|
|
||||||
|
for i, d := range data {
|
||||||
|
buf := bytes.NewBuffer(d)
|
||||||
|
|
||||||
|
decoder := gob.NewDecoder(buf)
|
||||||
|
|
||||||
|
var s Status
|
||||||
|
|
||||||
|
if err := decoder.Decode(&s); err != nil {
|
||||||
|
return []Status{}, fmt.Errorf("unable to decode data, %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
statuses[i] = s
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Sort(ByStatusOrder(statuses))
|
||||||
|
|
||||||
return statuses, nil
|
return statuses, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Finish implementation.
|
// TODO: Finish implementation.
|
||||||
func ReadStatus(db *bolt.DB) (status.Status, error) {
|
func ReadStatus(db *bolt.DB) (Status, error) {
|
||||||
return status.Status{}, nil
|
return Status{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Finish implementation.
|
// TODO: Finish implementation.
|
||||||
|
@ -66,7 +89,7 @@ func DeleteStatus(db *bolt.DB) error {
|
||||||
|
|
||||||
// CreateCard creates a card in the database.
|
// CreateCard creates a card in the database.
|
||||||
func CreateCard(db *bolt.DB, title, content string) error {
|
func CreateCard(db *bolt.DB, title, content string) error {
|
||||||
statusList, err := database.ReadStatusList(db)
|
statusList, err := ReadStatusList(db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to read the status list, %w", err)
|
return fmt.Errorf("unable to read the status list, %w", err)
|
||||||
}
|
}
|
||||||
|
@ -75,52 +98,64 @@ func CreateCard(db *bolt.DB, title, content string) error {
|
||||||
return statusListEmptyError{}
|
return statusListEmptyError{}
|
||||||
}
|
}
|
||||||
|
|
||||||
card := card.Card{
|
card := Card{
|
||||||
ID: -1,
|
ID: -1,
|
||||||
Title: title,
|
Title: title,
|
||||||
Content: content,
|
Content: content,
|
||||||
}
|
}
|
||||||
|
|
||||||
cardID, err := database.WriteCard(db, card)
|
cardID, err := database.Write(db, database.CardBucket, &card)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to write card to the database, %w", err)
|
return fmt.Errorf("unable to write card to the database, %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cardIDs := statusList[0].CardIds
|
initialStatus := statusList[0]
|
||||||
|
|
||||||
cardIDs = append(cardIDs, cardID)
|
initialStatus.AddCardID(cardID)
|
||||||
|
|
||||||
statusList[0].CardIds = cardIDs
|
if _, err := database.Write(db, database.StatusBucket, &initialStatus); err != nil {
|
||||||
|
return fmt.Errorf("unable to write the %s status to the database, %w", initialStatus.Name, err)
|
||||||
// TODO: change the below to save a single status
|
|
||||||
if err := database.WriteStatusList(db, statusList); err != nil {
|
|
||||||
return fmt.Errorf("unable to write status list to the database, %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadCard returns a Card value from the database.
|
// ReadCard returns a Card value from the database.
|
||||||
func ReadCard(db *bolt.DB, id int) (card.Card, error) {
|
func ReadCard(db *bolt.DB, id int) (Card, error) {
|
||||||
c, err := database.ReadCard(db, id)
|
data, err := database.Read(db, database.CardBucket, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return card.Card{}, fmt.Errorf("unable to read card [%d] from the database, %w", id, err)
|
return Card{}, fmt.Errorf("unable to read card [%d] from the database, %w", id, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return c, nil
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateCard modifies an existing card and saves the modification to the database.
|
// UpdateCard modifies an existing card and saves the modification to the database.
|
||||||
func UpdateCard(db *bolt.DB, id int, title, content string) error {
|
func UpdateCard(db *bolt.DB, id int, title, content string) error {
|
||||||
c, err := ReadCard(db, id)
|
card, err := ReadCard(db, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Title = title
|
if len(title) > 0 {
|
||||||
c.Content = content
|
card.Title = title
|
||||||
|
}
|
||||||
|
|
||||||
if _, err := database.WriteCard(db, c); err != nil {
|
if len(content) > 0 {
|
||||||
|
card.Content = content
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := database.Write(db, database.CardBucket, &card); err != nil {
|
||||||
return fmt.Errorf("unable to write card to the database, %w", err)
|
return fmt.Errorf("unable to write card to the database, %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,6 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"forge.dananglin.me.uk/code/dananglin/pelican/internal/board"
|
"forge.dananglin.me.uk/code/dananglin/pelican/internal/board"
|
||||||
"forge.dananglin.me.uk/code/dananglin/pelican/internal/card"
|
|
||||||
bolt "go.etcd.io/bbolt"
|
bolt "go.etcd.io/bbolt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -32,17 +31,21 @@ func TestCardLifecycle(t *testing.T) {
|
||||||
_ = db.Close()
|
_ = db.Close()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
cardTitle := "A test card."
|
initialCardTitle := "A test card."
|
||||||
cardContent := "Ensure that this card is safely stored in the database."
|
initialCardContent := "Ensure that this card is safely stored in the database."
|
||||||
expectedCardID := 1
|
expectedCardID := 1
|
||||||
|
|
||||||
testCreateCard(t, db, cardTitle, cardContent, expectedCardID)
|
testCreateCard(t, db, initialCardTitle, initialCardContent, expectedCardID)
|
||||||
testReadCard(t, db, expectedCardID, cardTitle, cardContent)
|
testReadCard(t, db, expectedCardID, initialCardTitle, initialCardContent)
|
||||||
|
|
||||||
newCardTitle := "Test card updated."
|
modifiedCardTitle := "Test card updated."
|
||||||
newCardContent := "Ensure that this card is safely updated in the database."
|
modifiedCardContent1 := "Ensure that this card is safely updated in the database."
|
||||||
|
|
||||||
testUpdateCard(t, db, expectedCardID, newCardTitle, newCardContent)
|
testUpdateCard(t, db, expectedCardID, modifiedCardTitle, modifiedCardContent1)
|
||||||
|
|
||||||
|
modifiedCardContent2 := "Updated card content only."
|
||||||
|
|
||||||
|
testUpdateCardContent(t, db, expectedCardID, modifiedCardTitle, modifiedCardContent2)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testCreateCard(t *testing.T, db *bolt.DB, title, content string, wantID int) {
|
func testCreateCard(t *testing.T, db *bolt.DB, title, content string, wantID int) {
|
||||||
|
@ -107,7 +110,7 @@ func testUpdateCard(t *testing.T, db *bolt.DB, cardID int, newTitle, newContent
|
||||||
t.Fatalf("Unable to read the modified test card, %s", err)
|
t.Fatalf("Unable to read the modified test card, %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
want := card.Card{
|
want := board.Card{
|
||||||
ID: cardID,
|
ID: cardID,
|
||||||
Title: newTitle,
|
Title: newTitle,
|
||||||
Content: newContent,
|
Content: newContent,
|
||||||
|
@ -120,6 +123,31 @@ func testUpdateCard(t *testing.T, db *bolt.DB, cardID int, newTitle, newContent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testUpdateCardContent(t *testing.T, db *bolt.DB, cardID int, expectedTitle, newContent string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
if err := board.UpdateCard(db, cardID, "", newContent); err != nil {
|
||||||
|
t.Fatalf("Unable to update the test card, %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := board.ReadCard(db, cardID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unable to read the modified test card, %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := board.Card{
|
||||||
|
ID: cardID,
|
||||||
|
Title: expectedTitle,
|
||||||
|
Content: newContent,
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(got, want) {
|
||||||
|
t.Errorf("Unexpected card read from the database, want: %+v, got: %+v", want, got)
|
||||||
|
} else {
|
||||||
|
t.Logf("Expected card read from the database, got: %+v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func projectRoot() (string, error) {
|
func projectRoot() (string, error) {
|
||||||
cwd, err := os.Getwd()
|
cwd, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
18
internal/board/card.go
Normal file
18
internal/board/card.go
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
package board
|
||||||
|
|
||||||
|
// Card represents a card on a Kanban board.
|
||||||
|
type Card struct {
|
||||||
|
ID int
|
||||||
|
Title string
|
||||||
|
Content string
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateId updates the ID of the Card value.
|
||||||
|
func (c *Card) UpdateId(id int) {
|
||||||
|
c.ID = id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Id returns the ID of the Card value.
|
||||||
|
func (c *Card) Id() int {
|
||||||
|
return c.ID
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package status
|
package board
|
||||||
|
|
||||||
// Status represents the status of the Kanban board.
|
// Status represents the status of the Kanban board.
|
||||||
type Status struct {
|
type Status struct {
|
||||||
|
@ -8,6 +8,20 @@ type Status struct {
|
||||||
Order int
|
Order int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateId updates the ID of the Status value.
|
||||||
|
func (s *Status) UpdateId(id int) {
|
||||||
|
s.ID = id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Id returns the ID of the Status value.
|
||||||
|
func (s *Status) Id() int {
|
||||||
|
return s.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Status) AddCardID(id int) {
|
||||||
|
s.CardIds = append(s.CardIds, id)
|
||||||
|
}
|
||||||
|
|
||||||
// ByStatusOrder implements sort.Interface for []Status based on the Order field.
|
// ByStatusOrder implements sort.Interface for []Status based on the Order field.
|
||||||
type ByStatusOrder []Status
|
type ByStatusOrder []Status
|
||||||
|
|
||||||
|
@ -23,8 +37,8 @@ func (s ByStatusOrder) Less(i, j int) bool {
|
||||||
return s[i].Order < s[j].Order
|
return s[i].Order < s[j].Order
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDefaultStatusList creates the default list of statuses and saves the statuses to the database.
|
// defaultStatusList returns the default list of statuses.
|
||||||
func NewDefaultStatusList() []Status {
|
func defaultStatusList() []Status {
|
||||||
return []Status{
|
return []Status{
|
||||||
{
|
{
|
||||||
ID: -1,
|
ID: -1,
|
|
@ -1,8 +0,0 @@
|
||||||
package card
|
|
||||||
|
|
||||||
// Card represents a card on a Kanban board.
|
|
||||||
type Card struct {
|
|
||||||
ID int
|
|
||||||
Title string
|
|
||||||
Content string
|
|
||||||
}
|
|
|
@ -10,16 +10,19 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"forge.dananglin.me.uk/code/dananglin/pelican/internal/card"
|
|
||||||
"forge.dananglin.me.uk/code/dananglin/pelican/internal/status"
|
|
||||||
bolt "go.etcd.io/bbolt"
|
bolt "go.etcd.io/bbolt"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
statusBucket string = "status"
|
StatusBucket string = "status"
|
||||||
cardBucket string = "card"
|
CardBucket string = "card"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type BoltItem interface {
|
||||||
|
UpdateId(int)
|
||||||
|
Id() int
|
||||||
|
}
|
||||||
|
|
||||||
// OpenDatabase opens the database, at a given path, for reading and writing.
|
// OpenDatabase opens the database, at a given path, for reading and writing.
|
||||||
// If the file does not exist it will be created.
|
// If the file does not exist it will be created.
|
||||||
func OpenDatabase(path string) (*bolt.DB, error) {
|
func OpenDatabase(path string) (*bolt.DB, error) {
|
||||||
|
@ -48,13 +51,13 @@ func OpenDatabase(path string) (*bolt.DB, error) {
|
||||||
return db, nil
|
return db, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteStatusList saves one or more statuses to the status bucket.
|
// WriteMany saves one or more statuses to the status bucket.
|
||||||
func WriteStatusList(db *bolt.DB, statuses []status.Status) error {
|
func WriteMany(db *bolt.DB, bucketName string, items []BoltItem) error {
|
||||||
if len(statuses) == 0 {
|
if len(items) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
bucket := []byte(statusBucket)
|
bucket := []byte(bucketName)
|
||||||
|
|
||||||
err := db.Update(func(tx *bolt.Tx) error {
|
err := db.Update(func(tx *bolt.Tx) error {
|
||||||
b := tx.Bucket(bucket)
|
b := tx.Bucket(bucket)
|
||||||
|
@ -63,24 +66,24 @@ func WriteStatusList(db *bolt.DB, statuses []status.Status) error {
|
||||||
return bucketNotExistError{bucket: string(bucket)}
|
return bucketNotExistError{bucket: string(bucket)}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, v := range statuses {
|
for _, i := range items {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
if v.ID < 1 {
|
if i.Id() < 1 {
|
||||||
var id uint64
|
var id uint64
|
||||||
if id, err = b.NextSequence(); err != nil {
|
if id, err = b.NextSequence(); err != nil {
|
||||||
return fmt.Errorf("unable to generate ID, %w", err)
|
return fmt.Errorf("unable to generate ID, %w", err)
|
||||||
}
|
}
|
||||||
v.ID = int(id)
|
i.UpdateId(int(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
encoder := gob.NewEncoder(buf)
|
encoder := gob.NewEncoder(buf)
|
||||||
if err = encoder.Encode(v); err != nil {
|
if err = encoder.Encode(i); err != nil {
|
||||||
return fmt.Errorf("unable to encode data, %w", err)
|
return fmt.Errorf("unable to encode data, %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = b.Put([]byte(strconv.Itoa(v.ID)), buf.Bytes()); err != nil {
|
if err = b.Put([]byte(strconv.Itoa(i.Id())), buf.Bytes()); err != nil {
|
||||||
return fmt.Errorf("unable to add the status to the bucket, %w", err)
|
return fmt.Errorf("unable to add the status to the bucket, %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -94,11 +97,13 @@ func WriteStatusList(db *bolt.DB, statuses []status.Status) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadStatusList retrieves all the statuses from the status bucket.
|
// TODO: Create ReadMany
|
||||||
func ReadStatusList(db *bolt.DB) ([]status.Status, error) {
|
|
||||||
var statuses []status.Status
|
|
||||||
|
|
||||||
bucket := []byte(statusBucket)
|
// ReadAll retrieves all the statuses from the status bucket.
|
||||||
|
func ReadAll(db *bolt.DB, bucketName string) ([][]byte, error) {
|
||||||
|
bucket := []byte(bucketName)
|
||||||
|
|
||||||
|
var output [][]byte
|
||||||
|
|
||||||
err := db.View(func(tx *bolt.Tx) error {
|
err := db.View(func(tx *bolt.Tx) error {
|
||||||
b := tx.Bucket(bucket)
|
b := tx.Bucket(bucket)
|
||||||
|
@ -108,14 +113,7 @@ func ReadStatusList(db *bolt.DB) ([]status.Status, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := b.ForEach(func(_, v []byte) error {
|
if err := b.ForEach(func(_, v []byte) error {
|
||||||
var s status.Status
|
output = append(output, v)
|
||||||
buf := bytes.NewBuffer(v)
|
|
||||||
decoder := gob.NewDecoder(buf)
|
|
||||||
if err := decoder.Decode(&s); err != nil {
|
|
||||||
return fmt.Errorf("unable to decode data, %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
statuses = append(statuses, s)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
|
@ -125,15 +123,15 @@ func ReadStatusList(db *bolt.DB) ([]status.Status, error) {
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return statuses, fmt.Errorf("error while loading statuses from the database, %w", err)
|
return output, fmt.Errorf("error while loading statuses from the database, %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return statuses, nil
|
return output, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteCard creates or updates a card to the card bucket in BoltDB.
|
// Write creates or updates a Bolt item to a specified bucket.
|
||||||
func WriteCard(db *bolt.DB, card card.Card) (int, error) {
|
func Write(db *bolt.DB, bucketName string, item BoltItem) (int, error) {
|
||||||
bucket := []byte(cardBucket)
|
bucket := []byte(bucketName)
|
||||||
|
|
||||||
err := db.Update(func(tx *bolt.Tx) error {
|
err := db.Update(func(tx *bolt.Tx) error {
|
||||||
var err error
|
var err error
|
||||||
|
@ -143,60 +141,54 @@ func WriteCard(db *bolt.DB, card card.Card) (int, error) {
|
||||||
return bucketNotExistError{bucket: string(bucket)}
|
return bucketNotExistError{bucket: string(bucket)}
|
||||||
}
|
}
|
||||||
|
|
||||||
if card.ID < 1 {
|
if item.Id() < 1 {
|
||||||
var id uint64
|
var id uint64
|
||||||
if id, err = b.NextSequence(); err != nil {
|
if id, err = b.NextSequence(); err != nil {
|
||||||
return fmt.Errorf("unable to generate an ID for the card, %w", err)
|
return fmt.Errorf("unable to generate an ID for the card, %w", err)
|
||||||
}
|
}
|
||||||
card.ID = int(id)
|
item.UpdateId(int(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
encoder := gob.NewEncoder(buf)
|
encoder := gob.NewEncoder(buf)
|
||||||
if err = encoder.Encode(card); err != nil {
|
if err = encoder.Encode(item); err != nil {
|
||||||
return fmt.Errorf("unable to encode data, %w", err)
|
return fmt.Errorf("unable to encode data, %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = b.Put([]byte(strconv.Itoa(card.ID)), buf.Bytes()); err != nil {
|
if err = b.Put([]byte(strconv.Itoa(item.Id())), buf.Bytes()); err != nil {
|
||||||
return fmt.Errorf("unable to write the card to the bucket, %w", err)
|
return fmt.Errorf("unable to write the card to the bucket, %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return card.ID, fmt.Errorf("error while saving the card to the database, %w", err)
|
return 0, fmt.Errorf("error while saving the card to the database, %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return card.ID, nil
|
return item.Id(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadCard reads a card from the cards bucket in BoltDB.
|
// Read retrieves a Bolt item from a specified bucket and returns the data in bytes.
|
||||||
func ReadCard(db *bolt.DB, id int) (card.Card, error) {
|
func Read(db *bolt.DB, bucketName string, id int) ([]byte, error) {
|
||||||
var card card.Card
|
bucket := []byte(bucketName)
|
||||||
|
|
||||||
bucket := []byte(cardBucket)
|
var data []byte
|
||||||
|
|
||||||
err := db.View(func(tx *bolt.Tx) error {
|
if err := db.View(func(tx *bolt.Tx) error {
|
||||||
b := tx.Bucket(bucket)
|
b := tx.Bucket(bucket)
|
||||||
|
|
||||||
if b == nil {
|
if b == nil {
|
||||||
return bucketNotExistError{bucket: string(bucket)}
|
return bucketNotExistError{bucket: string(bucket)}
|
||||||
}
|
}
|
||||||
|
|
||||||
buf := bytes.NewBuffer(b.Get([]byte(strconv.Itoa(id))))
|
data = b.Get([]byte(strconv.Itoa(id)))
|
||||||
|
|
||||||
decoder := gob.NewDecoder(buf)
|
|
||||||
if err := decoder.Decode(&card); err != nil {
|
|
||||||
return fmt.Errorf("unable to decode card data, %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
}); err != nil {
|
||||||
if err != nil {
|
return []byte{}, fmt.Errorf("error while reading the Bolt item from the database, %w", err)
|
||||||
return card, fmt.Errorf("error while loading the card from the database, %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return card, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// dbPath returns the path to the database file. If a path is given then that is returned. Otherwise the default path is returned.
|
// dbPath returns the path to the database file. If a path is given then that is returned. Otherwise the default path is returned.
|
||||||
|
@ -246,7 +238,7 @@ func mkDataDir(path string) error {
|
||||||
|
|
||||||
// ensureBuckets ensures that the required buckets are created in the database.
|
// ensureBuckets ensures that the required buckets are created in the database.
|
||||||
func ensureBuckets(db *bolt.DB) error {
|
func ensureBuckets(db *bolt.DB) error {
|
||||||
buckets := []string{statusBucket, cardBucket}
|
buckets := []string{StatusBucket, CardBucket}
|
||||||
|
|
||||||
err := db.Update(func(tx *bolt.Tx) error {
|
err := db.Update(func(tx *bolt.Tx) error {
|
||||||
for _, v := range buckets {
|
for _, v := range buckets {
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
package database_test
|
package database_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/gob"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"forge.dananglin.me.uk/code/dananglin/pelican/internal/card"
|
"forge.dananglin.me.uk/code/dananglin/pelican/internal/board"
|
||||||
"forge.dananglin.me.uk/code/dananglin/pelican/internal/database"
|
"forge.dananglin.me.uk/code/dananglin/pelican/internal/database"
|
||||||
"forge.dananglin.me.uk/code/dananglin/pelican/internal/status"
|
|
||||||
bolt "go.etcd.io/bbolt"
|
bolt "go.etcd.io/bbolt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -78,7 +79,7 @@ func TestWriteAndReadStatusList(t *testing.T) {
|
||||||
func testWriteStatusList(t *testing.T, db *bolt.DB) {
|
func testWriteStatusList(t *testing.T, db *bolt.DB) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
newStatusList := []status.Status{
|
newStatusList := []board.Status{
|
||||||
{
|
{
|
||||||
ID: -1,
|
ID: -1,
|
||||||
Name: "Backlog",
|
Name: "Backlog",
|
||||||
|
@ -105,7 +106,13 @@ func testWriteStatusList(t *testing.T, db *bolt.DB) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := database.WriteStatusList(db, newStatusList); err != nil {
|
boltItems := make([]database.BoltItem, len(newStatusList))
|
||||||
|
|
||||||
|
for i := range newStatusList {
|
||||||
|
boltItems[i] = &newStatusList[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := database.WriteMany(db, database.StatusBucket, boltItems); err != nil {
|
||||||
t.Fatalf("An error occurred whilst writing the initial status list to the database, %s", err)
|
t.Fatalf("An error occurred whilst writing the initial status list to the database, %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -113,12 +120,28 @@ func testWriteStatusList(t *testing.T, db *bolt.DB) {
|
||||||
func testReadStatusList(t *testing.T, db *bolt.DB) {
|
func testReadStatusList(t *testing.T, db *bolt.DB) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
got, err := database.ReadStatusList(db)
|
data, err := database.ReadAll(db, database.StatusBucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("An error occurred whilst reading the modified status list from the database, %s", err)
|
t.Fatalf("An error occurred whilst reading the modified status list from the database, %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
want := []status.Status{
|
got := make([]board.Status, len(data))
|
||||||
|
|
||||||
|
for i, d := range data {
|
||||||
|
buf := bytes.NewBuffer(d)
|
||||||
|
|
||||||
|
decoder := gob.NewDecoder(buf)
|
||||||
|
|
||||||
|
var s board.Status
|
||||||
|
|
||||||
|
if err := decoder.Decode(&s); err != nil {
|
||||||
|
t.Fatalf("An error occurred whilst decoding data, %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got[i] = s
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []board.Status{
|
||||||
{
|
{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
Name: "Backlog",
|
Name: "Backlog",
|
||||||
|
@ -182,13 +205,13 @@ func TestReadAndWriteCard(t *testing.T) {
|
||||||
func testWriteCard(t *testing.T, db *bolt.DB) int {
|
func testWriteCard(t *testing.T, db *bolt.DB) int {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
newCard := card.Card{
|
newCard := board.Card{
|
||||||
ID: -1,
|
ID: -1,
|
||||||
Title: "A test task.",
|
Title: "A test task.",
|
||||||
Content: "This task should be completed.",
|
Content: "This task should be completed.",
|
||||||
}
|
}
|
||||||
|
|
||||||
cardID, err := database.WriteCard(db, newCard)
|
cardID, err := database.Write(db, database.CardBucket, &newCard)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("An error occurred whilst writing the card to the database, %s", err)
|
t.Fatalf("An error occurred whilst writing the card to the database, %s", err)
|
||||||
}
|
}
|
||||||
|
@ -199,12 +222,22 @@ func testWriteCard(t *testing.T, db *bolt.DB) int {
|
||||||
func testReadCard(t *testing.T, db *bolt.DB, cardID int) {
|
func testReadCard(t *testing.T, db *bolt.DB, cardID int) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
got, err := database.ReadCard(db, cardID)
|
data, err := database.Read(db, database.CardBucket, cardID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("An error occurred whilst loading the modified from the database, %s", err)
|
t.Fatalf("An error occurred whilst loading the modified from the database, %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
want := card.Card{
|
var got board.Card
|
||||||
|
|
||||||
|
buf := bytes.NewBuffer(data)
|
||||||
|
|
||||||
|
decoder := gob.NewDecoder(buf)
|
||||||
|
|
||||||
|
if err := decoder.Decode(&got); err != nil {
|
||||||
|
t.Fatalf("Unable to decode data, %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := board.Card{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
Title: "A test task.",
|
Title: "A test task.",
|
||||||
Content: "This task should be completed.",
|
Content: "This task should be completed.",
|
||||||
|
|
Loading…
Reference in a new issue