@@ -3,6 +3,15 @@ import { createLogger } from '@sim/logger'
33import { toError } from '@sim/utils/errors'
44import { generateId } from '@sim/utils/id'
55import { type NextRequest , NextResponse } from 'next/server'
6+ import {
7+ csvExtensionSchema ,
8+ csvImportCreateColumnsSchema ,
9+ csvImportFormSchema ,
10+ csvImportMappingSchema ,
11+ csvImportModeSchema ,
12+ tableIdParamsSchema ,
13+ } from '@/lib/api/contracts/tables'
14+ import { getValidationErrorMessage } from '@/lib/api/server'
615import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
716import { generateRequestId } from '@/lib/core/utils/request'
817import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
@@ -11,7 +20,6 @@ import {
1120 batchInsertRowsWithTx ,
1221 buildAutoMapping ,
1322 CSV_MAX_BATCH_SIZE ,
14- CSV_MAX_FILE_SIZE_BYTES ,
1523 type CsvHeaderMapping ,
1624 CsvImportValidationError ,
1725 coerceRowsForTable ,
@@ -27,15 +35,13 @@ import { accessError, checkAccess } from '@/app/api/table/utils'
2735
2836const logger = createLogger ( 'TableImportCSVExisting' )
2937
30- const IMPORT_MODES = new Set ( [ 'append' , 'replace' ] )
31-
3238interface RouteParams {
3339 params : Promise < { tableId : string } >
3440}
3541
3642export const POST = withRouteHandler ( async ( request : NextRequest , { params } : RouteParams ) => {
3743 const requestId = generateRequestId ( )
38- const { tableId } = await params
44+ const { tableId } = tableIdParamsSchema . parse ( await params )
3945
4046 try {
4147 const authResult = await checkSessionOrInternalAuth ( request , { requireWorkflowId : false } )
@@ -44,40 +50,39 @@ export const POST = withRouteHandler(async (request: NextRequest, { params }: Ro
4450 }
4551
4652 const formData = await request . formData ( )
47- const file = formData . get ( 'file' )
48- const workspaceId = formData . get ( 'workspaceId' ) as string | null
49- const rawMode = ( formData . get ( 'mode' ) as string | null ) ?? 'append'
50- const rawMapping = formData . get ( 'mapping' ) as string | null
51- const rawCreateColumns = formData . get ( 'createColumns' ) as string | null
52-
53- if ( ! file || ! ( file instanceof File ) ) {
54- return NextResponse . json ( { error : 'CSV file is required' } , { status : 400 } )
55- }
56-
57- if ( file . size > CSV_MAX_FILE_SIZE_BYTES ) {
53+ const formValidation = csvImportFormSchema . safeParse ( {
54+ file : formData . get ( 'file' ) ,
55+ workspaceId : formData . get ( 'workspaceId' ) ,
56+ } )
57+ const rawMode = formData . get ( 'mode' ) ?? 'append'
58+ const rawMapping = formData . get ( 'mapping' )
59+ const rawCreateColumns = formData . get ( 'createColumns' )
60+
61+ if ( ! formValidation . success ) {
5862 return NextResponse . json (
59- {
60- error : `File exceeds maximum allowed size of ${ CSV_MAX_FILE_SIZE_BYTES / ( 1024 * 1024 ) } MB` ,
61- } ,
63+ { error : getValidationErrorMessage ( formValidation . error ) } ,
6264 { status : 400 }
6365 )
6466 }
6567
66- if ( ! workspaceId ) {
67- return NextResponse . json ( { error : 'Workspace ID is required' } , { status : 400 } )
68- }
68+ const { file, workspaceId } = formValidation . data
6969
70- if ( ! IMPORT_MODES . has ( rawMode ) ) {
70+ const modeValidation = csvImportModeSchema . safeParse ( rawMode )
71+ if ( ! modeValidation . success ) {
7172 return NextResponse . json (
72- { error : `Invalid mode "${ rawMode } ". Must be "append" or "replace".` } ,
73+ { error : `Invalid mode "${ String ( rawMode ) } ". Must be "append" or "replace".` } ,
7374 { status : 400 }
7475 )
7576 }
76- const mode = rawMode as 'append' | 'replace'
77+ const mode = modeValidation . data
7778
7879 const ext = file . name . split ( '.' ) . pop ( ) ?. toLowerCase ( )
79- if ( ext !== 'csv' && ext !== 'tsv' ) {
80- return NextResponse . json ( { error : 'Only CSV and TSV files are supported' } , { status : 400 } )
80+ const extensionValidation = csvExtensionSchema . safeParse ( ext )
81+ if ( ! extensionValidation . success ) {
82+ return NextResponse . json (
83+ { error : getValidationErrorMessage ( extensionValidation . error ) } ,
84+ { status : 400 }
85+ )
8186 }
8287
8388 const accessResult = await checkAccess ( tableId , authResult . userId , 'write' )
@@ -98,38 +103,30 @@ export const POST = withRouteHandler(async (request: NextRequest, { params }: Ro
98103
99104 let mapping : CsvHeaderMapping | undefined
100105 if ( rawMapping ) {
101- try {
102- const parsed = JSON . parse ( rawMapping )
103- if ( parsed === null || typeof parsed !== 'object' || Array . isArray ( parsed ) ) {
104- return NextResponse . json (
105- { error : 'mapping must be a JSON object mapping CSV headers to column names' } ,
106- { status : 400 }
107- )
108- }
109- mapping = parsed as CsvHeaderMapping
110- } catch {
111- return NextResponse . json ( { error : 'mapping must be valid JSON' } , { status : 400 } )
106+ const mappingValidation = csvImportMappingSchema . safeParse ( rawMapping )
107+ if ( ! mappingValidation . success ) {
108+ return NextResponse . json (
109+ { error : getValidationErrorMessage ( mappingValidation . error ) } ,
110+ { status : 400 }
111+ )
112112 }
113+ mapping = mappingValidation . data
113114 }
114115
115116 let createColumns : string [ ] | undefined
116117 if ( rawCreateColumns ) {
117- try {
118- const parsed = JSON . parse ( rawCreateColumns )
119- if ( ! Array . isArray ( parsed ) || parsed . some ( ( h ) => typeof h !== 'string' ) ) {
120- return NextResponse . json (
121- { error : 'createColumns must be a JSON array of CSV header names' } ,
122- { status : 400 }
123- )
124- }
125- createColumns = parsed as string [ ]
126- } catch {
127- return NextResponse . json ( { error : 'createColumns must be valid JSON' } , { status : 400 } )
118+ const createColumnsValidation = csvImportCreateColumnsSchema . safeParse ( rawCreateColumns )
119+ if ( ! createColumnsValidation . success ) {
120+ return NextResponse . json (
121+ { error : getValidationErrorMessage ( createColumnsValidation . error ) } ,
122+ { status : 400 }
123+ )
128124 }
125+ createColumns = createColumnsValidation . data
129126 }
130127
131128 const buffer = Buffer . from ( await file . arrayBuffer ( ) )
132- const delimiter = ext === 'tsv' ? '\t' : ','
129+ const delimiter = extensionValidation . data === 'tsv' ? '\t' : ','
133130 const { headers, rows } = await parseCsvBuffer ( buffer , delimiter )
134131
135132 let effectiveMapping = mapping ?? buildAutoMapping ( headers , table . schema )
0 commit comments