Skip to content

feat(api): expand v2 tables with stateless multipart transfers - #6188

Merged
TheodoreSpeaks merged 14 commits into
improvement/v2-endpointsfrom
feat/v2-tables-coverage
Aug 4, 2026
Merged

feat(api): expand v2 tables with stateless multipart transfers#6188
TheodoreSpeaks merged 14 commits into
improvement/v2-endpointsfrom
feat/v2-tables-coverage

Conversation

@TheodoreSpeaks

@TheodoreSpeaks TheodoreSpeaks commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • expand the public v2 Tables surface with table, row, view, workflow-group, import, and export operations
  • use one signed stateless multipart flow for Files and upload-backed table imports across S3, Azure Blob, GCS, and local storage
  • keep part state in the storage provider, pass part ETags only at completion, and use the existing table_jobs rows for import progress and terminal state
  • move the first-party Files and Tables UI onto the same transfer contracts as the API
  • remove the upload_sessions and table_imports schema and migrations; retain only the additive nullable workspace_files.size_bytes migration required for files above the PostgreSQL int4 limit
  • regenerate the v2 Files and Tables OpenAPI artifacts

Type of Change

  • New feature

Testing

  • bun run lint
  • bun run type-check
  • bun run check:migrations
  • bun run check:api-validation:strict
  • bun run check:openapi
  • full ship audit suite
  • 19 focused Vitest tests for file uploads, multipart clients, and signed upload tokens

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added and updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

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.
@vercel

vercel Bot commented Aug 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Aug 4, 2026 12:35am

Request Review

@cursor

cursor Bot commented Aug 2, 2026

Copy link
Copy Markdown

PR Summary

Low Risk
Documentation-only OpenAPI JSON changes; no runtime behavior in this diff, though the described API surface is large and affects external integrators.

Overview
Regenerates the v2 Files and v2 Tables OpenAPI artifacts so public docs match the new transfer and Tables surfaces.

Files: The buffered POST /api/v2/files upload is no longer a public post operation (marked x-removed-buffered-post). The spec adds a stateless multipart flow—create session, signed part PUT URLs, complete, and abort—plus a required upload-token header on upload follow-up calls. Descriptions note up to 5 GB via multipart versus the old ~100MB buffered path.

Tables: List/create examples and the Table model now include folderId, locks, and job. New documented operations include PATCH (rename/move with non-atomic partial-success semantics), restore, views, workflow groups (add/update/delete and column runs), find rows, and cancel-runs. Legacy import-async, export-async, /jobs, export/download, and job/cancel routes are retained only as x-removed-* stubs. The replacement model is /tables/imports (multipart upload + job-backed processing) and /tables/exports with /exports/{exportId}/download. Shared upload-token / optional upload-token parameters tie table imports to the same multipart contract as Files.

The tables spec also grows the components section (predicates, views, workflow groups, job envelopes, etc.) and adds Conflict, Locked, PayloadTooLarge, and Gone response refs.

Reviewed by Cursor Bugbot for commit 739bd6d. Bugbot is set up for automated code reviews on this repo. Configure here.

@greptile-apps

greptile-apps Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR substantially expands the public v2 Tables API and introduces shared stateless multipart transfer contracts for Files and table imports.

  • Adds table, row, view, workflow-group, import, and export operations.
  • Unifies multipart uploads across supported storage providers and first-party clients.
  • Updates database migrations, API contracts, tests, and generated OpenAPI artifacts.

Confidence Score: 5/5

The PR appears safe to merge with respect to the previously reported issues.

No blocking failure remains: deterministic PATCH rejections occur before writes, and every post-write failure path now reports the operations that were applied.

Important Files Changed

Filename Overview
apps/sim/app/api/v2/tables/[tableId]/route.ts The PATCH route now prevalidates deterministic failures, reports committed operations on every post-write error path, and signals schema changes after partial success.
apps/sim/lib/api/contracts/v2/tables.ts Defines the expanded v2 Tables boundary contracts and strict update validation used by the routes.
apps/sim/lib/uploads/multipart-session/service.ts Implements the shared stateless multipart transfer lifecycle used by file uploads and table imports.
packages/db/schema.ts Aligns persistent schema with stateless transfer state while retaining the larger file-size field.
packages/db/migrations/0280_smart_la_nuit.sql Applies the additive workspace-file size migration required by the revised upload limits.

Reviews (12): Last reviewed commit: "fix(api): make table import completion r..." | Re-trigger Greptile

Comment thread apps/sim/app/api/v2/tables/[tableId]/route.ts
Comment thread apps/sim/app/api/v2/tables/utils.ts
…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.
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/app/api/v2/tables/[tableId]/route.ts
…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.
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/docs/openapi-v2-tables.json Outdated
`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.
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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.
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

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 renameTable, moveTableToFolder and updateTableLocks. Those are shared service functions with four non-test callers — the v2 route, the first-party PATCH /api/table/[tableId], and two copilot tool handlers (table/user-table.ts, vfs-mutate.ts) — and each currently opens its own transaction and emits its own audit post-commit. Making them composable means adding WithTx variants across all three and deferring their audits to the outer commit, since an audit describing a write that later rolls back is worse than the partial state it would be fixing. That is a refactor of shared write paths, with its own failure modes, and it does not belong in a PR about the public v2 surface.

What landed instead. The endpoint now adopts partial-success explicitly rather than implying atomicity it does not have:

  • Every rejectable condition is already pre-validated (previous commit), so no bad request can produce partial state. What remains is only a genuine fault: a lost race, the table archived mid-request, a database error.
  • When such a fault follows a successful operation, the error carries error.details.applied listing what is nevertheless live — ["locks"], ["name"], ["folderId"], or a combination. A caller reconciles from that instead of re-reading and diffing to discover it.
  • The field is absent when nothing applied, so its presence always means "these changes took effect despite the error" — the distinction that makes it actionable rather than decorative.
  • Documented on the operation in the OpenAPI spec, so it is part of the published contract rather than undocumented behaviour.

Two tests pin both halves: a move failing after a successful rename asserts details equals { applied: ["name"] }, and a first-operation failure asserts details is undefined and that no later operation ran.

This required giving v2ErrorForOrchestration an optional details parameter, which it arguably should have had — the lock-kind work in the previous commit had to route around its absence.

Worth flagging for the human reviewer: the first-party PATCH /api/table/[tableId] has the same non-atomic shape and no such reporting. This PR does not otherwise touch that file, so I have left it rather than expanding scope — happy to follow up separately.

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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.

…eat/v2-tables-coverage

# Conflicts:
#	scripts/check-api-validation-contracts.ts
The new PATCH /api/v2/tables/[tableId] accepted a `locks` object, gated
on workspace admin plus the table-locks feature. That still lets an API
key clear the guard placed there to stop it: `write` is the floor for
the endpoint, and admin keys are ordinary API keys, so a lock is no
longer a boundary the key cannot cross.

Locks stay readable on the table resource and enforcement is unchanged
(a locked verb still returns 423). Changing one is now a first-party
admin action only.

The v2 body is declared here rather than reusing the first-party
updateTableBodySchema, which keeps its `locks` field so the UI can still
toggle them. It is .strict(), so a request carrying `locks` is rejected
with a 400 naming the field instead of silently succeeding without
applying it.
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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 03e6ee9. Configure here.

Drops the three synchronous entry points: POST /tables/[tableId]/import,
POST /tables/import-csv, and GET /tables/[tableId]/export.

Sync import tied a write to the lifetime of an HTTP request. The body
*was* the data, so it carried a 10 MB cap that Next silently truncates
past — a partial import reporting success. It also had no job, so a
timeout mid-write left rows in place with nothing to poll and nothing to
cancel. The async path reads the file from storage instead: upload via
POST /api/v2/files for a key, start with POST /import-async, watch
GET /tables/[tableId] -> job, stop with POST /job/cancel.

Sync export carried no such hazard, but one shape per operation beats
two: with both removed the surface has exactly one way to move a table
in or out, and the CLI wraps the extra calls.

This also removes the last multipart handling in v2 tables. Those were
the only routes bypassing parseRequest — form fields were parsed by hand
against separate form schemas, outside the contract system every other
v2 write goes through.

Create-a-table-from-CSV is now two calls: POST /tables, then
/import-async with createColumns. csvImportModeSchema is append|replace,
so there is no single-call create.

Route baseline 1064 -> 1061.
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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 f6f5084. Configure here.

The docstring claimed there is no synchronous upload endpoint and so no
request-body size cliff. Both are wrong: POST /api/v2/files is a
synchronous multipart upload with a 100 MB cap, and it is the only v2
upload path (presigned is deliberately absent).

What async-only actually bought: the cap went 10 MB -> 100 MB, it fails
on an explicit size check and a bounded body read rather than a proxy cap
that silently truncates, authorization completes before any body is
buffered, and the table write is a job that can be watched and cancelled.
@TheodoreSpeaks TheodoreSpeaks changed the title feat(api): expand the public v2 tables surface feat(api): unify v2 file and table transfers Aug 3, 2026
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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 6614484. Configure here.

@TheodoreSpeaks TheodoreSpeaks changed the title feat(api): unify v2 file and table transfers feat(api): expand v2 tables and unify durable transfers Aug 3, 2026
@TheodoreSpeaks TheodoreSpeaks changed the title feat(api): expand v2 tables and unify durable transfers feat(api): expand v2 tables with stateless multipart transfers Aug 4, 2026
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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 a36741f. Configure here.

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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 739bd6d. Configure here.

@TheodoreSpeaks
TheodoreSpeaks merged commit 5692261 into improvement/v2-endpoints Aug 4, 2026
5 checks passed
@TheodoreSpeaks
TheodoreSpeaks deleted the feat/v2-tables-coverage branch August 4, 2026 00:58
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