package board import ( "bytes" "encoding/gob" "fmt" "sort" "time" "codeflow.dananglin.me.uk/apollo/pelican/internal/db" bolt "go.etcd.io/bbolt" ) // Board is probably the heart of Pelican. 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 database. 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 } // StatusArgs is an argument type for creating or updating statuses. 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{ Identity: Identity{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.normaliseStatusesPositionValuesFromDatabase(); err != nil { return fmt.Errorf("unable to normalise the statuses position values; %w", err) } return nil } // RepositionStatus re-positions a Status value on a slice of Statuses. func (b *Board) RepositionStatus(currentIndex, targetIndex int) error { statuses, err := b.StatusList() if err != nil { return fmt.Errorf("unable to get the list of statuses; %w", err) } statuses = shuffle(statuses, currentIndex, targetIndex) if err := b.normaliseStatusesPositionValues(statuses); err != nil { return fmt.Errorf("unable to normalise the statuses position values; %w", err) } return nil } // normaliseStatusesPositionValuesFromDatabase retrieves the ordered list of statuses from the database and sets // each status' positional value based on its position in the list before saving the updates to the database. func (b *Board) normaliseStatusesPositionValuesFromDatabase() error { statuses, err := b.StatusList() if err != nil { return fmt.Errorf("unable to get the list of statuses; %w", err) } return b.normaliseStatusesPositionValues(statuses) } // normaliseStatusesPositionValues takes a list of statuses and sets // each status' positional value based on its position in the list before // saving the updates to the database. func (b *Board) normaliseStatusesPositionValues(statuses []Status) error { 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 } // MoveToStatusArgs is an argument type for moving a card between statuses. 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{¤tStatus, &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 } // CardArgs is an argument type for creating or updating cards. 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{ Identity: Identity{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 } // UpdateCardArgs is an argument type for updating a card. 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 } // DeleteCardArgs is an argument type for deleting a card. 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 }