Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
9669ca4
feat(microsoft-excel): add SharePoint drive support for Excel integra…
waleedlatif1 Apr 14, 2026
6162253
fix(microsoft-excel): address PR review comments
waleedlatif1 Apr 14, 2026
a3c93ee
fix(microsoft-excel): validate driveId in files route
waleedlatif1 Apr 14, 2026
780fa90
fix(microsoft-excel): unblock OneDrive users and validate driveId in …
waleedlatif1 Apr 14, 2026
f18af3c
fix(microsoft-excel): validate driveId in getItemBasePath utility
waleedlatif1 Apr 14, 2026
65308e4
fix(microsoft-excel): use centralized input validation
waleedlatif1 Apr 14, 2026
2884587
lint
waleedlatif1 Apr 14, 2026
649c3e6
improvement(microsoft-excel): add File Source dropdown to control Sha…
waleedlatif1 Apr 14, 2026
8b1c88c
fix(microsoft-excel): fix canonical param test failures
waleedlatif1 Apr 15, 2026
326114d
fix(microsoft-excel): address PR review feedback for SharePoint drive…
waleedlatif1 Apr 15, 2026
3be18ca
fix(microsoft-excel): use validateMicrosoftGraphId for driveId valida…
waleedlatif1 Apr 15, 2026
12231db
fix(microsoft-excel): use validatePathSegment with strict pattern for…
waleedlatif1 Apr 15, 2026
8148260
lint
waleedlatif1 Apr 15, 2026
16ad6ce
fix(microsoft-excel): reorder driveId before spreadsheetId in v1 block
waleedlatif1 Apr 15, 2026
d1b8778
fix(microsoft-excel): clear manualDriveId when fileSource changes
waleedlatif1 Apr 15, 2026
def6e90
refactor(microsoft-excel): use getItemBasePath in sheets route to rem…
waleedlatif1 Apr 15, 2026
5334c2b
lint
waleedlatif1 Apr 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/docs/content/docs/en/tools/microsoft_excel.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ Read data from a specific sheet in a Microsoft Excel spreadsheet
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `spreadsheetId` | string | Yes | The ID of the spreadsheet/workbook to read from \(e.g., "01ABC123DEF456"\) |
| `driveId` | string | No | The ID of the drive containing the spreadsheet. Required for SharePoint files. If omitted, uses personal OneDrive. |
| `range` | string | No | The range of cells to read from. Accepts "SheetName!A1:B2" for explicit ranges or just "SheetName" to read the used range of that sheet. If omitted, reads the used range of the first sheet. |

#### Output
Expand All @@ -67,6 +68,7 @@ Write data to a specific sheet in a Microsoft Excel spreadsheet
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `spreadsheetId` | string | Yes | The ID of the spreadsheet/workbook to write to \(e.g., "01ABC123DEF456"\) |
| `driveId` | string | No | The ID of the drive containing the spreadsheet. Required for SharePoint files. If omitted, uses personal OneDrive. |
| `range` | string | No | The range of cells to write to \(e.g., "Sheet1!A1:B2"\) |
| `values` | array | Yes | The data to write as a 2D array \(e.g., \[\["Name", "Age"\], \["Alice", 30\]\]\) or array of objects |
| `valueInputOption` | string | No | The format of the data to write |
Expand Down
17 changes: 16 additions & 1 deletion apps/sim/app/api/auth/oauth/microsoft/files/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { validatePathSegment } from '@/lib/core/security/input-validation'
import { generateRequestId } from '@/lib/core/utils/request'
import { getCredential, refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'

Expand All @@ -19,6 +20,7 @@ export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const credentialId = searchParams.get('credentialId')
const query = searchParams.get('query') || ''
const driveId = searchParams.get('driveId') || undefined
const workflowId = searchParams.get('workflowId') || undefined

if (!credentialId) {
Expand Down Expand Up @@ -72,8 +74,21 @@ export async function GET(request: NextRequest) {
)
searchParams_new.append('$top', '50')

// When driveId is provided (SharePoint), search within that specific drive.
// Otherwise, search the user's personal OneDrive.
if (driveId) {
const driveIdValidation = validatePathSegment(driveId, {
paramName: 'driveId',
customPattern: /^[a-zA-Z0-9!_-]+$/,
})
if (!driveIdValidation.isValid) {
return NextResponse.json({ error: driveIdValidation.error }, { status: 400 })
}
}
const drivePath = driveId ? `drives/${driveId}` : 'me/drive'

const response = await fetch(
`https://graph.microsoft.com/v1.0/me/drive/root/search(q='${encodeURIComponent(searchQuery)}')?${searchParams_new.toString()}`,
`https://graph.microsoft.com/v1.0/${drivePath}/root/search(q='${encodeURIComponent(searchQuery)}')?${searchParams_new.toString()}`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
Expand Down
134 changes: 134 additions & 0 deletions apps/sim/app/api/tools/microsoft_excel/drives/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { validatePathSegment, validateSharePointSiteId } from '@/lib/core/security/input-validation'
import { generateRequestId } from '@/lib/core/utils/request'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'

export const dynamic = 'force-dynamic'

const logger = createLogger('MicrosoftExcelDrivesAPI')

interface GraphDrive {
id: string
name: string
driveType: string
webUrl?: string
}

/**
* List document libraries (drives) for a SharePoint site.
* Used by the microsoft.excel.drives selector to let users pick
* which drive contains their Excel file.
*/
export async function POST(request: NextRequest) {
const requestId = generateRequestId()

try {
const body = await request.json()
const { credential, workflowId, siteId, driveId } = body

if (!credential) {
logger.warn(`[${requestId}] Missing credential in request`)
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
}

if (!siteId) {
logger.warn(`[${requestId}] Missing siteId in request`)
return NextResponse.json({ error: 'Site ID is required' }, { status: 400 })
}

const siteIdValidation = validateSharePointSiteId(siteId, 'siteId')
if (!siteIdValidation.isValid) {
logger.warn(`[${requestId}] Invalid siteId format`)
return NextResponse.json({ error: siteIdValidation.error }, { status: 400 })
}

const authz = await authorizeCredentialUse(request, {
credentialId: credential,
workflowId,
})
if (!authz.ok || !authz.credentialOwnerUserId) {
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
}

const accessToken = await refreshAccessTokenIfNeeded(
credential,
authz.credentialOwnerUserId,
requestId
)
if (!accessToken) {
logger.warn(`[${requestId}] Failed to obtain valid access token`)
return NextResponse.json(
{ error: 'Failed to obtain valid access token', authRequired: true },
{ status: 401 }
)
}

// Single-drive lookup when driveId is provided (used by fetchById)
if (driveId) {
const driveIdValidation = validatePathSegment(driveId, {
paramName: 'driveId',
customPattern: /^[a-zA-Z0-9!_-]+$/,
})
if (!driveIdValidation.isValid) {
return NextResponse.json({ error: driveIdValidation.error }, { status: 400 })
}

const url = `https://graph.microsoft.com/v1.0/sites/${siteId}/drives/${driveId}?$select=id,name,driveType,webUrl`
const response = await fetch(url, {
headers: { Authorization: `Bearer ${accessToken}` },
})

if (!response.ok) {
const errorData = await response
.json()
.catch(() => ({ error: { message: 'Unknown error' } }))
return NextResponse.json(
{ error: errorData.error?.message || 'Failed to fetch drive' },
{ status: response.status }
)
}

const data: GraphDrive = await response.json()
return NextResponse.json(
{ drive: { id: data.id, name: data.name, driveType: data.driveType } },
{ status: 200 }
)
}

// List all drives for the site
const url = `https://graph.microsoft.com/v1.0/sites/${siteId}/drives?$select=id,name,driveType,webUrl`

const response = await fetch(url, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
})

if (!response.ok) {
const errorData = await response.json().catch(() => ({ error: { message: 'Unknown error' } }))
logger.error(`[${requestId}] Microsoft Graph API error fetching drives`, {
status: response.status,
error: errorData.error?.message,
})
return NextResponse.json(
{ error: errorData.error?.message || 'Failed to fetch drives' },
{ status: response.status }
)
}

const data = await response.json()
const drives = (data.value || []).map((drive: GraphDrive) => ({
id: drive.id,
name: drive.name,
driveType: drive.driveType,
}))

logger.info(`[${requestId}] Successfully fetched ${drives.length} drives for site ${siteId}`)
return NextResponse.json({ drives }, { status: 200 })
} catch (error) {
logger.error(`[${requestId}] Error fetching drives`, error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}
30 changes: 19 additions & 11 deletions apps/sim/app/api/tools/microsoft_excel/sheets/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { generateRequestId } from '@/lib/core/utils/request'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
import { getItemBasePath } from '@/tools/microsoft_excel/utils'

export const dynamic = 'force-dynamic'

Expand Down Expand Up @@ -30,6 +31,7 @@ export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const credentialId = searchParams.get('credentialId')
const spreadsheetId = searchParams.get('spreadsheetId')
const driveId = searchParams.get('driveId') || undefined
const workflowId = searchParams.get('workflowId') || undefined

if (!credentialId) {
Expand Down Expand Up @@ -61,17 +63,23 @@ export async function GET(request: NextRequest) {
`[${requestId}] Fetching worksheets from Microsoft Graph API for workbook ${spreadsheetId}`
)

// Fetch worksheets from Microsoft Graph API
const worksheetsResponse = await fetch(
`https://graph.microsoft.com/v1.0/me/drive/items/${spreadsheetId}/workbook/worksheets`,
{
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
}
)
let basePath: string
try {
basePath = getItemBasePath(spreadsheetId, driveId)
} catch (error) {
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Invalid parameters' },
{ status: 400 }
)
}

const worksheetsResponse = await fetch(`${basePath}/workbook/worksheets`, {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
})

if (!worksheetsResponse.ok) {
const errorData = await worksheetsResponse
Expand Down
Loading
Loading