Compare commits

...

3 Commits

9 changed files with 116 additions and 23 deletions

View File

@ -29,6 +29,7 @@ func NewAdd(localRepo storage.LocalID, eventRepo storage.Event, syncRepo storage
FlagFor: &FlagDuration{},
FlagRecStart: &FlagDate{},
FlagRecPeriod: &FlagPeriod{},
FlagRecCount: &FlagInt{},
},
},
}
@ -70,8 +71,14 @@ func (add *Add) Execute(main []string, flags map[string]string) error {
return fmt.Errorf("could not set duration to 24 hours")
}
}
if as.IsSet(FlagRecStart) != as.IsSet(FlagRecPeriod) {
return fmt.Errorf("rec-start required rec-period and vice versa")
var recCount int
for _, f := range []string{FlagRecStart, FlagRecPeriod, FlagRecCount} {
if as.IsSet(f) {
recCount++
}
}
if recCount != 0 && recCount != 3 {
return fmt.Errorf("rec-start, rec-period and rec-count must either all be present, or none of them")
}
return add.do()
@ -102,6 +109,7 @@ func (add *Add) do() error {
e.Recurrer = &item.Recur{
Start: as.GetTime(FlagRecStart),
Period: as.GetRecurPeriod(FlagRecPeriod),
Count: as.GetInt(FlagRecCount),
}
}

View File

@ -121,18 +121,20 @@ func TestAdd(t *testing.T) {
expErr: true,
},
{
name: "rec-start with rec-period",
name: "rec-start with rec-period and rec-count",
main: []string{"add", "title"},
flags: map[string]string{
command.FlagOn: aDateStr,
command.FlagRecStart: "2024-12-08",
command.FlagRecPeriod: "day",
command.FlagRecCount: "1",
},
expEvent: item.Event{
ID: "title",
Recurrer: &item.Recur{
Start: time.Date(2024, 12, 8, 0, 0, 0, 0, time.UTC),
Period: item.PeriodDay,
Count: 1,
},
RecurNext: time.Time{},
EventBody: item.EventBody{

View File

@ -75,3 +75,15 @@ func (as *ArgSet) GetRecurPeriod(name string) item.RecurPeriod {
}
return val
}
func (as *ArgSet) GetInt(name string) int {
flag, ok := as.Flags[name]
if !ok {
return 0
}
val, ok := flag.Get().(int)
if !ok {
return 0
}
return val
}

View File

@ -13,6 +13,7 @@ const (
FlagFor = "for"
FlagRecStart = "rec-start"
FlagRecPeriod = "rec-period"
FlagRecCount = "rec-count"
)
type Command interface {

View File

@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"slices"
"strconv"
"time"
"go-mod.ewintr.nl/planner/item"
@ -131,3 +132,26 @@ func (fp *FlagPeriod) IsSet() bool {
func (fp *FlagPeriod) Get() any {
return fp.Value
}
type FlagInt struct {
Name string
Value int
}
func (fi *FlagInt) Set(val string) error {
i, err := strconv.Atoi(val)
if err != nil {
return fmt.Errorf("not a valid integer: %v", val)
}
fi.Value = i
return nil
}
func (fi *FlagInt) IsSet() bool {
return fi.Value != 0
}
func (fi *FlagInt) Get() any {
return fi.Value
}

View File

@ -34,6 +34,7 @@ var migrations = []string{
`ALTER TABLE events DROP COLUMN recur_start`,
`ALTER TABLE events DROP COLUMN recur_next`,
`ALTER TABLE events ADD COLUMN recur TEXT`,
`ALTER TABLE items ADD COLUMN recurrer TEXT`,
}
var (

View File

@ -2,6 +2,7 @@ package sqlite
import (
"database/sql"
"encoding/json"
"fmt"
"time"
@ -17,7 +18,7 @@ func NewSqliteSync(db *sql.DB) *SqliteSync {
}
func (s *SqliteSync) FindAll() ([]item.Item, error) {
rows, err := s.db.Query("SELECT id, kind, updated, deleted, body FROM items")
rows, err := s.db.Query("SELECT id, kind, updated, deleted, recurrer, body FROM items")
if err != nil {
return nil, fmt.Errorf("%w: failed to query items: %v", ErrSqliteFailure, err)
}
@ -26,20 +27,27 @@ func (s *SqliteSync) FindAll() ([]item.Item, error) {
var items []item.Item
for rows.Next() {
var i item.Item
var updatedStr string
err := rows.Scan(&i.ID, &i.Kind, &updatedStr, &i.Deleted, &i.Body)
var updatedStr, recurStr string
err := rows.Scan(&i.ID, &i.Kind, &updatedStr, &i.Deleted, &recurStr, &i.Body)
if err != nil {
return nil, fmt.Errorf("%w: failed to scan item: %v", ErrSqliteFailure, err)
}
i.Updated, err = time.Parse(time.RFC3339, updatedStr)
if err != nil {
return nil, fmt.Errorf("%w: failed to parse updated time: %v", ErrSqliteFailure, err)
return nil, fmt.Errorf("failed to parse updated time: %v", err)
}
if recurStr != "" {
var recurrer item.Recur
if err := json.Unmarshal([]byte(recurStr), &recurrer); err != nil {
return nil, fmt.Errorf("failed to unmarshal recurrer: %v", err)
}
i.Recurrer = &recurrer
}
items = append(items, i)
}
if err = rows.Err(); err != nil {
return nil, fmt.Errorf("%w: error iterating over rows: %v", ErrSqliteFailure, err)
return nil, fmt.Errorf("error iterating over rows: %v", err)
}
return items, nil
@ -51,12 +59,22 @@ func (s *SqliteSync) Store(i item.Item) error {
i.Updated = time.Now()
}
var recurStr string
if i.Recurrer != nil {
recurBytes, err := json.Marshal(i.Recurrer)
if err != nil {
return fmt.Errorf("failed to marshal recurrer: %v", err)
}
recurStr = string(recurBytes)
}
_, err := s.db.Exec(
"INSERT OR REPLACE INTO items (id, kind, updated, deleted, body) VALUES (?, ?, ?, ?, ?)",
"INSERT OR REPLACE INTO items (id, kind, updated, deleted, recurrer, body) VALUES (?, ?, ?, ?, ?, ?)",
i.ID,
i.Kind,
i.Updated.UTC().Format(time.RFC3339),
i.Deleted,
recurStr,
sql.NullString{String: i.Body, Valid: i.Body != ""}, // This allows empty string but not NULL
)
if err != nil {

View File

@ -45,7 +45,8 @@ func (c *HTTP) Update(items []item.Item) error {
return fmt.Errorf("could not make request: %v", err)
}
if res.StatusCode != http.StatusNoContent {
return fmt.Errorf("server returned status %d", res.StatusCode)
body, _ := io.ReadAll(res.Body)
return fmt.Errorf("server returned status %d, body: %s", res.StatusCode, body)
}
return nil

View File

@ -2,6 +2,7 @@ package main
import (
"database/sql"
"encoding/json"
"errors"
"fmt"
"strings"
@ -57,8 +58,17 @@ func NewPostgres(host, port, dbname, user, password string) (*Postgres, error) {
return p, nil
}
func (p *Postgres) Update(item item.Item, ts time.Time) error {
_, err := p.db.Exec(`
func (p *Postgres) Update(i item.Item, ts time.Time) error {
var recurrerJSON []byte
var err error
if i.Recurrer != nil {
recurrerJSON, err = json.Marshal(i.Recurrer)
if err != nil {
return fmt.Errorf("%w: %v", ErrPostgresFailure, err)
}
}
_, err = p.db.Exec(`
INSERT INTO items (id, kind, updated, deleted, body, recurrer, recur_next)
VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT (id) DO UPDATE
@ -68,7 +78,7 @@ func (p *Postgres) Update(item item.Item, ts time.Time) error {
body = EXCLUDED.body,
recurrer = EXCLUDED.recurrer,
recur_next = EXCLUDED.recur_next`,
item.ID, item.Kind, ts, item.Deleted, item.Body, item.Recurrer, item.RecurNext)
i.ID, i.Kind, ts, i.Deleted, i.Body, recurrerJSON, i.RecurNext)
if err != nil {
return fmt.Errorf("%w: %v", ErrPostgresFailure, err)
}
@ -99,15 +109,23 @@ func (p *Postgres) Updated(ks []item.Kind, t time.Time) ([]item.Item, error) {
result := make([]item.Item, 0)
for rows.Next() {
var item item.Item
var i item.Item
var recurNext sql.NullTime
if err := rows.Scan(&item.ID, &item.Kind, &item.Updated, &item.Deleted, &item.Body, &item.Recurrer, &recurNext); err != nil {
var recurrerJSON []byte
if err := rows.Scan(&i.ID, &i.Kind, &i.Updated, &i.Deleted, &i.Body, &recurrerJSON, &recurNext); err != nil {
return nil, fmt.Errorf("%w: %v", ErrPostgresFailure, err)
}
if recurNext.Valid {
item.RecurNext = recurNext.Time
if len(recurrerJSON) > 0 {
var recurrer item.Recur
if err := json.Unmarshal(recurrerJSON, &recurrer); err != nil {
return nil, fmt.Errorf("%w: %v", ErrPostgresFailure, err)
}
i.Recurrer = &recurrer
}
result = append(result, item)
if recurNext.Valid {
i.RecurNext = recurNext.Time
}
result = append(result, i)
}
return result, nil
@ -127,15 +145,23 @@ func (p *Postgres) RecursBefore(date time.Time) ([]item.Item, error) {
result := make([]item.Item, 0)
for rows.Next() {
var item item.Item
var i item.Item
var recurNext sql.NullTime
if err := rows.Scan(&item.ID, &item.Kind, &item.Updated, &item.Deleted, &item.Body, &item.Recurrer, &recurNext); err != nil {
var recurrerJSON []byte
if err := rows.Scan(&i.ID, &i.Kind, &i.Updated, &i.Deleted, &i.Body, &recurrerJSON, &recurNext); err != nil {
return nil, fmt.Errorf("%w: %v", ErrPostgresFailure, err)
}
if recurNext.Valid {
item.RecurNext = recurNext.Time
if len(recurrerJSON) > 0 {
var recurrer item.Recur
if err := json.Unmarshal(recurrerJSON, &recurrer); err != nil {
return nil, fmt.Errorf("%w: %v", ErrPostgresFailure, err)
}
i.Recurrer = &recurrer
}
result = append(result, item)
if recurNext.Valid {
i.RecurNext = recurNext.Time
}
result = append(result, i)
}
return result, nil