Skip to content

Commit 368576b

Browse files
aadamgoughAdam Gough
andauthored
feat(create-excel): onedrive create excel (#1745)
* added onedrive upload excel * added * updated docs * lint * cleaned * use lib --------- Co-authored-by: Adam Gough <[email protected]>
1 parent aace306 commit 368576b

File tree

5 files changed

+350
-74
lines changed

5 files changed

+350
-74
lines changed

apps/docs/content/docs/en/tools/onedrive.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ In Sim, the OneDrive integration enables your agents to directly interact with y
5151

5252
## Usage Instructions
5353

54-
Integrate OneDrive into the workflow. Can create, upload, and list files.
54+
Integrate OneDrive into the workflow. Can create text and Excel files, upload files, and list files.
5555

5656

5757

@@ -68,6 +68,7 @@ Upload a file to OneDrive
6868
| `fileName` | string | Yes | The name of the file to upload |
6969
| `file` | file | No | The file to upload \(binary\) |
7070
| `content` | string | No | The text content to upload \(if no file is provided\) |
71+
| `mimeType` | string | No | The MIME type of the file to create \(e.g., text/plain for .txt, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet for .xlsx\) |
7172
| `folderSelector` | string | No | Select the folder to upload the file to |
7273
| `manualFolderId` | string | No | Manually entered folder ID \(advanced mode\) |
7374

apps/sim/app/api/tools/onedrive/upload/route.ts

Lines changed: 242 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { type NextRequest, NextResponse } from 'next/server'
2+
import * as XLSX from 'xlsx'
23
import { z } from 'zod'
34
import { checkHybridAuth } from '@/lib/auth/hybrid'
45
import { createLogger } from '@/lib/logs/console/logger'
@@ -14,8 +15,11 @@ const MICROSOFT_GRAPH_BASE = 'https://graph.microsoft.com/v1.0'
1415
const 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

2125
export 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

Comments
 (0)