local id
This commit is contained in:
parent
d57a0a8162
commit
ebe2c401ef
|
@ -1,6 +1,8 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.ewintr.nl/gte/cmd/cli/format"
|
||||
"git.ewintr.nl/gte/internal/configuration"
|
||||
"git.ewintr.nl/gte/internal/process"
|
||||
|
@ -21,8 +23,22 @@ func NewDone(conf *configuration.Configuration, cmdArgs []string) (*Done, error)
|
|||
|
||||
disp := storage.NewDispatcher(msend.NewSSLSMTP(conf.SMTP()))
|
||||
fields := process.UpdateFields{"done": "true"}
|
||||
localIds, err := local.LocalIds()
|
||||
if err != nil {
|
||||
return &Done{}, err
|
||||
}
|
||||
var tId string
|
||||
for id, localId := range localIds {
|
||||
if fmt.Sprintf("%d", localId) == cmdArgs[0] {
|
||||
tId = id
|
||||
break
|
||||
}
|
||||
}
|
||||
if tId == "" {
|
||||
return &Done{}, fmt.Errorf("could not find task")
|
||||
}
|
||||
|
||||
updater := process.NewUpdate(local, disp, cmdArgs[0], fields)
|
||||
updater := process.NewUpdate(local, disp, tId, fields)
|
||||
|
||||
return &Done{
|
||||
doner: updater,
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
|
||||
// Today lists all task that are due today or past their due date
|
||||
type Today struct {
|
||||
local storage.LocalRepository
|
||||
todayer *process.List
|
||||
}
|
||||
|
||||
|
@ -25,6 +26,7 @@ func NewToday(conf *configuration.Configuration) (*Today, error) {
|
|||
todayer := process.NewList(local, reqs)
|
||||
|
||||
return &Today{
|
||||
local: local,
|
||||
todayer: todayer,
|
||||
}, nil
|
||||
}
|
||||
|
@ -38,5 +40,5 @@ func (t *Today) Do() string {
|
|||
return "nothing left\n"
|
||||
}
|
||||
|
||||
return format.FormatTaskTable(res.Tasks)
|
||||
return format.FormatTaskTable(t.local, res.Tasks)
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
|
||||
// Tomorrow lists all tasks that are due tomorrow
|
||||
type Tomorrow struct {
|
||||
local storage.LocalRepository
|
||||
tomorrower *process.List
|
||||
}
|
||||
|
||||
|
@ -25,6 +26,7 @@ func NewTomorrow(conf *configuration.Configuration) (*Tomorrow, error) {
|
|||
tomorrower := process.NewList(local, reqs)
|
||||
|
||||
return &Tomorrow{
|
||||
local: local,
|
||||
tomorrower: tomorrower,
|
||||
}, nil
|
||||
}
|
||||
|
@ -39,5 +41,5 @@ func (t *Tomorrow) Do() string {
|
|||
return "nothing to do tomorrow\n"
|
||||
}
|
||||
|
||||
return format.FormatTaskTable(res.Tasks)
|
||||
return format.FormatTaskTable(t.local, res.Tasks)
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package format
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
"git.ewintr.nl/gte/internal/storage"
|
||||
"git.ewintr.nl/gte/internal/task"
|
||||
)
|
||||
|
||||
|
@ -10,10 +11,19 @@ func FormatError(err error) string {
|
|||
return fmt.Sprintf("could not perform command.\n\nerror: %s\n", err.Error())
|
||||
}
|
||||
|
||||
func FormatTaskTable(tasks []*task.Task) string {
|
||||
func FormatTaskTable(local storage.LocalRepository, tasks []*task.Task) string {
|
||||
if len(tasks) == 0 {
|
||||
return "no tasks to display\n"
|
||||
}
|
||||
|
||||
localIds, err := local.LocalIds()
|
||||
if err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
|
||||
var output string
|
||||
for _, t := range tasks {
|
||||
output += fmt.Sprintf("%s\t%s\t%s\n", t.Id, t.Due.String(), t.Action)
|
||||
output += fmt.Sprintf("%d\t%s\t%s\n", localIds[t.Id], t.Due.String(), t.Action)
|
||||
}
|
||||
|
||||
return output
|
||||
|
|
|
@ -17,4 +17,41 @@ type LocalRepository interface {
|
|||
FindAllInFolder(folder string) ([]*task.Task, error)
|
||||
FindAllInProject(project string) ([]*task.Task, error)
|
||||
FindById(id string) (*task.Task, error)
|
||||
FindByLocalId(id int) (*task.Task, error)
|
||||
LocalIds() (map[string]int, error)
|
||||
}
|
||||
|
||||
func NextLocalId(used []int) int {
|
||||
if len(used) == 0 {
|
||||
return 1
|
||||
}
|
||||
|
||||
usedMax := 1
|
||||
for _, u := range used {
|
||||
if u > usedMax {
|
||||
usedMax = u
|
||||
}
|
||||
}
|
||||
|
||||
var limit int
|
||||
for limit = 1; limit <= len(used) || limit < usedMax; limit *= 10 {
|
||||
}
|
||||
|
||||
newId := used[len(used)-1] + 1
|
||||
if newId < limit {
|
||||
return newId
|
||||
}
|
||||
|
||||
usedMap := map[int]bool{}
|
||||
for _, u := range used {
|
||||
usedMap[u] = true
|
||||
}
|
||||
|
||||
for i := 1; i < limit; i++ {
|
||||
if _, ok := usedMap[i]; !ok {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
return limit
|
||||
}
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
package storage_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.ewintr.nl/go-kit/test"
|
||||
"git.ewintr.nl/gte/internal/storage"
|
||||
)
|
||||
|
||||
func TestNextLocalId(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
used []int
|
||||
exp int
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
used: []int{},
|
||||
exp: 1,
|
||||
},
|
||||
{
|
||||
name: "not empty",
|
||||
used: []int{5},
|
||||
exp: 6,
|
||||
},
|
||||
{
|
||||
name: "multiple",
|
||||
used: []int{2, 3, 4},
|
||||
exp: 5,
|
||||
},
|
||||
{
|
||||
name: "holes",
|
||||
used: []int{1, 5, 8},
|
||||
exp: 9,
|
||||
},
|
||||
{
|
||||
name: "expand limit",
|
||||
used: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
|
||||
exp: 11,
|
||||
},
|
||||
{
|
||||
name: "wrap if possible",
|
||||
used: []int{8, 9},
|
||||
exp: 1,
|
||||
},
|
||||
{
|
||||
name: "find hole",
|
||||
used: []int{1, 2, 3, 4, 5, 7, 8, 9},
|
||||
exp: 6,
|
||||
},
|
||||
{
|
||||
name: "dont wrap if expanded before",
|
||||
used: []int{15, 16},
|
||||
exp: 17,
|
||||
},
|
||||
{
|
||||
name: "do wrap if expanded limit is reached",
|
||||
used: []int{99},
|
||||
exp: 1,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
test.Equals(t, tc.exp, storage.NextLocalId(tc.used))
|
||||
})
|
||||
}
|
||||
}
|
|
@ -10,11 +10,13 @@ import (
|
|||
type Memory struct {
|
||||
tasks []*task.Task
|
||||
latestSync time.Time
|
||||
localIds map[string]int
|
||||
}
|
||||
|
||||
func NewMemory() *Memory {
|
||||
return &Memory{
|
||||
tasks: []*task.Task{},
|
||||
localIds: map[string]int{},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -28,6 +30,7 @@ func (m *Memory) SetTasks(tasks []*task.Task) error {
|
|||
nt := *t
|
||||
nt.Message = nil
|
||||
nTasks = append(nTasks, &nt)
|
||||
m.setLocalId(t.Id)
|
||||
}
|
||||
m.tasks = nTasks
|
||||
m.latestSync = time.Now()
|
||||
|
@ -35,6 +38,16 @@ func (m *Memory) SetTasks(tasks []*task.Task) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (m *Memory) setLocalId(id string) {
|
||||
used := []int{}
|
||||
for _, id := range m.localIds {
|
||||
used = append(used, id)
|
||||
}
|
||||
|
||||
next := NextLocalId(used)
|
||||
m.localIds[id] = next
|
||||
}
|
||||
|
||||
func (m *Memory) FindAllInFolder(folder string) ([]*task.Task, error) {
|
||||
tasks := []*task.Task{}
|
||||
for _, t := range m.tasks {
|
||||
|
@ -67,3 +80,17 @@ func (m *Memory) FindById(id string) (*task.Task, error) {
|
|||
|
||||
return &task.Task{}, ErrTaskNotFound
|
||||
}
|
||||
|
||||
func (m *Memory) FindByLocalId(localId int) (*task.Task, error) {
|
||||
for _, t := range m.tasks {
|
||||
if m.localIds[t.Id] == localId {
|
||||
return t, nil
|
||||
}
|
||||
}
|
||||
|
||||
return &task.Task{}, ErrTaskNotFound
|
||||
}
|
||||
|
||||
func (m *Memory) LocalIds() (map[string]int, error) {
|
||||
return m.localIds, nil
|
||||
}
|
||||
|
|
|
@ -86,4 +86,25 @@ func TestMemory(t *testing.T) {
|
|||
test.OK(t, err)
|
||||
test.Equals(t, task2, act)
|
||||
})
|
||||
|
||||
t.Run("findbylocalid", func(t *testing.T) {
|
||||
mem := storage.NewMemory()
|
||||
test.OK(t, mem.SetTasks(tasks))
|
||||
act, err := mem.FindByLocalId(2)
|
||||
test.OK(t, err)
|
||||
test.Equals(t, task2, act)
|
||||
})
|
||||
|
||||
t.Run("localids", func(t *testing.T) {
|
||||
mem := storage.NewMemory()
|
||||
test.OK(t, mem.SetTasks(tasks))
|
||||
act, err := mem.LocalIds()
|
||||
test.OK(t, err)
|
||||
exp := map[string]int{
|
||||
"id-1": 1,
|
||||
"id-2": 2,
|
||||
"id-3": 3,
|
||||
}
|
||||
test.Equals(t, exp, act)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ var sqliteMigrations = []sqliteMigration{
|
|||
`CREATE TABLE task ("id" TEXT, "version" INTEGER, "folder" TEXT, "action" TEXT, "project" TEXT, "due" TEXT, "recur" TEXT)`,
|
||||
`CREATE TABLE system ("latest_sync" INTEGER)`,
|
||||
`INSERT INTO system (latest_sync) VALUES (0)`,
|
||||
`CREATE TABLE local_id ("id" TEXT, "local_id" INTEGER)`,
|
||||
}
|
||||
|
||||
var (
|
||||
|
@ -68,10 +69,12 @@ func (s *Sqlite) LatestSync() (time.Time, error) {
|
|||
}
|
||||
|
||||
func (s *Sqlite) SetTasks(tasks []*task.Task) error {
|
||||
// set tasks
|
||||
if _, err := s.db.Exec(`DELETE FROM task`); err != nil {
|
||||
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||
}
|
||||
|
||||
newIds := []string{}
|
||||
for _, t := range tasks {
|
||||
var recurStr string
|
||||
if t.Recur != nil {
|
||||
|
@ -86,8 +89,56 @@ VALUES
|
|||
if err != nil {
|
||||
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||
}
|
||||
|
||||
newIds = append(newIds, t.Id)
|
||||
}
|
||||
|
||||
// set local_ids
|
||||
oldIds := map[string]int{}
|
||||
rows, err := s.db.Query(`SELECT id, local_id FROM local_id`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var id string
|
||||
var local_id int
|
||||
if err := rows.Scan(&id, &local_id); err != nil {
|
||||
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||
}
|
||||
oldIds[id] = local_id
|
||||
}
|
||||
|
||||
usedLocalIds := []int{}
|
||||
newLocalIds := map[string]int{}
|
||||
for _, n := range newIds {
|
||||
if localId, ok := oldIds[n]; ok {
|
||||
newLocalIds[n] = localId
|
||||
usedLocalIds = append(usedLocalIds, localId)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
localId := NextLocalId(usedLocalIds)
|
||||
newLocalIds[n] = localId
|
||||
usedLocalIds = append(usedLocalIds, localId)
|
||||
}
|
||||
|
||||
if _, err := s.db.Exec(`DELETE FROM local_id`); err != nil {
|
||||
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||
}
|
||||
for id, localId := range newLocalIds {
|
||||
_, err := s.db.Exec(`
|
||||
INSERT INTO local_id
|
||||
(id, local_id)
|
||||
VALUES
|
||||
(?, ?)`, id, localId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||
}
|
||||
}
|
||||
|
||||
// update system
|
||||
if _, err := s.db.Exec(`
|
||||
UPDATE system
|
||||
SET latest_sync = ?`,
|
||||
|
@ -145,6 +196,41 @@ LIMIT 1`, id)
|
|||
}, nil
|
||||
}
|
||||
|
||||
func (s *Sqlite) FindByLocalId(localId int) (*task.Task, error) {
|
||||
var id string
|
||||
row := s.db.QueryRow(`SELECT id FROM local_id WHERE local_id = ?`, localId)
|
||||
if err := row.Scan(&id); err != nil {
|
||||
return &task.Task{}, fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||
}
|
||||
|
||||
t, err := s.FindById(id)
|
||||
if err != nil {
|
||||
return &task.Task{}, nil
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (s *Sqlite) LocalIds() (map[string]int, error) {
|
||||
rows, err := s.db.Query(`SELECT id, local_id FROM local_id`)
|
||||
if err != nil {
|
||||
return map[string]int{}, fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||
}
|
||||
|
||||
idMap := map[string]int{}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var id string
|
||||
var local_id int
|
||||
if err := rows.Scan(&id, &local_id); err != nil {
|
||||
return map[string]int{}, fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||
}
|
||||
idMap[id] = local_id
|
||||
}
|
||||
|
||||
return idMap, nil
|
||||
}
|
||||
|
||||
func tasksFromRows(rows *sql.Rows) ([]*task.Task, error) {
|
||||
tasks := []*task.Task{}
|
||||
|
||||
|
|
Loading…
Reference in New Issue