Skip to content

Commit 0dcbc56

Browse files
feat(tables): per-table mutation locks (schema/insert/update/delete) (#5960)
* feat(tables): per-table mutation locks (schema/insert/update/delete) Adds four independent, admin-only locks to a table so a workspace can make it append-only, read-only, or schema-frozen. Enforced at the lib/table service layer rather than the routes, so the API, workflow blocks, and Mothership (which calls the services directly) are all covered by one assert. Violations return 423; changing a lock requires workspace admin and returns 403. * fix(tables): address review — CI snapshot, lock-aware jobs, UI gating - Add the drizzle meta snapshot the hand-written migration was missing, which made CI regenerate the lock columns as a phantom 0271 - Stop a queued or retried delete/update job that starts after its lock is enabled; a run that has already committed pages still finishes, since pages are never rolled back and aborting would leave an uncancelled half-done state - Map TableLockedError to 423 on the internal single-row DELETE (was a 500) - Gate the lock settings UI behind NEXT_PUBLIC_TABLE_LOCKS so it can't open onto a Save that 403s against the server-side flag - Respect the delete lock on column drops in the grid, matching assertColumnDestructive, instead of failing only on click * fix(tables): close lock gaps in async import and undo/redo - Assert insert (and delete for replace) at the start of the import worker. Replace deleted every row before the first insert assert, so an insert-locked table was wiped and then failed, leaving it empty. - Run the delete/update worker start checks through assertRowDelete / assertRowUpdate instead of reading the lock flags directly, so the worker and its enqueue site apply identical rules — including the workflow-column exemption a bulk update was being cancelled despite. - Make undo/redo verb mapping direction-aware for column ops: dropping or retyping a column needs the delete lock clear too, matching assertColumnDestructive, so those steps are skipped instead of stranded. * fix(tables): stop locks over-blocking workflow runs, duplicate, and imports - Run / Re-run / Stop no longer inherit the cell-edit gate: they write only workflow-output columns (which the update lock exempts) and Stop is a cancel - Duplicate needs only the insert lock — it inserts a full copied row, so it stays available on an append-only table - updateWorkflowGroup asserts the destructive rule only when a patch actually drops or remaps output columns; rename / autoRun / mapping edits need just the schema lock - Gate the import-async enqueue on insert (and delete for replace) so a locked table 423s instead of claiming the write-job slot and failing in the worker - Disable Import CSV under an insert lock, and wire the expanded cell editor and the previously-unused stable add-row handler to the lock-aware flags * fix(tables): keep column menu usable and let locks always be cleared - Pass the blocked-delete handler instead of undefined so ColumnOptionsMenu still mounts on a delete-locked table; withholding it hid the entire menu, including Insert column, which the delete lock must not affect - Restore ColumnHeaderMenu readOnly to permission-only: it swaps the header for a static label, which was also disabling pin, column-select and open-config - Assert the schema lock at the import-async enqueue when createColumns is set - Allow a locks PATCH that only clears locks even when the feature flag is off, and keep the settings entry reachable on a locked table, so flipping the kill switch can't strand a table with locks nobody can remove * fix(tables): scope the update-lock exemption and re-check locks per page - Make the workflow-output carve-out opt-in via `computedWrite`, set only by the cell-write path. It was caller-agnostic, so an ordinary API caller could PATCH a workflow-output column on an update-locked table - Re-read the lock before every page in the delete and update runners. The single worker-start check went stale immediately, so enabling a lock could not stop a job already deleting or overwriting rows. Committed pages still stay committed, exactly as with an explicit cancel. Import keeps its one-shot check: a half-imported table needs the deletes the lock now forbids - Route Insert column to the locked-action modal under a schema lock instead of leaving it live (regular header) or hiding the whole menu (group header) * fix(tables): revalidate locks in-transaction and unbreak backfill + partial unlock - Re-assert the lock inside each page-write transaction, under the same advisory lock updateTableLocks takes, so check-then-write is atomic against a lock change. The per-page check alone still left the page-selection await between the assert and the write - Pass computedWrite from the backfill runner: batchUpdateRows is the workflow-output backfill path, so scoping the exemption had made rebuilding outputs 423 on an update-locked table while live cell writes still worked - Gate the feature flag on a lock actually going off->on rather than on an all-false payload. The settings UI always submits all four flags, so with the flag off an admin could not clear one lock while another stayed on - Hoist the lock-kind -> flag map to lib/table/types as TABLE_LOCK_FLAGS * fix(tables): guard import batches in-transaction and split paste by verb - Re-assert the insert lock inside each import batch's insert transaction, under the same advisory lock updateTableLocks takes. Reversing the earlier "let the file finish" call: an admin can lift the lock to clean up a partial import, so honouring it beats letting the rest of the file land - Gate paste per verb. Overwriting existing rows is an update and extending past the last row is a full-row insert, so a paste-append still works on an append-only table while an overwrite explains itself. Refuses the whole paste rather than applying half of it - Space opens the same row editor as double-click, so it now follows the update lock instead of filling in a form that 423s on save * fix(tables): guard the remaining import writes and explain blocked Shift+Enter - Route the replace-mode wipe, the inferred-schema write and createColumns through the same guardBatch transaction as the batch inserts, so a delete or schema lock committed while the file is downloading or being sampled is seen instead of the job-start snapshot - Shift+Enter takes the manual-add path, so it now opens the lock modal like the Add row button instead of returning silently * improvement(tables): surface blocked lock actions as a toast, not a modal - Replace TableLockedModal with a warning toast carrying a "Lock settings" action button for admins. Being told you can't edit shouldn't cost a dismiss click, and the button still routes admins straight to the panel - Dedupe by id so repeated attempts on a locked cell replace one notice instead of stacking a column of them - Move the copy into lock-copy.ts alongside the rest of the lock vocabulary and tighten it for toast length * fix(tables): don't show the update-lock notice to users without write access Double-click checked the update lock before any permission check, so a read-only member on a locked table got "Editing rows is locked" — misleading (the lock isn't why they can't edit) and it swallowed the expanded-cell viewer, which is a legitimate read-only affordance. * improvement(tables): trim the update-lock toast copy * fix(tables): map append-import locks to 423 and stop conflating locks with permissions - The sync append branch returns instead of rethrowing, so the outer catch's mapper never saw a TableLockedError and every lock violation became a 500. Map it in that catch, with a regression test (replace mode already rethrows) - Stop mounting the workflow-group column menu for users without edit access: passing the blocked handlers unconditionally made it appear for read-only members and report a lock even on an unlocked table - Enter/F2 now raises the same lock notice as double-click and Space instead of silently doing nothing - guardBatch returns the freshly-read definition so addTableColumnsWithTx asserts live state; a schema lock cleared mid-import no longer fails the createColumns step. The snapshot pre-asserts now only run as the fallback for callers that pass no revalidator - Append-only no longer labels a table whose schema is also locked, which claimed columns were mutable when they weren't - Import dialog withholds Replace on a delete-locked table and create-column on a schema-locked one, instead of offering a configuration that only 423s * fix(tables): revalidate sync imports under the advisory lock, explain every blocked key - The sync append/replace paths asserted only the request-start snapshot, so a lock committed while the CSV was parsed still let the write through. Both now re-read under the schema advisory lock at the top of their own transaction, taken before acquireRowOrderLock so the order stays advisory -> rows_pos -> definitions - Delete/Backspace, Cmd+D, typeahead and cut raised no notice on an update-locked table; they now explain the lock, and stay silent for users without write access - The multipart import fetch threw a plain Error, dropping the 423 status, so the lock self-heal never ran and the stale detail cache survived. It throws ApiClientError now and both import-into-table hooks call handleTableLockRejection - Force append at submit when the delete lock landed while the dialog was open with Replace already selected * fix(tables): restore the workspace ownership check on copilot row deletes Switching deleteRow/deleteRowsByIds to take a TableDefinition dropped the workspaceId argument that previously scoped the query, and the two branches I added loaded the table without the ownership comparison every other operation in the tool performs — letting a caller delete rows from a table in another workspace. Both now reject a foreign table as not found.
1 parent 8329dac commit 0dcbc56

65 files changed

Lines changed: 19937 additions & 165 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/sim/app/api/table/[tableId]/columns/route.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,13 @@ import {
1919
updateColumnType,
2020
} from '@/lib/table'
2121
import { columnMatchesRef } from '@/lib/table/column-keys'
22-
import { accessError, checkAccess, normalizeColumn, rootErrorMessage } from '@/app/api/table/utils'
22+
import {
23+
accessError,
24+
checkAccess,
25+
normalizeColumn,
26+
rootErrorMessage,
27+
tableLockErrorResponse,
28+
} from '@/app/api/table/utils'
2329

2430
const logger = createLogger('TableColumnsAPI')
2531

@@ -61,6 +67,8 @@ export const POST = withRouteHandler(async (request: NextRequest, context: Colum
6167
},
6268
})
6369
} catch (error) {
70+
const lockError = tableLockErrorResponse(error)
71+
if (lockError) return lockError
6472
if (isZodError(error)) {
6573
return validationErrorResponse(error, 'Invalid request data')
6674
}
@@ -190,6 +198,8 @@ export const PATCH = withRouteHandler(async (request: NextRequest, context: Colu
190198
},
191199
})
192200
} catch (error) {
201+
const lockError = tableLockErrorResponse(error)
202+
if (lockError) return lockError
193203
if (isZodError(error)) {
194204
return validationErrorResponse(error, 'Invalid request data')
195205
}
@@ -255,6 +265,8 @@ export const DELETE = withRouteHandler(
255265
},
256266
})
257267
} catch (error) {
268+
const lockError = tableLockErrorResponse(error)
269+
if (lockError) return lockError
258270
if (isZodError(error)) {
259271
return validationErrorResponse(error, 'Invalid request data')
260272
}

apps/sim/app/api/table/[tableId]/delete-async/route.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { generateRequestId } from '@/lib/core/utils/request'
1010
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
1111
import { markTableDeleteFailed, runTableDelete } from '@/lib/table/delete-runner'
1212
import { markTableJobRunning, releaseJobClaim } from '@/lib/table/jobs/service'
13+
import { assertRowDelete } from '@/lib/table/mutation-locks'
1314
import type { TableDeleteJobPayload } from '@/lib/table/types'
1415
import { accessError, checkAccess, tableFilterError } from '@/app/api/table/utils'
1516

@@ -55,6 +56,11 @@ export const POST = withRouteHandler(async (request: NextRequest, { params }: Ro
5556
return NextResponse.json({ error: 'Cannot delete from an archived table' }, { status: 400 })
5657
}
5758

59+
// Gate the delete lock at enqueue: an admitted job runs to completion, so the
60+
// lock must be checked before the job is created (the worker is a trusted
61+
// continuation and does not re-check).
62+
assertRowDelete(table)
63+
5864
// Validate the filter up front so the caller gets immediate feedback (the worker reuses it).
5965
const filterError = tableFilterError(filter, table.schema.columns)
6066
if (filterError) return filterError

apps/sim/app/api/table/[tableId]/groups/route.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@ import {
1515
deleteWorkflowGroup,
1616
updateWorkflowGroup,
1717
} from '@/lib/table/workflow-groups/service'
18-
import { accessError, checkAccess, normalizeColumn } from '@/app/api/table/utils'
18+
import {
19+
accessError,
20+
checkAccess,
21+
normalizeColumn,
22+
tableLockErrorResponse,
23+
} from '@/app/api/table/utils'
1924

2025
const logger = createLogger('TableWorkflowGroupsAPI')
2126

@@ -47,6 +52,8 @@ async function validateWorkflowInWorkspace(
4752
* share this mapper instead of repeating the if-chain three times.
4853
*/
4954
function mapWorkflowGroupError(error: unknown, fallbackMessage: string): NextResponse {
55+
const lockError = tableLockErrorResponse(error)
56+
if (lockError) return lockError
5057
if (error instanceof Error) {
5158
const msg = error.message
5259
if (msg === 'Table not found' || msg.includes('not found')) {

apps/sim/app/api/table/[tableId]/import-async/route.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { generateRequestId } from '@/lib/core/utils/request'
1010
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
1111
import { runTableImport, type TableImportPayload } from '@/lib/table/import-runner'
1212
import { markTableJobRunning, releaseJobClaim } from '@/lib/table/jobs/service'
13+
import { assertRowDelete, assertRowInsert, assertSchemaMutable } from '@/lib/table/mutation-locks'
1314
import { getUserSettings } from '@/lib/users/queries'
1415
import { accessError, checkAccess } from '@/app/api/table/utils'
1516

@@ -53,6 +54,12 @@ export const POST = withRouteHandler(async (request: NextRequest, { params }: Ro
5354
return NextResponse.json({ error: 'Cannot import into an archived table' }, { status: 400 })
5455
}
5556

57+
// Gate the locks before claiming the single write-job slot, so a locked table
58+
// reports 423 here instead of holding the slot and failing inside the worker.
59+
assertRowInsert(table)
60+
if (mode === 'replace') assertRowDelete(table)
61+
if (createColumns && createColumns.length > 0) assertSchemaMutable(table)
62+
5663
const ext = fileName.split('.').pop()?.toLowerCase()
5764
if (ext !== 'csv' && ext !== 'tsv') {
5865
return NextResponse.json({ error: 'Only CSV and TSV files are supported' }, { status: 400 })

apps/sim/app/api/table/[tableId]/import/route.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,18 @@ vi.mock('@sim/utils/id', () => ({
3131

3232
vi.mock('@/app/api/table/utils', async () => {
3333
const { NextResponse } = await import('next/server')
34+
const { TableLockedError } = await import('@/lib/table/mutation-locks')
3435
return {
3536
checkAccess: mockCheckAccess,
3637
accessError: (result: { status: number }) => {
3738
const message = result.status === 404 ? 'Table not found' : 'Access denied'
3839
return NextResponse.json({ error: message }, { status: result.status })
3940
},
4041
csvProxyBodyCapResponse: () => null,
42+
tableLockErrorResponse: (error: unknown) =>
43+
error instanceof TableLockedError
44+
? NextResponse.json({ error: error.message, lock: error.lock }, { status: 423 })
45+
: null,
4146
multipartErrorResponse: (error: { code: string; message: string }) =>
4247
NextResponse.json(
4348
{ error: error.message },
@@ -74,6 +79,7 @@ vi.mock('@/lib/table/billing', () => ({
7479
limit >= 0 && current + added > limit,
7580
}))
7681

82+
import { TableLockedError } from '@/lib/table/mutation-locks'
7783
import { POST } from '@/app/api/table/[tableId]/import/route'
7884

7985
function createCsvFile(contents: string, name = 'data.csv', type = 'text/csv'): File {
@@ -377,6 +383,21 @@ describe('POST /api/table/[tableId]/import', () => {
377383
expect(data.data?.insertedCount).toBe(0)
378384
})
379385

386+
it('maps a lock violation from importAppendRows to 423, not 500', async () => {
387+
// The append branch returns instead of rethrowing, so it must map the lock
388+
// error itself — the outer catch's mapper never sees it.
389+
mockImportAppendRows.mockRejectedValueOnce(new TableLockedError('insert'))
390+
const response = await callPost(
391+
createFormData(createCsvFile('name,age\nAlice,30'), { mode: 'append' })
392+
)
393+
expect(response.status).toBe(423)
394+
const data = await response.json()
395+
expect(data.lock).toBe('insert')
396+
// A `details` array would make the client treat it as a validation error
397+
// and swallow the toast.
398+
expect(data.details).toBeUndefined()
399+
})
400+
380401
it('accepts TSV files', async () => {
381402
const response = await callPost(
382403
createFormData(

apps/sim/app/api/table/[tableId]/import/route.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import {
4444
checkAccess,
4545
csvProxyBodyCapResponse,
4646
multipartErrorResponse,
47+
tableLockErrorResponse,
4748
} from '@/app/api/table/utils'
4849

4950
const logger = createLogger('TableImportCSVExisting')
@@ -336,6 +337,12 @@ export const POST = withRouteHandler(async (request: NextRequest, { params }: Ro
336337
},
337338
})
338339
} catch (err) {
340+
// This branch returns rather than rethrowing, so the outer catch's
341+
// mapper is unreachable from here — map the lock error first or a 423
342+
// degrades into a generic 500 (replace mode rethrows and maps fine).
343+
const lockError = tableLockErrorResponse(err)
344+
if (lockError) return lockError
345+
339346
const message = toError(err).message
340347
logger.warn(`[${requestId}] Append failed for table ${tableId}`, {
341348
total: coerced.length,
@@ -408,6 +415,8 @@ export const POST = withRouteHandler(async (request: NextRequest, { params }: Ro
408415
throw err
409416
}
410417
} catch (error) {
418+
const lockError = tableLockErrorResponse(error)
419+
if (lockError) return lockError
411420
if (isMultipartError(error)) return multipartErrorResponse(error)
412421

413422
const message = toError(error).message

apps/sim/app/api/table/[tableId]/route.ts

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,29 @@
11
import { createLogger } from '@sim/logger'
22
import { getErrorMessage } from '@sim/utils/errors'
33
import { type NextRequest, NextResponse } from 'next/server'
4-
import { getTableQuerySchema, renameTableContract } from '@/lib/api/contracts/tables'
4+
import { getTableQuerySchema, updateTableContract } from '@/lib/api/contracts/tables'
55
import { isZodError, parseRequest, validationErrorResponse } from '@/lib/api/server/validation'
66
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
7+
import { isFeatureEnabled } from '@/lib/core/config/feature-flags'
78
import { generateRequestId } from '@/lib/core/utils/request'
89
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
910
import { captureServerEvent } from '@/lib/posthog/server'
10-
import { deleteTable, renameTable, TableConflictError, type TableSchema } from '@/lib/table'
11+
import {
12+
deleteTable,
13+
getTableById,
14+
renameTable,
15+
TableConflictError,
16+
type TableSchema,
17+
updateTableLocks,
18+
} from '@/lib/table'
1119
import { getWorkspaceTableLimits } from '@/lib/table/billing'
12-
import { accessError, checkAccess, normalizeColumn } from '@/app/api/table/utils'
20+
import { TABLE_LOCK_FLAGS, TABLE_LOCK_KINDS } from '@/lib/table/types'
21+
import {
22+
accessError,
23+
checkAccess,
24+
normalizeColumn,
25+
tableLockErrorResponse,
26+
} from '@/app/api/table/utils'
1327

1428
const logger = createLogger('TableDetailAPI')
1529

@@ -65,6 +79,7 @@ export const GET = withRouteHandler(async (request: NextRequest, { params }: Tab
6579
metadata: table.metadata ?? null,
6680
rowCount: table.rowCount,
6781
maxRows: maxRowsPerTable,
82+
locks: table.locks,
6883
createdAt:
6984
table.createdAt instanceof Date
7085
? table.createdAt.toISOString()
@@ -104,7 +119,7 @@ export const PATCH = withRouteHandler(
104119
}
105120

106121
const parsed = await parseRequest(
107-
renameTableContract,
122+
updateTableContract,
108123
request,
109124
{ params },
110125
{
@@ -116,6 +131,8 @@ export const PATCH = withRouteHandler(
116131
const { tableId } = parsed.data.params
117132
const validated = parsed.data.body
118133

134+
// `write` is the floor for either operation; a `locks` change additionally
135+
// requires `admin` (checked below), matching the workflow-lock precedent.
119136
const result = await checkAccess(tableId, authResult.userId, 'write')
120137
if (!result.ok) return accessError(result, requestId, tableId)
121138

@@ -125,20 +142,56 @@ export const PATCH = withRouteHandler(
125142
return NextResponse.json({ error: 'Invalid workspace ID' }, { status: 400 })
126143
}
127144

128-
const updated = await renameTable(tableId, validated.name, requestId, authResult.userId)
145+
if (validated.locks !== undefined) {
146+
// With the flag off you may still CLEAR locks — otherwise flipping the
147+
// kill switch would strand an already-locked table with no way to
148+
// unlock it, while enforcement of those stored locks keeps running.
149+
// Only a lock actually transitioning off→on needs the feature enabled;
150+
// comparing against the stored state (rather than "every value is
151+
// false") is what lets the settings UI, which always submits the full
152+
// four-flag draft, clear one lock while another stays on.
153+
const enablesALock = TABLE_LOCK_KINDS.some((kind) => {
154+
const flag = TABLE_LOCK_FLAGS[kind]
155+
return validated.locks?.[flag] === true && !table.locks[flag]
156+
})
157+
if (enablesALock && !(await isFeatureEnabled('table-locks'))) {
158+
return NextResponse.json({ error: 'Table locks are not enabled' }, { status: 403 })
159+
}
160+
const adminResult = await checkAccess(tableId, authResult.userId, 'admin')
161+
if (!adminResult.ok) {
162+
return NextResponse.json(
163+
{ error: 'Admin access required to change table locks' },
164+
{ status: 403 }
165+
)
166+
}
167+
await updateTableLocks(tableId, validated.locks, authResult.userId, requestId)
168+
}
169+
170+
if (validated.name !== undefined) {
171+
await renameTable(tableId, validated.name, requestId, authResult.userId)
172+
}
173+
174+
// Re-read so the response reflects both a rename and a lock change.
175+
const updated = await getTableById(tableId)
176+
if (!updated) {
177+
return NextResponse.json({ error: 'Table not found' }, { status: 404 })
178+
}
129179

130180
return NextResponse.json({
131181
success: true,
132182
data: { table: updated },
133183
})
134184
} catch (error) {
185+
const lockError = tableLockErrorResponse(error)
186+
if (lockError) return lockError
187+
135188
if (error instanceof TableConflictError) {
136189
return NextResponse.json({ error: error.message }, { status: 409 })
137190
}
138191

139-
logger.error(`[${requestId}] Error renaming table:`, error)
192+
logger.error(`[${requestId}] Error updating table:`, error)
140193
return NextResponse.json(
141-
{ error: getErrorMessage(error, 'Failed to rename table') },
194+
{ error: getErrorMessage(error, 'Failed to update table') },
142195
{ status: 500 }
143196
)
144197
}
@@ -188,6 +241,8 @@ export const DELETE = withRouteHandler(
188241
},
189242
})
190243
} catch (error) {
244+
const lockError = tableLockErrorResponse(error)
245+
if (lockError) return lockError
191246
if (isZodError(error)) {
192247
return validationErrorResponse(error)
193248
}

apps/sim/app/api/table/[tableId]/rows/[rowId]/route.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
checkAccess,
2222
rootErrorMessage,
2323
rowWriteErrorResponse,
24+
tableLockErrorResponse,
2425
} from '@/app/api/table/utils'
2526

2627
const logger = createLogger('TableRowAPI')
@@ -211,7 +212,7 @@ export const DELETE = withRouteHandler(async (request: NextRequest, context: Row
211212
return NextResponse.json({ error: 'Invalid workspace ID' }, { status: 400 })
212213
}
213214

214-
await deleteRow(tableId, rowId, validated.workspaceId, requestId)
215+
await deleteRow(table, rowId, requestId)
215216

216217
return NextResponse.json({
217218
success: true,
@@ -221,6 +222,9 @@ export const DELETE = withRouteHandler(async (request: NextRequest, context: Row
221222
},
222223
})
223224
} catch (error) {
225+
const lockError = tableLockErrorResponse(error)
226+
if (lockError) return lockError
227+
224228
const errorMessage = toError(error).message
225229

226230
if (errorMessage === 'Row not found') {

apps/sim/app/api/table/[tableId]/rows/route.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,7 @@ export const DELETE = withRouteHandler(
433433

434434
if (validated.rowIds) {
435435
const result = await deleteRowsByIds(
436+
table,
436437
{ tableId, rowIds: validated.rowIds, workspaceId: validated.workspaceId },
437438
requestId
438439
)

apps/sim/app/api/table/route.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
127127
},
128128
rowCount: table.rowCount,
129129
maxRows: table.maxRows,
130+
locks: table.locks,
130131
createdAt:
131132
table.createdAt instanceof Date
132133
? table.createdAt.toISOString()
@@ -206,6 +207,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
206207
},
207208
rowCount: t.rowCount,
208209
maxRows: t.maxRows,
210+
locks: t.locks,
209211
workspaceId: t.workspaceId,
210212
createdBy: t.createdBy,
211213
createdAt: t.createdAt instanceof Date ? t.createdAt.toISOString() : String(t.createdAt),

0 commit comments

Comments
 (0)