77 v2UpdateTableContract ,
88} from '@/lib/api/contracts/v2/tables'
99import { parseRequest } from '@/lib/api/server'
10+ import { asOrchestrationError } from '@/lib/core/orchestration/types'
1011import { generateRequestId } from '@/lib/core/utils/request'
1112import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
1213import { findActiveFolder } from '@/lib/folders/queries'
@@ -21,7 +22,6 @@ import { checkAccess } from '@/app/api/table/utils'
2122import { checkRateLimit , resolveWorkspaceScope } from '@/app/api/v1/middleware'
2223import { v2ApiGateError } from '@/app/api/v2/lib/gate'
2324import {
24- v2CaughtOrchestrationError ,
2525 v2Data ,
2626 v2Error ,
2727 v2RateLimitError ,
@@ -38,6 +38,17 @@ import {
3838
3939const logger = createLogger ( 'V2TableDetailAPI' )
4040
41+ /**
42+ * `details` payload naming the operations of a composite write that committed,
43+ * or `undefined` when none did — so `details.applied` being present always
44+ * means "these changes are live despite the error".
45+ */
46+ function appliedDetails (
47+ applied : readonly ( 'name' | 'folderId' ) [ ]
48+ ) : { applied : readonly string [ ] } | undefined {
49+ return applied . length > 0 ? { applied } : undefined
50+ }
51+
4152export const dynamic = 'force-dynamic'
4253export const revalidate = 0
4354
@@ -101,6 +112,15 @@ export const GET = withRouteHandler(async (request: NextRequest, context: TableR
101112export const PATCH = withRouteHandler ( async ( request : NextRequest , context : TableRouteParams ) => {
102113 const requestId = generateRequestId ( )
103114
115+ /**
116+ * Hoisted above the `try` so every exit path can report it. Once a write has
117+ * committed, the response must say so even when the failure came *after* the
118+ * writes — a throw in the final re-read, or the re-read finding the table
119+ * archived. Reporting a bare 500 there tells the caller nothing landed, and
120+ * it retries into a duplicate-name conflict or a repeated move.
121+ */
122+ const applied : ( 'name' | 'folderId' ) [ ] = [ ]
123+
104124 try {
105125 const rateLimit = await checkRateLimit ( request , 'table-detail' )
106126 if ( ! rateLimit . allowed ) return v2RateLimitError ( rateLimit )
@@ -151,7 +171,6 @@ export const PATCH = withRouteHandler(async (request: NextRequest, context: Tabl
151171 // per-operation audits — so instead of pretending atomicity the response
152172 // states exactly which operations landed. A caller that gets an error can
153173 // then reconcile rather than having to re-read and diff.
154- const applied : ( 'name' | 'folderId' ) [ ] = [ ]
155174 let failure : { outcome : OrchestrationOutcome ; fallback : string } | null = null
156175
157176 if ( validated . name !== undefined ) {
@@ -190,31 +209,38 @@ export const PATCH = withRouteHandler(async (request: NextRequest, context: Tabl
190209 // Live-collab: tell open viewers the definition changed so they refetch.
191210 if ( applied . length > 0 ) signalTableSchemaChanged ( tableId )
192211 if ( failure ) {
193- return v2TableOrchestrationError (
194- failure . outcome ,
195- failure . fallback ,
196- // Omitted when nothing landed, so `details.applied` present always
197- // means "these changes are live despite the error".
198- applied . length > 0 ? { applied } : undefined
199- )
212+ return v2TableOrchestrationError ( failure . outcome , failure . fallback , appliedDetails ( applied ) )
200213 }
201214
202- // Re-read so the response reflects every applied change at once.
215+ // Re-read so the response reflects every applied change at once. A miss
216+ // means the table was archived after the writes committed, so the caller
217+ // still has to be told what landed.
203218 const updated = await getTableById ( tableId )
204- if ( ! updated ) return v2Error ( 'NOT_FOUND' , 'Table not found' )
219+ if ( ! updated ) {
220+ return v2Error ( 'NOT_FOUND' , 'Table not found' , { details : appliedDetails ( applied ) } )
221+ }
205222
206223 return v2Data ( { table : toApiTable ( updated ) } , { rateLimit } )
207224 } catch ( error ) {
208- const lockError = v2TableLockError ( error )
225+ const details = appliedDetails ( applied )
226+
227+ const lockError = v2TableLockError ( error , details )
209228 if ( lockError ) return lockError
210229
211- const classified = v2CaughtOrchestrationError ( error )
212- if ( classified ) return classified
230+ const classified = asOrchestrationError ( error )
231+ if ( classified ) {
232+ return v2TableOrchestrationError (
233+ { errorCode : classified . code , error : classified . message } ,
234+ 'Failed to update table' ,
235+ details
236+ )
237+ }
213238
214239 logger . error ( `[${ requestId } ] Error updating table` , {
215240 error : getErrorMessage ( error , 'Unknown error' ) ,
241+ applied,
216242 } )
217- return v2Error ( 'INTERNAL_ERROR' , 'Internal server error' )
243+ return v2Error ( 'INTERNAL_ERROR' , 'Internal server error' , { details } )
218244 }
219245} )
220246
0 commit comments