feat(api): expand the public v2 tables surface - #6188
Conversation
Adds 16 operations so a v2 caller can do what the internal surface can: rename/move/lock a table, restore it, manage saved views, run enrichment columns, look up rows, and import/export with observable job control. Extracts lib/table/orchestration/import.ts (performTableCsvImport, performCreateTableFromCsv) and lib/table/export-stream.ts from the first-party routes, then repoints those routes at them, so v1 and v2 cannot drift on what an import or export actually does. events/stream, metadata and dispatches stay internal — they are editor state, not public API.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
PR SummaryMedium Risk Overview Refactors first-party table routes so v1 and v2 share behavior: streaming export moves to v2 error plumbing now accepts optional Reviewed by Cursor Bugbot for commit b25c426. Bugbot is set up for automated code reviews on this repo. Configure here. |
Greptile SummaryThe PR substantially expands the public v2 Tables API and extracts shared import/export orchestration for reuse by first- and second-generation routes. The follow-up changes also address the previously reported PATCH consistency issue.
Confidence Score: 5/5The PR appears safe to merge. The previously reported PATCH issue is resolved: deterministic rejection checks now occur before mutation, every successful sequential write is recorded before a later failure, and schema-change signaling runs whenever any operation commits, so no blocking failure remains.
|
| Filename | Overview |
|---|---|
| apps/sim/app/api/v2/tables/[tableId]/route.ts | Implements PATCH prevalidation, sequential mutation tracking, partial-success reporting, and schema-change signaling; the previously reported stale-client path is addressed. |
| apps/sim/lib/table/orchestration/tables.ts | Centralizes table mutation outcomes and preserves structured errors for the expanded v2 surface. |
| apps/sim/lib/table/orchestration/import.ts | Extracts shared import orchestration so first-party and public routes use the same behavior. |
| apps/sim/lib/table/export-stream.ts | Extracts shared streaming export behavior for reuse across API versions. |
| apps/sim/lib/api/contracts/v2/tables.ts | Defines the expanded v2 Tables request and response contracts, including update, view, enrichment, import/export, and job operations. |
| apps/docs/openapi-v2-tables.json | Documents the expanded public Tables API and its explicit PATCH partial-success contract. |
Sequence Diagram
sequenceDiagram
participant Client
participant Route as PATCH /api/v2/tables/{tableId}
participant Auth as Workspace authorization
participant DB as Table services
participant Events as Table event stream
Client->>Route: name / folderId / locks
Route->>Auth: Check write/admin access
Route->>Route: Prevalidate feature and folder constraints
alt Rejected before mutation
Route-->>Client: 4xx with no changes
else Prevalidation succeeds
Route->>DB: Apply locks
Route->>DB: Apply rename
Route->>DB: Apply folder move
Route->>Events: Signal when any operation committed
alt Later operation fails
Route-->>Client: Error with details.applied
else All operations succeed
Route->>DB: Re-read table
Route-->>Client: Updated table
end
end
Reviews (5): Last reviewed commit: "feat(api): make v2 table PATCH state whi..." | Re-trigger Greptile
…ry 423 Greptile P1: PATCH applied locks, rename and move as three sequential transactions, so a folder rejected mid-request left the earlier writes persisted while the response reported failure — and the schema-changed signal was skipped, leaving open clients on stale state. Every rejectable condition now runs before the first write, and the signal fires whenever anything did land. Cursor: v2TableLockError dropped the lock kind, so async import, column run, enrichment and table mutations returned a bare LOCKED. A table has four independent locks, so the caller could not tell which to clear.
|
@cursor review |
…n ones
The previous commit named the lock only where the rejection was thrown and
caught at the route boundary. Where it instead arrives as a classified
`errorCode: 'locked'` outcome — delete table, delete row, update column,
and the table mutations — the kind was dropped, so those 423s stayed
unactionable while their neighbours improved.
The orchestration results now carry `lock`, and a shared
`v2TableOrchestrationError` renders both arrival paths into the same
`{ code, message, details: { lock } }` body. `details` is omitted rather
than sent null when the kind is unknown, so a caller branching on it sees
absence instead of a phantom value.
|
@cursor review |
`POST /import-async` pointed callers at `GET /api/v2/tables/jobs` to track progress, but that endpoint filters to `type = 'export'` — imports are derived onto the table itself, one write job at a time, and exports get a separate list precisely because they are excluded from that derivation. The public Table shape omitted those derived fields, so an async import could be started and cancelled but never observed to completion, failure, or progress. That is the gap the import/export/job-control set was meant to close. Table now carries `job` — id, type, status, rowsProcessed, error, or null when idle — and the import-async docs point at the table rather than the export list.
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 83e04cb. Configure here.
Greptile held the PR at 4/5 on the residual non-atomicity and named two acceptable resolutions: make PATCH atomic, or have the contract adopt and expose partial-success explicitly. Atomicity would mean threading one transaction through renameTable, moveTableToFolder and updateTableLocks — three shared service functions with four non-test callers including the first-party route and two copilot tools — and deferring their per-operation audits to commit time. That is a refactor of shared write paths well outside this PR. So the contract states it instead. Every rejectable condition is already pre-validated, so a failure here is a genuine fault; when one follows a successful operation the error now carries `details.applied` listing what is live. Absent when nothing applied, so its presence always means "these changes took effect despite the error". Documented on the operation. `v2ErrorForOrchestration` gained the optional `details` this needs.
|
Addressing the 4/5 hold on non-atomic PATCH (b25c426) — the finding is fair, and I took the second of the two resolutions you named. Why not atomicity. It would require one transaction spanning What landed instead. The endpoint now adopts partial-success explicitly rather than implying atomicity it does not have:
Two tests pin both halves: a move failing after a successful rename asserts This required giving Worth flagging for the human reviewer: the first-party |
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit b25c426. Configure here.
Summary
/api/v2/tablesso a public caller can do what the internal surface can — previously it could read and write rows but not rename a table, restore one, manage views, run an enrichment column, or import/exportPATCH /tables/[tableId](rename/move/lock — neither v1 nor v2 had it),POST /restore, saved views (GET/POST /views,GET/PATCH/DELETE /views/[viewId]),POST /columns/run+POST /rows/[rowId]/enrichment/[groupId],GET /groups,POST /rows/findPOST /import,/import-async,/import-csv;GET /export,POST /export-async,GET /export/download;GET /jobs,POST /job/cancel,POST /cancel-runslib/table/orchestration/import.tsandlib/table/export-stream.tsout of the first-party routes and repoints those routes at them, so v1 and v2 can't drift on what an import or export doesevents/stream,metadataanddispatchesstay internal — editor state, not public APINotes for review
TablegainsfolderIdandlocks(toApiTable,v2ApiTableSchema, OpenAPITable). Without themPATCHcould write two fields the surface couldn't read back. Additive; existing response examples updated.PATCHcarries the first-party permission split:name/folderIdneedwrite,locksneedsadmin+ thetable-locksfeature. Clearing a lock still works with the feature off, so a locked table is never stranded.runColumnBodySchemaandcancelTableRunsBodySchemawere split into un-refined base objects plus shared refine helpers (following the existinginsertTableRowBodyBaseSchema/rowAnchorMutexRefineprecedent) — Zod forbids.extend()on a refined schema and v2 narrowsfilterto predicate-only. Internal behavior unchanged.GET /exportis the one v2 success body that isn't{ data }— it's the file.mode: 'stream'contract, rate-limit headers set by hand.parseRequest(nobodyon the contract); form fields are parsed against contract form schemas,workspaceIdis required ahead of the file part, and the 10 MB proxy cap is enforced — without it Next silently truncates and a partial import reports success.lockfield was being dropped. The orchestration now threads the lock kind through, so v1 renders{ error, lock }again (nodetails, which would make the client swallow the toast) and v2 surfaces it asdetails: { lock }.improvement/v2-endpoints, same as feat(api): complete the v2 workflows resource with versions and CRUD #6184 and feat(cli): Sim CLI with AWS-style profiles and a platform key exchange #6147.Type of Change
Testing
bun run check:api-validation:strict— passes (route baseline 1046 → 1062)bun run check:openapi— passes, 109 operations / 100 contracts cross-checkedbun run type-check,bun run lint:check— passbunx vitest run app/api/v2/tables app/api/table lib/table— 82 files, 1079 tests passlib/billing/storage/tracking.test.ts,executor/handlers/pi/cloud-review-tools.test.ts) reproduce on a clean tree with these changes stashed — pre-existing and unrelatedroute.test.tsper new route: gate off → 404, invalid body → 400, access denied → 403, rate limited → 429, happy path asserting the exactdatashape and the lib callChecklist