Skip to content

Commit fc43edc

Browse files
committed
feat(table): model wire keying and actor attribution on row write use cases
The row write use cases assumed every caller speaks column names. That holds for /api/v2, /api/v1 and the Copilot tools, but not for the first-party grid or the internal /api/table routes, which address cells by stable storage id. Feeding id-keyed data through the name remap drops every key it does not recognise — a storage id names no column name — so the write would store nothing and still report success. Make the wire an explicit, required property of the input rather than an assumption. `dataKeying: 'names' | 'ids'` sits alongside `strictWrite` and is required for the same reason: a new write surface must state which contract it publishes. Strictness now means the same thing on either wire — an unknown column id is refused exactly as an unknown column name already was. Single-row writes also gain optional actor attribution, so the acting tab can skip refetching its own write. It is optional and absent by default, so every existing caller keeps broadcasting to all subscribers as before. Only the single-row create, update and delete paths accept it; a batch write is not reconciled locally by the actor and must still refetch. The attribution pin moves with the behaviour: what selects the actor-scoped signal is no longer which file calls it but which surface supplies an actor, so that is now what the test pins. Verified to fail: ignoring the keying discriminator, and dropping actor attribution, each turn the corresponding tests red.
1 parent e8f17b2 commit fc43edc

10 files changed

Lines changed: 408 additions & 36 deletions

File tree

apps/sim/app/api/v2/tables/[tableId]/rows/[rowId]/route.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ describe('/api/v2/tables/[tableId]/rows/[rowId]', () => {
132132
assertedWorkspaceId: WORKSPACE_ID,
133133
data: { name: 'Ada' },
134134
strictWrite: true,
135+
dataKeying: 'names',
135136
},
136137
request: req,
137138
})

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export const PATCH = defineV2JsonRoute({
4242
assertedWorkspaceId: body.workspaceId,
4343
data: body.data,
4444
strictWrite: true,
45+
dataKeying: 'names' as const,
4546
}),
4647
useCase: updateTableRow,
4748
present: ({ table, row }) => ({

apps/sim/app/api/v2/tables/[tableId]/rows/route.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ describe('/api/v2/tables/[tableId]/rows', () => {
190190
// or a value the column cannot hold is a 400, not a dropped key or a
191191
// nulled cell. Every first-party surface leaves this unset.
192192
strictWrite: true,
193+
dataKeying: 'names',
193194
},
194195
request: single,
195196
})
@@ -207,6 +208,7 @@ describe('/api/v2/tables/[tableId]/rows', () => {
207208
assertedWorkspaceId: WORKSPACE_ID,
208209
rows: [{ name: 'Ada' }],
209210
strictWrite: true,
211+
dataKeying: 'names',
210212
},
211213
request: batch,
212214
})

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ export const POST = defineV2JsonRoute({
7171
assertedWorkspaceId: body.workspaceId,
7272
rows: body.rows,
7373
strictWrite: true,
74+
dataKeying: 'names' as const,
7475
}
7576
: {
7677
kind: 'single' as const,
@@ -80,6 +81,7 @@ export const POST = defineV2JsonRoute({
8081
afterRowId: body.afterRowId,
8182
beforeRowId: body.beforeRowId,
8283
strictWrite: true,
84+
dataKeying: 'names' as const,
8385
},
8486
useCase: createTableRows,
8587
present: (result) => {
@@ -108,6 +110,7 @@ export const PATCH = defineV2JsonRoute({
108110
data: body.data,
109111
limit: body.limit,
110112
strictWrite: true,
113+
dataKeying: 'names' as const,
111114
}),
112115
useCase: updateTableRows,
113116
present: ({ affectedCount, affectedRowIds }) => ({

apps/sim/app/api/v2/tables/[tableId]/rows/upsert/route.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ describe('POST /api/v2/tables/[tableId]/rows/upsert', () => {
103103
data: { email: 'ada@example.com' },
104104
conflictTarget: 'email',
105105
strictWrite: true,
106+
dataKeying: 'names',
106107
},
107108
request,
108109
})

apps/sim/app/api/v2/tables/[tableId]/rows/upsert/route.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export const POST = defineV2JsonRoute({
2121
data: body.data,
2222
conflictTarget: body.conflictTarget,
2323
strictWrite: true,
24+
dataKeying: 'names' as const,
2425
}),
2526
useCase: upsertTableRow,
2627
present: ({ table, row, operation }) => ({

apps/sim/lib/copilot/tools/server/table/user-table.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,7 @@ export const userTableServerTool: BaseServerTool<UserTableArgs, UserTableResult>
286286
tableId: args.tableId,
287287
assertedWorkspaceId: workspaceId,
288288
strictWrite: false,
289+
dataKeying: 'names',
289290
data: args.data,
290291
position: args.position as number | undefined,
291292
secretProvenance: createExactEmptyTableRowSecretProvenance(args.data),
@@ -329,6 +330,7 @@ export const userTableServerTool: BaseServerTool<UserTableArgs, UserTableResult>
329330
tableId: args.tableId,
330331
assertedWorkspaceId: workspaceId,
331332
strictWrite: false,
333+
dataKeying: 'names',
332334
rows: sourceRows,
333335
secretProvenance: sourceRows.map(createExactEmptyTableRowSecretProvenance),
334336
},
@@ -476,6 +478,7 @@ export const userTableServerTool: BaseServerTool<UserTableArgs, UserTableResult>
476478
tableId: args.tableId,
477479
assertedWorkspaceId: workspaceId,
478480
strictWrite: false,
481+
dataKeying: 'names',
479482
rowId: args.rowId,
480483
data: args.data,
481484
secretProvenance: createExactEmptyTableRowSecretProvenance(args.data),

0 commit comments

Comments
 (0)