feat(create-excel): onedrive create excel (#1745)

* added onedrive upload excel

* added

* updated docs

* lint

* cleaned

* use lib

---------

Co-authored-by: Adam Gough <adamgough@Adams-MacBook-Pro.local>
This commit is contained in:
Adam Gough
2025-10-28 12:26:17 -07:00
committed by GitHub
parent aace3066aa
commit 368576b082
5 changed files with 350 additions and 74 deletions

View File

@@ -51,7 +51,7 @@ In Sim, the OneDrive integration enables your agents to directly interact with y
## Usage Instructions
Integrate OneDrive into the workflow. Can create, upload, and list files.
Integrate OneDrive into the workflow. Can create text and Excel files, upload files, and list files.
@@ -68,6 +68,7 @@ Upload a file to OneDrive
| `fileName` | string | Yes | The name of the file to upload |
| `file` | file | No | The file to upload \(binary\) |
| `content` | string | No | The text content to upload \(if no file is provided\) |
| `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\) |
| `folderSelector` | string | No | Select the folder to upload the file to |
| `manualFolderId` | string | No | Manually entered folder ID \(advanced mode\) |

View File

@@ -1,4 +1,5 @@
import { type NextRequest, NextResponse } from 'next/server'
import * as XLSX from 'xlsx'
import { z } from 'zod'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { createLogger } from '@/lib/logs/console/logger'
@@ -14,8 +15,11 @@ const MICROSOFT_GRAPH_BASE = 'https://graph.microsoft.com/v1.0'
const OneDriveUploadSchema = z.object({
accessToken: z.string().min(1, 'Access token is required'),
fileName: z.string().min(1, 'File name is required'),
file: z.any(), // UserFile object
file: z.any().optional(), // UserFile object (optional for blank Excel creation)
folderId: z.string().optional().nullable(),
mimeType: z.string().optional(),
// Optional Excel write-after-create inputs
values: z.array(z.array(z.union([z.string(), z.number(), z.boolean(), z.null()]))).optional(),
})
export async function POST(request: NextRequest) {
@@ -42,17 +46,30 @@ export async function POST(request: NextRequest) {
const body = await request.json()
const validatedData = OneDriveUploadSchema.parse(body)
logger.info(`[${requestId}] Uploading file to OneDrive`, {
fileName: validatedData.fileName,
folderId: validatedData.folderId || 'root',
})
let fileBuffer: Buffer
let mimeType: string
// Handle array or single file
const rawFile = validatedData.file
let fileToProcess
// Check if we're creating a blank Excel file
const isExcelCreation =
validatedData.mimeType ===
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' && !validatedData.file
if (Array.isArray(rawFile)) {
if (rawFile.length === 0) {
if (isExcelCreation) {
// Create a blank Excel workbook
const workbook = XLSX.utils.book_new()
const worksheet = XLSX.utils.aoa_to_sheet([[]])
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1')
// Generate XLSX file as buffer
const xlsxBuffer = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' })
fileBuffer = Buffer.from(xlsxBuffer)
mimeType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
} else {
// Handle regular file upload
const rawFile = validatedData.file
if (!rawFile) {
return NextResponse.json(
{
success: false,
@@ -61,40 +78,51 @@ export async function POST(request: NextRequest) {
{ status: 400 }
)
}
fileToProcess = rawFile[0]
} else {
fileToProcess = rawFile
}
// Convert to UserFile format
let userFile
try {
userFile = processSingleFileToUserFile(fileToProcess, requestId, logger)
} catch (error) {
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Failed to process file',
},
{ status: 400 }
)
}
let fileToProcess
if (Array.isArray(rawFile)) {
if (rawFile.length === 0) {
return NextResponse.json(
{
success: false,
error: 'No file provided',
},
{ status: 400 }
)
}
fileToProcess = rawFile[0]
} else {
fileToProcess = rawFile
}
logger.info(`[${requestId}] Downloading file from storage: ${userFile.key}`)
// Convert to UserFile format
let userFile
try {
userFile = processSingleFileToUserFile(fileToProcess, requestId, logger)
} catch (error) {
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Failed to process file',
},
{ status: 400 }
)
}
let fileBuffer: Buffer
try {
fileBuffer = await downloadFileFromStorage(userFile, requestId, logger)
} catch (error) {
logger.error(`[${requestId}] Failed to download file from storage:`, error)
return NextResponse.json(
{
success: false,
error: `Failed to download file: ${error instanceof Error ? error.message : 'Unknown error'}`,
},
{ status: 500 }
)
}
try {
fileBuffer = await downloadFileFromStorage(userFile, requestId, logger)
} catch (error) {
logger.error(`[${requestId}] Failed to download file from storage:`, error)
return NextResponse.json(
{
success: false,
error: `Failed to download file: ${error instanceof Error ? error.message : 'Unknown error'}`,
},
{ status: 500 }
)
mimeType = userFile.type || 'application/octet-stream'
}
const maxSize = 250 * 1024 * 1024 // 250MB
@@ -110,7 +138,11 @@ export async function POST(request: NextRequest) {
)
}
const fileName = validatedData.fileName || userFile.name
// Ensure file name has correct extension for Excel files
let fileName = validatedData.fileName
if (isExcelCreation && !fileName.endsWith('.xlsx')) {
fileName = `${fileName.replace(/\.[^.]*$/, '')}.xlsx`
}
let uploadUrl: string
const folderId = validatedData.folderId?.trim()
@@ -121,10 +153,6 @@ export async function POST(request: NextRequest) {
uploadUrl = `${MICROSOFT_GRAPH_BASE}/me/drive/root:/${encodeURIComponent(fileName)}:/content`
}
logger.info(`[${requestId}] Uploading to OneDrive: ${uploadUrl}`)
const mimeType = userFile.type || 'application/octet-stream'
const uploadResponse = await fetch(uploadUrl, {
method: 'PUT',
headers: {
@@ -136,11 +164,6 @@ export async function POST(request: NextRequest) {
if (!uploadResponse.ok) {
const errorText = await uploadResponse.text()
logger.error(`[${requestId}] OneDrive upload failed:`, {
status: uploadResponse.status,
statusText: uploadResponse.statusText,
error: errorText,
})
return NextResponse.json(
{
success: false,
@@ -153,11 +176,174 @@ export async function POST(request: NextRequest) {
const fileData = await uploadResponse.json()
logger.info(`[${requestId}] File uploaded successfully to OneDrive`, {
fileId: fileData.id,
fileName: fileData.name,
size: fileData.size,
})
// If this is an Excel creation and values were provided, write them using the Excel API
let excelWriteResult: any | undefined
const shouldWriteExcelContent =
isExcelCreation && Array.isArray(validatedData.values) && validatedData.values.length > 0
if (shouldWriteExcelContent) {
try {
// Create a workbook session to ensure reliability and persistence of changes
let workbookSessionId: string | undefined
const sessionResp = await fetch(
`${MICROSOFT_GRAPH_BASE}/me/drive/items/${encodeURIComponent(fileData.id)}/workbook/createSession`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${validatedData.accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ persistChanges: true }),
}
)
if (sessionResp.ok) {
const sessionData = await sessionResp.json()
workbookSessionId = sessionData?.id
}
// Determine the first worksheet name
let sheetName = 'Sheet1'
try {
const listUrl = `${MICROSOFT_GRAPH_BASE}/me/drive/items/${encodeURIComponent(
fileData.id
)}/workbook/worksheets?$select=name&$orderby=position&$top=1`
const listResp = await fetch(listUrl, {
headers: {
Authorization: `Bearer ${validatedData.accessToken}`,
...(workbookSessionId ? { 'workbook-session-id': workbookSessionId } : {}),
},
})
if (listResp.ok) {
const listData = await listResp.json()
const firstSheetName = listData?.value?.[0]?.name
if (firstSheetName) {
sheetName = firstSheetName
}
} else {
const listErr = await listResp.text()
logger.warn(`[${requestId}] Failed to list worksheets, using default Sheet1`, {
status: listResp.status,
error: listErr,
})
}
} catch (listError) {
logger.warn(`[${requestId}] Error listing worksheets, using default Sheet1`, listError)
}
let processedValues: any = validatedData.values || []
if (
Array.isArray(processedValues) &&
processedValues.length > 0 &&
typeof processedValues[0] === 'object' &&
!Array.isArray(processedValues[0])
) {
const ws = XLSX.utils.json_to_sheet(processedValues)
processedValues = XLSX.utils.sheet_to_json(ws, { header: 1, defval: '' })
}
const rowsCount = processedValues.length
const colsCount = Math.max(...processedValues.map((row: any[]) => row.length), 0)
processedValues = processedValues.map((row: any[]) => {
const paddedRow = [...row]
while (paddedRow.length < colsCount) paddedRow.push('')
return paddedRow
})
// Compute concise end range from A1 and matrix size (no network round-trip)
const indexToColLetters = (index: number): string => {
let n = index
let s = ''
while (n > 0) {
const rem = (n - 1) % 26
s = String.fromCharCode(65 + rem) + s
n = Math.floor((n - 1) / 26)
}
return s
}
const endColLetters = colsCount > 0 ? indexToColLetters(colsCount) : 'A'
const endRow = rowsCount > 0 ? rowsCount : 1
const computedRangeAddress = `A1:${endColLetters}${endRow}`
const url = new URL(
`${MICROSOFT_GRAPH_BASE}/me/drive/items/${encodeURIComponent(
fileData.id
)}/workbook/worksheets('${encodeURIComponent(
sheetName
)}')/range(address='${encodeURIComponent(computedRangeAddress)}')`
)
const excelWriteResponse = await fetch(url.toString(), {
method: 'PATCH',
headers: {
Authorization: `Bearer ${validatedData.accessToken}`,
'Content-Type': 'application/json',
...(workbookSessionId ? { 'workbook-session-id': workbookSessionId } : {}),
},
body: JSON.stringify({ values: processedValues }),
})
if (!excelWriteResponse || !excelWriteResponse.ok) {
const errorText = excelWriteResponse ? await excelWriteResponse.text() : 'no response'
logger.error(`[${requestId}] Excel content write failed`, {
status: excelWriteResponse?.status,
statusText: excelWriteResponse?.statusText,
error: errorText,
})
// Do not fail the entire request; return upload success with write error details
excelWriteResult = {
success: false,
error: `Excel write failed: ${excelWriteResponse?.statusText || 'unknown'}`,
details: errorText,
}
} else {
const writeData = await excelWriteResponse.json()
// The Range PATCH returns a Range object; log address and values length
const addr = writeData.address || writeData.addressLocal
const v = writeData.values || []
excelWriteResult = {
success: true,
updatedRange: addr,
updatedRows: Array.isArray(v) ? v.length : undefined,
updatedColumns: Array.isArray(v) && v[0] ? v[0].length : undefined,
updatedCells: Array.isArray(v) && v[0] ? v.length * (v[0] as any[]).length : undefined,
}
}
// Attempt to close the workbook session if one was created
if (workbookSessionId) {
try {
const closeResp = await fetch(
`${MICROSOFT_GRAPH_BASE}/me/drive/items/${encodeURIComponent(fileData.id)}/workbook/closeSession`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${validatedData.accessToken}`,
'workbook-session-id': workbookSessionId,
},
}
)
if (!closeResp.ok) {
const closeText = await closeResp.text()
logger.warn(`[${requestId}] Failed to close Excel session`, {
status: closeResp.status,
error: closeText,
})
}
} catch (closeErr) {
logger.warn(`[${requestId}] Error closing Excel session`, closeErr)
}
}
} catch (err) {
logger.error(`[${requestId}] Exception during Excel content write`, err)
excelWriteResult = {
success: false,
error: err instanceof Error ? err.message : 'Unknown error during Excel write',
}
}
}
return NextResponse.json({
success: true,
@@ -173,6 +359,7 @@ export async function POST(request: NextRequest) {
modifiedTime: fileData.lastModifiedDateTime,
parentReference: fileData.parentReference,
},
...(excelWriteResult ? { excelWriteResult } : {}),
},
})
} catch (error) {

View File

@@ -8,7 +8,8 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
name: 'OneDrive',
description: 'Create, upload, and list files',
authMode: AuthMode.OAuth,
longDescription: 'Integrate OneDrive into the workflow. Can create, upload, and list files.',
longDescription:
'Integrate OneDrive into the workflow. Can create text and Excel files, upload files, and list files.',
docsLink: 'https://docs.sim.ai/tools/onedrive',
category: 'tools',
bgColor: '#E0E0E0',
@@ -51,10 +52,45 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
title: 'File Name',
type: 'short-input',
layout: 'full',
placeholder: 'Name of the file (e.g., document.txt)',
placeholder: 'Name of the file',
condition: { field: 'operation', value: ['create_file', 'upload'] },
required: true,
},
// File Type selector for create_file operation
{
id: 'mimeType',
title: 'File Type',
type: 'dropdown',
layout: 'full',
options: [
{ label: 'Text File (.txt)', id: 'text/plain' },
{
label: 'Excel File (.xlsx)',
id: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
},
],
placeholder: 'Select file type',
condition: { field: 'operation', value: 'create_file' },
required: true,
},
// Excel values input when creating an .xlsx file
{
id: 'values',
title: 'Values',
type: 'long-input',
layout: 'full',
placeholder:
'Enter values as JSON array of arrays (e.g., [["A1","B1"],["A2","B2"]]) or an array of objects',
condition: {
field: 'operation',
value: 'create_file',
and: {
field: 'mimeType',
value: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
},
},
required: false,
},
// File upload (basic mode)
{
id: 'file',
@@ -86,7 +122,14 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
type: 'long-input',
layout: 'full',
placeholder: 'Text content for the file',
condition: { field: 'operation', value: 'create_file' },
condition: {
field: 'operation',
value: 'create_file',
and: {
field: 'mimeType',
value: 'text/plain',
},
},
required: true,
},
@@ -234,14 +277,22 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
}
},
params: (params) => {
const { credential, folderSelector, manualFolderId, mimeType, ...rest } = params
const { credential, folderSelector, manualFolderId, mimeType, values, ...rest } = params
// Use folderSelector if provided, otherwise use manualFolderId
const effectiveFolderId = (folderSelector || manualFolderId || '').trim()
let parsedValues
try {
parsedValues = values ? JSON.parse(values as string) : undefined
} catch (error) {
throw new Error('Invalid JSON format for values')
}
return {
credential,
...rest,
values: parsedValues,
folderId: effectiveFolderId || undefined,
pageSize: rest.pageSize ? Number.parseInt(rest.pageSize as string, 10) : undefined,
mimeType: mimeType,
@@ -257,6 +308,8 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
file: { type: 'json', description: 'File to upload (UserFile object)' },
fileReference: { type: 'json', description: 'File reference from previous block' },
content: { type: 'string', description: 'Text content to upload' },
mimeType: { type: 'string', description: 'MIME type of file to create' },
values: { type: 'string', description: 'Cell values for new Excel as JSON' },
// Get Content operation inputs
// fileId: { type: 'string', required: false },
// List operation inputs

View File

@@ -43,6 +43,15 @@ export interface OneDriveListResponse extends ToolResponse {
export interface OneDriveUploadResponse extends ToolResponse {
output: {
file: OneDriveFile
excelWriteResult?: {
success: boolean
updatedRange?: string
updatedRows?: number
updatedColumns?: number
updatedCells?: number
error?: string
details?: string
}
}
}
@@ -60,6 +69,8 @@ export interface OneDriveToolParams {
pageSize?: number
pageToken?: string
exportMimeType?: string
// Optional Excel write parameters (used when creating an .xlsx without file content)
values?: (string | number | boolean | null)[][]
}
export type OneDriveResponse = OneDriveUploadResponse | OneDriveListResponse

View File

@@ -48,6 +48,13 @@ export const uploadTool: ToolConfig<OneDriveToolParams, OneDriveUploadResponse>
visibility: 'user-or-llm',
description: 'The text content to upload (if no file is provided)',
},
mimeType: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'The MIME type of the file to create (e.g., text/plain for .txt, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet for .xlsx)',
},
folderSelector: {
type: 'string',
required: false,
@@ -64,15 +71,17 @@ export const uploadTool: ToolConfig<OneDriveToolParams, OneDriveUploadResponse>
request: {
url: (params) => {
// If file is provided, use custom API route for binary upload
if (params.file) {
// If file is provided OR Excel file is being created, use custom API route
const isExcelFile =
params.mimeType === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
if (params.file || isExcelFile) {
return '/api/tools/onedrive/upload'
}
// Text-only upload - use direct Microsoft Graph API
// Direct upload for text files - use Microsoft Graph API
let fileName = params.fileName || 'untitled'
// Always create .txt files for text content
// For text files, ensure .txt extension
if (!fileName.endsWith('.txt')) {
// Remove any existing extensions and add .txt
fileName = `${fileName.replace(/\.[^.]*$/, '')}.txt`
@@ -87,32 +96,44 @@ export const uploadTool: ToolConfig<OneDriveToolParams, OneDriveUploadResponse>
return `https://graph.microsoft.com/v1.0/me/drive/root:/${fileName}:/content`
},
method: (params) => {
// Use POST for custom API route, PUT for direct upload
return params.file ? 'POST' : 'PUT'
// Use POST for custom API route (file uploads or Excel creation), PUT for direct text upload
const isExcelFile =
params.mimeType === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
return params.file || isExcelFile ? 'POST' : 'PUT'
},
headers: (params) => {
const headers: Record<string, string> = {}
// For file uploads via custom API, send JSON
if (params.file) {
const isExcelFile =
params.mimeType === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
// For file uploads or Excel creation via custom API, send JSON
if (params.file || isExcelFile) {
headers['Content-Type'] = 'application/json'
} else {
// For text-only uploads, use direct PUT with access token
// For direct text uploads, use direct PUT with access token
headers.Authorization = `Bearer ${params.accessToken}`
headers['Content-Type'] = 'text/plain'
}
return headers
},
body: (params) => {
// For file uploads, send all params as JSON to custom API route
if (params.file) {
const isExcelFile =
params.mimeType === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
// For file uploads or Excel creation, send all params as JSON to custom API route
if (params.file || isExcelFile) {
return {
accessToken: params.accessToken,
fileName: params.fileName,
file: params.file,
folderId: params.manualFolderId || params.folderSelector,
mimeType: params.mimeType,
// Optional Excel content write-after-create
values: params.values,
}
}
// For text-only uploads, send content directly
// For text files, send content directly
return (params.content || '') as unknown as Record<string, unknown>
},
},
@@ -120,8 +141,11 @@ export const uploadTool: ToolConfig<OneDriveToolParams, OneDriveUploadResponse>
transformResponse: async (response: Response, params?: OneDriveToolParams) => {
const data = await response.json()
// Handle response from custom API route (for file uploads)
if (params?.file && data.success !== undefined) {
const isExcelFile =
params?.mimeType === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
// Handle response from custom API route (for file uploads or Excel creation)
if ((params?.file || isExcelFile) && data.success !== undefined) {
if (!data.success) {
throw new Error(data.error || 'Failed to upload file')
}