Skip to content

Commit 592dd46

Browse files
committed
migrate to use tanstack query for all server state
1 parent b476b8d commit 592dd46

File tree

17 files changed

+1011
-661
lines changed

17 files changed

+1011
-661
lines changed

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

Lines changed: 65 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,16 @@ const DeleteRowsByFilterSchema = z.object({
7777
.optional(),
7878
})
7979

80+
const DeleteRowsByIdsSchema = z.object({
81+
workspaceId: z.string().min(1, 'Workspace ID is required'),
82+
rowIds: z
83+
.array(z.string().min(1), { required_error: 'Row IDs are required' })
84+
.min(1, 'At least one row ID is required')
85+
.max(1000, 'Cannot delete more than 1000 rows per operation'),
86+
})
87+
88+
const DeleteRowsRequestSchema = z.union([DeleteRowsByFilterSchema, DeleteRowsByIdsSchema])
89+
8090
interface TableRowsRouteParams {
8191
params: Promise<{ tableId: string }>
8292
}
@@ -577,7 +587,7 @@ export async function DELETE(request: NextRequest, { params }: TableRowsRoutePar
577587
}
578588

579589
const body: unknown = await request.json()
580-
const validated = DeleteRowsByFilterSchema.parse(body)
590+
const validated = DeleteRowsRequestSchema.parse(body)
581591

582592
const accessResult = await checkAccess(tableId, authResult.userId, 'write')
583593
if (!accessResult.ok) return accessError(accessResult, requestId, tableId)
@@ -596,41 +606,73 @@ export async function DELETE(request: NextRequest, { params }: TableRowsRoutePar
596606
eq(userTableRows.workspaceId, validated.workspaceId),
597607
]
598608

599-
const filterClause = buildFilterClause(validated.filter as Filter, USER_TABLE_ROWS_SQL_NAME)
600-
if (filterClause) {
601-
baseConditions.push(filterClause)
602-
}
609+
let rowIds: string[] = []
610+
let missingRowIds: string[] | undefined
611+
let requestedCount: number | undefined
603612

604-
let matchingRowsQuery = db
605-
.select({ id: userTableRows.id })
606-
.from(userTableRows)
607-
.where(and(...baseConditions))
613+
if ('rowIds' in validated) {
614+
const uniqueRequestedRowIds = Array.from(new Set(validated.rowIds))
615+
requestedCount = uniqueRequestedRowIds.length
608616

609-
if (validated.limit) {
610-
matchingRowsQuery = matchingRowsQuery.limit(validated.limit) as typeof matchingRowsQuery
611-
}
617+
const matchingRows = await db
618+
.select({ id: userTableRows.id })
619+
.from(userTableRows)
620+
.where(
621+
and(
622+
...baseConditions,
623+
sql`${userTableRows.id} = ANY(ARRAY[${sql.join(
624+
uniqueRequestedRowIds.map((id) => sql`${id}`),
625+
sql`, `
626+
)}])`
627+
)
628+
)
612629

613-
const matchingRows = await matchingRowsQuery
630+
const matchedRowIds = matchingRows.map((r) => r.id)
631+
const matchedIdSet = new Set(matchedRowIds)
632+
missingRowIds = uniqueRequestedRowIds.filter((id) => !matchedIdSet.has(id))
633+
rowIds = matchedRowIds
634+
} else {
635+
const filterClause = buildFilterClause(validated.filter as Filter, USER_TABLE_ROWS_SQL_NAME)
636+
if (filterClause) {
637+
baseConditions.push(filterClause)
638+
}
614639

615-
if (matchingRows.length === 0) {
640+
let matchingRowsQuery = db
641+
.select({ id: userTableRows.id })
642+
.from(userTableRows)
643+
.where(and(...baseConditions))
644+
645+
if (validated.limit) {
646+
matchingRowsQuery = matchingRowsQuery.limit(validated.limit) as typeof matchingRowsQuery
647+
}
648+
649+
const matchingRows = await matchingRowsQuery
650+
rowIds = matchingRows.map((r) => r.id)
651+
}
652+
653+
if (rowIds.length === 0) {
616654
return NextResponse.json(
617655
{
618656
success: true,
619657
data: {
620-
message: 'No rows matched the filter criteria',
658+
message:
659+
'rowIds' in validated
660+
? 'No matching rows found for the provided IDs'
661+
: 'No rows matched the filter criteria',
621662
deletedCount: 0,
663+
deletedRowIds: [],
664+
...(requestedCount !== undefined ? { requestedCount } : {}),
665+
...(missingRowIds ? { missingRowIds } : {}),
622666
},
623667
},
624668
{ status: 200 }
625669
)
626670
}
627671

628-
if (matchingRows.length > TABLE_LIMITS.DELETE_BATCH_SIZE) {
629-
logger.warn(`[${requestId}] Deleting ${matchingRows.length} rows. This may take some time.`)
672+
if (rowIds.length > TABLE_LIMITS.DELETE_BATCH_SIZE) {
673+
logger.warn(`[${requestId}] Deleting ${rowIds.length} rows. This may take some time.`)
630674
}
631675

632-
const rowIds = matchingRows.map((r) => r.id)
633-
634676
await db.transaction(async (trx) => {
635677
let totalDeleted = 0
636678

@@ -653,14 +695,16 @@ export async function DELETE(request: NextRequest, { params }: TableRowsRoutePar
653695
}
654696
})
655697

656-
logger.info(`[${requestId}] Deleted ${matchingRows.length} rows from table ${tableId}`)
698+
logger.info(`[${requestId}] Deleted ${rowIds.length} rows from table ${tableId}`)
657699

658700
return NextResponse.json({
659701
success: true,
660702
data: {
661703
message: 'Rows deleted successfully',
662-
deletedCount: matchingRows.length,
704+
deletedCount: rowIds.length,
663705
deletedRowIds: rowIds,
706+
...(requestedCount !== undefined ? { requestedCount } : {}),
707+
...(missingRowIds ? { missingRowIds } : {}),
664708
},
665709
})
666710
} catch (error) {

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/row-modal.tsx

Lines changed: 38 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client'
22

3-
import { useEffect, useState } from 'react'
3+
import { useState } from 'react'
44
import { createLogger } from '@sim/logger'
55
import { AlertCircle } from 'lucide-react'
66
import { useParams } from 'next/navigation'
@@ -17,6 +17,12 @@ import {
1717
Textarea,
1818
} from '@/components/emcn'
1919
import type { ColumnDefinition, TableInfo, TableRow } from '@/lib/table'
20+
import {
21+
useCreateTableRow,
22+
useDeleteTableRow,
23+
useDeleteTableRows,
24+
useUpdateTableRow,
25+
} from '@/hooks/queries/tables'
2026

2127
const logger = createLogger('RowModal')
2228

@@ -92,126 +98,78 @@ function formatValueForInput(value: unknown, type: string): string {
9298
return String(value)
9399
}
94100

101+
function getInitialRowData(
102+
mode: RowModalProps['mode'],
103+
columns: ColumnDefinition[],
104+
row?: TableRow
105+
): Record<string, unknown> {
106+
if (mode === 'add' && columns.length > 0) {
107+
return createInitialRowData(columns)
108+
}
109+
if (mode === 'edit' && row) {
110+
return row.data
111+
}
112+
return {}
113+
}
114+
95115
export function RowModal({ mode, isOpen, onClose, table, row, rowIds, onSuccess }: RowModalProps) {
96116
const params = useParams()
97117
const workspaceId = params.workspaceId as string
118+
const tableId = table.id
98119

99120
const schema = table?.schema
100121
const columns = schema?.columns || []
101122

102-
const [rowData, setRowData] = useState<Record<string, unknown>>({})
123+
const [rowData, setRowData] = useState<Record<string, unknown>>(() =>
124+
getInitialRowData(mode, columns, row)
125+
)
103126
const [error, setError] = useState<string | null>(null)
104-
const [isSubmitting, setIsSubmitting] = useState(false)
105-
106-
// Initialize form data based on mode
107-
useEffect(() => {
108-
if (!isOpen) return
109-
110-
if (mode === 'add' && columns.length > 0) {
111-
setRowData(createInitialRowData(columns))
112-
} else if (mode === 'edit' && row) {
113-
setRowData(row.data)
114-
}
115-
}, [isOpen, mode, columns, row])
127+
const createRowMutation = useCreateTableRow({ workspaceId, tableId })
128+
const updateRowMutation = useUpdateTableRow({ workspaceId, tableId })
129+
const deleteRowMutation = useDeleteTableRow({ workspaceId, tableId })
130+
const deleteRowsMutation = useDeleteTableRows({ workspaceId, tableId })
131+
const isSubmitting =
132+
createRowMutation.isPending ||
133+
updateRowMutation.isPending ||
134+
deleteRowMutation.isPending ||
135+
deleteRowsMutation.isPending
116136

117137
const handleFormSubmit = async (e: React.FormEvent) => {
118138
e.preventDefault()
119139
setError(null)
120-
setIsSubmitting(true)
121140

122141
try {
123142
const cleanData = cleanRowData(columns, rowData)
124143

125144
if (mode === 'add') {
126-
const res = await fetch(`/api/table/${table?.id}/rows`, {
127-
method: 'POST',
128-
headers: { 'Content-Type': 'application/json' },
129-
body: JSON.stringify({ workspaceId, data: cleanData }),
130-
})
131-
132-
const result: { error?: string } = await res.json()
133-
if (!res.ok) {
134-
throw new Error(result.error || 'Failed to add row')
135-
}
145+
await createRowMutation.mutateAsync(cleanData)
136146
} else if (mode === 'edit' && row) {
137-
const res = await fetch(`/api/table/${table?.id}/rows/${row.id}`, {
138-
method: 'PATCH',
139-
headers: { 'Content-Type': 'application/json' },
140-
body: JSON.stringify({ workspaceId, data: cleanData }),
141-
})
142-
143-
const result: { error?: string } = await res.json()
144-
if (!res.ok) {
145-
throw new Error(result.error || 'Failed to update row')
146-
}
147+
await updateRowMutation.mutateAsync({ rowId: row.id, data: cleanData })
147148
}
148149

149150
onSuccess()
150151
} catch (err) {
151152
logger.error(`Failed to ${mode} row:`, err)
152153
setError(err instanceof Error ? err.message : `Failed to ${mode} row`)
153-
} finally {
154-
setIsSubmitting(false)
155154
}
156155
}
157156

158157
const handleDelete = async () => {
159158
setError(null)
160-
setIsSubmitting(true)
161159

162160
const idsToDelete = rowIds ?? (row ? [row.id] : [])
163161

164162
try {
165163
if (idsToDelete.length === 1) {
166-
const res = await fetch(`/api/table/${table?.id}/rows/${idsToDelete[0]}`, {
167-
method: 'DELETE',
168-
headers: { 'Content-Type': 'application/json' },
169-
body: JSON.stringify({ workspaceId }),
170-
})
171-
172-
if (!res.ok) {
173-
const result: { error?: string } = await res.json()
174-
throw new Error(result.error || 'Failed to delete row')
175-
}
164+
await deleteRowMutation.mutateAsync(idsToDelete[0])
176165
} else {
177-
const results = await Promise.allSettled(
178-
idsToDelete.map(async (rowId) => {
179-
const res = await fetch(`/api/table/${table?.id}/rows/${rowId}`, {
180-
method: 'DELETE',
181-
headers: { 'Content-Type': 'application/json' },
182-
body: JSON.stringify({ workspaceId }),
183-
})
184-
185-
if (!res.ok) {
186-
const result: { error?: string } = await res.json().catch(() => ({}))
187-
throw new Error(result.error || `Failed to delete row ${rowId}`)
188-
}
189-
190-
return rowId
191-
})
192-
)
193-
194-
const failures = results.filter((r) => r.status === 'rejected')
195-
196-
if (failures.length > 0) {
197-
const failureCount = failures.length
198-
const totalCount = idsToDelete.length
199-
const successCount = totalCount - failureCount
200-
const firstError =
201-
failures[0].status === 'rejected' ? failures[0].reason?.message || 'Unknown error' : ''
202-
203-
throw new Error(
204-
`Failed to delete ${failureCount} of ${totalCount} row(s)${successCount > 0 ? ` (${successCount} deleted successfully)` : ''}. ${firstError}`
205-
)
206-
}
166+
await deleteRowsMutation.mutateAsync(idsToDelete)
207167
}
208168

209169
onSuccess()
210170
} catch (err) {
211171
logger.error('Failed to delete row(s):', err)
212172
setError(err instanceof Error ? err.message : 'Failed to delete row(s)')
213-
} finally {
214-
setIsSubmitting(false)
215173
}
216174
}
217175

0 commit comments

Comments
 (0)