Skip to content

refactor(tables): make lib/table/orchestration the single implementation - #6134

Merged
TheodoreSpeaks merged 10 commits into
improvement/v2-endpointsfrom
improvement/orchestration-migration
Aug 1, 2026
Merged

refactor(tables): make lib/table/orchestration the single implementation#6134
TheodoreSpeaks merged 10 commits into
improvement/v2-endpointsfrom
improvement/orchestration-migration

Conversation

@TheodoreSpeaks

@TheodoreSpeaks TheodoreSpeaks commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #5273base is improvement/v2-endpoints, not staging.

The repo's convention is that business logic lives in lib/[resource]/orchestration so the Sim UI, the public API, and the copilot tools share one implementation. lib/table/orchestration exported exactly one function (performRestoreTable); everything else was implemented per-caller. Column update existed four times — the UI route, v1, v2, and the copilot table tool — each calling the same column services but owning its own guards, error mapping, and audit.

The copies had drifted, and the drift was the bug. This PR extracts the logic and deletes the copies; the fixes fall out of the extraction rather than being patched at each call site.

performUpdateTableColumn, performDeleteTable, and performDeleteTableRow now own the logic, and all ten call sites reduce to auth → parse → call → render. Net ~570 lines deleted from routes.

Behavior this consolidates

Each of these was previously true on only some paths:

  • The typeChanging guard. updateColumnType early-returns on an unchanged type and drops any options sent with it, so restating the current type alongside new options silently discarded them. v2 had no guard at all — and since its contract shares v1's body schema, it accepted options and ignored them, returning 200.
  • The select-unique guard. Each write is its own locked transaction, so a rename or type change paired with a constraint write that is going to fail commits first and then throws, leaving a half-applied schema change. Missing from v2.
  • Stable select-option ids. Cells reference the option id, so an edit that re-sends an option by name must reuse it or every cell holding it is orphaned. Only the copilot path did this. normalizeSelectOptionsInput moves to lib/table/select-options and now covers every caller — it preserves a supplied id, so it is a no-op for the fully-formed options the HTTP contracts accept and a repair for name-only options.
  • required forwarded into the type and options writes, so a conversion validates against the constraint the same request is setting rather than the column's current one.
  • An audit on every successful update. The UI route and the copilot tool emitted none — column edits from the Sim UI were silently unaudited.
  • Single-row delete through the row service. v2 did a raw db.delete on user_table_rows, skipping assertRowDelete and deleteOrderedRow: a delete-locked table returned 200 and the row-count/order bookkeeping never ran. v1 carries a comment warning against exactly this.
  • The delete actor handed to deleteTable, which audits only when a row was actually archived — omitting the actor is how rollback callers opt out. v1 and v2 omitted it and audited themselves outside that check, emitting TABLE_DELETED for a no-op delete of an already-archived table.

Error contract

Failure classes come back as OrchestrationErrorCode. The shared contract moves out of lib/workflows/orchestration/types into lib/core/orchestration/types — resource-neutral code (lib/folders) already had to import it from a workflow path — and gains a locked class mapping to 423, since both tables and workflows have a lock that forbids a mutation and every caller was translating that itself.

v2 renders these through a new v2ErrorForOrchestration, mirroring statusForOrchestrationError on the v1/UI surfaces, so a given failure maps to the same status on every surface.

Review guide

The guard logic is in lib/table/orchestration/columns.ts — read that once instead of four route diffs. The route diffs are mechanical deletions.

Tests follow the same split: lib/table/orchestration/columns.test.ts (12 cases) and tables.test.ts cover the guards, classification, and audit; the route tests are now thin, asserting delegation and the failure-class → envelope mapping.

Not in scope

performCreateTable (where the copilot path has no explicit permission check of its own and the UI has folder validation the others lack) and the knowledge resource (where the copilot path emits no audit for any KB mutation, defaults minSize: 1 against the API's 100, and KB search has ~1200 duplicated lines) follow in the next PR on this branch.

One user-visible bug found but left for the connector consolidation: the copilot's delete_connector reports "Associated documents have been removed", but reaches the route by internal HTTP self-call without the deleteDocuments param, which defaults to false — the documents are kept.

Verification

app/api/v2 + app/api/v1/tables + app/api/table + lib/table + lib/copilot1838 pass.

bun run check:api-validation, bun run check:openapi (6 specs, 57 operations, 48 contracts), bun run type-check, and full bun run lint:check all clean.

🤖 Generated with Claude Code

@vercel

vercel Bot commented Jul 31, 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 1, 2026 12:49am

Request Review

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor

cursor Bot commented Jul 31, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Broad refactor across UI, v1, v2, and copilot table APIs with intentional behavior fixes (locks, audits, column guards); regression risk is mitigated by orchestration tests but many HTTP paths changed.

Overview
Moves table column updates, deletes, renames, folder moves, and lock changes into lib/table/orchestration, so the Sim UI routes, v1/v2 APIs, copilot tools, and VFS mutations all call the same functions instead of four divergent column-update implementations (~570 route lines removed).

performUpdateTableColumn owns the guards that had drifted: unchanged-type + options routing, pre-write validation (currency, rename collision, workflow-output constraints, select-as-unique), folding renames into the last transactional write, and normalizeSelectOptionsInput for stable select option ids. Successful updates now always recordAudit.

performDeleteTable / performDeleteTableRow centralize archive/delete (audit and PostHog only when a row was actually archived), and v2 row delete no longer uses a raw db.delete that skipped delete locks and row bookkeeping.

Error handling introduces shared OrchestrationError and lib/core/orchestration/types (moved out of workflows). Routes replace message-substring status mapping with orchestrationErrorResponse / statusForOrchestrationError and v2 v2ErrorForOrchestration / v2CaughtOrchestrationError. Table/column/import services throw classified errors instead of bare Error where failures are caller-fixable.

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

@greptile-apps

greptile-apps Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR consolidates table mutation behavior into shared orchestration functions.

  • Routes now delegate column updates, table deletion, and row deletion to shared implementations.
  • Column writes combine renames, typed changes, and constraints within one transaction while normalizing select-option IDs.
  • HTTP callers forward request context so successful mutation audits retain client provenance.
  • Shared orchestration errors provide consistent status and response-envelope mapping across UI, v1, and v2 APIs.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains; the previously reported audit-provenance and partial-column-update paths are addressed at the current head.

Important Files Changed

Filename Overview
apps/sim/lib/table/orchestration/columns.ts Centralizes column-update guards, option normalization, transactional write selection, error classification, and auditing.
apps/sim/lib/table/columns/service.ts Applies pending renames and requested constraints inside the same locked transaction as type, options, or currency updates.
apps/sim/lib/table/orchestration/tables.ts Centralizes table and row deletion behavior while forwarding actor and request context to successful audits.
apps/sim/lib/table/select-options.ts Normalizes name-only select options while preserving supplied or existing stable option IDs.
apps/sim/lib/core/orchestration/types.ts Defines resource-neutral orchestration errors, status mapping, and optional HTTP request context.
apps/sim/app/api/v2/lib/response.ts Adds consistent v2 response-envelope rendering for orchestration failure classes.
apps/sim/app/api/v2/tables/[tableId]/columns/route.ts Reduces the v2 column endpoint to authorization, parsing, orchestration delegation, and response rendering.
apps/sim/app/api/v2/tables/[tableId]/rows/[rowId]/route.ts Delegates single-row deletion to shared orchestration instead of bypassing row-lock and ordering services.

Sequence Diagram

sequenceDiagram
  participant Caller as UI / v1 / v2 / Copilot
  participant Route as Route or Tool Handler
  participant Orch as Table Orchestration
  participant Service as Table Service
  participant DB as Database
  participant Audit as Audit Log

  Caller->>Route: Authenticated table mutation
  Route->>Orch: Parsed input + actor + optional request
  Orch->>Orch: Normalize and preflight guards
  Orch->>Service: Single coordinated mutation
  Service->>DB: Locked transaction
  DB-->>Service: Updated table
  Service-->>Orch: Mutation result
  Orch->>Audit: Record successful mutation
  Orch-->>Route: Typed outcome
  Route-->>Caller: Surface-specific response
Loading

Reviews (5): Last reviewed commit: "refactor(tables): classify failures by t..." | Re-trigger Greptile

Comment thread apps/sim/app/api/v2/tables/[tableId]/route.ts Outdated
Comment thread apps/sim/app/api/v2/tables/[tableId]/columns/route.ts Outdated
Comment thread apps/sim/app/api/v2/tables/[tableId]/columns/route.ts Outdated
Comment thread apps/sim/app/api/v2/tables/[tableId]/columns/route.ts Outdated
TheodoreSpeaks and others added 2 commits July 31, 2026 14:47
…rkflows

OrchestrationErrorCode and statusForOrchestrationError are the contract every
lib/[resource]/orchestration module returns against, but they lived inside the
workflows module, so resource-neutral code (lib/folders) already had to import
from a workflow path. Moved to lib/core/orchestration/types.

Adds a 'locked' class mapping to 423. Both tables and workflows have a lock
that forbids a mutation, and each caller was translating that to a status
itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Column update was implemented four times — the UI route, v1, v2, and the
copilot table tool — each calling the same column services but owning its own
guards, error mapping, and audit. The copies had drifted, and the drift was the
bug: v2 was missing both guards, only the copilot copy minted stable option
ids, and only v1/v2 audited.

performUpdateTableColumn, performDeleteTable, and performDeleteTableRow now own
that logic; all ten call sites reduce to auth, parse, call, render. The guards
are asserted once in lib/table/orchestration rather than four times against
four routes.

Behavior this consolidates, previously true on only some paths:

- The typeChanging guard. updateColumnType early-returns on an unchanged type
  and drops any options sent with it, so restating the current type alongside
  new options silently discarded them. v2 had no guard at all and, since its
  contract shares v1's body schema, accepted options and ignored them.
- The select-unique guard. Each write is its own locked transaction, so a
  rename or type change paired with a constraint write that is going to fail
  commits first and then throws, half-applying the schema change.
- Stable select-option ids. Cells reference the option id, so an edit that
  re-sends an option by name has to reuse it or every cell holding it is
  orphaned. Only the copilot path did this; normalizeSelectOptionsInput moves to
  lib/table/select-options and now covers every caller. It preserves a supplied
  id, so it is a no-op for the fully-formed options the HTTP contracts accept.
- required forwarded into the type and options writes, so a conversion
  validates against the constraint the same request is setting.
- An audit on every successful update. The UI route and the copilot tool
  emitted none.
- Single-row delete through the row service. v2 did a raw db.delete, skipping
  assertRowDelete and deleteOrderedRow, so a delete-locked table returned 200
  and the row-count bookkeeping never ran.
- The delete actor handed to deleteTable, which audits only when a row was
  actually archived. v1 and v2 omitted it and audited themselves outside that
  check, emitting TABLE_DELETED for a no-op delete of an archived table.

Failure classes come back as OrchestrationErrorCode; v2 renders them through a
new v2ErrorForOrchestration, mirroring statusForOrchestrationError on the v1
and UI surfaces, so a given failure maps to the same status everywhere.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@TheodoreSpeaks
TheodoreSpeaks force-pushed the improvement/orchestration-migration branch from 569f89f to 9736288 Compare July 31, 2026 21:48
@TheodoreSpeaks TheodoreSpeaks changed the title fix(tables): correctness gaps found auditing the orchestration convention refactor(tables): make lib/table/orchestration the single implementation Jul 31, 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 9736288. Configure here.

TheodoreSpeaks and others added 4 commits July 31, 2026 16:02
The base rewrote column update while this branch was extracting it: writes now
address the column by stable id, a rename and constraint change ride inside the
typed write's transaction instead of following it, and `currencyCode` joins the
update shape for the new `currency` column type.

performUpdateTableColumn is rebuilt from that implementation rather than the
one this branch originally extracted, so the extraction carries the base's
logic forward instead of regressing it. All four callers delegate unchanged.

lib/table/select-options.ts arrived independently on the base as a client-safe
leaf module for option-id resolution; normalizeSelectOptionsInput joins it there
rather than replacing it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The base's route tests assert which column service each payload reaches — the
behavior that now lives in performUpdateTableColumn. They mocked the `@/lib/table`
barrel; the orchestration module imports the service directly, so they mock that
too and keep asserting the same thing through the extracted implementation.

The orchestration tests move onto the base's semantics: writes address the
stable column id, a rename rides inside the write it accompanies rather than
running first, and the currency guards replace the non-select options guard the
service now owns.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment thread apps/sim/lib/table/orchestration/tables.ts
`lib/table/service.ts` wrote its own audit rows, so whether an operation was
audited depended on which function a caller reached for rather than on a user
having performed it. That is what let v1 and v2 audit a no-op delete, and what
made `deleteTable`'s optional `actingUserId` double as an audit opt-out flag.

Worse, most sites fell back to `actingUserId ?? createdBy`, so an unattributed
call was logged against the table's *creator*. The copilot `mv` path passed no
actor at all: renaming someone else's table recorded them as the renamer.

Audit now lives in the orchestration functions — performDeleteTable,
performRenameTable, performMoveTableToFolder, performUpdateTableLocks — and
the services just write. Internal callers (folder cascade, import rollback)
keep calling the service and are silent by construction rather than by
remembering to omit an argument.

Two services now return what the audit needs: `deleteTable` reports whether it
actually archived a row, so a repeat delete logs nothing; `updateTableLocks`
returns the before/after locks, since only the locked write can observe the
transition its description names.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment thread apps/sim/lib/table/orchestration/tables.ts
Comment thread apps/sim/lib/table/orchestration/tables.ts
Comment thread apps/sim/lib/copilot/tools/server/table/user-table.ts
Comment thread apps/sim/lib/table/orchestration/columns.ts
…ation

Moving the audits into the orchestration functions dropped three things the
routes had been carrying, and added one the orchestration now owns twice.

- The v1 and v2 column-update routes passed `request` to `recordAudit`, so
  their audit rows recorded the caller's IP and user-agent. The orchestration
  function had no way to receive it. Every table orchestration function now
  takes an optional `OrchestrationRequestContext` and every HTTP route
  forwards it; the copilot and VFS callers, which have no request, omit it.
- `classifyTableMutation` matched `TableConflictError` on "already exists"
  appearing in the message and reported it as `validation`, turning the UI
  route's 409 on a duplicate table rename into a 400. It now matches the type,
  the way `performRestoreTable` already did.
- `captureServerEvent` ran on every delete while the audit was gated on a row
  actually being archived, so a repeat delete of an archived table still
  reported `table_deleted`. Both now hang off the same evidence.
- The copilot delete path kept its own `captureServerEvent` from when the
  service did not emit one, double-counting every copilot table delete.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YGzbVDZpe2dEALbu2BUU8a
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/lib/copilot/tools/server/table/user-table.ts Outdated
A copilot `update_column` payload whose only content was the column's current
type used to return success with the live schema, while the v1, v2, and UI
routes rejected the same payload with "No updates specified". Delegating to
`performUpdateTableColumn` unified them onto the routes' rejection — correct,
but the message tells the caller its request was empty when it named a type.

The orchestration function now reports the same thing `updateColumnType` reports
when it loses this race concurrently: the column is already that type, re-issue
without the type change. An empty payload still reads "No updates specified".

Drops the copilot's `outcome.table ?? tableForUpdate` fallback with it — the
comment described the no-op that can no longer reach that line, and a success
always carries a table.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YGzbVDZpe2dEALbu2BUU8a
@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 0ee4499. Configure here.

The table module decided HTTP statuses by searching error messages for
phrases. `VALIDATION_MESSAGE_FRAGMENTS` and `ROW_WRITE_ERROR_PATTERNS` held 32
substrings between them, and fifteen more lists were inlined in routes — 83
matchers over 17 files, each its own copy of the guesswork and already drifted
apart. It made message wording load-bearing: `TableRowLimitError`'s own doc
comment noted that its text had to contain "row limit" for a route to answer
400, and adding "already exists" to a rename message silently demoted a 409 to
a 400 (the bug fixed one commit ago, by adding another special case).

Services now throw `OrchestrationError`, which carries the transport-neutral
`OrchestrationErrorCode` the layers above already speak. Classification is one
`instanceof` in `orchestrationErrorResponse` (UI + v1) and
`v2CaughtOrchestrationError` (v2). Every pattern list is gone. Wording is free
to change; an unclassified error still becomes a generic 500, which is what an
unexpected fault should be.

`asOrchestrationError` walks the `cause` chain rather than testing the caught
value directly: drizzle wraps a throw raised inside a transaction callback in a
`DrizzleQueryError` whose own message is the failed SQL, so a bare `instanceof`
would drop every failure raised inside `withLockedTable`. That is the same
reason `rootErrorMessage` had to dig for a root cause before.

Three throws stay bare `Error` deliberately — `Table ID mismatch`, `Workspace
ID mismatch`, and `Failed to build upsert conflict predicate` are internal
invariants no consumer classified, and they keep falling through to a 500.
`Insufficient capacity` was in the pattern list with no producer anywhere in
the codebase.

Status changes, all deliberate:

- `'forbidden'` joins the code union so the table-row-limit ceiling keeps its
  403; without it this refactor would have flattened it to 400.
- import-async's table-limit rejection: 400 -> 403, matching the two other
  create routes it had drifted from.
- Renaming a table to an invalid name: 500 -> 400. `validateTableName`
  messages don't contain "Invalid", so no matcher ever caught them.
- Restoring a table that isn't archived, or into an archived workspace:
  500 -> 400.
- A duplicate *column* name stays `validation`/400 rather than becoming a 409
  like a duplicate table name. Both v1 and the orchestration have always
  answered 400 for it; changing a published status is not this refactor's job.

The twelve tests that changed were asserting the substring mechanism itself,
constructing plain `Error`s with magic strings. They now assert the real
contract, plus new cases pinning that identical wording carrying no
classification stays internal and keeps its message off the wire.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YGzbVDZpe2dEALbu2BUU8a
@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 d449610. Configure here.

@TheodoreSpeaks
TheodoreSpeaks merged commit 5fea5f7 into improvement/v2-endpoints Aug 1, 2026
5 checks passed
@TheodoreSpeaks
TheodoreSpeaks deleted the improvement/orchestration-migration branch August 1, 2026 01:24
TheodoreSpeaks added a commit that referenced this pull request Aug 1, 2026
#6150 branched before #6134, so skill-lifecycle.ts imports
@/lib/workflows/orchestration/types — the module #6134 moved to
@/lib/core/orchestration/types. Git merged a file deletion on one side with a
new file referencing it on the other: no textual conflict, broken build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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