11import { type NextRequest , NextResponse } from 'next/server'
2+ import * as XLSX from 'xlsx'
23import { z } from 'zod'
34import { checkHybridAuth } from '@/lib/auth/hybrid'
45import { createLogger } from '@/lib/logs/console/logger'
@@ -14,8 +15,11 @@ const MICROSOFT_GRAPH_BASE = 'https://graph.microsoft.com/v1.0'
1415const OneDriveUploadSchema = z . object ( {
1516 accessToken : z . string ( ) . min ( 1 , 'Access token is required' ) ,
1617 fileName : z . string ( ) . min ( 1 , 'File name is required' ) ,
17- file : z . any ( ) , // UserFile object
18+ file : z . any ( ) . optional ( ) , // UserFile object (optional for blank Excel creation)
1819 folderId : z . string ( ) . optional ( ) . nullable ( ) ,
20+ mimeType : z . string ( ) . optional ( ) ,
21+ // Optional Excel write-after-create inputs
22+ values : z . array ( z . array ( z . union ( [ z . string ( ) , z . number ( ) , z . boolean ( ) , z . null ( ) ] ) ) ) . optional ( ) ,
1923} )
2024
2125export async function POST ( request : NextRequest ) {
@@ -42,17 +46,30 @@ export async function POST(request: NextRequest) {
4246 const body = await request . json ( )
4347 const validatedData = OneDriveUploadSchema . parse ( body )
4448
45- logger . info ( `[${ requestId } ] Uploading file to OneDrive` , {
46- fileName : validatedData . fileName ,
47- folderId : validatedData . folderId || 'root' ,
48- } )
49+ let fileBuffer : Buffer
50+ let mimeType : string
51+
52+ // Check if we're creating a blank Excel file
53+ const isExcelCreation =
54+ validatedData . mimeType ===
55+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' && ! validatedData . file
56+
57+ if ( isExcelCreation ) {
58+ // Create a blank Excel workbook
4959
50- // Handle array or single file
51- const rawFile = validatedData . file
52- let fileToProcess
60+ const workbook = XLSX . utils . book_new ( )
61+ const worksheet = XLSX . utils . aoa_to_sheet ( [ [ ] ] )
62+ XLSX . utils . book_append_sheet ( workbook , worksheet , 'Sheet1' )
5363
54- if ( Array . isArray ( rawFile ) ) {
55- if ( rawFile . length === 0 ) {
64+ // Generate XLSX file as buffer
65+ const xlsxBuffer = XLSX . write ( workbook , { type : 'buffer' , bookType : 'xlsx' } )
66+ fileBuffer = Buffer . from ( xlsxBuffer )
67+ mimeType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
68+ } else {
69+ // Handle regular file upload
70+ const rawFile = validatedData . file
71+
72+ if ( ! rawFile ) {
5673 return NextResponse . json (
5774 {
5875 success : false ,
@@ -61,40 +78,51 @@ export async function POST(request: NextRequest) {
6178 { status : 400 }
6279 )
6380 }
64- fileToProcess = rawFile [ 0 ]
65- } else {
66- fileToProcess = rawFile
67- }
6881
69- // Convert to UserFile format
70- let userFile
71- try {
72- userFile = processSingleFileToUserFile ( fileToProcess , requestId , logger )
73- } catch ( error ) {
74- return NextResponse . json (
75- {
76- success : false ,
77- error : error instanceof Error ? error . message : 'Failed to process file' ,
78- } ,
79- { status : 400 }
80- )
81- }
82+ let fileToProcess
83+ if ( Array . isArray ( rawFile ) ) {
84+ if ( rawFile . length === 0 ) {
85+ return NextResponse . json (
86+ {
87+ success : false ,
88+ error : 'No file provided' ,
89+ } ,
90+ { status : 400 }
91+ )
92+ }
93+ fileToProcess = rawFile [ 0 ]
94+ } else {
95+ fileToProcess = rawFile
96+ }
8297
83- logger . info ( `[${ requestId } ] Downloading file from storage: ${ userFile . key } ` )
98+ // Convert to UserFile format
99+ let userFile
100+ try {
101+ userFile = processSingleFileToUserFile ( fileToProcess , requestId , logger )
102+ } catch ( error ) {
103+ return NextResponse . json (
104+ {
105+ success : false ,
106+ error : error instanceof Error ? error . message : 'Failed to process file' ,
107+ } ,
108+ { status : 400 }
109+ )
110+ }
84111
85- let fileBuffer : Buffer
112+ try {
113+ fileBuffer = await downloadFileFromStorage ( userFile , requestId , logger )
114+ } catch ( error ) {
115+ logger . error ( `[${ requestId } ] Failed to download file from storage:` , error )
116+ return NextResponse . json (
117+ {
118+ success : false ,
119+ error : `Failed to download file: ${ error instanceof Error ? error . message : 'Unknown error' } ` ,
120+ } ,
121+ { status : 500 }
122+ )
123+ }
86124
87- try {
88- fileBuffer = await downloadFileFromStorage ( userFile , requestId , logger )
89- } catch ( error ) {
90- logger . error ( `[${ requestId } ] Failed to download file from storage:` , error )
91- return NextResponse . json (
92- {
93- success : false ,
94- error : `Failed to download file: ${ error instanceof Error ? error . message : 'Unknown error' } ` ,
95- } ,
96- { status : 500 }
97- )
125+ mimeType = userFile . type || 'application/octet-stream'
98126 }
99127
100128 const maxSize = 250 * 1024 * 1024 // 250MB
@@ -110,7 +138,11 @@ export async function POST(request: NextRequest) {
110138 )
111139 }
112140
113- const fileName = validatedData . fileName || userFile . name
141+ // Ensure file name has correct extension for Excel files
142+ let fileName = validatedData . fileName
143+ if ( isExcelCreation && ! fileName . endsWith ( '.xlsx' ) ) {
144+ fileName = `${ fileName . replace ( / \. [ ^ . ] * $ / , '' ) } .xlsx`
145+ }
114146
115147 let uploadUrl : string
116148 const folderId = validatedData . folderId ?. trim ( )
@@ -121,10 +153,6 @@ export async function POST(request: NextRequest) {
121153 uploadUrl = `${ MICROSOFT_GRAPH_BASE } /me/drive/root:/${ encodeURIComponent ( fileName ) } :/content`
122154 }
123155
124- logger . info ( `[${ requestId } ] Uploading to OneDrive: ${ uploadUrl } ` )
125-
126- const mimeType = userFile . type || 'application/octet-stream'
127-
128156 const uploadResponse = await fetch ( uploadUrl , {
129157 method : 'PUT' ,
130158 headers : {
@@ -136,11 +164,6 @@ export async function POST(request: NextRequest) {
136164
137165 if ( ! uploadResponse . ok ) {
138166 const errorText = await uploadResponse . text ( )
139- logger . error ( `[${ requestId } ] OneDrive upload failed:` , {
140- status : uploadResponse . status ,
141- statusText : uploadResponse . statusText ,
142- error : errorText ,
143- } )
144167 return NextResponse . json (
145168 {
146169 success : false ,
@@ -153,11 +176,174 @@ export async function POST(request: NextRequest) {
153176
154177 const fileData = await uploadResponse . json ( )
155178
156- logger . info ( `[${ requestId } ] File uploaded successfully to OneDrive` , {
157- fileId : fileData . id ,
158- fileName : fileData . name ,
159- size : fileData . size ,
160- } )
179+ // If this is an Excel creation and values were provided, write them using the Excel API
180+ let excelWriteResult : any | undefined
181+ const shouldWriteExcelContent =
182+ isExcelCreation && Array . isArray ( validatedData . values ) && validatedData . values . length > 0
183+
184+ if ( shouldWriteExcelContent ) {
185+ try {
186+ // Create a workbook session to ensure reliability and persistence of changes
187+ let workbookSessionId : string | undefined
188+ const sessionResp = await fetch (
189+ `${ MICROSOFT_GRAPH_BASE } /me/drive/items/${ encodeURIComponent ( fileData . id ) } /workbook/createSession` ,
190+ {
191+ method : 'POST' ,
192+ headers : {
193+ Authorization : `Bearer ${ validatedData . accessToken } ` ,
194+ 'Content-Type' : 'application/json' ,
195+ } ,
196+ body : JSON . stringify ( { persistChanges : true } ) ,
197+ }
198+ )
199+
200+ if ( sessionResp . ok ) {
201+ const sessionData = await sessionResp . json ( )
202+ workbookSessionId = sessionData ?. id
203+ }
204+
205+ // Determine the first worksheet name
206+ let sheetName = 'Sheet1'
207+ try {
208+ const listUrl = `${ MICROSOFT_GRAPH_BASE } /me/drive/items/${ encodeURIComponent (
209+ fileData . id
210+ ) } /workbook/worksheets?$select=name&$orderby=position&$top=1`
211+ const listResp = await fetch ( listUrl , {
212+ headers : {
213+ Authorization : `Bearer ${ validatedData . accessToken } ` ,
214+ ...( workbookSessionId ? { 'workbook-session-id' : workbookSessionId } : { } ) ,
215+ } ,
216+ } )
217+ if ( listResp . ok ) {
218+ const listData = await listResp . json ( )
219+ const firstSheetName = listData ?. value ?. [ 0 ] ?. name
220+ if ( firstSheetName ) {
221+ sheetName = firstSheetName
222+ }
223+ } else {
224+ const listErr = await listResp . text ( )
225+ logger . warn ( `[${ requestId } ] Failed to list worksheets, using default Sheet1` , {
226+ status : listResp . status ,
227+ error : listErr ,
228+ } )
229+ }
230+ } catch ( listError ) {
231+ logger . warn ( `[${ requestId } ] Error listing worksheets, using default Sheet1` , listError )
232+ }
233+
234+ let processedValues : any = validatedData . values || [ ]
235+
236+ if (
237+ Array . isArray ( processedValues ) &&
238+ processedValues . length > 0 &&
239+ typeof processedValues [ 0 ] === 'object' &&
240+ ! Array . isArray ( processedValues [ 0 ] )
241+ ) {
242+ const ws = XLSX . utils . json_to_sheet ( processedValues )
243+ processedValues = XLSX . utils . sheet_to_json ( ws , { header : 1 , defval : '' } )
244+ }
245+
246+ const rowsCount = processedValues . length
247+ const colsCount = Math . max ( ...processedValues . map ( ( row : any [ ] ) => row . length ) , 0 )
248+ processedValues = processedValues . map ( ( row : any [ ] ) => {
249+ const paddedRow = [ ...row ]
250+ while ( paddedRow . length < colsCount ) paddedRow . push ( '' )
251+ return paddedRow
252+ } )
253+
254+ // Compute concise end range from A1 and matrix size (no network round-trip)
255+ const indexToColLetters = ( index : number ) : string => {
256+ let n = index
257+ let s = ''
258+ while ( n > 0 ) {
259+ const rem = ( n - 1 ) % 26
260+ s = String . fromCharCode ( 65 + rem ) + s
261+ n = Math . floor ( ( n - 1 ) / 26 )
262+ }
263+ return s
264+ }
265+
266+ const endColLetters = colsCount > 0 ? indexToColLetters ( colsCount ) : 'A'
267+ const endRow = rowsCount > 0 ? rowsCount : 1
268+ const computedRangeAddress = `A1:${ endColLetters } ${ endRow } `
269+
270+ const url = new URL (
271+ `${ MICROSOFT_GRAPH_BASE } /me/drive/items/${ encodeURIComponent (
272+ fileData . id
273+ ) } /workbook/worksheets('${ encodeURIComponent (
274+ sheetName
275+ ) } ')/range(address='${ encodeURIComponent ( computedRangeAddress ) } ')`
276+ )
277+
278+ const excelWriteResponse = await fetch ( url . toString ( ) , {
279+ method : 'PATCH' ,
280+ headers : {
281+ Authorization : `Bearer ${ validatedData . accessToken } ` ,
282+ 'Content-Type' : 'application/json' ,
283+ ...( workbookSessionId ? { 'workbook-session-id' : workbookSessionId } : { } ) ,
284+ } ,
285+ body : JSON . stringify ( { values : processedValues } ) ,
286+ } )
287+
288+ if ( ! excelWriteResponse || ! excelWriteResponse . ok ) {
289+ const errorText = excelWriteResponse ? await excelWriteResponse . text ( ) : 'no response'
290+ logger . error ( `[${ requestId } ] Excel content write failed` , {
291+ status : excelWriteResponse ?. status ,
292+ statusText : excelWriteResponse ?. statusText ,
293+ error : errorText ,
294+ } )
295+ // Do not fail the entire request; return upload success with write error details
296+ excelWriteResult = {
297+ success : false ,
298+ error : `Excel write failed: ${ excelWriteResponse ?. statusText || 'unknown' } ` ,
299+ details : errorText ,
300+ }
301+ } else {
302+ const writeData = await excelWriteResponse . json ( )
303+ // The Range PATCH returns a Range object; log address and values length
304+ const addr = writeData . address || writeData . addressLocal
305+ const v = writeData . values || [ ]
306+ excelWriteResult = {
307+ success : true ,
308+ updatedRange : addr ,
309+ updatedRows : Array . isArray ( v ) ? v . length : undefined ,
310+ updatedColumns : Array . isArray ( v ) && v [ 0 ] ? v [ 0 ] . length : undefined ,
311+ updatedCells : Array . isArray ( v ) && v [ 0 ] ? v . length * ( v [ 0 ] as any [ ] ) . length : undefined ,
312+ }
313+ }
314+
315+ // Attempt to close the workbook session if one was created
316+ if ( workbookSessionId ) {
317+ try {
318+ const closeResp = await fetch (
319+ `${ MICROSOFT_GRAPH_BASE } /me/drive/items/${ encodeURIComponent ( fileData . id ) } /workbook/closeSession` ,
320+ {
321+ method : 'POST' ,
322+ headers : {
323+ Authorization : `Bearer ${ validatedData . accessToken } ` ,
324+ 'workbook-session-id' : workbookSessionId ,
325+ } ,
326+ }
327+ )
328+ if ( ! closeResp . ok ) {
329+ const closeText = await closeResp . text ( )
330+ logger . warn ( `[${ requestId } ] Failed to close Excel session` , {
331+ status : closeResp . status ,
332+ error : closeText ,
333+ } )
334+ }
335+ } catch ( closeErr ) {
336+ logger . warn ( `[${ requestId } ] Error closing Excel session` , closeErr )
337+ }
338+ }
339+ } catch ( err ) {
340+ logger . error ( `[${ requestId } ] Exception during Excel content write` , err )
341+ excelWriteResult = {
342+ success : false ,
343+ error : err instanceof Error ? err . message : 'Unknown error during Excel write' ,
344+ }
345+ }
346+ }
161347
162348 return NextResponse . json ( {
163349 success : true ,
@@ -173,6 +359,7 @@ export async function POST(request: NextRequest) {
173359 modifiedTime : fileData . lastModifiedDateTime ,
174360 parentReference : fileData . parentReference ,
175361 } ,
362+ ...( excelWriteResult ? { excelWriteResult } : { } ) ,
176363 } ,
177364 } )
178365 } catch ( error ) {
0 commit comments