feat(tasks): add GetTaskByProjectAndIndex resolver

This commit is contained in:
kolaente 2026-04-11 01:33:24 +02:00 committed by kolaente
parent ced7ebd97f
commit 8f9b50bdcb
3 changed files with 102 additions and 2 deletions

View File

@ -99,7 +99,7 @@ type Task struct {
// The task identifier, based on the project identifier and the task's index
Identifier string `xorm:"-" json:"identifier"`
// The task index, calculated per project
Index int64 `xorm:"bigint not null default 0" json:"index"`
Index int64 `xorm:"bigint not null default 0" json:"index" param:"index"`
// The UID is currently not used for anything other than CalDAV, which is why we don't expose it over json
UID string `xorm:"varchar(250) null" json:"-"`
@ -353,6 +353,41 @@ func GetTaskByIDSimple(s *xorm.Session, taskID int64) (task Task, err error) {
return GetTaskSimple(s, &Task{ID: taskID})
}
// GetTaskByProjectAndIndex returns a task by its per-project index.
// Returns ErrTaskDoesNotExist if nothing matches.
func GetTaskByProjectAndIndex(s *xorm.Session, projectID, index int64) (task Task, err error) {
if projectID < 1 || index < 1 {
return Task{}, ErrTaskDoesNotExist{}
}
has, err := s.
Where("project_id = ? AND `index` = ?", projectID, index).
Get(&task)
if err != nil {
return Task{}, err
}
if !has {
return Task{}, ErrTaskDoesNotExist{}
}
return task, nil
}
// resolveIDFromProjectAndIndex populates t.ID from (ProjectID, Index) for the
// by-index route, which binds project+index from the URL but not id. No-op
// when id is already set.
func (t *Task) resolveIDFromProjectAndIndex(s *xorm.Session) error {
if t.ID != 0 || t.ProjectID < 1 || t.Index < 1 {
return nil
}
resolved, err := GetTaskByProjectAndIndex(s, t.ProjectID, t.Index)
if err != nil {
return err
}
t.ID = resolved.ID
return nil
}
// GetTaskSimple returns a raw task without extra data
func GetTaskSimple(s *xorm.Session, t *Task) (task Task, err error) {
task = *t
@ -1933,6 +1968,9 @@ func (t *Task) ReadOne(s *xorm.Session, a web.Auth) (err error) {
t.Expand = append(t.Expand, t.ExpandArr...)
expand := t.Expand
if err = t.resolveIDFromProjectAndIndex(s); err != nil {
return
}
*t, err = GetTaskByIDSimple(s, t.ID)
if err != nil {
return

View File

@ -42,7 +42,9 @@ func (t *Task) CanCreate(s *xorm.Session, a web.Auth) (bool, error) {
func (t *Task) CanRead(s *xorm.Session, a web.Auth) (canRead bool, maxPermission int, err error) {
t.Expand = append(t.Expand, t.ExpandArr...)
expand := t.Expand
// Get the task, error out if it doesn't exist
if err = t.resolveIDFromProjectAndIndex(s); err != nil {
return
}
*t, err = GetTaskByIDSimple(s, t.ID)
if err != nil {
return

View File

@ -1303,3 +1303,63 @@ func TestGetTasksByUIDs(t *testing.T) {
assert.Equal(t, int64(1), tasks[0].ID, "only user 1's task should be returned")
})
}
func TestGetTaskByProjectAndIndex(t *testing.T) {
t.Run("existing task", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
task, err := GetTaskByProjectAndIndex(s, 1, 1)
require.NoError(t, err)
assert.Equal(t, int64(1), task.ID)
assert.Equal(t, "task #1", task.Title)
})
t.Run("nonexistent index", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
_, err := GetTaskByProjectAndIndex(s, 1, 99999)
require.Error(t, err)
assert.True(t, IsErrTaskDoesNotExist(err))
})
t.Run("wrong project", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
// Project 4 has no tasks at all.
_, err := GetTaskByProjectAndIndex(s, 4, 1)
require.Error(t, err)
assert.True(t, IsErrTaskDoesNotExist(err))
})
t.Run("index exists only in another project", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
// Project 2 has indexes 1 and 2; index 5 lives under project 1 (task 5).
// A non-scoped WHERE clause would leak task 5 here.
_, err := GetTaskByProjectAndIndex(s, 2, 5)
require.Error(t, err)
assert.True(t, IsErrTaskDoesNotExist(err))
})
t.Run("invalid input", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
_, err := GetTaskByProjectAndIndex(s, 0, 1)
require.Error(t, err)
assert.True(t, IsErrTaskDoesNotExist(err))
_, err = GetTaskByProjectAndIndex(s, 1, 0)
require.Error(t, err)
assert.True(t, IsErrTaskDoesNotExist(err))
})
}