Add Filter builder, Archive/Unarchive, WithRepeat, and GoDoc examples
- Implement client-side Filter builder with tag, status, due date, and search filters - Add unit tests for Filter with performance benchmark - Add Archive/Unarchive methods to ChecklistService - Add WithRepeat method to TaskBuilder for recurring tasks - Create GoDoc examples for all major functionality
This commit is contained in:
parent
45e6b6eb18
commit
cb30b178be
7 changed files with 793 additions and 10 deletions
|
|
@ -1,26 +1,26 @@
|
||||||
{"id":"checkvist-api-0l6","title":"Implement Checklist archive/unarchive","description":"Add P2 (nice-to-have) archive functionality to ChecklistService:\n- Archive(ctx, id) (*Checklist, error)\n- Unarchive(ctx, id) (*Checklist, error)\nUse PUT /checklists/{id}.json with archived field.","status":"open","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:56.538852525+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T12:30:56.538852525+01:00","dependencies":[{"issue_id":"checkvist-api-0l6","depends_on_id":"checkvist-api-c2k","type":"blocks","created_at":"2026-01-14T12:33:02.869871005+01:00","created_by":"Oliver Jakoubek"}]}
|
{"id":"checkvist-api-0l6","title":"Implement Checklist archive/unarchive","description":"Add P2 (nice-to-have) archive functionality to ChecklistService:\n- Archive(ctx, id) (*Checklist, error)\n- Unarchive(ctx, id) (*Checklist, error)\nUse PUT /checklists/{id}.json with archived field.","status":"closed","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:56.538852525+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:36:35.386071331+01:00","closed_at":"2026-01-14T14:36:35.386071331+01:00","close_reason":"Archive und Unarchive Methoden implementiert und getestet","dependencies":[{"issue_id":"checkvist-api-0l6","depends_on_id":"checkvist-api-c2k","type":"blocks","created_at":"2026-01-14T12:33:02.869871005+01:00","created_by":"Oliver Jakoubek"}]}
|
||||||
{"id":"checkvist-api-1e6","title":"Project Setup \u0026 Foundation","description":"Phase 1: Set up Go module structure, basic client, authentication, and error handling. This is the foundation for all other work.","status":"open","priority":0,"issue_type":"epic","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:05.916898362+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T12:31:05.916898362+01:00"}
|
{"id":"checkvist-api-1e6","title":"Project Setup \u0026 Foundation","description":"Phase 1: Set up Go module structure, basic client, authentication, and error handling. This is the foundation for all other work.","status":"closed","priority":0,"issue_type":"epic","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:05.916898362+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:29:26.984811878+01:00","closed_at":"2026-01-14T14:29:26.984811878+01:00","close_reason":"Alle zugehörigen Tasks abgeschlossen"}
|
||||||
{"id":"checkvist-api-1ki","title":"Write GoDoc examples","description":"Create example_test.go with runnable examples:\n- Example_basicUsage\n- ExampleNewClient\n- ExampleClient_Authenticate\n- ExampleChecklistService_List\n- ExampleTaskService_Create\n- ExampleNewTask (builder pattern)\n- ExampleNewFilter (filter builder)\n- ExampleFilter_Apply\nAll examples should be runnable and appear in GoDoc.","status":"open","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:38.443075101+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T12:31:38.443075101+01:00","dependencies":[{"issue_id":"checkvist-api-1ki","depends_on_id":"checkvist-api-c2k","type":"blocks","created_at":"2026-01-14T12:33:14.730667505+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-1ki","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:33:15.039931257+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-1ki","depends_on_id":"checkvist-api-1ze","type":"blocks","created_at":"2026-01-14T12:33:15.388830036+01:00","created_by":"Oliver Jakoubek"}]}
|
{"id":"checkvist-api-1ki","title":"Write GoDoc examples","description":"Create example_test.go with runnable examples:\n- Example_basicUsage\n- ExampleNewClient\n- ExampleClient_Authenticate\n- ExampleChecklistService_List\n- ExampleTaskService_Create\n- ExampleNewTask (builder pattern)\n- ExampleNewFilter (filter builder)\n- ExampleFilter_Apply\nAll examples should be runnable and appear in GoDoc.","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:38.443075101+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:35:43.090941735+01:00","closed_at":"2026-01-14T14:35:43.090941735+01:00","close_reason":"GoDoc examples für alle geforderten Funktionen implementiert","dependencies":[{"issue_id":"checkvist-api-1ki","depends_on_id":"checkvist-api-c2k","type":"blocks","created_at":"2026-01-14T12:33:14.730667505+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-1ki","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:33:15.039931257+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-1ki","depends_on_id":"checkvist-api-1ze","type":"blocks","created_at":"2026-01-14T12:33:15.388830036+01:00","created_by":"Oliver Jakoubek"}]}
|
||||||
{"id":"checkvist-api-1ze","title":"Implement client-side Filter builder","description":"Create filter.go with Filter builder (since Checkvist API has no server-side filtering):\n- NewFilter(tasks []Task) *Filter\n- WithTag(tag string) *Filter\n- WithTags(tags ...string) *Filter (AND logic)\n- WithStatus(status TaskStatus) *Filter\n- WithDueBefore(t time.Time) *Filter\n- WithDueAfter(t time.Time) *Filter\n- WithDueOn(t time.Time) *Filter\n- WithOverdue() *Filter\n- WithSearch(query string) *Filter (searches in Content)\n- Apply() []Task\nPerformance target: \u003c10ms for 1000+ tasks\nFilters are combined with AND logic.","status":"open","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:56.24379077+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T12:30:56.24379077+01:00","dependencies":[{"issue_id":"checkvist-api-1ze","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:33:02.543121262+01:00","created_by":"Oliver Jakoubek"}]}
|
{"id":"checkvist-api-1ze","title":"Implement client-side Filter builder","description":"Create filter.go with Filter builder (since Checkvist API has no server-side filtering):\n- NewFilter(tasks []Task) *Filter\n- WithTag(tag string) *Filter\n- WithTags(tags ...string) *Filter (AND logic)\n- WithStatus(status TaskStatus) *Filter\n- WithDueBefore(t time.Time) *Filter\n- WithDueAfter(t time.Time) *Filter\n- WithDueOn(t time.Time) *Filter\n- WithOverdue() *Filter\n- WithSearch(query string) *Filter (searches in Content)\n- Apply() []Task\nPerformance target: \u003c10ms for 1000+ tasks\nFilters are combined with AND logic.","status":"closed","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:56.24379077+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:33:38.687033358+01:00","closed_at":"2026-01-14T14:33:38.687033358+01:00","close_reason":"Filter builder mit allen geforderten Methoden implementiert","dependencies":[{"issue_id":"checkvist-api-1ze","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:33:02.543121262+01:00","created_by":"Oliver Jakoubek"}]}
|
||||||
{"id":"checkvist-api-1zf","title":"Write unit tests for Filter","description":"Create filter_test.go with tests:\n- TestFilter_WithTag\n- TestFilter_WithMultipleTags\n- TestFilter_WithStatus\n- TestFilter_WithDueBefore\n- TestFilter_WithDueAfter\n- TestFilter_WithDueOn\n- TestFilter_WithOverdue\n- TestFilter_WithSearch\n- TestFilter_Combined\n- TestFilter_Performance_1000Tasks\nUse table-driven tests.","status":"open","priority":1,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:38.136650557+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T12:31:38.136650557+01:00","dependencies":[{"issue_id":"checkvist-api-1zf","depends_on_id":"checkvist-api-1ze","type":"blocks","created_at":"2026-01-14T12:33:14.4378645+01:00","created_by":"Oliver Jakoubek"}]}
|
{"id":"checkvist-api-1zf","title":"Write unit tests for Filter","description":"Create filter_test.go with tests:\n- TestFilter_WithTag\n- TestFilter_WithMultipleTags\n- TestFilter_WithStatus\n- TestFilter_WithDueBefore\n- TestFilter_WithDueAfter\n- TestFilter_WithDueOn\n- TestFilter_WithOverdue\n- TestFilter_WithSearch\n- TestFilter_Combined\n- TestFilter_Performance_1000Tasks\nUse table-driven tests.","status":"closed","priority":1,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:38.136650557+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:34:50.361097261+01:00","closed_at":"2026-01-14T14:34:50.361097261+01:00","close_reason":"Alle geforderten Tests implementiert und bestanden","dependencies":[{"issue_id":"checkvist-api-1zf","depends_on_id":"checkvist-api-1ze","type":"blocks","created_at":"2026-01-14T12:33:14.4378645+01:00","created_by":"Oliver Jakoubek"}]}
|
||||||
{"id":"checkvist-api-347","title":"Write unit tests for Checklists","description":"Create checklists_test.go with tests:\n- TestChecklists_List\n- TestChecklists_ListArchived\n- TestChecklists_Get\n- TestChecklists_Get_NotFound\n- TestChecklists_Create\n- TestChecklists_Update\n- TestChecklists_Delete\nUse table-driven tests. Create testdata/checklists/ fixtures.","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:37.243525209+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:41:58.076714398+01:00","closed_at":"2026-01-14T13:41:58.076714398+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-347","depends_on_id":"checkvist-api-c2k","type":"blocks","created_at":"2026-01-14T12:33:13.512887479+01:00","created_by":"Oliver Jakoubek"}]}
|
{"id":"checkvist-api-347","title":"Write unit tests for Checklists","description":"Create checklists_test.go with tests:\n- TestChecklists_List\n- TestChecklists_ListArchived\n- TestChecklists_Get\n- TestChecklists_Get_NotFound\n- TestChecklists_Create\n- TestChecklists_Update\n- TestChecklists_Delete\nUse table-driven tests. Create testdata/checklists/ fixtures.","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:37.243525209+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:41:58.076714398+01:00","closed_at":"2026-01-14T13:41:58.076714398+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-347","depends_on_id":"checkvist-api-c2k","type":"blocks","created_at":"2026-01-14T12:33:13.512887479+01:00","created_by":"Oliver Jakoubek"}]}
|
||||||
{"id":"checkvist-api-47y","title":"Quality \u0026 Documentation","description":"Phase 4: Complete unit tests, GoDoc examples, README, and CHANGELOG. Target \u003e80% test coverage. All public functions documented with examples.","status":"open","priority":0,"issue_type":"epic","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:36.655509387+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T12:31:36.655509387+01:00"}
|
{"id":"checkvist-api-47y","title":"Quality \u0026 Documentation","description":"Phase 4: Complete unit tests, GoDoc examples, README, and CHANGELOG. Target \u003e80% test coverage. All public functions documented with examples.","status":"closed","priority":0,"issue_type":"epic","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:36.655509387+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:37:52.12671299+01:00","closed_at":"2026-01-14T14:37:52.12671299+01:00","close_reason":"Alle zugehörigen Features und Tasks abgeschlossen"}
|
||||||
{"id":"checkvist-api-5ab","title":"Implement Note operations","description":"Create notes.go with NoteService:\n- client.Notes(checklistID, taskID) returns NoteService\n- List(ctx) ([]Note, error) - GET /checklists/{id}/tasks/{task_id}/comments.json\n- Create(ctx, comment string) (*Note, error) - POST /checklists/{id}/tasks/{task_id}/comments.json\n- Update(ctx, noteID, comment string) (*Note, error) - PUT /checklists/{id}/tasks/{task_id}/comments/{note_id}.json\n- Delete(ctx, noteID) error - DELETE /checklists/{id}/tasks/{task_id}/comments/{note_id}.json\nContext support for all methods.","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:54.268124634+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:40:31.06124818+01:00","closed_at":"2026-01-14T13:40:31.06124818+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-5ab","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:32:55.843507717+01:00","created_by":"Oliver Jakoubek"}]}
|
{"id":"checkvist-api-5ab","title":"Implement Note operations","description":"Create notes.go with NoteService:\n- client.Notes(checklistID, taskID) returns NoteService\n- List(ctx) ([]Note, error) - GET /checklists/{id}/tasks/{task_id}/comments.json\n- Create(ctx, comment string) (*Note, error) - POST /checklists/{id}/tasks/{task_id}/comments.json\n- Update(ctx, noteID, comment string) (*Note, error) - PUT /checklists/{id}/tasks/{task_id}/comments/{note_id}.json\n- Delete(ctx, noteID) error - DELETE /checklists/{id}/tasks/{task_id}/comments/{note_id}.json\nContext support for all methods.","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:54.268124634+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:40:31.06124818+01:00","closed_at":"2026-01-14T13:40:31.06124818+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-5ab","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:32:55.843507717+01:00","created_by":"Oliver Jakoubek"}]}
|
||||||
{"id":"checkvist-api-5wr","title":"Initialize Go module and project structure","description":"Create go.mod with module path code.beautifulmachines.dev/jakoubek/checkvist-api. Set up flat package structure with placeholder files: client.go, checklists.go, tasks.go, notes.go, filter.go, models.go, errors.go, options.go. Create magefiles/ directory with separate go.mod for Mage build targets. Add LICENSE (MIT) file.","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:06.285510329+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T12:43:45.392753825+01:00","closed_at":"2026-01-14T12:43:45.392753825+01:00","close_reason":"Closed"}
|
{"id":"checkvist-api-5wr","title":"Initialize Go module and project structure","description":"Create go.mod with module path code.beautifulmachines.dev/jakoubek/checkvist-api. Set up flat package structure with placeholder files: client.go, checklists.go, tasks.go, notes.go, filter.go, models.go, errors.go, options.go. Create magefiles/ directory with separate go.mod for Mage build targets. Add LICENSE (MIT) file.","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:06.285510329+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T12:43:45.392753825+01:00","closed_at":"2026-01-14T12:43:45.392753825+01:00","close_reason":"Closed"}
|
||||||
{"id":"checkvist-api-8bn","title":"Write unit tests for Client and Auth","description":"Create client_test.go with tests using httptest.Server:\n- TestNewClient_Defaults\n- TestNewClient_WithOptions\n- TestAuthenticate_Success\n- TestAuthenticate_InvalidCredentials\n- TestAuthenticate_2FA\n- TestTokenRefresh_Auto\n- TestTokenRefresh_Manual\n- TestCurrentUser\n- TestRetryLogic_429\n- TestRetryLogic_5xx\n- TestRetryLogic_NetworkError\nUse table-driven tests. Create testdata/auth/ fixtures.","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:36.964610587+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:35:19.981723023+01:00","closed_at":"2026-01-14T13:35:19.981723023+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-8bn","depends_on_id":"checkvist-api-lpn","type":"blocks","created_at":"2026-01-14T12:33:12.783142853+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-8bn","depends_on_id":"checkvist-api-8u6","type":"blocks","created_at":"2026-01-14T12:33:13.232028837+01:00","created_by":"Oliver Jakoubek"}]}
|
{"id":"checkvist-api-8bn","title":"Write unit tests for Client and Auth","description":"Create client_test.go with tests using httptest.Server:\n- TestNewClient_Defaults\n- TestNewClient_WithOptions\n- TestAuthenticate_Success\n- TestAuthenticate_InvalidCredentials\n- TestAuthenticate_2FA\n- TestTokenRefresh_Auto\n- TestTokenRefresh_Manual\n- TestCurrentUser\n- TestRetryLogic_429\n- TestRetryLogic_5xx\n- TestRetryLogic_NetworkError\nUse table-driven tests. Create testdata/auth/ fixtures.","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:36.964610587+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:35:19.981723023+01:00","closed_at":"2026-01-14T13:35:19.981723023+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-8bn","depends_on_id":"checkvist-api-lpn","type":"blocks","created_at":"2026-01-14T12:33:12.783142853+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-8bn","depends_on_id":"checkvist-api-8u6","type":"blocks","created_at":"2026-01-14T12:33:13.232028837+01:00","created_by":"Oliver Jakoubek"}]}
|
||||||
{"id":"checkvist-api-8jh","title":"Implement repeating tasks configuration","description":"Add P2 (nice-to-have) repeat support to TaskBuilder:\n- WithRepeat(pattern string) *TaskBuilder\nSupport Checkvist smart syntax for repeats.\nDocument common patterns in GoDoc.","status":"open","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:56.826106108+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T12:30:56.826106108+01:00","dependencies":[{"issue_id":"checkvist-api-8jh","depends_on_id":"checkvist-api-tjk","type":"blocks","created_at":"2026-01-14T12:33:03.159849575+01:00","created_by":"Oliver Jakoubek"}]}
|
{"id":"checkvist-api-8jh","title":"Implement repeating tasks configuration","description":"Add P2 (nice-to-have) repeat support to TaskBuilder:\n- WithRepeat(pattern string) *TaskBuilder\nSupport Checkvist smart syntax for repeats.\nDocument common patterns in GoDoc.","status":"closed","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:56.826106108+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:37:33.200281826+01:00","closed_at":"2026-01-14T14:37:33.200281826+01:00","close_reason":"WithRepeat Methode zu TaskBuilder hinzugefügt mit GoDoc-Dokumentation","dependencies":[{"issue_id":"checkvist-api-8jh","depends_on_id":"checkvist-api-tjk","type":"blocks","created_at":"2026-01-14T12:33:03.159849575+01:00","created_by":"Oliver Jakoubek"}]}
|
||||||
{"id":"checkvist-api-8q3","title":"Set up Mage build targets","description":"Create magefiles/magefile.go with:\n- Test() - run go test -v ./...\n- Coverage() - run go test -coverprofile=coverage.out ./...\n- Lint() - run staticcheck ./...\n- Fmt() - run gofmt -w .\n- Check() - run all quality checks (fmt, vet, staticcheck, test)\nEnsure magefiles has its own go.mod importing magefile.org/mage","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:09.228450637+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:33:08.511791793+01:00","closed_at":"2026-01-14T13:33:08.511791793+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-8q3","depends_on_id":"checkvist-api-5wr","type":"blocks","created_at":"2026-01-14T12:32:48.556022687+01:00","created_by":"Oliver Jakoubek"}]}
|
{"id":"checkvist-api-8q3","title":"Set up Mage build targets","description":"Create magefiles/magefile.go with:\n- Test() - run go test -v ./...\n- Coverage() - run go test -coverprofile=coverage.out ./...\n- Lint() - run staticcheck ./...\n- Fmt() - run gofmt -w .\n- Check() - run all quality checks (fmt, vet, staticcheck, test)\nEnsure magefiles has its own go.mod importing magefile.org/mage","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:09.228450637+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:33:08.511791793+01:00","closed_at":"2026-01-14T13:33:08.511791793+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-8q3","depends_on_id":"checkvist-api-5wr","type":"blocks","created_at":"2026-01-14T12:32:48.556022687+01:00","created_by":"Oliver Jakoubek"}]}
|
||||||
{"id":"checkvist-api-8u6","title":"Implement HTTP request helper with retry logic","description":"Add internal HTTP helper to client.go:\n- doRequest(ctx, method, path, body) helper for all API calls\n- Automatic authentication check before requests\n- JSON marshaling/unmarshaling\n- Exponential backoff retry for:\n - HTTP 429 (Too Many Requests)\n - HTTP 5xx (Server Errors)\n - Network errors (timeout, connection reset)\n- Respect context cancellation\n- Optional debug logging of requests/responses via slog","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:08.780244392+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:27:52.914675409+01:00","closed_at":"2026-01-14T13:27:52.914675409+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-8u6","depends_on_id":"checkvist-api-ymg","type":"blocks","created_at":"2026-01-14T12:32:47.973194416+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-8u6","depends_on_id":"checkvist-api-mnh","type":"blocks","created_at":"2026-01-14T12:32:48.268500727+01:00","created_by":"Oliver Jakoubek"}]}
|
{"id":"checkvist-api-8u6","title":"Implement HTTP request helper with retry logic","description":"Add internal HTTP helper to client.go:\n- doRequest(ctx, method, path, body) helper for all API calls\n- Automatic authentication check before requests\n- JSON marshaling/unmarshaling\n- Exponential backoff retry for:\n - HTTP 429 (Too Many Requests)\n - HTTP 5xx (Server Errors)\n - Network errors (timeout, connection reset)\n- Respect context cancellation\n- Optional debug logging of requests/responses via slog","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:08.780244392+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:27:52.914675409+01:00","closed_at":"2026-01-14T13:27:52.914675409+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-8u6","depends_on_id":"checkvist-api-ymg","type":"blocks","created_at":"2026-01-14T12:32:47.973194416+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-8u6","depends_on_id":"checkvist-api-mnh","type":"blocks","created_at":"2026-01-14T12:32:48.268500727+01:00","created_by":"Oliver Jakoubek"}]}
|
||||||
{"id":"checkvist-api-93m","title":"Create CHANGELOG","description":"Create CHANGELOG.md following Keep a Changelog format:\n- [Unreleased] section for ongoing work\n- Initial release preparation notes\n- Document all features implemented","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:39.009748936+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:37:00.644317886+01:00","closed_at":"2026-01-14T13:37:00.644317886+01:00","close_reason":"Closed"}
|
{"id":"checkvist-api-93m","title":"Create CHANGELOG","description":"Create CHANGELOG.md following Keep a Changelog format:\n- [Unreleased] section for ongoing work\n- Initial release preparation notes\n- Document all features implemented","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:39.009748936+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:37:00.644317886+01:00","closed_at":"2026-01-14T13:37:00.644317886+01:00","close_reason":"Closed"}
|
||||||
{"id":"checkvist-api-bbx","title":"Write unit tests for Notes","description":"Create notes_test.go with tests:\n- TestNotes_List\n- TestNotes_Create\n- TestNotes_Update\n- TestNotes_Delete\nUse table-driven tests. Create testdata/notes/ fixtures.","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:37.829382141+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:47:17.141162679+01:00","closed_at":"2026-01-14T13:47:17.141162679+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-bbx","depends_on_id":"checkvist-api-5ab","type":"blocks","created_at":"2026-01-14T12:33:14.119755191+01:00","created_by":"Oliver Jakoubek"}]}
|
{"id":"checkvist-api-bbx","title":"Write unit tests for Notes","description":"Create notes_test.go with tests:\n- TestNotes_List\n- TestNotes_Create\n- TestNotes_Update\n- TestNotes_Delete\nUse table-driven tests. Create testdata/notes/ fixtures.","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:37.829382141+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:47:17.141162679+01:00","closed_at":"2026-01-14T13:47:17.141162679+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-bbx","depends_on_id":"checkvist-api-5ab","type":"blocks","created_at":"2026-01-14T12:33:14.119755191+01:00","created_by":"Oliver Jakoubek"}]}
|
||||||
{"id":"checkvist-api-br3","title":"Core API Operations","description":"Phase 2: Implement CRUD operations for Checklists, Tasks, and Notes. All P0 (must-have) features for the library.","status":"open","priority":0,"issue_type":"epic","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:53.20627925+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T12:30:53.20627925+01:00"}
|
{"id":"checkvist-api-br3","title":"Core API Operations","description":"Phase 2: Implement CRUD operations for Checklists, Tasks, and Notes. All P0 (must-have) features for the library.","status":"closed","priority":0,"issue_type":"epic","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:53.20627925+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:29:26.991139668+01:00","closed_at":"2026-01-14T14:29:26.991139668+01:00","close_reason":"Alle zugehörigen Tasks abgeschlossen"}
|
||||||
{"id":"checkvist-api-c2k","title":"Implement Checklist operations","description":"Create checklists.go with ChecklistService:\n- client.Checklists() returns ChecklistService\n- List(ctx) ([]Checklist, error) - GET /checklists.json\n- Get(ctx, id) (*Checklist, error) - GET /checklists/{id}.json\n- Create(ctx, name) (*Checklist, error) - POST /checklists.json\n- Update(ctx, id, name) (*Checklist, error) - PUT /checklists/{id}.json\n- Delete(ctx, id) error - DELETE /checklists/{id}.json\n- Support archived filter in List\nContext support for all methods.","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:53.566197933+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:38:06.162666425+01:00","closed_at":"2026-01-14T13:38:06.162666425+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-c2k","depends_on_id":"checkvist-api-8u6","type":"blocks","created_at":"2026-01-14T12:32:54.533462004+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-c2k","depends_on_id":"checkvist-api-lpn","type":"blocks","created_at":"2026-01-14T12:32:54.859645166+01:00","created_by":"Oliver Jakoubek"}]}
|
{"id":"checkvist-api-c2k","title":"Implement Checklist operations","description":"Create checklists.go with ChecklistService:\n- client.Checklists() returns ChecklistService\n- List(ctx) ([]Checklist, error) - GET /checklists.json\n- Get(ctx, id) (*Checklist, error) - GET /checklists/{id}.json\n- Create(ctx, name) (*Checklist, error) - POST /checklists.json\n- Update(ctx, id, name) (*Checklist, error) - PUT /checklists/{id}.json\n- Delete(ctx, id) error - DELETE /checklists/{id}.json\n- Support archived filter in List\nContext support for all methods.","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:53.566197933+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:38:06.162666425+01:00","closed_at":"2026-01-14T13:38:06.162666425+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-c2k","depends_on_id":"checkvist-api-8u6","type":"blocks","created_at":"2026-01-14T12:32:54.533462004+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-c2k","depends_on_id":"checkvist-api-lpn","type":"blocks","created_at":"2026-01-14T12:32:54.859645166+01:00","created_by":"Oliver Jakoubek"}]}
|
||||||
{"id":"checkvist-api-cb8","title":"Extended Features","description":"Phase 3: Implement P1 (should-have) features including client-side filtering and builder patterns for fluent interfaces.","status":"open","priority":1,"issue_type":"epic","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:55.624242123+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T12:30:55.624242123+01:00"}
|
{"id":"checkvist-api-cb8","title":"Extended Features","description":"Phase 3: Implement P1 (should-have) features including client-side filtering and builder patterns for fluent interfaces.","status":"closed","priority":1,"issue_type":"epic","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:55.624242123+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:37:52.141188241+01:00","closed_at":"2026-01-14T14:37:52.141188241+01:00","close_reason":"Alle zugehörigen Features und Tasks abgeschlossen"}
|
||||||
{"id":"checkvist-api-e9p","title":"Implement data models","description":"Create models.go with all data structures:\n- Checklist struct (ID, Name, Public, Archived, ReadOnly, TaskCount, TaskCompleted, Tags, TagsAsText, UpdatedAt)\n- Task struct (ID, ChecklistID, ParentID, Content, Status, Position, Priority, Tags, TagsAsText, DueDateRaw, DueDate, AssigneeIDs, CommentsCount, UpdateLine, UpdatedAt, CreatedAt, Children, Notes)\n- TaskStatus enum (StatusOpen=0, StatusClosed=1, StatusInvalidated=2)\n- Note struct (ID, TaskID, Comment, UpdatedAt, CreatedAt)\n- Tags type (map[string]bool)\n- User struct (ID, Username, Email)\n- DueDate struct with constructors (DueAt, DueString, DueInDays) and constants (DueToday, DueTomorrow, DueNextWeek, DueNextMonth)","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:06.900391036+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:22:22.273934664+01:00","closed_at":"2026-01-14T13:22:22.273934664+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-e9p","depends_on_id":"checkvist-api-5wr","type":"blocks","created_at":"2026-01-14T12:32:46.433908937+01:00","created_by":"Oliver Jakoubek"}]}
|
{"id":"checkvist-api-e9p","title":"Implement data models","description":"Create models.go with all data structures:\n- Checklist struct (ID, Name, Public, Archived, ReadOnly, TaskCount, TaskCompleted, Tags, TagsAsText, UpdatedAt)\n- Task struct (ID, ChecklistID, ParentID, Content, Status, Position, Priority, Tags, TagsAsText, DueDateRaw, DueDate, AssigneeIDs, CommentsCount, UpdateLine, UpdatedAt, CreatedAt, Children, Notes)\n- TaskStatus enum (StatusOpen=0, StatusClosed=1, StatusInvalidated=2)\n- Note struct (ID, TaskID, Comment, UpdatedAt, CreatedAt)\n- Tags type (map[string]bool)\n- User struct (ID, Username, Email)\n- DueDate struct with constructors (DueAt, DueString, DueInDays) and constants (DueToday, DueTomorrow, DueNextWeek, DueNextMonth)","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:06.900391036+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:22:22.273934664+01:00","closed_at":"2026-01-14T13:22:22.273934664+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-e9p","depends_on_id":"checkvist-api-5wr","type":"blocks","created_at":"2026-01-14T12:32:46.433908937+01:00","created_by":"Oliver Jakoubek"}]}
|
||||||
{"id":"checkvist-api-lpn","title":"Implement authentication with auto token renewal","description":"Add to client.go:\n- Authenticate(ctx context.Context) error - explicit login\n- refreshToken(ctx context.Context) error - token renewal\n- ensureAuthenticated(ctx context.Context) error - auto-auth before requests\n- CurrentUser(ctx context.Context) (*User, error) - get logged in user\n- Token management: store token and expiry, auto-refresh before expiry\n- Thread-safe token access using mutex\n- Support for optional 2FA token\nAPI endpoints:\n- POST /auth/login.json?version=2 (login)\n- POST /auth/refresh_token.json?version=2 (refresh)\n- GET /auth/curr_user.json (current user)\nToken sent via X-Client-Token header","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:08.358878117+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:26:32.668016856+01:00","closed_at":"2026-01-14T13:26:32.668016856+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-lpn","depends_on_id":"checkvist-api-ymg","type":"blocks","created_at":"2026-01-14T12:32:47.656124681+01:00","created_by":"Oliver Jakoubek"}]}
|
{"id":"checkvist-api-lpn","title":"Implement authentication with auto token renewal","description":"Add to client.go:\n- Authenticate(ctx context.Context) error - explicit login\n- refreshToken(ctx context.Context) error - token renewal\n- ensureAuthenticated(ctx context.Context) error - auto-auth before requests\n- CurrentUser(ctx context.Context) (*User, error) - get logged in user\n- Token management: store token and expiry, auto-refresh before expiry\n- Thread-safe token access using mutex\n- Support for optional 2FA token\nAPI endpoints:\n- POST /auth/login.json?version=2 (login)\n- POST /auth/refresh_token.json?version=2 (refresh)\n- GET /auth/curr_user.json (current user)\nToken sent via X-Client-Token header","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:08.358878117+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:26:32.668016856+01:00","closed_at":"2026-01-14T13:26:32.668016856+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-lpn","depends_on_id":"checkvist-api-ymg","type":"blocks","created_at":"2026-01-14T12:32:47.656124681+01:00","created_by":"Oliver Jakoubek"}]}
|
||||||
{"id":"checkvist-api-mnh","title":"Implement error types and sentinel errors","description":"Create errors.go with:\n- APIError struct (StatusCode, Message, RequestID, Err) with Error() and Unwrap() methods\n- Sentinel errors: ErrUnauthorized, ErrNotFound, ErrRateLimited, ErrBadRequest, ErrServerError\n- Helper function to create APIError from HTTP response","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:07.619359293+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:23:43.296883265+01:00","closed_at":"2026-01-14T13:23:43.296883265+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-mnh","depends_on_id":"checkvist-api-5wr","type":"blocks","created_at":"2026-01-14T12:32:46.754134799+01:00","created_by":"Oliver Jakoubek"}]}
|
{"id":"checkvist-api-mnh","title":"Implement error types and sentinel errors","description":"Create errors.go with:\n- APIError struct (StatusCode, Message, RequestID, Err) with Error() and Unwrap() methods\n- Sentinel errors: ErrUnauthorized, ErrNotFound, ErrRateLimited, ErrBadRequest, ErrServerError\n- Helper function to create APIError from HTTP response","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:07.619359293+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:23:43.296883265+01:00","closed_at":"2026-01-14T13:23:43.296883265+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-mnh","depends_on_id":"checkvist-api-5wr","type":"blocks","created_at":"2026-01-14T12:32:46.754134799+01:00","created_by":"Oliver Jakoubek"}]}
|
||||||
{"id":"checkvist-api-nrk","title":"Create README with quickstart","description":"Create README.md (in English) with:\n- Project description\n- Installation: go get code.beautifulmachines.dev/jakoubek/checkvist-api\n- Quick start example (init client, list checklists, create task)\n- API overview (Checklists, Tasks, Notes, Filters)\n- Builder pattern examples\n- Error handling examples\n- Configuration options\n- Link to GoDoc\n- License (MIT)","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:38.724338606+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:48:30.163762004+01:00","closed_at":"2026-01-14T13:48:30.163762004+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-nrk","depends_on_id":"checkvist-api-c2k","type":"blocks","created_at":"2026-01-14T12:33:15.785698203+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-nrk","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:33:16.125115134+01:00","created_by":"Oliver Jakoubek"}]}
|
{"id":"checkvist-api-nrk","title":"Create README with quickstart","description":"Create README.md (in English) with:\n- Project description\n- Installation: go get code.beautifulmachines.dev/jakoubek/checkvist-api\n- Quick start example (init client, list checklists, create task)\n- API overview (Checklists, Tasks, Notes, Filters)\n- Builder pattern examples\n- Error handling examples\n- Configuration options\n- Link to GoDoc\n- License (MIT)","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:38.724338606+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:48:30.163762004+01:00","closed_at":"2026-01-14T13:48:30.163762004+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-nrk","depends_on_id":"checkvist-api-c2k","type":"blocks","created_at":"2026-01-14T12:33:15.785698203+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-nrk","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:33:16.125115134+01:00","created_by":"Oliver Jakoubek"}]}
|
||||||
{"id":"checkvist-api-rl9","title":"Implement Task operations","description":"Create tasks.go with TaskService:\n- client.Tasks(checklistID) returns TaskService\n- List(ctx) ([]Task, error) - GET /checklists/{id}/tasks.json\n- Get(ctx, taskID) (*Task, error) - GET /checklists/{id}/tasks/{task_id}.json (includes parent hierarchy)\n- Create(ctx, builder *TaskBuilder) (*Task, error) - POST /checklists/{id}/tasks.json\n- Update(ctx, taskID, opts) (*Task, error) - PUT /checklists/{id}/tasks/{task_id}.json\n- Delete(ctx, taskID) error - DELETE /checklists/{id}/tasks/{task_id}.json\n- Close(ctx, taskID) (*Task, error) - POST /checklists/{id}/tasks/{task_id}/close.json\n- Reopen(ctx, taskID) (*Task, error) - POST /checklists/{id}/tasks/{task_id}/reopen.json\n- Invalidate(ctx, taskID) (*Task, error) - POST /checklists/{id}/tasks/{task_id}/invalidate.json\nParse DueDate from DueDateRaw when retrieving tasks.","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:53.90629838+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:39:29.443727155+01:00","closed_at":"2026-01-14T13:39:29.443727155+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-rl9","depends_on_id":"checkvist-api-8u6","type":"blocks","created_at":"2026-01-14T12:32:55.247292478+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-rl9","depends_on_id":"checkvist-api-lpn","type":"blocks","created_at":"2026-01-14T12:32:55.536931433+01:00","created_by":"Oliver Jakoubek"}]}
|
{"id":"checkvist-api-rl9","title":"Implement Task operations","description":"Create tasks.go with TaskService:\n- client.Tasks(checklistID) returns TaskService\n- List(ctx) ([]Task, error) - GET /checklists/{id}/tasks.json\n- Get(ctx, taskID) (*Task, error) - GET /checklists/{id}/tasks/{task_id}.json (includes parent hierarchy)\n- Create(ctx, builder *TaskBuilder) (*Task, error) - POST /checklists/{id}/tasks.json\n- Update(ctx, taskID, opts) (*Task, error) - PUT /checklists/{id}/tasks/{task_id}.json\n- Delete(ctx, taskID) error - DELETE /checklists/{id}/tasks/{task_id}.json\n- Close(ctx, taskID) (*Task, error) - POST /checklists/{id}/tasks/{task_id}/close.json\n- Reopen(ctx, taskID) (*Task, error) - POST /checklists/{id}/tasks/{task_id}/reopen.json\n- Invalidate(ctx, taskID) (*Task, error) - POST /checklists/{id}/tasks/{task_id}/invalidate.json\nParse DueDate from DueDateRaw when retrieving tasks.","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:53.90629838+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:39:29.443727155+01:00","closed_at":"2026-01-14T13:39:29.443727155+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-rl9","depends_on_id":"checkvist-api-8u6","type":"blocks","created_at":"2026-01-14T12:32:55.247292478+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-rl9","depends_on_id":"checkvist-api-lpn","type":"blocks","created_at":"2026-01-14T12:32:55.536931433+01:00","created_by":"Oliver Jakoubek"}]}
|
||||||
{"id":"checkvist-api-tjk","title":"Implement TaskBuilder fluent interface","description":"Enhance task creation with builder pattern:\n- NewTask(content string) *TaskBuilder\n- WithTags(tags ...string) *TaskBuilder\n- WithDueDate(due DueDate) *TaskBuilder\n- WithPriority(p int) *TaskBuilder\n- WithParent(parentID int64) *TaskBuilder\n- WithPosition(pos int) *TaskBuilder\n- Build() returns parameters for API call\nTaskBuilder should be chainable and return itself for fluent usage:\n checkvist.NewTask(\"Content\").WithTags(\"tag1\").WithDueDate(checkvist.DueTomorrow).WithPriority(1)","status":"open","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:55.929907579+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T12:30:55.929907579+01:00","dependencies":[{"issue_id":"checkvist-api-tjk","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:33:02.20339206+01:00","created_by":"Oliver Jakoubek"}]}
|
{"id":"checkvist-api-tjk","title":"Implement TaskBuilder fluent interface","description":"Enhance task creation with builder pattern:\n- NewTask(content string) *TaskBuilder\n- WithTags(tags ...string) *TaskBuilder\n- WithDueDate(due DueDate) *TaskBuilder\n- WithPriority(p int) *TaskBuilder\n- WithParent(parentID int64) *TaskBuilder\n- WithPosition(pos int) *TaskBuilder\n- Build() returns parameters for API call\nTaskBuilder should be chainable and return itself for fluent usage:\n checkvist.NewTask(\"Content\").WithTags(\"tag1\").WithDueDate(checkvist.DueTomorrow).WithPriority(1)","status":"closed","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:55.929907579+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:30:42.405125498+01:00","closed_at":"2026-01-14T14:30:42.405125498+01:00","close_reason":"TaskBuilder war bereits vollständig implementiert und getestet","dependencies":[{"issue_id":"checkvist-api-tjk","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:33:02.20339206+01:00","created_by":"Oliver Jakoubek"}]}
|
||||||
{"id":"checkvist-api-v2f","title":"Write unit tests for Tasks","description":"Create tasks_test.go with tests:\n- TestTasks_List\n- TestTasks_Get\n- TestTasks_Get_WithParentHierarchy\n- TestTasks_Create\n- TestTasks_Create_WithBuilder\n- TestTasks_Update\n- TestTasks_Delete\n- TestTasks_Close\n- TestTasks_Reopen\n- TestTasks_Invalidate\n- TestDueDate_Parsing\nUse table-driven tests. Create testdata/tasks/ fixtures.","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:37.538754679+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:46:01.50434559+01:00","closed_at":"2026-01-14T13:46:01.50434559+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-v2f","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:33:13.81085058+01:00","created_by":"Oliver Jakoubek"}]}
|
{"id":"checkvist-api-v2f","title":"Write unit tests for Tasks","description":"Create tasks_test.go with tests:\n- TestTasks_List\n- TestTasks_Get\n- TestTasks_Get_WithParentHierarchy\n- TestTasks_Create\n- TestTasks_Create_WithBuilder\n- TestTasks_Update\n- TestTasks_Delete\n- TestTasks_Close\n- TestTasks_Reopen\n- TestTasks_Invalidate\n- TestDueDate_Parsing\nUse table-driven tests. Create testdata/tasks/ fixtures.","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:37.538754679+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:46:01.50434559+01:00","closed_at":"2026-01-14T13:46:01.50434559+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-v2f","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:33:13.81085058+01:00","created_by":"Oliver Jakoubek"}]}
|
||||||
{"id":"checkvist-api-ymg","title":"Implement Client struct with functional options","description":"Create client.go with:\n- Client struct (baseURL, username, remoteKey, token, tokenExp, httpClient, retryConf, logger, mu sync.RWMutex)\n- NewClient(username, remoteKey string, opts ...Option) *Client constructor\n- Create options.go with Option type and functional options:\n - WithHTTPClient(*http.Client)\n - WithTimeout(time.Duration)\n - WithRetryConfig(RetryConfig)\n - WithLogger(*slog.Logger)\n - WithBaseURL(string) for testing\n- RetryConfig struct (MaxRetries, BaseDelay, MaxDelay, Jitter)\n- Default values: 3 retries, 1s base delay, 30s max delay, jitter enabled","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:08.021154076+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:24:55.101093793+01:00","closed_at":"2026-01-14T13:24:55.101093793+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-ymg","depends_on_id":"checkvist-api-e9p","type":"blocks","created_at":"2026-01-14T12:32:47.077541448+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-ymg","depends_on_id":"checkvist-api-mnh","type":"blocks","created_at":"2026-01-14T12:32:47.358639632+01:00","created_by":"Oliver Jakoubek"}]}
|
{"id":"checkvist-api-ymg","title":"Implement Client struct with functional options","description":"Create client.go with:\n- Client struct (baseURL, username, remoteKey, token, tokenExp, httpClient, retryConf, logger, mu sync.RWMutex)\n- NewClient(username, remoteKey string, opts ...Option) *Client constructor\n- Create options.go with Option type and functional options:\n - WithHTTPClient(*http.Client)\n - WithTimeout(time.Duration)\n - WithRetryConfig(RetryConfig)\n - WithLogger(*slog.Logger)\n - WithBaseURL(string) for testing\n- RetryConfig struct (MaxRetries, BaseDelay, MaxDelay, Jitter)\n- Default values: 3 retries, 1s base delay, 30s max delay, jitter enabled","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:08.021154076+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:24:55.101093793+01:00","closed_at":"2026-01-14T13:24:55.101093793+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-ymg","depends_on_id":"checkvist-api-e9p","type":"blocks","created_at":"2026-01-14T12:32:47.077541448+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-ymg","depends_on_id":"checkvist-api-mnh","type":"blocks","created_at":"2026-01-14T12:32:47.358639632+01:00","created_by":"Oliver Jakoubek"}]}
|
||||||
|
|
|
||||||
|
|
@ -91,3 +91,32 @@ func (s *ChecklistService) Delete(ctx context.Context, id int) error {
|
||||||
path := fmt.Sprintf("/checklists/%d.json", id)
|
path := fmt.Sprintf("/checklists/%d.json", id)
|
||||||
return s.client.doDelete(ctx, path)
|
return s.client.doDelete(ctx, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// archiveRequest is the request body for archiving/unarchiving a checklist.
|
||||||
|
type archiveRequest struct {
|
||||||
|
Archived bool `json:"archived"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Archive archives a checklist by ID.
|
||||||
|
func (s *ChecklistService) Archive(ctx context.Context, id int) (*Checklist, error) {
|
||||||
|
path := fmt.Sprintf("/checklists/%d.json", id)
|
||||||
|
body := archiveRequest{Archived: true}
|
||||||
|
|
||||||
|
var checklist Checklist
|
||||||
|
if err := s.client.doPut(ctx, path, body, &checklist); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &checklist, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unarchive unarchives a checklist by ID.
|
||||||
|
func (s *ChecklistService) Unarchive(ctx context.Context, id int) (*Checklist, error) {
|
||||||
|
path := fmt.Sprintf("/checklists/%d.json", id)
|
||||||
|
body := archiveRequest{Archived: false}
|
||||||
|
|
||||||
|
var checklist Checklist
|
||||||
|
if err := s.client.doPut(ctx, path, body, &checklist); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &checklist, nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -258,3 +258,91 @@ func TestChecklists_Delete(t *testing.T) {
|
||||||
t.Error("expected DELETE to be called")
|
t.Error("expected DELETE to be called")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestChecklists_Archive(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
switch r.URL.Path {
|
||||||
|
case "/auth/login.json":
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{"token": "test-token"})
|
||||||
|
case "/checklists/1.json":
|
||||||
|
if r.Method != http.MethodPut {
|
||||||
|
t.Errorf("expected PUT, got %s", r.Method)
|
||||||
|
}
|
||||||
|
|
||||||
|
var req archiveRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
t.Fatalf("failed to decode request: %v", err)
|
||||||
|
}
|
||||||
|
if !req.Archived {
|
||||||
|
t.Error("expected archived=true")
|
||||||
|
}
|
||||||
|
|
||||||
|
response := Checklist{
|
||||||
|
ID: 1,
|
||||||
|
Name: "Archived Checklist",
|
||||||
|
Archived: true,
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
json.NewEncoder(w).Encode(response)
|
||||||
|
default:
|
||||||
|
t.Errorf("unexpected path: %s", r.URL.Path)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client := NewClient("user@example.com", "api-key", WithBaseURL(server.URL))
|
||||||
|
checklist, err := client.Checklists().Archive(context.Background(), 1)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if !checklist.Archived {
|
||||||
|
t.Error("expected checklist to be archived")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChecklists_Unarchive(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
switch r.URL.Path {
|
||||||
|
case "/auth/login.json":
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{"token": "test-token"})
|
||||||
|
case "/checklists/1.json":
|
||||||
|
if r.Method != http.MethodPut {
|
||||||
|
t.Errorf("expected PUT, got %s", r.Method)
|
||||||
|
}
|
||||||
|
|
||||||
|
var req archiveRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
t.Fatalf("failed to decode request: %v", err)
|
||||||
|
}
|
||||||
|
if req.Archived {
|
||||||
|
t.Error("expected archived=false")
|
||||||
|
}
|
||||||
|
|
||||||
|
response := Checklist{
|
||||||
|
ID: 1,
|
||||||
|
Name: "Unarchived Checklist",
|
||||||
|
Archived: false,
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
json.NewEncoder(w).Encode(response)
|
||||||
|
default:
|
||||||
|
t.Errorf("unexpected path: %s", r.URL.Path)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client := NewClient("user@example.com", "api-key", WithBaseURL(server.URL))
|
||||||
|
checklist, err := client.Checklists().Unarchive(context.Background(), 1)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if checklist.Archived {
|
||||||
|
t.Error("expected checklist to not be archived")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
163
example_test.go
Normal file
163
example_test.go
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
package checkvist_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.beautifulmachines.dev/jakoubek/checkvist-api"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This file contains GoDoc examples that appear in the package documentation.
|
||||||
|
// All examples are runnable but require valid credentials to actually work.
|
||||||
|
|
||||||
|
func Example_basicUsage() {
|
||||||
|
// Create a new client with your credentials
|
||||||
|
client := checkvist.NewClient("user@example.com", "your-api-key")
|
||||||
|
|
||||||
|
// List all checklists
|
||||||
|
ctx := context.Background()
|
||||||
|
checklists, err := client.Checklists().List(ctx)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cl := range checklists {
|
||||||
|
fmt.Printf("Checklist: %s (%d tasks)\n", cl.Name, cl.TaskCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleNewClient() {
|
||||||
|
// Basic client creation
|
||||||
|
client := checkvist.NewClient("user@example.com", "your-api-key")
|
||||||
|
_ = client
|
||||||
|
|
||||||
|
// With custom options
|
||||||
|
client = checkvist.NewClient("user@example.com", "your-api-key",
|
||||||
|
checkvist.WithTimeout(60*time.Second),
|
||||||
|
checkvist.WithRetryConfig(checkvist.RetryConfig{
|
||||||
|
MaxRetries: 5,
|
||||||
|
BaseDelay: 500 * time.Millisecond,
|
||||||
|
MaxDelay: 30 * time.Second,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
_ = client
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleClient_Authenticate() {
|
||||||
|
client := checkvist.NewClient("user@example.com", "your-api-key")
|
||||||
|
|
||||||
|
// Explicit authentication (usually not needed - auto-authenticates on first request)
|
||||||
|
ctx := context.Background()
|
||||||
|
err := client.Authenticate(ctx)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Authentication failed:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println("Authenticated successfully")
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleChecklistService_List() {
|
||||||
|
client := checkvist.NewClient("user@example.com", "your-api-key")
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// List all active checklists
|
||||||
|
checklists, err := client.Checklists().List(ctx)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cl := range checklists {
|
||||||
|
fmt.Printf("- %s (ID: %d)\n", cl.Name, cl.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleTaskService_Create() {
|
||||||
|
client := checkvist.NewClient("user@example.com", "your-api-key")
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Create a task using the builder pattern
|
||||||
|
task, err := client.Tasks(123).Create(ctx,
|
||||||
|
checkvist.NewTask("Buy groceries").
|
||||||
|
WithTags("shopping", "urgent").
|
||||||
|
WithDueDate(checkvist.DueTomorrow).
|
||||||
|
WithPriority(1),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("Created task: %s (ID: %d)\n", task.Content, task.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleNewTask() {
|
||||||
|
// Simple task
|
||||||
|
task := checkvist.NewTask("Complete project report")
|
||||||
|
|
||||||
|
// Task with all options
|
||||||
|
task = checkvist.NewTask("Review pull request").
|
||||||
|
WithTags("code-review", "urgent").
|
||||||
|
WithDueDate(checkvist.DueTomorrow).
|
||||||
|
WithPriority(1).
|
||||||
|
WithParent(456). // Makes this a subtask
|
||||||
|
WithPosition(1) // First position among siblings
|
||||||
|
|
||||||
|
_ = task
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleNewFilter() {
|
||||||
|
// Assume we have a list of tasks from the API
|
||||||
|
tasks := []checkvist.Task{
|
||||||
|
{ID: 1, Content: "Buy groceries", TagsAsText: "shopping", Status: checkvist.StatusOpen},
|
||||||
|
{ID: 2, Content: "Call doctor", TagsAsText: "health", Status: checkvist.StatusClosed},
|
||||||
|
{ID: 3, Content: "Review PR", TagsAsText: "work, urgent", Status: checkvist.StatusOpen},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter to open tasks with "urgent" tag
|
||||||
|
filtered := checkvist.NewFilter(tasks).
|
||||||
|
WithTag("urgent").
|
||||||
|
WithStatus(checkvist.StatusOpen).
|
||||||
|
Apply()
|
||||||
|
|
||||||
|
fmt.Printf("Found %d matching tasks\n", len(filtered))
|
||||||
|
// Output: Found 1 matching tasks
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleFilter_Apply() {
|
||||||
|
// Create sample tasks with due dates
|
||||||
|
yesterday := time.Now().AddDate(0, 0, -1)
|
||||||
|
tomorrow := time.Now().AddDate(0, 0, 1)
|
||||||
|
|
||||||
|
tasks := []checkvist.Task{
|
||||||
|
{ID: 1, Content: "Overdue task", DueDate: &yesterday, Status: checkvist.StatusOpen},
|
||||||
|
{ID: 2, Content: "Future task", DueDate: &tomorrow, Status: checkvist.StatusOpen},
|
||||||
|
{ID: 3, Content: "Closed task", DueDate: &yesterday, Status: checkvist.StatusClosed},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find overdue open tasks
|
||||||
|
overdue := checkvist.NewFilter(tasks).
|
||||||
|
WithOverdue().
|
||||||
|
Apply()
|
||||||
|
|
||||||
|
for _, t := range overdue {
|
||||||
|
fmt.Printf("Overdue: %s\n", t.Content)
|
||||||
|
}
|
||||||
|
// Output: Overdue: Overdue task
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleDueAt() {
|
||||||
|
// Set due date to a specific date
|
||||||
|
deadline := time.Date(2026, 3, 15, 0, 0, 0, 0, time.UTC)
|
||||||
|
task := checkvist.NewTask("Project deadline").
|
||||||
|
WithDueDate(checkvist.DueAt(deadline))
|
||||||
|
_ = task
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleDueInDays() {
|
||||||
|
// Set due date to 7 days from now
|
||||||
|
task := checkvist.NewTask("Follow up").
|
||||||
|
WithDueDate(checkvist.DueInDays(7))
|
||||||
|
_ = task
|
||||||
|
}
|
||||||
147
filter.go
147
filter.go
|
|
@ -1,5 +1,152 @@
|
||||||
package checkvist
|
package checkvist
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
// filter.go contains the Filter builder for client-side task filtering.
|
// filter.go contains the Filter builder for client-side task filtering.
|
||||||
// The Checkvist API does not support server-side filtering, so filtering
|
// The Checkvist API does not support server-side filtering, so filtering
|
||||||
// is performed locally after fetching all tasks.
|
// is performed locally after fetching all tasks.
|
||||||
|
|
||||||
|
// Filter provides a builder pattern for filtering tasks client-side.
|
||||||
|
type Filter struct {
|
||||||
|
tasks []Task
|
||||||
|
filters []func(Task) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFilter creates a new Filter with the given tasks.
|
||||||
|
func NewFilter(tasks []Task) *Filter {
|
||||||
|
return &Filter{tasks: tasks}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithTag filters tasks that have the specified tag.
|
||||||
|
func (f *Filter) WithTag(tag string) *Filter {
|
||||||
|
f.filters = append(f.filters, func(t Task) bool {
|
||||||
|
return taskHasTag(t, tag)
|
||||||
|
})
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithTags filters tasks that have all of the specified tags (AND logic).
|
||||||
|
func (f *Filter) WithTags(tags ...string) *Filter {
|
||||||
|
f.filters = append(f.filters, func(t Task) bool {
|
||||||
|
for _, tag := range tags {
|
||||||
|
if !taskHasTag(t, tag) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithStatus filters tasks by their status.
|
||||||
|
func (f *Filter) WithStatus(status TaskStatus) *Filter {
|
||||||
|
f.filters = append(f.filters, func(t Task) bool {
|
||||||
|
return t.Status == status
|
||||||
|
})
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithDueBefore filters tasks with due dates before the specified time.
|
||||||
|
func (f *Filter) WithDueBefore(deadline time.Time) *Filter {
|
||||||
|
f.filters = append(f.filters, func(t Task) bool {
|
||||||
|
if t.DueDate == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return t.DueDate.Before(deadline)
|
||||||
|
})
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithDueAfter filters tasks with due dates after the specified time.
|
||||||
|
func (f *Filter) WithDueAfter(after time.Time) *Filter {
|
||||||
|
f.filters = append(f.filters, func(t Task) bool {
|
||||||
|
if t.DueDate == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return t.DueDate.After(after)
|
||||||
|
})
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithDueOn filters tasks with due dates on the specified day.
|
||||||
|
func (f *Filter) WithDueOn(day time.Time) *Filter {
|
||||||
|
year, month, d := day.Date()
|
||||||
|
f.filters = append(f.filters, func(t Task) bool {
|
||||||
|
if t.DueDate == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
ty, tm, td := t.DueDate.Date()
|
||||||
|
return ty == year && tm == month && td == d
|
||||||
|
})
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithOverdue filters tasks that are overdue (due date is before today).
|
||||||
|
func (f *Filter) WithOverdue() *Filter {
|
||||||
|
today := time.Now().Truncate(24 * time.Hour)
|
||||||
|
f.filters = append(f.filters, func(t Task) bool {
|
||||||
|
if t.DueDate == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return t.DueDate.Before(today) && t.Status == StatusOpen
|
||||||
|
})
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithSearch filters tasks whose content contains the search query (case-insensitive).
|
||||||
|
func (f *Filter) WithSearch(query string) *Filter {
|
||||||
|
lowerQuery := strings.ToLower(query)
|
||||||
|
f.filters = append(f.filters, func(t Task) bool {
|
||||||
|
return strings.Contains(strings.ToLower(t.Content), lowerQuery)
|
||||||
|
})
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply applies all filters and returns the filtered tasks.
|
||||||
|
func (f *Filter) Apply() []Task {
|
||||||
|
if len(f.filters) == 0 {
|
||||||
|
result := make([]Task, len(f.tasks))
|
||||||
|
copy(result, f.tasks)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]Task, 0, len(f.tasks))
|
||||||
|
for _, task := range f.tasks {
|
||||||
|
if f.matches(task) {
|
||||||
|
result = append(result, task)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// matches checks if a task matches all filters.
|
||||||
|
func (f *Filter) matches(task Task) bool {
|
||||||
|
for _, filter := range f.filters {
|
||||||
|
if !filter(task) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// taskHasTag checks if a task has a specific tag.
|
||||||
|
func taskHasTag(t Task, tag string) bool {
|
||||||
|
// Check parsed Tags map first
|
||||||
|
if t.Tags != nil && t.Tags[tag] {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// Fall back to checking TagsAsText
|
||||||
|
if t.TagsAsText == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
lowerTag := strings.ToLower(tag)
|
||||||
|
for _, part := range strings.Split(t.TagsAsText, ",") {
|
||||||
|
if strings.ToLower(strings.TrimSpace(part)) == lowerTag {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
|
||||||
338
filter_test.go
Normal file
338
filter_test.go
Normal file
|
|
@ -0,0 +1,338 @@
|
||||||
|
package checkvist
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFilter_WithTag(t *testing.T) {
|
||||||
|
tasks := []Task{
|
||||||
|
{ID: 1, Content: "Task 1", TagsAsText: "important, urgent"},
|
||||||
|
{ID: 2, Content: "Task 2", TagsAsText: "urgent"},
|
||||||
|
{ID: 3, Content: "Task 3", TagsAsText: ""},
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
tag string
|
||||||
|
expected []int
|
||||||
|
}{
|
||||||
|
{"filter by important", "important", []int{1}},
|
||||||
|
{"filter by urgent", "urgent", []int{1, 2}},
|
||||||
|
{"filter by nonexistent", "nonexistent", []int{}},
|
||||||
|
{"case insensitive", "IMPORTANT", []int{1}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := NewFilter(tasks).WithTag(tt.tag).Apply()
|
||||||
|
if len(result) != len(tt.expected) {
|
||||||
|
t.Errorf("expected %d tasks, got %d", len(tt.expected), len(result))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i, task := range result {
|
||||||
|
if task.ID != tt.expected[i] {
|
||||||
|
t.Errorf("expected task ID %d at index %d, got %d", tt.expected[i], i, task.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilter_WithMultipleTags(t *testing.T) {
|
||||||
|
tasks := []Task{
|
||||||
|
{ID: 1, Content: "Task 1", TagsAsText: "important, urgent, work"},
|
||||||
|
{ID: 2, Content: "Task 2", TagsAsText: "urgent, work"},
|
||||||
|
{ID: 3, Content: "Task 3", TagsAsText: "important"},
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
tags []string
|
||||||
|
expected []int
|
||||||
|
}{
|
||||||
|
{"single tag", []string{"urgent"}, []int{1, 2}},
|
||||||
|
{"two tags AND", []string{"urgent", "work"}, []int{1, 2}},
|
||||||
|
{"three tags AND", []string{"important", "urgent", "work"}, []int{1}},
|
||||||
|
{"no match", []string{"important", "urgent", "missing"}, []int{}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := NewFilter(tasks).WithTags(tt.tags...).Apply()
|
||||||
|
if len(result) != len(tt.expected) {
|
||||||
|
t.Errorf("expected %d tasks, got %d", len(tt.expected), len(result))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i, task := range result {
|
||||||
|
if task.ID != tt.expected[i] {
|
||||||
|
t.Errorf("expected task ID %d at index %d, got %d", tt.expected[i], i, task.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilter_WithStatus(t *testing.T) {
|
||||||
|
tasks := []Task{
|
||||||
|
{ID: 1, Content: "Open task", Status: StatusOpen},
|
||||||
|
{ID: 2, Content: "Closed task", Status: StatusClosed},
|
||||||
|
{ID: 3, Content: "Invalidated task", Status: StatusInvalidated},
|
||||||
|
{ID: 4, Content: "Another open", Status: StatusOpen},
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
status TaskStatus
|
||||||
|
expected []int
|
||||||
|
}{
|
||||||
|
{"open tasks", StatusOpen, []int{1, 4}},
|
||||||
|
{"closed tasks", StatusClosed, []int{2}},
|
||||||
|
{"invalidated tasks", StatusInvalidated, []int{3}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := NewFilter(tasks).WithStatus(tt.status).Apply()
|
||||||
|
if len(result) != len(tt.expected) {
|
||||||
|
t.Errorf("expected %d tasks, got %d", len(tt.expected), len(result))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i, task := range result {
|
||||||
|
if task.ID != tt.expected[i] {
|
||||||
|
t.Errorf("expected task ID %d at index %d, got %d", tt.expected[i], i, task.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilter_WithDueBefore(t *testing.T) {
|
||||||
|
now := time.Now()
|
||||||
|
yesterday := now.AddDate(0, 0, -1)
|
||||||
|
tomorrow := now.AddDate(0, 0, 1)
|
||||||
|
nextWeek := now.AddDate(0, 0, 7)
|
||||||
|
|
||||||
|
tasks := []Task{
|
||||||
|
{ID: 1, Content: "Yesterday", DueDate: &yesterday},
|
||||||
|
{ID: 2, Content: "Tomorrow", DueDate: &tomorrow},
|
||||||
|
{ID: 3, Content: "Next week", DueDate: &nextWeek},
|
||||||
|
{ID: 4, Content: "No due date", DueDate: nil},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := NewFilter(tasks).WithDueBefore(now).Apply()
|
||||||
|
if len(result) != 1 {
|
||||||
|
t.Errorf("expected 1 task, got %d", len(result))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if result[0].ID != 1 {
|
||||||
|
t.Errorf("expected task ID 1, got %d", result[0].ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilter_WithDueAfter(t *testing.T) {
|
||||||
|
now := time.Now()
|
||||||
|
yesterday := now.AddDate(0, 0, -1)
|
||||||
|
tomorrow := now.AddDate(0, 0, 1)
|
||||||
|
nextWeek := now.AddDate(0, 0, 7)
|
||||||
|
|
||||||
|
tasks := []Task{
|
||||||
|
{ID: 1, Content: "Yesterday", DueDate: &yesterday},
|
||||||
|
{ID: 2, Content: "Tomorrow", DueDate: &tomorrow},
|
||||||
|
{ID: 3, Content: "Next week", DueDate: &nextWeek},
|
||||||
|
{ID: 4, Content: "No due date", DueDate: nil},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := NewFilter(tasks).WithDueAfter(now).Apply()
|
||||||
|
if len(result) != 2 {
|
||||||
|
t.Errorf("expected 2 tasks, got %d", len(result))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if result[0].ID != 2 || result[1].ID != 3 {
|
||||||
|
t.Errorf("expected task IDs [2, 3], got [%d, %d]", result[0].ID, result[1].ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilter_WithDueOn(t *testing.T) {
|
||||||
|
today := time.Now().Truncate(24 * time.Hour)
|
||||||
|
todayMorning := today.Add(9 * time.Hour)
|
||||||
|
todayEvening := today.Add(18 * time.Hour)
|
||||||
|
tomorrow := today.AddDate(0, 0, 1)
|
||||||
|
|
||||||
|
tasks := []Task{
|
||||||
|
{ID: 1, Content: "Today morning", DueDate: &todayMorning},
|
||||||
|
{ID: 2, Content: "Today evening", DueDate: &todayEvening},
|
||||||
|
{ID: 3, Content: "Tomorrow", DueDate: &tomorrow},
|
||||||
|
{ID: 4, Content: "No due date", DueDate: nil},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := NewFilter(tasks).WithDueOn(today).Apply()
|
||||||
|
if len(result) != 2 {
|
||||||
|
t.Errorf("expected 2 tasks, got %d", len(result))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if result[0].ID != 1 || result[1].ID != 2 {
|
||||||
|
t.Errorf("expected task IDs [1, 2], got [%d, %d]", result[0].ID, result[1].ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilter_WithOverdue(t *testing.T) {
|
||||||
|
today := time.Now().Truncate(24 * time.Hour)
|
||||||
|
yesterday := today.AddDate(0, 0, -1)
|
||||||
|
tomorrow := today.AddDate(0, 0, 1)
|
||||||
|
|
||||||
|
tasks := []Task{
|
||||||
|
{ID: 1, Content: "Overdue open", DueDate: &yesterday, Status: StatusOpen},
|
||||||
|
{ID: 2, Content: "Overdue closed", DueDate: &yesterday, Status: StatusClosed},
|
||||||
|
{ID: 3, Content: "Future open", DueDate: &tomorrow, Status: StatusOpen},
|
||||||
|
{ID: 4, Content: "No due date", DueDate: nil, Status: StatusOpen},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := NewFilter(tasks).WithOverdue().Apply()
|
||||||
|
if len(result) != 1 {
|
||||||
|
t.Errorf("expected 1 task, got %d", len(result))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if result[0].ID != 1 {
|
||||||
|
t.Errorf("expected task ID 1, got %d", result[0].ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilter_WithSearch(t *testing.T) {
|
||||||
|
tasks := []Task{
|
||||||
|
{ID: 1, Content: "Buy groceries"},
|
||||||
|
{ID: 2, Content: "Call the doctor"},
|
||||||
|
{ID: 3, Content: "Review PR for grocery app"},
|
||||||
|
{ID: 4, Content: "Send email"},
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
query string
|
||||||
|
expected []int
|
||||||
|
}{
|
||||||
|
{"exact match", "groceries", []int{1}},
|
||||||
|
{"partial match", "grocer", []int{1, 3}},
|
||||||
|
{"case insensitive", "CALL", []int{2}},
|
||||||
|
{"no match", "xyz", []int{}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := NewFilter(tasks).WithSearch(tt.query).Apply()
|
||||||
|
if len(result) != len(tt.expected) {
|
||||||
|
t.Errorf("expected %d tasks, got %d", len(tt.expected), len(result))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i, task := range result {
|
||||||
|
if task.ID != tt.expected[i] {
|
||||||
|
t.Errorf("expected task ID %d at index %d, got %d", tt.expected[i], i, task.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilter_Combined(t *testing.T) {
|
||||||
|
today := time.Now().Truncate(24 * time.Hour)
|
||||||
|
yesterday := today.AddDate(0, 0, -1)
|
||||||
|
tomorrow := today.AddDate(0, 0, 1)
|
||||||
|
|
||||||
|
tasks := []Task{
|
||||||
|
{ID: 1, Content: "Important task", TagsAsText: "important", Status: StatusOpen, DueDate: &yesterday},
|
||||||
|
{ID: 2, Content: "Important closed", TagsAsText: "important", Status: StatusClosed, DueDate: &yesterday},
|
||||||
|
{ID: 3, Content: "Regular task", TagsAsText: "regular", Status: StatusOpen, DueDate: &yesterday},
|
||||||
|
{ID: 4, Content: "Important future", TagsAsText: "important", Status: StatusOpen, DueDate: &tomorrow},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter: important AND open AND due before today
|
||||||
|
result := NewFilter(tasks).
|
||||||
|
WithTag("important").
|
||||||
|
WithStatus(StatusOpen).
|
||||||
|
WithDueBefore(today).
|
||||||
|
Apply()
|
||||||
|
|
||||||
|
if len(result) != 1 {
|
||||||
|
t.Errorf("expected 1 task, got %d", len(result))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if result[0].ID != 1 {
|
||||||
|
t.Errorf("expected task ID 1, got %d", result[0].ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilter_EmptyFilters(t *testing.T) {
|
||||||
|
tasks := []Task{
|
||||||
|
{ID: 1, Content: "Task 1"},
|
||||||
|
{ID: 2, Content: "Task 2"},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := NewFilter(tasks).Apply()
|
||||||
|
if len(result) != 2 {
|
||||||
|
t.Errorf("expected 2 tasks, got %d", len(result))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilter_EmptyTasks(t *testing.T) {
|
||||||
|
result := NewFilter([]Task{}).WithTag("test").Apply()
|
||||||
|
if len(result) != 0 {
|
||||||
|
t.Errorf("expected 0 tasks, got %d", len(result))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilter_Performance_1000Tasks(t *testing.T) {
|
||||||
|
// Create 1000 tasks with various attributes
|
||||||
|
tasks := make([]Task, 1000)
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
for i := 0; i < 1000; i++ {
|
||||||
|
due := now.AddDate(0, 0, i-500) // -500 to +499 days from now
|
||||||
|
tasks[i] = Task{
|
||||||
|
ID: i + 1,
|
||||||
|
Content: "Task with some content to search through",
|
||||||
|
TagsAsText: "tag1, tag2, tag3",
|
||||||
|
Status: TaskStatus(i % 3),
|
||||||
|
DueDate: &due,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark important tasks
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
tasks[i].TagsAsText = "important, urgent"
|
||||||
|
}
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
// Run a complex filter
|
||||||
|
result := NewFilter(tasks).
|
||||||
|
WithTag("important").
|
||||||
|
WithStatus(StatusOpen).
|
||||||
|
WithDueBefore(now).
|
||||||
|
WithSearch("content").
|
||||||
|
Apply()
|
||||||
|
|
||||||
|
elapsed := time.Since(start)
|
||||||
|
|
||||||
|
// Performance target: <10ms for 1000 tasks
|
||||||
|
if elapsed > 10*time.Millisecond {
|
||||||
|
t.Errorf("filter took %v, expected <10ms", elapsed)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify we got some results
|
||||||
|
if len(result) == 0 {
|
||||||
|
t.Log("No matching tasks found, but performance test passed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilter_TagsMap(t *testing.T) {
|
||||||
|
// Test that Tags map is also checked
|
||||||
|
tasks := []Task{
|
||||||
|
{ID: 1, Content: "Task 1", Tags: Tags{"important": true}, TagsAsText: ""},
|
||||||
|
{ID: 2, Content: "Task 2", Tags: nil, TagsAsText: "important"},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := NewFilter(tasks).WithTag("important").Apply()
|
||||||
|
if len(result) != 2 {
|
||||||
|
t.Errorf("expected 2 tasks, got %d", len(result))
|
||||||
|
}
|
||||||
|
}
|
||||||
18
tasks.go
18
tasks.go
|
|
@ -60,6 +60,7 @@ type CreateTaskRequest struct {
|
||||||
Due string `json:"due,omitempty"`
|
Due string `json:"due,omitempty"`
|
||||||
Priority int `json:"priority,omitempty"`
|
Priority int `json:"priority,omitempty"`
|
||||||
Tags string `json:"tags,omitempty"`
|
Tags string `json:"tags,omitempty"`
|
||||||
|
Repeat string `json:"repeat,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TaskBuilder provides a fluent interface for building task creation requests.
|
// TaskBuilder provides a fluent interface for building task creation requests.
|
||||||
|
|
@ -70,6 +71,7 @@ type TaskBuilder struct {
|
||||||
due string
|
due string
|
||||||
priority int
|
priority int
|
||||||
tags []string
|
tags []string
|
||||||
|
repeat string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTask creates a new TaskBuilder with the given content.
|
// NewTask creates a new TaskBuilder with the given content.
|
||||||
|
|
@ -107,6 +109,21 @@ func (b *TaskBuilder) WithTags(tags ...string) *TaskBuilder {
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithRepeat sets the repeat pattern for the task using Checkvist's smart syntax.
|
||||||
|
// Common patterns include:
|
||||||
|
// - "daily" - repeats every day
|
||||||
|
// - "weekly" - repeats every week
|
||||||
|
// - "monthly" - repeats every month
|
||||||
|
// - "yearly" - repeats every year
|
||||||
|
// - "every 2 days" - repeats every 2 days
|
||||||
|
// - "every week on monday" - repeats weekly on Monday
|
||||||
|
// - "every month on 15" - repeats monthly on the 15th
|
||||||
|
// - "every 2 weeks on friday" - repeats every 2 weeks on Friday
|
||||||
|
func (b *TaskBuilder) WithRepeat(pattern string) *TaskBuilder {
|
||||||
|
b.repeat = pattern
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
// build converts the TaskBuilder to a CreateTaskRequest.
|
// build converts the TaskBuilder to a CreateTaskRequest.
|
||||||
func (b *TaskBuilder) build() CreateTaskRequest {
|
func (b *TaskBuilder) build() CreateTaskRequest {
|
||||||
req := CreateTaskRequest{
|
req := CreateTaskRequest{
|
||||||
|
|
@ -115,6 +132,7 @@ func (b *TaskBuilder) build() CreateTaskRequest {
|
||||||
Position: b.position,
|
Position: b.position,
|
||||||
Due: b.due,
|
Due: b.due,
|
||||||
Priority: b.priority,
|
Priority: b.priority,
|
||||||
|
Repeat: b.repeat,
|
||||||
}
|
}
|
||||||
if len(b.tags) > 0 {
|
if len(b.tags) > 0 {
|
||||||
for i, tag := range b.tags {
|
for i, tag := range b.tags {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue