Implement TaskScope tag methods with read-modify-write (CRITICAL)
- GetTags: retrieve task tags as map[int]string - SetTags: replace all tags on a task - ClearTags: remove all tags - AddTag: read-modify-write, idempotent (no-op if tag exists) - RemoveTag: read-modify-write, idempotent (no-op if tag doesn't exist) - HasTag: check if task has a specific tag WARNING: Tag operations are NOT atomic. Concurrent modifications may cause data loss due to the read-modify-write pattern required by Kanboard's setTaskTags API. Comprehensive test coverage including idempotency tests.
This commit is contained in:
parent
371bdb8ba9
commit
c8eea853e5
3 changed files with 503 additions and 1 deletions
112
task_scope.go
112
task_scope.go
|
|
@ -51,3 +51,115 @@ func (t *TaskScope) MoveToProject(ctx context.Context, projectID int) error {
|
|||
func (t *TaskScope) Update(ctx context.Context, params *TaskUpdateParams) error {
|
||||
return t.client.UpdateTaskFromParams(ctx, t.taskID, params)
|
||||
}
|
||||
|
||||
// GetTags returns the tags assigned to this task as a map of tagID to tag name.
|
||||
func (t *TaskScope) GetTags(ctx context.Context) (map[int]string, error) {
|
||||
return t.client.GetTaskTags(ctx, t.taskID)
|
||||
}
|
||||
|
||||
// SetTags sets the tags for this task, replacing all existing tags.
|
||||
// Tags are specified by name. Non-existent tags will be auto-created.
|
||||
//
|
||||
// WARNING: This operation is not atomic. Concurrent tag modifications may cause data loss.
|
||||
func (t *TaskScope) SetTags(ctx context.Context, tags ...string) error {
|
||||
task, err := t.Get(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return t.client.SetTaskTags(ctx, int(task.ProjectID), t.taskID, tags)
|
||||
}
|
||||
|
||||
// ClearTags removes all tags from this task.
|
||||
//
|
||||
// WARNING: This operation is not atomic. Concurrent tag modifications may cause data loss.
|
||||
func (t *TaskScope) ClearTags(ctx context.Context) error {
|
||||
return t.SetTags(ctx)
|
||||
}
|
||||
|
||||
// AddTag adds a tag to this task using a read-modify-write pattern.
|
||||
// If the tag already exists on the task, this is a no-op (idempotent).
|
||||
//
|
||||
// WARNING: This operation is not atomic. Concurrent tag modifications may cause data loss.
|
||||
func (t *TaskScope) AddTag(ctx context.Context, tag string) error {
|
||||
// Get task info for project_id
|
||||
task, err := t.Get(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get current tags
|
||||
currentTags, err := t.client.GetTaskTags(ctx, t.taskID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if tag already exists (idempotent)
|
||||
for _, existingTag := range currentTags {
|
||||
if existingTag == tag {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Build new tag list
|
||||
tagNames := make([]string, 0, len(currentTags)+1)
|
||||
for _, name := range currentTags {
|
||||
tagNames = append(tagNames, name)
|
||||
}
|
||||
tagNames = append(tagNames, tag)
|
||||
|
||||
// Set updated tags
|
||||
return t.client.SetTaskTags(ctx, int(task.ProjectID), t.taskID, tagNames)
|
||||
}
|
||||
|
||||
// RemoveTag removes a tag from this task using a read-modify-write pattern.
|
||||
// If the tag doesn't exist on the task, this is a no-op (idempotent).
|
||||
//
|
||||
// WARNING: This operation is not atomic. Concurrent tag modifications may cause data loss.
|
||||
func (t *TaskScope) RemoveTag(ctx context.Context, tag string) error {
|
||||
// Get task info for project_id
|
||||
task, err := t.Get(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get current tags
|
||||
currentTags, err := t.client.GetTaskTags(ctx, t.taskID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Filter out the tag to remove
|
||||
tagNames := make([]string, 0, len(currentTags))
|
||||
found := false
|
||||
for _, name := range currentTags {
|
||||
if name == tag {
|
||||
found = true
|
||||
continue
|
||||
}
|
||||
tagNames = append(tagNames, name)
|
||||
}
|
||||
|
||||
// If tag wasn't found, nothing to do (idempotent)
|
||||
if !found {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set updated tags
|
||||
return t.client.SetTaskTags(ctx, int(task.ProjectID), t.taskID, tagNames)
|
||||
}
|
||||
|
||||
// HasTag checks if this task has a specific tag.
|
||||
func (t *TaskScope) HasTag(ctx context.Context, tag string) (bool, error) {
|
||||
tags, err := t.client.GetTaskTags(ctx, t.taskID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, name := range tags {
|
||||
if name == tag {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue