Skip to content

feat(api): add the Task API's read endpoints - #71

Closed
HecFranco wants to merge 11 commits into
mainfrom
feat/213-s3-read-api
Closed

HecFranco wants to merge 11 commits into
mainfrom
feat/213-s3-read-api

Conversation

@HecFranco

@HecFranco HecFranco commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fourth slice of spec 213: the read API. Stacked on #70 (and #69, #67); the base is main because CI runs only on PRs to it. Audit status re-verification pending.

Endpoints: spaces, a space, folders (always none), lists, a list, a list's tasks and a team's tasks with ClickUp's filters and 100 per page, a task, comments (25 per page, newest first), members, tags, imported custom fields, time entries (through the host's time hook). Thin controllers over TaskApiReadOperations; queries in Api\Repository\TaskApiQueryRepository.

Access (SC-001, read half)

ApiReadMatrixTest: 16 read endpoints × the four levels × own task, another's task, another project, a board outside where, a removed member, a revoked/expired token, the plan without the module (per tenant and everywhere). Every answer is asserted, and lists are checked to show and count only what the token may read.

Query budget (SC-003, FR-010)

ApiQueryBudgetTest, every query of the request counted, each task with members, a subtask, a dependency, time and a comment:

Endpoint 25 tasks 100 tasks
list tasks (subtasks, closed) 9 9
team tasks 10 10
list tasks, My tasks 9 9
lists of a space 6 6
tags of a space 5 5
comments of a task 5 5

Also in this PR

Gates run locally

composer test-coverage-100 (100%), PHPStan, cs-check on PHP 8.2 and SQLite; tests/Functional/Api also on PostgreSQL 16.

Not for merge until the coordinator's audit step.

Audit IA fixes (second round)

  • IA-M2: subtasks shown are those the token may open; for My tasks the checklist items of its own tasks stay (owner). Tests both ways.
  • IA-L3: the tag filter runs in SQL (NOWO_TB_JSON_TEXT, mapped with api.enabled only); budget cases for list and team by tag. Tested on SQLite, PostgreSQL 16, MySQL 8.4.
  • IA-I7: dependencies and links to tasks the token may not open are left out.
  • IA-I8: board columns load with the tasks: team tasks cost 9 queries with 1 board or 10; per-board host checks documented.
  • IA-I9: time entries without task_id rely on the host hook, documented.

…pec 213 S0)

Pins what the UI answers before the rules behind it move in the next slice:
status, Location, flashes, log lines, events and history for the comment and
restore routes, and the constraints of the six write forms. No src change.

With TASK_BOARD_CHARACTERIZE_DIR set the tests also write the pages with ids and
dates masked, so two checkouts on one vendor can be compared byte by byte.
… 213 S1)

A write path other than the UI's forms must apply the same rules, so they leave
the controller and the form types: TaskCommentPolicy, TaskManager::restore() and
Validation\TaskConstraints. tests/Functional/Characterization from the previous
commit is unchanged and passes, and the rendered pages are byte-identical.

Adds what the UI does not have and the API will need, with no caller yet:
TaskBoardEraser::eraseTask() (leaf-first, so PreRemove keeps every minute; refuses
a task that follows a provider), TaskBoardEditor, TaskManager::archive(),
TaskDependencyManager::findBetween(), TaskFormData::fromTask().
Off by default: no route, service, listener, parameter or table for a host that
changes nothing. On: stateless routes under api.prefix, GET /v2/user and /v2/team,
and what every later endpoint goes through: the request pipeline, TaskApiGuard
(level, the person's access and where, per request), the capability table and one
{err, ECODE} shape.

The bundle stores no tokens. The hooks live in Api\Hook, the authenticator is
required and every other default denies. It fails closed when the security user is
not the token's user, as access checkers read the security token, and the build
refuses a configuration that would open every board to every token.

Audit status: re-verification pending.
The HTML coverage report ran out of 512M with the API tests: main already peaks
near 440M before it. The PHPUnit ceiling goes to 1G, and the API tests compile
fewer kernels: one host configuration for the pipeline and the guard, and the
"off equals unset" check moves to the container builder.
Spaces, folders (none), lists, tasks with ClickUp's filters and 100 per page,
a task, comments, members, tags, imported custom fields and time entries.
Every endpoint goes through the guard; lists hold and count only what the token
may read. Each list costs the same queries at 25 and 100 tasks (9 for a list's
tasks). Read access matrix: four levels by own task, another's, another
project, a board outside where, a removed member, a revoked token and the plan.
…d-moves

# Conflicts:
#	docs/CHANGELOG.md
#	docs/UPGRADING.md
…audit IA)

IA-M1: eraseTask() handed the host no stored-file refs. After an unlink the
sync's attachment rows outlive the link (GWd-M1), so the files were never named
again; on SQLite the rows stayed orphaned. The refs of the subtree are read
first, passed per task in TaskErasedEvent, and the rows removed. Proof test
ported from the auditor; run on SQLite and PostgreSQL 16.

IA-I1: TaskBoardManageController keeps $syncLimits in its place, deprecated
and ignored, so positional arguments do not move in a minor release.

Also takes main (2.1.1) and the 1G PHPUnit ceiling the coverage report needs.
…t IA)

IA-L1: a valid token whose user the access checker refuses keeps its 401 and
now counts as a failed token, so it cannot call the authenticator unthrottled.

IA-L2: with tenancy the guard asks the bundle's tenant resolver for the board's
tenant, as the UI does through owns(), so a permissive workspace hook opens no
other tenant; a board without a tenant is not served under tenancy.

IA-I4: where's board ids grant on their own, documented as intended.
IA-I2, I3, I5: notes in SECURITY and API docs, no code change.
Takes main (2.1.1), IA-M1, IA-I1, IA-L1, IA-L2 and IA-I4. The tenant check of
IA-L2 also applies to spaces and to the boards a space lists.
IA-M2: a task's subtasks are those the token may open; for My tasks the checklist
items of its own tasks stay, whoever created them (owner's decision).
IA-I7: dependencies and links leave out tasks the token may not open.
IA-L3: the tag filter runs in SQL (NOWO_TB_JSON_TEXT, with api.enabled only),
so pages and last_page cost what any filter costs; budget cases added.
IA-I8: the boards' columns load with the tasks, so a team's tasks cost the same
queries with 1 board or 10 (9); the per-board access cost is documented.
IA-I9: documented that time entries without task_id rely on the host hook.

Tag filter tested on SQLite, PostgreSQL 16 and MySQL 8.4.
@HecFranco

Copy link
Copy Markdown
Contributor Author

Included in #76 (squash ebbd373), released in v2.2.0.

@HecFranco HecFranco closed this Nov 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant