feat(api): add project listing#6473
Conversation
db9ca4f to
081038c
Compare
081038c to
2c23b7a
Compare
2c23b7a to
cd47d0d
Compare
|
I noticed the new list endpoint treats the cursor as a raw project id lower bound and intentionally returns the first row of the next page as the next cursor. The current pagination test covers the generated cursor path well; I would add one more regression around a caller-supplied cursor that is malformed or belongs to another workspace. That would lock in two useful guarantees: arbitrary cursor values do not leak projects across workspaces because the workspace predicate still wins, and the endpoint terminates cleanly with an empty page or a valid next page instead of depending on the cursor having come from this exact response stream. |
There was a problem hiding this comment.
ℹ️ No critical issues — one informational note on index coverage.
Reviewed changes — the new POST /v2/projects.listProjects endpoint, its cursor pagination, the read_project RBAC action, and the supporting OpenAPI + dashboard wiring.
- Add
listProjectshandler —svc/api/routes/v2_projects_list_projects/handler.goauthorizesproject.*.read_project, fetcheslimit+1rows, and deriveshasMore/nextCursorfrom the extra row before trimming. - Keyset query
ListProjectsByWorkspaceId—WHERE workspace_id = ? AND id >= ? ORDER BY id ASC LIMIT ?, an inclusive-boundary cursor scan over the uniqueidcolumn. - New
read_projectRBAC action — registered inpkg/rbac/permissions.go, theweb/internal/rbaczod enum, and both dashboard permission lists. - OpenAPI surface — request/response/data schemas, examples, and Speakeasy cursor-pagination hints, with regenerated Go and yaml.
- Tests — 200 (empty / populated / stale cursor / pagination / workspace isolation), 400 (limit bounds + invalid JSON), 401, 403.
Pagination is sound: project IDs are random (pkg/uid/new.go uses math/rand/v2, not a monotonic scheme), but a keyset scan over the unique id column with an inclusive id >= cursor boundary and a limit+1 lookahead is still stable, complete, and duplicate-free — the pagination test confirms this. Workspace scoping on the query also means a cursor borrowed from another workspace cannot leak rows.
ℹ️ efficient pagination claim isn't backed by a composite index
The PR description frames the keyset query as efficient pagination, but the projects table only has workspace_idx on (workspace_id) — the primary key is the auto-increment pk, and there is no (workspace_id, id) index. MySQL can use workspace_idx to narrow to the workspace, but the id >= ? range plus ORDER BY id ASC then requires sorting that workspace's rows rather than walking an index in order. This is negligible at typical project counts and not a correctness issue, but it does mean the scan is not a true index-ordered keyset walk as workspaces grow.
Technical details
# `efficient pagination` claim isn't backed by a composite index
## Affected sites
- `pkg/db/queries/project_list_by_workspace.sql` — `WHERE workspace_id = ? AND id >= ? ORDER BY id ASC LIMIT ?`
- `pkg/mysql/schema/projects.sql` — table has `PRIMARY KEY(pk)`, `UNIQUE(id)`, `UNIQUE(workspace_id, slug)`, and `CREATE INDEX workspace_idx ON projects (workspace_id)`. No `(workspace_id, id)` index.
## Required outcome
- Either add a composite `(workspace_id, id)` index so the keyset scan is index-ordered and the `ORDER BY` is satisfied without a filesort, or soften the "efficient pagination" wording so the claim matches the current index coverage.
## Open questions for the human
- Is this endpoint expected to serve workspaces with large project counts? If projects-per-workspace stays small (tens), the missing index is a non-issue and this can be dropped. If it can grow large, the composite index is the cheap fix and belongs in a migration alongside this change.Claude Opus | 𝕏
1876780 to
586a415
Compare
40760e9 to
41fd10f
Compare
b6f42a8 to
dfd3b04
Compare
dfd3b04 to
281b208
Compare
4850277 to
379d65d
Compare
281b208 to
086a639
Compare
086a639 to
2147117
Compare


Important
Unkey is not accepting external pull requests at this time. Pull requests from people outside the Unkey team will not be reviewed or merged.
What does this PR do?
Adds the
POST /v2/projects.listProjectsendpoint, which returns a cursor-paginated list of projects scoped to the authenticated workspace. Results are ordered by project ID and support configurable page sizes (1–100, defaulting to 100).This also introduces the
read_projectRBAC action (project.*.read_project), required to call the new endpoint. The permission is wired into both the root key permissions page and the root key creation dialog in the dashboard, and is registered in the shared RBAC TypeScript package.The underlying database query (
ListProjectsByWorkspace) uses a keyset/cursor pattern (id >= ?) for efficient pagination.Fixes # (issue)
If there is not an issue for this, create one first. This is used for tracking purposes and helps us understand why this PR exists.
Type of change
How should this be tested?
POST /v2/projects.listProjectswith a valid root key bearingproject.*.read_project— expect a200with adataarray andpaginationobject.limit: 2across a workspace with 5 projects and paginate using the returnedcursoruntilhasMore: false— verify all 5 unique projects are returned with no duplicates.project.*.read_project— expect403.Authorizationheader or an invalid token — expect401.limit: 0orlimit: 101— expect400with validation errors.Checklist
Required
pnpm buildpnpm fmtmise run fmtconsole.logsgit pull origin mainAppreciated