mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
feat(sharepoint): validate against Graph API and add delete/update/download tools (#5369)
* feat(sharepoint): validate against Graph API and add delete/update/download tools - Fix bugs found validating existing tools against live Graph docs: malformed nested $expand syntax in get_list, reversed site-resolution precedence in create_list, unsanitized field fallback in add_list_items, missing URL encoding in read_page, and an incorrect root/serverRelativeUrl field shape - Fix V1 block gaps vs V2: upload_file required flag, missing required on pageName/listDisplayName, wrong site-picker mimeType, dead subBlock refs - Add 8 new tools within already-granted scopes for CRUD completion: delete_list_item, get_list_item, delete_page, update_page, publish_page, download_file, delete_file, get_drive_item - Add opt-in stripAuthOnRedirect option to secureFetchWithPinnedIP so the SharePoint file-download route doesn't resend the bearer token to the preauthenticated redirect target (default off, no behavior change elsewhere) - Dedupe read-only list-item field sanitization into a shared utils helper * fix(sharepoint): encode siteId consistently and fix create_page precedence - URL-encode siteId/groupId everywhere it's interpolated into a Graph request path (Graph site IDs like "host,guid,guid" contain commas that must be encoded) - addresses Cursor Bugbot findings on update_page, delete_page, publish_page, and applies the same fix consistently across every other sharepoint tool for consistency - Fix create_page's site-resolution precedence (was siteSelector before siteId, inconsistent with every sibling tool) * fix(sharepoint): escape HTML in page content and stop fallback from throwing - create_page/update_page only escaped quotes when building innerHtml; add a shared escapeHtml() helper that also escapes &, <, > so page content with angle brackets/ampersands doesn't corrupt the canvas layout - add_list_items' fields fallback (sanitized re-derivation when Graph's response omits fields) could throw on a malformed fallback input after the item was already created successfully; catch and fall back to undefined instead of failing an already-successful response * fix(sharepoint): scope columnDefinitions->pageContent mapping to create_list Both V1 and V2 tools.config.params mapped columnDefinitions onto pageContent whenever columnDefinitions was truthy, with no operation check. Stale list-column JSON left in block state after switching to create_page/ update_page would silently replace the user's intended page text. Gate the mapping on operation === create_list (V1) / sharepoint_create_list (V2). * fix(sharepoint): cap download-file content fetch at MAX_FILE_SIZE Greptile flagged the content fetch as unbounded - a large file would buffer entirely in memory before base64-encoding into the JSON response. Pass maxResponseBytes: MAX_FILE_SIZE (100MB, same constant used by the upload path) to secureFetchWithPinnedIP so oversized files reject early with a clear PayloadSizeLimitError instead of exhausting memory. * fix(sharepoint): close V1 block input/output gaps and encode upload URLs Final validation pass (4 parallel audit agents against live Graph docs) surfaced remaining gaps: - apps/sim/app/api/tools/sharepoint/upload/route.ts: siteId/driveId were not encodeURIComponent-ed in the Graph upload URL, unlike every other sharepoint route/tool - same encoding-gap class fixed everywhere else - read_page.ts: transformResponse recomputed siteId with a raw `||` in 3 spots instead of the optionalTrim(...) || optionalTrim(...) || 'root' precedence used by request.url and every sibling tool - SharepointBlock (V1, legacy/hidden-from-toolbar but still executable for existing saved workflows): listItemFields and listItemId were missing `required` for update_list despite the tool requiring them; maxPages/groupId/includeColumns/includeItems/nextPageUrl subBlocks were entirely absent, making those tool params unreachable from the UI; block-level outputs were missing site/pages/content/totalPages/ nextPageUrl/lists/skippedFiles/skippedCount/errors even though the underlying tools return them - V2 already covered all of these
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { sharepointDownloadFileContract } from '@/lib/api/contracts/tools/microsoft'
|
||||
import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import {
|
||||
secureFetchWithPinnedIP,
|
||||
validateUrlWithDNS,
|
||||
} from '@/lib/core/security/input-validation.server'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { MAX_FILE_SIZE } from '@/lib/uploads/utils/validation'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
/** Microsoft Graph API error response structure */
|
||||
interface GraphApiError {
|
||||
error?: {
|
||||
code?: string
|
||||
message?: string
|
||||
}
|
||||
}
|
||||
|
||||
/** Microsoft Graph API drive item metadata response */
|
||||
interface DriveItemMetadata {
|
||||
id?: string
|
||||
name?: string
|
||||
folder?: Record<string, unknown>
|
||||
file?: {
|
||||
mimeType?: string
|
||||
}
|
||||
}
|
||||
|
||||
const logger = createLogger('SharepointDownloadFileAPI')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
logger.warn(`[${requestId}] Unauthorized SharePoint download attempt: ${authResult.error}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: authResult.error || 'Authentication required',
|
||||
},
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const parsed = await parseRequest(sharepointDownloadFileContract, request, {})
|
||||
if (!parsed.success) return parsed.response
|
||||
const { accessToken, driveId, itemId, fileName } = parsed.data.body
|
||||
const authHeader = `Bearer ${accessToken}`
|
||||
|
||||
logger.info(`[${requestId}] Getting file metadata from SharePoint`, { driveId, itemId })
|
||||
|
||||
const metadataUrl = `https://graph.microsoft.com/v1.0/drives/${encodeURIComponent(driveId)}/items/${encodeURIComponent(itemId)}`
|
||||
const metadataUrlValidation = await validateUrlWithDNS(metadataUrl, 'metadataUrl')
|
||||
if (!metadataUrlValidation.isValid) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: metadataUrlValidation.error },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const metadataResponse = await secureFetchWithPinnedIP(
|
||||
metadataUrl,
|
||||
metadataUrlValidation.resolvedIP!,
|
||||
{
|
||||
headers: { Authorization: authHeader },
|
||||
}
|
||||
)
|
||||
|
||||
if (!metadataResponse.ok) {
|
||||
const errorDetails = (await metadataResponse.json().catch(() => ({}))) as GraphApiError
|
||||
logger.error(`[${requestId}] Failed to get file metadata`, {
|
||||
status: metadataResponse.status,
|
||||
error: errorDetails,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{ success: false, error: errorDetails.error?.message || 'Failed to get file metadata' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const metadata = (await metadataResponse.json()) as DriveItemMetadata
|
||||
|
||||
if (metadata.folder && !metadata.file) {
|
||||
logger.error(`[${requestId}] Attempted to download a folder`, {
|
||||
itemId: metadata.id,
|
||||
itemName: metadata.name,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: `Cannot download folder "${metadata.name}". Please select a file instead.`,
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const mimeType = metadata.file?.mimeType || 'application/octet-stream'
|
||||
|
||||
logger.info(`[${requestId}] Downloading file from SharePoint`, { driveId, itemId, mimeType })
|
||||
|
||||
const downloadUrl = `https://graph.microsoft.com/v1.0/drives/${encodeURIComponent(driveId)}/items/${encodeURIComponent(itemId)}/content`
|
||||
const downloadUrlValidation = await validateUrlWithDNS(downloadUrl, 'downloadUrl')
|
||||
if (!downloadUrlValidation.isValid) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: downloadUrlValidation.error },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const downloadResponse = await secureFetchWithPinnedIP(
|
||||
downloadUrl,
|
||||
downloadUrlValidation.resolvedIP!,
|
||||
{
|
||||
headers: { Authorization: authHeader },
|
||||
// The content endpoint 302s to a preauthenticated URL on a different origin that needs no auth.
|
||||
stripAuthOnRedirect: true,
|
||||
maxResponseBytes: MAX_FILE_SIZE,
|
||||
}
|
||||
)
|
||||
|
||||
if (!downloadResponse.ok) {
|
||||
const downloadError = (await downloadResponse.json().catch(() => ({}))) as GraphApiError
|
||||
logger.error(`[${requestId}] Failed to download file`, {
|
||||
status: downloadResponse.status,
|
||||
error: downloadError,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{ success: false, error: downloadError.error?.message || 'Failed to download file' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const arrayBuffer = await downloadResponse.arrayBuffer()
|
||||
const fileBuffer = Buffer.from(arrayBuffer)
|
||||
|
||||
const resolvedName = fileName || metadata.name || 'download'
|
||||
|
||||
logger.info(`[${requestId}] File downloaded successfully`, {
|
||||
driveId,
|
||||
itemId,
|
||||
name: resolvedName,
|
||||
size: fileBuffer.length,
|
||||
mimeType,
|
||||
})
|
||||
|
||||
const base64Data = fileBuffer.toString('base64')
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
file: {
|
||||
name: resolvedName,
|
||||
mimeType,
|
||||
data: base64Data,
|
||||
size: fileBuffer.length,
|
||||
},
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error downloading SharePoint file:`, error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: getErrorMessage(error, 'Unknown error occurred'),
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -135,8 +135,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
.join('/')
|
||||
|
||||
const uploadUrl = driveId
|
||||
? `https://graph.microsoft.com/v1.0/drives/${driveId}/root:${encodedPath}:/content`
|
||||
: `https://graph.microsoft.com/v1.0/sites/${siteId}/drive/root:${encodedPath}:/content`
|
||||
? `https://graph.microsoft.com/v1.0/drives/${encodeURIComponent(driveId)}/root:${encodedPath}:/content`
|
||||
: `https://graph.microsoft.com/v1.0/sites/${encodeURIComponent(siteId)}/drive/root:${encodedPath}:/content`
|
||||
|
||||
logger.info(`[${requestId}] Uploading to: ${uploadUrl}`)
|
||||
|
||||
|
||||
@@ -30,12 +30,20 @@ export const SharepointBlock: BlockConfig<SharepointResponse> = {
|
||||
options: [
|
||||
{ label: 'Create Page', id: 'create_page' },
|
||||
{ label: 'Read Page', id: 'read_page' },
|
||||
{ label: 'Update Page', id: 'update_page' },
|
||||
{ label: 'Publish Page', id: 'publish_page' },
|
||||
{ label: 'Delete Page', id: 'delete_page' },
|
||||
{ label: 'List Sites', id: 'list_sites' },
|
||||
{ label: 'Create List', id: 'create_list' },
|
||||
{ label: 'Read List', id: 'read_list' },
|
||||
{ label: 'Update List', id: 'update_list' },
|
||||
{ label: 'Add List Items', id: 'add_list_items' },
|
||||
{ label: 'Get List Item', id: 'get_list_item' },
|
||||
{ label: 'Delete List Item', id: 'delete_list_item' },
|
||||
{ label: 'Upload File', id: 'upload_file' },
|
||||
{ label: 'Download File', id: 'download_file' },
|
||||
{ label: 'Get Drive Item', id: 'get_drive_item' },
|
||||
{ label: 'Delete File', id: 'delete_file' },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -65,7 +73,7 @@ export const SharepointBlock: BlockConfig<SharepointResponse> = {
|
||||
serviceId: 'sharepoint',
|
||||
selectorKey: 'sharepoint.sites',
|
||||
requiredScopes: getScopesForService('sharepoint'),
|
||||
mimeType: 'application/vnd.microsoft.graph.folder',
|
||||
mimeType: 'application/vnd.microsoft.graph.site',
|
||||
placeholder: 'Select a site',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'basic',
|
||||
@@ -74,11 +82,16 @@ export const SharepointBlock: BlockConfig<SharepointResponse> = {
|
||||
value: [
|
||||
'create_page',
|
||||
'read_page',
|
||||
'update_page',
|
||||
'publish_page',
|
||||
'delete_page',
|
||||
'list_sites',
|
||||
'create_list',
|
||||
'read_list',
|
||||
'update_list',
|
||||
'add_list_items',
|
||||
'get_list_item',
|
||||
'delete_list_item',
|
||||
'upload_file',
|
||||
],
|
||||
},
|
||||
@@ -90,14 +103,37 @@ export const SharepointBlock: BlockConfig<SharepointResponse> = {
|
||||
type: 'short-input',
|
||||
placeholder: 'Name of the page',
|
||||
condition: { field: 'operation', value: ['create_page', 'read_page'] },
|
||||
required: { field: 'operation', value: 'create_page' },
|
||||
},
|
||||
|
||||
{
|
||||
id: 'pageTitle',
|
||||
title: 'Page Title',
|
||||
type: 'short-input',
|
||||
placeholder: 'Optional title (defaults to page name)',
|
||||
condition: { field: 'operation', value: ['create_page', 'update_page'] },
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
{
|
||||
id: 'pageContent',
|
||||
title: 'Page Content',
|
||||
type: 'long-input',
|
||||
placeholder: 'Optional text content for the page',
|
||||
condition: { field: 'operation', value: ['create_page', 'update_page'] },
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
{
|
||||
id: 'pageId',
|
||||
title: 'Page ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Page ID (alternative to page name)',
|
||||
condition: { field: 'operation', value: 'read_page' },
|
||||
placeholder: 'Page ID',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['read_page', 'update_page', 'publish_page', 'delete_page'],
|
||||
},
|
||||
required: { field: 'operation', value: ['update_page', 'publish_page', 'delete_page'] },
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
@@ -111,7 +147,14 @@ export const SharepointBlock: BlockConfig<SharepointResponse> = {
|
||||
placeholder: 'Select a list',
|
||||
dependsOn: ['credential', 'siteSelector'],
|
||||
mode: 'basic',
|
||||
condition: { field: 'operation', value: ['read_list', 'update_list', 'add_list_items'] },
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['read_list', 'update_list', 'add_list_items', 'get_list_item', 'delete_list_item'],
|
||||
},
|
||||
required: {
|
||||
field: 'operation',
|
||||
value: ['update_list', 'add_list_items', 'get_list_item', 'delete_list_item'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'listId',
|
||||
@@ -120,7 +163,14 @@ export const SharepointBlock: BlockConfig<SharepointResponse> = {
|
||||
canonicalParamId: 'listId',
|
||||
placeholder: 'Enter list ID (GUID). Required for Update; optional for Read.',
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: ['read_list', 'update_list', 'add_list_items'] },
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['read_list', 'update_list', 'add_list_items', 'get_list_item', 'delete_list_item'],
|
||||
},
|
||||
required: {
|
||||
field: 'operation',
|
||||
value: ['update_list', 'add_list_items', 'get_list_item', 'delete_list_item'],
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
@@ -129,7 +179,11 @@ export const SharepointBlock: BlockConfig<SharepointResponse> = {
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter item ID',
|
||||
canonicalParamId: 'itemId',
|
||||
condition: { field: 'operation', value: ['update_list'] },
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['update_list', 'get_list_item', 'delete_list_item'],
|
||||
},
|
||||
required: { field: 'operation', value: ['update_list', 'get_list_item', 'delete_list_item'] },
|
||||
},
|
||||
|
||||
{
|
||||
@@ -138,6 +192,7 @@ export const SharepointBlock: BlockConfig<SharepointResponse> = {
|
||||
type: 'short-input',
|
||||
placeholder: 'Name of the list',
|
||||
condition: { field: 'operation', value: 'create_list' },
|
||||
required: { field: 'operation', value: 'create_list' },
|
||||
},
|
||||
|
||||
{
|
||||
@@ -270,11 +325,16 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
|
||||
value: [
|
||||
'create_page',
|
||||
'read_page',
|
||||
'update_page',
|
||||
'publish_page',
|
||||
'delete_page',
|
||||
'list_sites',
|
||||
'create_list',
|
||||
'read_list',
|
||||
'update_list',
|
||||
'add_list_items',
|
||||
'get_list_item',
|
||||
'delete_list_item',
|
||||
'upload_file',
|
||||
],
|
||||
},
|
||||
@@ -288,6 +348,7 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
|
||||
'Enter list item fields as JSON (e.g., {"Title": "My Item", "Status": "Active"})',
|
||||
canonicalParamId: 'listItemFields',
|
||||
condition: { field: 'operation', value: ['update_list', 'add_list_items'] },
|
||||
required: { field: 'operation', value: ['update_list', 'add_list_items'] },
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate a JSON object for SharePoint list item fields based on the user's description.
|
||||
@@ -330,16 +391,81 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
|
||||
},
|
||||
},
|
||||
|
||||
// Upload File operation fields
|
||||
{
|
||||
id: 'maxPages',
|
||||
title: 'Max Pages',
|
||||
type: 'short-input',
|
||||
placeholder: 'Default 10, maximum 50',
|
||||
condition: { field: 'operation', value: 'read_page' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'groupId',
|
||||
title: 'Group ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Optional Microsoft 365 group ID',
|
||||
condition: { field: 'operation', value: 'list_sites' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'includeColumns',
|
||||
title: 'Include Columns',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'No', id: 'false' },
|
||||
{ label: 'Yes', id: 'true' },
|
||||
],
|
||||
value: () => 'false',
|
||||
condition: { field: 'operation', value: 'read_list' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'includeItems',
|
||||
title: 'Include Items',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Yes', id: 'true' },
|
||||
{ label: 'No', id: 'false' },
|
||||
],
|
||||
value: () => 'true',
|
||||
condition: { field: 'operation', value: 'read_list' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'nextPageUrl',
|
||||
title: 'Next Page URL',
|
||||
type: 'short-input',
|
||||
placeholder: 'Paste the @odata.nextLink URL from a previous result',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['read_page', 'list_sites', 'read_list'],
|
||||
},
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
// Upload / Download / Delete File / Get Drive Item operation fields
|
||||
{
|
||||
id: 'driveId',
|
||||
title: 'Document Library ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter document library (drive) ID',
|
||||
canonicalParamId: 'driveId',
|
||||
condition: { field: 'operation', value: 'upload_file' },
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['upload_file', 'download_file', 'delete_file', 'get_drive_item'],
|
||||
},
|
||||
required: { field: 'operation', value: ['download_file', 'delete_file', 'get_drive_item'] },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'driveItemId',
|
||||
title: 'File ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter the file (drive item) ID',
|
||||
canonicalParamId: 'driveItemId',
|
||||
condition: { field: 'operation', value: ['download_file', 'delete_file', 'get_drive_item'] },
|
||||
required: { field: 'operation', value: ['download_file', 'delete_file', 'get_drive_item'] },
|
||||
},
|
||||
{
|
||||
id: 'folderPath',
|
||||
title: 'Folder Path',
|
||||
@@ -352,8 +478,8 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
|
||||
id: 'fileName',
|
||||
title: 'File Name',
|
||||
type: 'short-input',
|
||||
placeholder: 'Optional: override uploaded file name',
|
||||
condition: { field: 'operation', value: 'upload_file' },
|
||||
placeholder: 'Optional: override uploaded/downloaded file name',
|
||||
condition: { field: 'operation', value: ['upload_file', 'download_file'] },
|
||||
mode: 'advanced',
|
||||
required: false,
|
||||
},
|
||||
@@ -367,7 +493,7 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
|
||||
condition: { field: 'operation', value: 'upload_file' },
|
||||
mode: 'basic',
|
||||
multiple: true,
|
||||
required: false,
|
||||
required: true,
|
||||
},
|
||||
// Variable reference (advanced mode)
|
||||
{
|
||||
@@ -378,19 +504,27 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
|
||||
placeholder: 'Reference files from previous blocks',
|
||||
condition: { field: 'operation', value: 'upload_file' },
|
||||
mode: 'advanced',
|
||||
required: false,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
tools: {
|
||||
access: [
|
||||
'sharepoint_create_page',
|
||||
'sharepoint_read_page',
|
||||
'sharepoint_update_page',
|
||||
'sharepoint_publish_page',
|
||||
'sharepoint_delete_page',
|
||||
'sharepoint_list_sites',
|
||||
'sharepoint_create_list',
|
||||
'sharepoint_get_list',
|
||||
'sharepoint_update_list',
|
||||
'sharepoint_add_list_items',
|
||||
'sharepoint_get_list_item',
|
||||
'sharepoint_delete_list_item',
|
||||
'sharepoint_upload_file',
|
||||
'sharepoint_download_file',
|
||||
'sharepoint_get_drive_item',
|
||||
'sharepoint_delete_file',
|
||||
],
|
||||
config: {
|
||||
tool: (params) => {
|
||||
@@ -399,6 +533,12 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
|
||||
return 'sharepoint_create_page'
|
||||
case 'read_page':
|
||||
return 'sharepoint_read_page'
|
||||
case 'update_page':
|
||||
return 'sharepoint_update_page'
|
||||
case 'publish_page':
|
||||
return 'sharepoint_publish_page'
|
||||
case 'delete_page':
|
||||
return 'sharepoint_delete_page'
|
||||
case 'list_sites':
|
||||
return 'sharepoint_list_sites'
|
||||
case 'create_list':
|
||||
@@ -409,8 +549,18 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
|
||||
return 'sharepoint_update_list'
|
||||
case 'add_list_items':
|
||||
return 'sharepoint_add_list_items'
|
||||
case 'get_list_item':
|
||||
return 'sharepoint_get_list_item'
|
||||
case 'delete_list_item':
|
||||
return 'sharepoint_delete_list_item'
|
||||
case 'upload_file':
|
||||
return 'sharepoint_upload_file'
|
||||
case 'download_file':
|
||||
return 'sharepoint_download_file'
|
||||
case 'get_drive_item':
|
||||
return 'sharepoint_get_drive_item'
|
||||
case 'delete_file':
|
||||
return 'sharepoint_delete_file'
|
||||
default:
|
||||
throw new Error(`Invalid Sharepoint operation: ${params.operation}`)
|
||||
}
|
||||
@@ -428,6 +578,7 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
|
||||
includeItems,
|
||||
files, // canonical param from uploadFiles (basic) or files (advanced)
|
||||
driveId, // canonical param from driveId
|
||||
driveItemId, // canonical param from driveItemId
|
||||
columnDefinitions,
|
||||
listId,
|
||||
...others
|
||||
@@ -458,19 +609,16 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
|
||||
}
|
||||
|
||||
if (others.operation === 'update_list' || others.operation === 'add_list_items') {
|
||||
try {
|
||||
logger.info('SharepointBlock list item param check', {
|
||||
siteId: effectiveSiteId || undefined,
|
||||
listId: listId,
|
||||
listTitle: (others as any)?.listTitle,
|
||||
itemId: sanitizedItemId,
|
||||
hasItemFields: !!parsedItemFields && typeof parsedItemFields === 'object',
|
||||
itemFieldKeys:
|
||||
parsedItemFields && typeof parsedItemFields === 'object'
|
||||
? Object.keys(parsedItemFields)
|
||||
: [],
|
||||
})
|
||||
} catch {}
|
||||
logger.info('SharepointBlock list item param check', {
|
||||
siteId: effectiveSiteId || undefined,
|
||||
listId: listId,
|
||||
itemId: sanitizedItemId,
|
||||
hasItemFields: !!parsedItemFields && typeof parsedItemFields === 'object',
|
||||
itemFieldKeys:
|
||||
parsedItemFields && typeof parsedItemFields === 'object'
|
||||
? Object.keys(parsedItemFields)
|
||||
: [],
|
||||
})
|
||||
}
|
||||
|
||||
// Handle file upload files parameter using canonical param
|
||||
@@ -478,11 +626,10 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
|
||||
const baseParams: Record<string, any> = {
|
||||
oauthCredential,
|
||||
siteId: effectiveSiteId || undefined,
|
||||
pageSize: others.pageSize ? Number.parseInt(others.pageSize as string, 10) : undefined,
|
||||
mimeType: mimeType,
|
||||
...others,
|
||||
...(listId ? { listId } : {}),
|
||||
...(driveId ? { driveId } : {}),
|
||||
...(driveItemId ? { driveItemId: String(driveItemId).trim() } : {}),
|
||||
itemId: sanitizedItemId,
|
||||
listItemFields: parsedItemFields,
|
||||
includeColumns: coerceBoolean(includeColumns),
|
||||
@@ -494,7 +641,7 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
|
||||
baseParams.files = normalizedFiles
|
||||
}
|
||||
|
||||
if (columnDefinitions) {
|
||||
if (columnDefinitions && others.operation === 'create_list') {
|
||||
baseParams.pageContent = columnDefinitions
|
||||
}
|
||||
|
||||
@@ -511,14 +658,13 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
|
||||
description: 'Column definitions for list creation (JSON array)',
|
||||
},
|
||||
pageTitle: { type: 'string', description: 'Page title' },
|
||||
pageContent: { type: 'string', description: 'Page text content' },
|
||||
pageId: { type: 'string', description: 'Page ID' },
|
||||
siteId: { type: 'string', description: 'Site ID' },
|
||||
pageSize: { type: 'number', description: 'Results per page' },
|
||||
listDisplayName: { type: 'string', description: 'List display name' },
|
||||
listDescription: { type: 'string', description: 'List description' },
|
||||
listTemplate: { type: 'string', description: 'List template' },
|
||||
listId: { type: 'string', description: 'List ID' },
|
||||
listTitle: { type: 'string', description: 'List title' },
|
||||
includeColumns: { type: 'boolean', description: 'Include columns in response' },
|
||||
includeItems: { type: 'boolean', description: 'Include items in response' },
|
||||
itemId: { type: 'string', description: 'List item ID (canonical param)' },
|
||||
@@ -527,9 +673,16 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
|
||||
type: 'string',
|
||||
description: 'Document library (drive) ID',
|
||||
},
|
||||
driveItemId: { type: 'string', description: 'File (drive item) ID (canonical param)' },
|
||||
folderPath: { type: 'string', description: 'Folder path for file upload' },
|
||||
fileName: { type: 'string', description: 'File name override' },
|
||||
files: { type: 'array', description: 'Files to upload' },
|
||||
maxPages: { type: 'number', description: 'Maximum pages to return when reading all pages' },
|
||||
groupId: { type: 'string', description: 'Microsoft 365 group ID for group-owned sites' },
|
||||
nextPageUrl: {
|
||||
type: 'string',
|
||||
description: 'Full Microsoft Graph @odata.nextLink URL from a previous result',
|
||||
},
|
||||
},
|
||||
outputs: {
|
||||
sites: {
|
||||
@@ -537,10 +690,40 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
|
||||
description:
|
||||
'An array of SharePoint site objects, each containing details such as id, name, and more.',
|
||||
},
|
||||
site: {
|
||||
type: 'json',
|
||||
description: 'Single SharePoint site object (id, name, displayName, webUrl)',
|
||||
},
|
||||
page: {
|
||||
type: 'json',
|
||||
description: 'SharePoint page object (id, name, title, webUrl, pageLayout)',
|
||||
},
|
||||
pages: {
|
||||
type: 'json',
|
||||
description: 'Array of SharePoint pages with content ([{page, content}])',
|
||||
},
|
||||
content: {
|
||||
type: 'json',
|
||||
description: 'Content of the SharePoint page (content, canvasLayout)',
|
||||
},
|
||||
totalPages: {
|
||||
type: 'number',
|
||||
description: 'Total number of pages found when listing all pages',
|
||||
},
|
||||
nextPageUrl: {
|
||||
type: 'string',
|
||||
description: 'Full Microsoft Graph @odata.nextLink URL for the next page of results',
|
||||
},
|
||||
published: { type: 'boolean', description: 'Whether the page was published' },
|
||||
deleted: { type: 'boolean', description: 'Whether the item/page/file was deleted' },
|
||||
list: {
|
||||
type: 'json',
|
||||
description: 'SharePoint list object (id, displayName, name, webUrl, etc.)',
|
||||
},
|
||||
lists: {
|
||||
type: 'json',
|
||||
description: 'Array of SharePoint list objects when no specific list is requested',
|
||||
},
|
||||
item: {
|
||||
type: 'json',
|
||||
description: 'SharePoint list item with fields',
|
||||
@@ -549,6 +732,14 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
|
||||
type: 'json',
|
||||
description: 'Array of SharePoint list items with fields',
|
||||
},
|
||||
driveItem: {
|
||||
type: 'json',
|
||||
description: 'SharePoint drive item metadata (id, name, size, webUrl, file/folder facet)',
|
||||
},
|
||||
file: {
|
||||
type: 'json',
|
||||
description: 'Downloaded file stored in execution files',
|
||||
},
|
||||
uploadedFiles: {
|
||||
type: 'json',
|
||||
description: 'Array of uploaded file objects with id, name, webUrl, size',
|
||||
@@ -557,6 +748,21 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
|
||||
type: 'number',
|
||||
description: 'Number of files uploaded',
|
||||
},
|
||||
skippedFiles: {
|
||||
type: 'json',
|
||||
description:
|
||||
'Files skipped during upload for exceeding the size limit (name, size, limit, reason)',
|
||||
},
|
||||
skippedCount: {
|
||||
type: 'number',
|
||||
description: 'Number of files skipped during upload',
|
||||
},
|
||||
errors: {
|
||||
type: 'json',
|
||||
description: 'Per-file upload errors ([{name, error, status}])',
|
||||
},
|
||||
itemId: { type: 'string', description: 'ID of the deleted list item or file' },
|
||||
pageId: { type: 'string', description: 'ID of the deleted or published page' },
|
||||
success: {
|
||||
type: 'boolean',
|
||||
description: 'Success status',
|
||||
@@ -571,21 +777,48 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
|
||||
const SHAREPOINT_V2_TOOL_IDS = [
|
||||
'sharepoint_create_page',
|
||||
'sharepoint_read_page',
|
||||
'sharepoint_update_page',
|
||||
'sharepoint_publish_page',
|
||||
'sharepoint_delete_page',
|
||||
'sharepoint_list_sites',
|
||||
'sharepoint_create_list',
|
||||
'sharepoint_get_list',
|
||||
'sharepoint_update_list',
|
||||
'sharepoint_add_list_items',
|
||||
'sharepoint_get_list_item',
|
||||
'sharepoint_delete_list_item',
|
||||
'sharepoint_upload_file',
|
||||
'sharepoint_download_file',
|
||||
'sharepoint_get_drive_item',
|
||||
'sharepoint_delete_file',
|
||||
] as const
|
||||
|
||||
const SHAREPOINT_V2_SITE_OPERATIONS = Array.from(SHAREPOINT_V2_TOOL_IDS)
|
||||
const SHAREPOINT_V2_DRIVE_ONLY_OPERATIONS = [
|
||||
'sharepoint_download_file',
|
||||
'sharepoint_delete_file',
|
||||
'sharepoint_get_drive_item',
|
||||
] as const
|
||||
|
||||
const SHAREPOINT_V2_SITE_OPERATIONS = SHAREPOINT_V2_TOOL_IDS.filter(
|
||||
(id) => !(SHAREPOINT_V2_DRIVE_ONLY_OPERATIONS as readonly string[]).includes(id)
|
||||
)
|
||||
|
||||
const SHAREPOINT_V2_LIST_ITEM_OPERATIONS = [
|
||||
'sharepoint_update_list',
|
||||
'sharepoint_add_list_items',
|
||||
] as const
|
||||
|
||||
const SHAREPOINT_V2_LIST_ITEM_LOOKUP_OPERATIONS = [
|
||||
'sharepoint_get_list_item',
|
||||
'sharepoint_delete_list_item',
|
||||
] as const
|
||||
|
||||
const SHAREPOINT_V2_PAGE_MUTATION_OPERATIONS = [
|
||||
'sharepoint_update_page',
|
||||
'sharepoint_publish_page',
|
||||
'sharepoint_delete_page',
|
||||
] as const
|
||||
|
||||
export const SharepointV2Block: BlockConfig<SharepointResponse> = {
|
||||
...SharepointBlock,
|
||||
type: 'sharepoint_v2',
|
||||
@@ -599,12 +832,20 @@ export const SharepointV2Block: BlockConfig<SharepointResponse> = {
|
||||
options: [
|
||||
{ label: 'Create Page', id: 'sharepoint_create_page' },
|
||||
{ label: 'Read Page', id: 'sharepoint_read_page' },
|
||||
{ label: 'Update Page', id: 'sharepoint_update_page' },
|
||||
{ label: 'Publish Page', id: 'sharepoint_publish_page' },
|
||||
{ label: 'Delete Page', id: 'sharepoint_delete_page' },
|
||||
{ label: 'List Sites', id: 'sharepoint_list_sites' },
|
||||
{ label: 'Create List', id: 'sharepoint_create_list' },
|
||||
{ label: 'Read List', id: 'sharepoint_get_list' },
|
||||
{ label: 'Update List Item', id: 'sharepoint_update_list' },
|
||||
{ label: 'Add List Item', id: 'sharepoint_add_list_items' },
|
||||
{ label: 'Get List Item', id: 'sharepoint_get_list_item' },
|
||||
{ label: 'Delete List Item', id: 'sharepoint_delete_list_item' },
|
||||
{ label: 'Upload File', id: 'sharepoint_upload_file' },
|
||||
{ label: 'Download File', id: 'sharepoint_download_file' },
|
||||
{ label: 'Get Drive Item', id: 'sharepoint_get_drive_item' },
|
||||
{ label: 'Delete File', id: 'sharepoint_delete_file' },
|
||||
],
|
||||
value: () => 'sharepoint_create_page',
|
||||
},
|
||||
@@ -664,8 +905,12 @@ export const SharepointV2Block: BlockConfig<SharepointResponse> = {
|
||||
id: 'pageId',
|
||||
title: 'Page ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Page ID (alternative to page name)',
|
||||
condition: { field: 'operation', value: 'sharepoint_read_page' },
|
||||
placeholder: 'Page ID (alternative to page name for Read Page)',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['sharepoint_read_page', ...SHAREPOINT_V2_PAGE_MUTATION_OPERATIONS],
|
||||
},
|
||||
required: { field: 'operation', value: [...SHAREPOINT_V2_PAGE_MUTATION_OPERATIONS] },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
@@ -673,7 +918,10 @@ export const SharepointV2Block: BlockConfig<SharepointResponse> = {
|
||||
title: 'Page Title',
|
||||
type: 'short-input',
|
||||
placeholder: 'Optional title (defaults to page name)',
|
||||
condition: { field: 'operation', value: 'sharepoint_create_page' },
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['sharepoint_create_page', 'sharepoint_update_page'],
|
||||
},
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
@@ -681,7 +929,10 @@ export const SharepointV2Block: BlockConfig<SharepointResponse> = {
|
||||
title: 'Page Content',
|
||||
type: 'long-input',
|
||||
placeholder: 'Optional text content for the page',
|
||||
condition: { field: 'operation', value: 'sharepoint_create_page' },
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['sharepoint_create_page', 'sharepoint_update_page'],
|
||||
},
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
@@ -712,9 +963,19 @@ export const SharepointV2Block: BlockConfig<SharepointResponse> = {
|
||||
mode: 'basic',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['sharepoint_get_list', ...SHAREPOINT_V2_LIST_ITEM_OPERATIONS],
|
||||
value: [
|
||||
'sharepoint_get_list',
|
||||
...SHAREPOINT_V2_LIST_ITEM_OPERATIONS,
|
||||
...SHAREPOINT_V2_LIST_ITEM_LOOKUP_OPERATIONS,
|
||||
],
|
||||
},
|
||||
required: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
...SHAREPOINT_V2_LIST_ITEM_OPERATIONS,
|
||||
...SHAREPOINT_V2_LIST_ITEM_LOOKUP_OPERATIONS,
|
||||
],
|
||||
},
|
||||
required: { field: 'operation', value: [...SHAREPOINT_V2_LIST_ITEM_OPERATIONS] },
|
||||
},
|
||||
{
|
||||
id: 'manualListId',
|
||||
@@ -725,9 +986,19 @@ export const SharepointV2Block: BlockConfig<SharepointResponse> = {
|
||||
mode: 'advanced',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['sharepoint_get_list', ...SHAREPOINT_V2_LIST_ITEM_OPERATIONS],
|
||||
value: [
|
||||
'sharepoint_get_list',
|
||||
...SHAREPOINT_V2_LIST_ITEM_OPERATIONS,
|
||||
...SHAREPOINT_V2_LIST_ITEM_LOOKUP_OPERATIONS,
|
||||
],
|
||||
},
|
||||
required: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
...SHAREPOINT_V2_LIST_ITEM_OPERATIONS,
|
||||
...SHAREPOINT_V2_LIST_ITEM_LOOKUP_OPERATIONS,
|
||||
],
|
||||
},
|
||||
required: { field: 'operation', value: [...SHAREPOINT_V2_LIST_ITEM_OPERATIONS] },
|
||||
},
|
||||
{
|
||||
id: 'includeColumns',
|
||||
@@ -821,8 +1092,14 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
|
||||
title: 'Item ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter item ID',
|
||||
condition: { field: 'operation', value: 'sharepoint_update_list' },
|
||||
required: { field: 'operation', value: 'sharepoint_update_list' },
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['sharepoint_update_list', ...SHAREPOINT_V2_LIST_ITEM_LOOKUP_OPERATIONS],
|
||||
},
|
||||
required: {
|
||||
field: 'operation',
|
||||
value: ['sharepoint_update_list', ...SHAREPOINT_V2_LIST_ITEM_LOOKUP_OPERATIONS],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'listItemFields',
|
||||
@@ -848,9 +1125,21 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
|
||||
title: 'Document Library ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter document library (drive) ID',
|
||||
condition: { field: 'operation', value: 'sharepoint_upload_file' },
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: [...SHAREPOINT_V2_DRIVE_ONLY_OPERATIONS, 'sharepoint_upload_file'],
|
||||
},
|
||||
required: { field: 'operation', value: [...SHAREPOINT_V2_DRIVE_ONLY_OPERATIONS] },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'driveItemId',
|
||||
title: 'File ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter the file (drive item) ID',
|
||||
condition: { field: 'operation', value: [...SHAREPOINT_V2_DRIVE_ONLY_OPERATIONS] },
|
||||
required: { field: 'operation', value: [...SHAREPOINT_V2_DRIVE_ONLY_OPERATIONS] },
|
||||
},
|
||||
{
|
||||
id: 'folderPath',
|
||||
title: 'Folder Path',
|
||||
@@ -864,8 +1153,11 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
|
||||
id: 'fileName',
|
||||
title: 'File Name',
|
||||
type: 'short-input',
|
||||
placeholder: 'Optional: override uploaded file name',
|
||||
condition: { field: 'operation', value: 'sharepoint_upload_file' },
|
||||
placeholder: 'Optional: override uploaded/downloaded file name',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['sharepoint_upload_file', 'sharepoint_download_file'],
|
||||
},
|
||||
mode: 'advanced',
|
||||
required: false,
|
||||
},
|
||||
@@ -919,6 +1211,7 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
|
||||
files,
|
||||
maxPages,
|
||||
driveId,
|
||||
driveItemId,
|
||||
...rest
|
||||
} = params
|
||||
|
||||
@@ -950,13 +1243,14 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
|
||||
listId: cleanString(listId),
|
||||
itemId: cleanString(itemId),
|
||||
driveId: cleanString(driveId),
|
||||
driveItemId: cleanString(driveItemId),
|
||||
includeColumns: coerceBoolean(includeColumns),
|
||||
includeItems: coerceBoolean(includeItems),
|
||||
listItemFields: parseJsonObject(listItemFields),
|
||||
maxPages: maxPages ? Number.parseInt(String(maxPages), 10) : undefined,
|
||||
}
|
||||
|
||||
if (columnDefinitions) {
|
||||
if (columnDefinitions && rest.operation === 'sharepoint_create_list') {
|
||||
result.pageContent = columnDefinitions
|
||||
}
|
||||
if (normalizedFiles) {
|
||||
@@ -991,6 +1285,7 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
|
||||
itemId: { type: 'string', description: 'List item ID' },
|
||||
listItemFields: { type: 'json', description: 'List item fields' },
|
||||
driveId: { type: 'string', description: 'Document library (drive) ID' },
|
||||
driveItemId: { type: 'string', description: 'File (drive item) ID' },
|
||||
folderPath: { type: 'string', description: 'Folder path for file upload' },
|
||||
fileName: { type: 'string', description: 'File name override' },
|
||||
files: { type: 'json', description: 'Files to upload' },
|
||||
@@ -1017,6 +1312,7 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
|
||||
description: 'SharePoint page content (content, canvasLayout)',
|
||||
},
|
||||
totalPages: { type: 'number', description: 'Number of pages returned' },
|
||||
published: { type: 'boolean', description: 'Whether the page was published' },
|
||||
list: {
|
||||
type: 'json',
|
||||
description: 'SharePoint list object (id, displayName, name, webUrl, columns, items)',
|
||||
@@ -1027,6 +1323,15 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
|
||||
},
|
||||
item: { type: 'json', description: 'SharePoint list item with fields' },
|
||||
items: { type: 'json', description: 'Array of SharePoint list items with fields' },
|
||||
deleted: { type: 'boolean', description: 'Whether the item/page/file was deleted' },
|
||||
driveItem: {
|
||||
type: 'json',
|
||||
description: 'SharePoint drive item metadata (id, name, size, webUrl, file/folder facet)',
|
||||
},
|
||||
file: {
|
||||
type: 'json',
|
||||
description: 'Downloaded file stored in execution files',
|
||||
},
|
||||
uploadedFiles: {
|
||||
type: 'json',
|
||||
description: 'Array of uploaded file objects with id, name, webUrl, size',
|
||||
@@ -1041,6 +1346,8 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
|
||||
type: 'json',
|
||||
description: 'Array of per-file upload errors (name, error, status)',
|
||||
},
|
||||
itemId: { type: 'string', description: 'ID of the deleted list item or file' },
|
||||
pageId: { type: 'string', description: 'ID of the deleted or published page' },
|
||||
nextPageUrl: {
|
||||
type: 'string',
|
||||
description: 'Microsoft Graph @odata.nextLink URL for the next page of results',
|
||||
|
||||
@@ -86,6 +86,13 @@ export const sharepointUploadBodySchema = z.object({
|
||||
files: RawFileInputArraySchema.optional().nullable(),
|
||||
})
|
||||
|
||||
export const sharepointDownloadFileBodySchema = z.object({
|
||||
accessToken: accessTokenSchema,
|
||||
driveId: z.string().min(1, 'Drive ID is required'),
|
||||
itemId: z.string().min(1, 'Item ID is required'),
|
||||
fileName: z.string().optional().nullable(),
|
||||
})
|
||||
|
||||
export const dataverseUploadFileBodySchema = z.object({
|
||||
accessToken: accessTokenSchema,
|
||||
environmentUrl: z.string().min(1, 'Environment URL is required'),
|
||||
@@ -190,6 +197,13 @@ export const sharepointUploadContract = defineRouteContract({
|
||||
response: { mode: 'json', schema: toolJsonResponseSchema },
|
||||
})
|
||||
|
||||
export const sharepointDownloadFileContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/sharepoint/download-file',
|
||||
body: sharepointDownloadFileBodySchema,
|
||||
response: { mode: 'json', schema: toolJsonResponseSchema },
|
||||
})
|
||||
|
||||
export const dataverseUploadFileContract = defineRouteContract({
|
||||
method: 'POST',
|
||||
path: '/api/tools/microsoft-dataverse/upload-file',
|
||||
@@ -210,4 +224,5 @@ export type TeamsDeleteChatMessageBody = ContractBody<typeof teamsDeleteChatMess
|
||||
export type OneDriveUploadBody = ContractBody<typeof onedriveUploadContract>
|
||||
export type OneDriveDownloadBody = ContractBody<typeof onedriveDownloadContract>
|
||||
export type SharepointUploadBody = ContractBody<typeof sharepointUploadContract>
|
||||
export type SharepointDownloadFileBody = ContractBody<typeof sharepointDownloadFileContract>
|
||||
export type DataverseUploadFileBody = z.output<typeof dataverseUploadFileBodySchema>
|
||||
|
||||
@@ -4,6 +4,7 @@ import https from 'https'
|
||||
import type { LookupFunction } from 'net'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { omit } from '@sim/utils/object'
|
||||
import * as ipaddr from 'ipaddr.js'
|
||||
import { Agent, type RequestInit as UndiciRequestInit, fetch as undiciFetch } from 'undici'
|
||||
import { isHosted, isPrivateDatabaseHostsAllowed } from '@/lib/core/config/env-flags'
|
||||
@@ -333,6 +334,8 @@ export interface SecureFetchOptions {
|
||||
maxRedirects?: number
|
||||
maxResponseBytes?: number
|
||||
signal?: AbortSignal
|
||||
/** Drop the Authorization header when following a redirect, so it is not sent to the redirect target's origin. */
|
||||
stripAuthOnRedirect?: boolean
|
||||
}
|
||||
|
||||
export class SecureFetchHeaders {
|
||||
@@ -500,10 +503,16 @@ export async function secureFetchWithPinnedIP(
|
||||
settledReject(new Error(`Redirect blocked: ${validation.error}`))
|
||||
return
|
||||
}
|
||||
const redirectOptions = options.stripAuthOnRedirect
|
||||
? {
|
||||
...options,
|
||||
headers: omit(options.headers ?? {}, ['Authorization', 'authorization']),
|
||||
}
|
||||
: options
|
||||
return secureFetchWithPinnedIP(
|
||||
redirectUrl,
|
||||
validation.resolvedIP!,
|
||||
options,
|
||||
redirectOptions,
|
||||
redirectCount + 1
|
||||
)
|
||||
})
|
||||
|
||||
@@ -3175,10 +3175,18 @@ import {
|
||||
sharepointAddListItemTool,
|
||||
sharepointCreateListTool,
|
||||
sharepointCreatePageTool,
|
||||
sharepointDeleteFileTool,
|
||||
sharepointDeleteListItemTool,
|
||||
sharepointDeletePageTool,
|
||||
sharepointDownloadFileTool,
|
||||
sharepointGetDriveItemTool,
|
||||
sharepointGetListItemTool,
|
||||
sharepointGetListTool,
|
||||
sharepointListSitesTool,
|
||||
sharepointPublishPageTool,
|
||||
sharepointReadPageTool,
|
||||
sharepointUpdateListItemTool,
|
||||
sharepointUpdatePageTool,
|
||||
sharepointUploadFileTool,
|
||||
} from '@/tools/sharepoint'
|
||||
import {
|
||||
@@ -7679,10 +7687,18 @@ export const tools: Record<string, ToolConfig> = {
|
||||
sharepoint_add_list_items: sharepointAddListItemTool,
|
||||
sharepoint_create_list: sharepointCreateListTool,
|
||||
sharepoint_create_page: sharepointCreatePageTool,
|
||||
sharepoint_delete_file: sharepointDeleteFileTool,
|
||||
sharepoint_delete_list_item: sharepointDeleteListItemTool,
|
||||
sharepoint_delete_page: sharepointDeletePageTool,
|
||||
sharepoint_download_file: sharepointDownloadFileTool,
|
||||
sharepoint_get_drive_item: sharepointGetDriveItemTool,
|
||||
sharepoint_get_list: sharepointGetListTool,
|
||||
sharepoint_get_list_item: sharepointGetListItemTool,
|
||||
sharepoint_list_sites: sharepointListSitesTool,
|
||||
sharepoint_publish_page: sharepointPublishPageTool,
|
||||
sharepoint_read_page: sharepointReadPageTool,
|
||||
sharepoint_update_list: sharepointUpdateListItemTool,
|
||||
sharepoint_update_page: sharepointUpdatePageTool,
|
||||
sharepoint_upload_file: sharepointUploadFileTool,
|
||||
stripe_create_payment_intent: stripeCreatePaymentIntentTool,
|
||||
stripe_retrieve_payment_intent: stripeRetrievePaymentIntentTool,
|
||||
|
||||
@@ -1,9 +1,28 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { SharepointAddListItemResponse, SharepointToolParams } from '@/tools/sharepoint/types'
|
||||
import { optionalTrim } from '@/tools/sharepoint/utils'
|
||||
import { optionalTrim, sanitizeListItemFields } from '@/tools/sharepoint/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('SharePointAddListItem')
|
||||
function resolveSanitizedFields(
|
||||
listItemFields: SharepointToolParams['listItemFields']
|
||||
): Record<string, unknown> {
|
||||
if (!listItemFields || Object.keys(listItemFields).length === 0) {
|
||||
throw new Error('listItemFields must not be empty')
|
||||
}
|
||||
|
||||
const providedFields =
|
||||
typeof listItemFields === 'object' &&
|
||||
listItemFields !== null &&
|
||||
'fields' in (listItemFields as Record<string, unknown>) &&
|
||||
Object.keys(listItemFields as Record<string, unknown>).length === 1
|
||||
? ((listItemFields as { fields: Record<string, unknown> }).fields as Record<string, unknown>)
|
||||
: (listItemFields as Record<string, unknown>)
|
||||
|
||||
if (!providedFields || Object.keys(providedFields).length === 0) {
|
||||
throw new Error('No fields provided to create the SharePoint list item')
|
||||
}
|
||||
|
||||
return sanitizeListItemFields(providedFields, { action: 'create' })
|
||||
}
|
||||
|
||||
export const addListItemTool: ToolConfig<SharepointToolParams, SharepointAddListItemResponse> = {
|
||||
id: 'sharepoint_add_list_items',
|
||||
@@ -58,7 +77,7 @@ export const addListItemTool: ToolConfig<SharepointToolParams, SharepointAddList
|
||||
throw new Error('listId must be provided')
|
||||
}
|
||||
const listSegment = encodeURIComponent(listId)
|
||||
return `https://graph.microsoft.com/v1.0/sites/${siteId}/lists/${listSegment}/items`
|
||||
return `https://graph.microsoft.com/v1.0/sites/${encodeURIComponent(siteId)}/lists/${listSegment}/items`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
@@ -66,87 +85,28 @@ export const addListItemTool: ToolConfig<SharepointToolParams, SharepointAddList
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
if (!params.listItemFields || Object.keys(params.listItemFields).length === 0) {
|
||||
throw new Error('listItemFields must not be empty')
|
||||
}
|
||||
|
||||
const providedFields =
|
||||
typeof params.listItemFields === 'object' &&
|
||||
params.listItemFields !== null &&
|
||||
'fields' in (params.listItemFields as Record<string, unknown>) &&
|
||||
Object.keys(params.listItemFields as Record<string, unknown>).length === 1
|
||||
? ((params.listItemFields as { fields: Record<string, unknown> }).fields as Record<
|
||||
string,
|
||||
unknown
|
||||
>)
|
||||
: (params.listItemFields as Record<string, unknown>)
|
||||
|
||||
if (!providedFields || Object.keys(providedFields).length === 0) {
|
||||
throw new Error('No fields provided to create the SharePoint list item')
|
||||
}
|
||||
|
||||
const readOnlyFields = new Set<string>([
|
||||
'Id',
|
||||
'id',
|
||||
'UniqueId',
|
||||
'GUID',
|
||||
'ContentTypeId',
|
||||
'Created',
|
||||
'Modified',
|
||||
'Author',
|
||||
'Editor',
|
||||
'CreatedBy',
|
||||
'ModifiedBy',
|
||||
'AuthorId',
|
||||
'EditorId',
|
||||
'_UIVersionString',
|
||||
'Attachments',
|
||||
'FileRef',
|
||||
'FileDirRef',
|
||||
'FileLeafRef',
|
||||
])
|
||||
|
||||
const entries = Object.entries(providedFields)
|
||||
const creatableEntries = entries.filter(([key]) => !readOnlyFields.has(key))
|
||||
|
||||
if (creatableEntries.length !== entries.length) {
|
||||
const removed = entries.filter(([key]) => readOnlyFields.has(key)).map(([key]) => key)
|
||||
logger.warn('Removed read-only SharePoint fields from create', {
|
||||
removed,
|
||||
})
|
||||
}
|
||||
|
||||
if (creatableEntries.length === 0) {
|
||||
const requestedKeys = Object.keys(providedFields)
|
||||
throw new Error(
|
||||
`All provided fields are read-only and cannot be set: ${requestedKeys.join(', ')}`
|
||||
)
|
||||
}
|
||||
|
||||
const sanitizedFields = Object.fromEntries(creatableEntries)
|
||||
|
||||
logger.info('Creating SharePoint list item', {
|
||||
listId: params.listId,
|
||||
fieldsKeys: Object.keys(sanitizedFields),
|
||||
})
|
||||
|
||||
return {
|
||||
fields: sanitizedFields,
|
||||
}
|
||||
},
|
||||
body: (params) => ({
|
||||
fields: resolveSanitizedFields(params.listItemFields),
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params) => {
|
||||
let data: any
|
||||
let data: Record<string, unknown> | undefined
|
||||
try {
|
||||
data = await response.json()
|
||||
} catch {
|
||||
data = undefined
|
||||
}
|
||||
|
||||
const itemId: string | undefined = data?.id
|
||||
const fields: Record<string, unknown> | undefined = data?.fields || params?.listItemFields
|
||||
const itemId = data?.id as string | undefined
|
||||
let fields = data?.fields as Record<string, unknown> | undefined
|
||||
if (!fields && params) {
|
||||
try {
|
||||
fields = resolveSanitizedFields(params.listItemFields)
|
||||
} catch {
|
||||
// Item was already created successfully; a malformed fallback input must not fail the response.
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
|
||||
@@ -70,8 +70,8 @@ export const createListTool: ToolConfig<SharepointToolParams, SharepointCreateLi
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const siteId = optionalTrim(params.siteSelector) || optionalTrim(params.siteId) || 'root'
|
||||
return `https://graph.microsoft.com/v1.0/sites/${siteId}/lists`
|
||||
const siteId = optionalTrim(params.siteId) || optionalTrim(params.siteSelector) || 'root'
|
||||
return `https://graph.microsoft.com/v1.0/sites/${encodeURIComponent(siteId)}/lists`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
|
||||
@@ -4,7 +4,7 @@ import type {
|
||||
SharepointPage,
|
||||
SharepointToolParams,
|
||||
} from '@/tools/sharepoint/types'
|
||||
import { optionalTrim } from '@/tools/sharepoint/utils'
|
||||
import { escapeHtml, optionalTrim } from '@/tools/sharepoint/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('SharePointCreatePage')
|
||||
@@ -61,8 +61,8 @@ export const createPageTool: ToolConfig<SharepointToolParams, SharepointCreatePa
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const siteId = optionalTrim(params.siteSelector) || optionalTrim(params.siteId) || 'root'
|
||||
return `https://graph.microsoft.com/v1.0/sites/${siteId}/pages`
|
||||
const siteId = optionalTrim(params.siteId) || optionalTrim(params.siteSelector) || 'root'
|
||||
return `https://graph.microsoft.com/v1.0/sites/${encodeURIComponent(siteId)}/pages`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
@@ -103,7 +103,7 @@ export const createPageTool: ToolConfig<SharepointToolParams, SharepointCreatePa
|
||||
webparts: [
|
||||
{
|
||||
id: '6f9230af-2a98-4952-b205-9ede4f9ef548',
|
||||
innerHtml: `<p>${pageContent.replace(/"/g, '"').replace(/'/g, ''')}</p>`,
|
||||
innerHtml: `<p>${escapeHtml(pageContent)}</p>`,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { SharepointDeleteFileResponse, SharepointToolParams } from '@/tools/sharepoint/types'
|
||||
import { optionalTrim } from '@/tools/sharepoint/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const deleteFileTool: ToolConfig<SharepointToolParams, SharepointDeleteFileResponse> = {
|
||||
id: 'sharepoint_delete_file',
|
||||
name: 'Delete SharePoint File',
|
||||
description: 'Delete a file (or folder) from a SharePoint document library',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'sharepoint',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The access token for the SharePoint API',
|
||||
},
|
||||
driveId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the document library (drive). Example: b!abc123def456',
|
||||
},
|
||||
driveItemId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the file (drive item) to delete',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const driveId = optionalTrim(params.driveId)
|
||||
const driveItemId = optionalTrim(params.driveItemId)
|
||||
if (!driveId) throw new Error('driveId must be provided')
|
||||
if (!driveItemId) throw new Error('driveItemId must be provided')
|
||||
return `https://graph.microsoft.com/v1.0/drives/${encodeURIComponent(driveId)}/items/${encodeURIComponent(driveItemId)}`
|
||||
},
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
Accept: 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (_response: Response, params) => {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
deleted: true,
|
||||
itemId: params?.driveItemId ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
deleted: { type: 'boolean', description: 'Whether the file was deleted' },
|
||||
itemId: { type: 'string', description: 'The ID of the deleted file' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import type {
|
||||
SharepointDeleteListItemResponse,
|
||||
SharepointToolParams,
|
||||
} from '@/tools/sharepoint/types'
|
||||
import { optionalTrim } from '@/tools/sharepoint/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const deleteListItemTool: ToolConfig<
|
||||
SharepointToolParams,
|
||||
SharepointDeleteListItemResponse
|
||||
> = {
|
||||
id: 'sharepoint_delete_list_item',
|
||||
name: 'Delete SharePoint List Item',
|
||||
description: 'Delete an item from a SharePoint list',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'sharepoint',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The access token for the SharePoint API',
|
||||
},
|
||||
siteSelector: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Select the SharePoint site',
|
||||
},
|
||||
siteId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'hidden',
|
||||
description: 'The ID of the SharePoint site (internal use)',
|
||||
},
|
||||
listId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The ID of the list containing the item. Example: b!abc123def456 or a GUID like 12345678-1234-1234-1234-123456789012',
|
||||
},
|
||||
itemId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the list item to delete. Example: 1, 42, or 123',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const siteId = optionalTrim(params.siteId) || optionalTrim(params.siteSelector) || 'root'
|
||||
const listId = optionalTrim(params.listId)
|
||||
const itemId = optionalTrim(params.itemId)
|
||||
if (!listId) throw new Error('listId must be provided')
|
||||
if (!itemId) throw new Error('itemId must be provided')
|
||||
const listSegment = encodeURIComponent(listId)
|
||||
const itemSegment = encodeURIComponent(itemId)
|
||||
return `https://graph.microsoft.com/v1.0/sites/${encodeURIComponent(siteId)}/lists/${listSegment}/items/${itemSegment}`
|
||||
},
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
Accept: 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (_response: Response, params) => {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
deleted: true,
|
||||
itemId: params?.itemId ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
deleted: { type: 'boolean', description: 'Whether the list item was deleted' },
|
||||
itemId: { type: 'string', description: 'The ID of the deleted list item' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import type { SharepointDeletePageResponse, SharepointToolParams } from '@/tools/sharepoint/types'
|
||||
import { optionalTrim } from '@/tools/sharepoint/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const deletePageTool: ToolConfig<SharepointToolParams, SharepointDeletePageResponse> = {
|
||||
id: 'sharepoint_delete_page',
|
||||
name: 'Delete SharePoint Page',
|
||||
description: 'Delete a page from a SharePoint site',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'sharepoint',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The access token for the SharePoint API',
|
||||
},
|
||||
siteSelector: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Select the SharePoint site',
|
||||
},
|
||||
siteId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'hidden',
|
||||
description: 'The ID of the SharePoint site (internal use)',
|
||||
},
|
||||
pageId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The ID of the page to delete. Example: a GUID like 12345678-1234-1234-1234-123456789012',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const siteId = optionalTrim(params.siteId) || optionalTrim(params.siteSelector) || 'root'
|
||||
const pageId = optionalTrim(params.pageId)
|
||||
if (!pageId) throw new Error('pageId must be provided')
|
||||
return `https://graph.microsoft.com/v1.0/sites/${encodeURIComponent(siteId)}/pages/${encodeURIComponent(pageId)}`
|
||||
},
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
Accept: 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (_response: Response, params) => {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
deleted: true,
|
||||
pageId: params?.pageId ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
deleted: { type: 'boolean', description: 'Whether the page was deleted' },
|
||||
pageId: { type: 'string', description: 'The ID of the deleted page' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { SharepointDownloadFileResponse, SharepointToolParams } from '@/tools/sharepoint/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const downloadFileTool: ToolConfig<SharepointToolParams, SharepointDownloadFileResponse> = {
|
||||
id: 'sharepoint_download_file',
|
||||
name: 'Download File from SharePoint',
|
||||
description: 'Download a file from a SharePoint document library',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'sharepoint',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The access token for the SharePoint API',
|
||||
},
|
||||
driveId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the document library (drive). Example: b!abc123def456',
|
||||
},
|
||||
driveItemId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the file (drive item) to download',
|
||||
},
|
||||
fileName: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Optional filename override (e.g., "report.pdf", "data.xlsx")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/sharepoint/download-file',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
accessToken: params.accessToken,
|
||||
driveId: params.driveId,
|
||||
itemId: params.driveItemId,
|
||||
fileName: params.fileName,
|
||||
}),
|
||||
},
|
||||
|
||||
outputs: {
|
||||
file: { type: 'file', description: 'Downloaded file stored in execution files' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import type {
|
||||
SharepointDriveItem,
|
||||
SharepointGetDriveItemResponse,
|
||||
SharepointToolParams,
|
||||
} from '@/tools/sharepoint/types'
|
||||
import { optionalTrim } from '@/tools/sharepoint/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const getDriveItemTool: ToolConfig<SharepointToolParams, SharepointGetDriveItemResponse> = {
|
||||
id: 'sharepoint_get_drive_item',
|
||||
name: 'Get SharePoint Drive Item',
|
||||
description: 'Get metadata for a file or folder in a SharePoint document library',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'sharepoint',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The access token for the SharePoint API',
|
||||
},
|
||||
driveId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the document library (drive). Example: b!abc123def456',
|
||||
},
|
||||
driveItemId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the file or folder (drive item) to retrieve',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const driveId = optionalTrim(params.driveId)
|
||||
const driveItemId = optionalTrim(params.driveItemId)
|
||||
if (!driveId) throw new Error('driveId must be provided')
|
||||
if (!driveItemId) throw new Error('driveItemId must be provided')
|
||||
return `https://graph.microsoft.com/v1.0/drives/${encodeURIComponent(driveId)}/items/${encodeURIComponent(driveItemId)}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
Accept: 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data: Record<string, unknown> = await response.json()
|
||||
|
||||
const driveItem: SharepointDriveItem = {
|
||||
id: data.id as string,
|
||||
name: data.name as string,
|
||||
webUrl: data.webUrl as string | undefined,
|
||||
size: data.size as number | undefined,
|
||||
createdDateTime: data.createdDateTime as string | undefined,
|
||||
lastModifiedDateTime: data.lastModifiedDateTime as string | undefined,
|
||||
file: (data.file as SharepointDriveItem['file']) ?? null,
|
||||
folder: (data.folder as SharepointDriveItem['folder']) ?? null,
|
||||
parentReference: (data.parentReference as SharepointDriveItem['parentReference']) ?? null,
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: { driveItem },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
driveItem: {
|
||||
type: 'object',
|
||||
description: 'Metadata for the SharePoint file or folder',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'The unique ID of the drive item' },
|
||||
name: { type: 'string', description: 'The name of the file or folder' },
|
||||
webUrl: { type: 'string', description: 'The URL to access the item' },
|
||||
size: { type: 'number', description: 'The size of the item in bytes', optional: true },
|
||||
createdDateTime: { type: 'string', description: 'When the item was created' },
|
||||
lastModifiedDateTime: { type: 'string', description: 'When the item was last modified' },
|
||||
file: {
|
||||
type: 'object',
|
||||
description: 'Present if the item is a file (contains mimeType)',
|
||||
optional: true,
|
||||
},
|
||||
folder: {
|
||||
type: 'object',
|
||||
description: 'Present if the item is a folder (contains childCount)',
|
||||
optional: true,
|
||||
},
|
||||
parentReference: {
|
||||
type: 'object',
|
||||
description: 'Reference to the parent folder/drive',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -73,10 +73,11 @@ export const getListTool: ToolConfig<SharepointToolParams, SharepointGetListResp
|
||||
}
|
||||
|
||||
const siteId = optionalTrim(params.siteId) || optionalTrim(params.siteSelector) || 'root'
|
||||
const encodedSiteId = encodeURIComponent(siteId)
|
||||
const listId = optionalTrim(params.listId)
|
||||
|
||||
if (!listId) {
|
||||
const baseUrl = `https://graph.microsoft.com/v1.0/sites/${siteId}/lists`
|
||||
const baseUrl = `https://graph.microsoft.com/v1.0/sites/${encodedSiteId}/lists`
|
||||
const url = new URL(baseUrl)
|
||||
const finalUrl = url.toString()
|
||||
logger.info('SharePoint List All Lists URL', {
|
||||
@@ -91,7 +92,7 @@ export const getListTool: ToolConfig<SharepointToolParams, SharepointGetListResp
|
||||
|
||||
if (wantsItems && !params.includeColumns) {
|
||||
const itemsUrl = new URL(
|
||||
`https://graph.microsoft.com/v1.0/sites/${siteId}/lists/${listSegment}/items`
|
||||
`https://graph.microsoft.com/v1.0/sites/${encodedSiteId}/lists/${listSegment}/items`
|
||||
)
|
||||
itemsUrl.searchParams.set('$expand', 'fields')
|
||||
const finalItemsUrl = itemsUrl.toString()
|
||||
@@ -103,11 +104,11 @@ export const getListTool: ToolConfig<SharepointToolParams, SharepointGetListResp
|
||||
return finalItemsUrl
|
||||
}
|
||||
|
||||
const baseUrl = `https://graph.microsoft.com/v1.0/sites/${siteId}/lists/${listSegment}`
|
||||
const baseUrl = `https://graph.microsoft.com/v1.0/sites/${encodedSiteId}/lists/${listSegment}`
|
||||
const url = new URL(baseUrl)
|
||||
const expandParts: string[] = []
|
||||
if (params.includeColumns) expandParts.push('columns')
|
||||
if (wantsItems) expandParts.push('items($expand=fields)')
|
||||
if (wantsItems) expandParts.push('items(expand=fields)')
|
||||
if (expandParts.length > 0) url.searchParams.append('$expand', expandParts.join(','))
|
||||
|
||||
const finalUrl = url.toString()
|
||||
@@ -128,21 +129,23 @@ export const getListTool: ToolConfig<SharepointToolParams, SharepointGetListResp
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params) => {
|
||||
const data = await response.json()
|
||||
const data: Record<string, unknown> = await response.json()
|
||||
const value = data.value
|
||||
|
||||
// If the response is a collection of items (from the items endpoint)
|
||||
if (
|
||||
Array.isArray((data as any).value) &&
|
||||
(data as any).value.length > 0 &&
|
||||
(data as any).value[0] &&
|
||||
'fields' in (data as any).value[0]
|
||||
Array.isArray(value) &&
|
||||
value.length > 0 &&
|
||||
value[0] &&
|
||||
typeof value[0] === 'object' &&
|
||||
'fields' in value[0]
|
||||
) {
|
||||
const items = (data as any).value.map((i: any) => ({
|
||||
id: i.id,
|
||||
const items = value.map((i: Record<string, unknown>) => ({
|
||||
id: i.id as string,
|
||||
fields: i.fields as Record<string, unknown>,
|
||||
}))
|
||||
|
||||
const nextPageUrl = getGraphNextPageUrl(data as Record<string, unknown>)
|
||||
const nextPageUrl = getGraphNextPageUrl(data)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
@@ -154,18 +157,18 @@ export const getListTool: ToolConfig<SharepointToolParams, SharepointGetListResp
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray((data as any).value)) {
|
||||
const lists: SharepointList[] = (data as any).value.map((l: any) => ({
|
||||
id: l.id,
|
||||
displayName: l.displayName ?? l.name,
|
||||
name: l.name,
|
||||
webUrl: l.webUrl,
|
||||
createdDateTime: l.createdDateTime,
|
||||
lastModifiedDateTime: l.lastModifiedDateTime,
|
||||
list: l.list,
|
||||
if (Array.isArray(value)) {
|
||||
const lists: SharepointList[] = value.map((l: Record<string, unknown>) => ({
|
||||
id: l.id as string,
|
||||
displayName: (l.displayName ?? l.name) as string | undefined,
|
||||
name: l.name as string | undefined,
|
||||
webUrl: l.webUrl as string | undefined,
|
||||
createdDateTime: l.createdDateTime as string | undefined,
|
||||
lastModifiedDateTime: l.lastModifiedDateTime as string | undefined,
|
||||
list: l.list as SharepointList['list'],
|
||||
}))
|
||||
|
||||
const nextPageUrl = getGraphNextPageUrl(data as Record<string, unknown>)
|
||||
const nextPageUrl = getGraphNextPageUrl(data)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
@@ -174,29 +177,32 @@ export const getListTool: ToolConfig<SharepointToolParams, SharepointGetListResp
|
||||
}
|
||||
|
||||
const list: SharepointList = {
|
||||
id: data.id,
|
||||
displayName: data.displayName ?? data.name,
|
||||
name: data.name,
|
||||
webUrl: data.webUrl,
|
||||
createdDateTime: data.createdDateTime,
|
||||
lastModifiedDateTime: data.lastModifiedDateTime,
|
||||
list: data.list,
|
||||
id: data.id as string,
|
||||
displayName: (data.displayName ?? data.name) as string | undefined,
|
||||
name: data.name as string | undefined,
|
||||
webUrl: data.webUrl as string | undefined,
|
||||
createdDateTime: data.createdDateTime as string | undefined,
|
||||
lastModifiedDateTime: data.lastModifiedDateTime as string | undefined,
|
||||
list: data.list as SharepointList['list'],
|
||||
columns: Array.isArray(data.columns)
|
||||
? data.columns.map((c: any) => ({
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
displayName: c.displayName,
|
||||
description: c.description,
|
||||
indexed: c.indexed,
|
||||
enforcedUniqueValues: c.enforcedUniqueValues,
|
||||
hidden: c.hidden,
|
||||
readOnly: c.readOnly,
|
||||
required: c.required,
|
||||
columnGroup: c.columnGroup,
|
||||
? data.columns.map((c: Record<string, unknown>) => ({
|
||||
id: c.id as string | undefined,
|
||||
name: c.name as string | undefined,
|
||||
displayName: c.displayName as string | undefined,
|
||||
description: c.description as string | undefined,
|
||||
indexed: c.indexed as boolean | undefined,
|
||||
enforcedUniqueValues: c.enforcedUniqueValues as boolean | undefined,
|
||||
hidden: c.hidden as boolean | undefined,
|
||||
readOnly: c.readOnly as boolean | undefined,
|
||||
required: c.required as boolean | undefined,
|
||||
columnGroup: c.columnGroup as string | undefined,
|
||||
}))
|
||||
: undefined,
|
||||
items: Array.isArray(data.items)
|
||||
? data.items.map((i: any) => ({ id: i.id, fields: i.fields as Record<string, unknown> }))
|
||||
? data.items.map((i: Record<string, unknown>) => ({
|
||||
id: i.id as string,
|
||||
fields: i.fields as Record<string, unknown>,
|
||||
}))
|
||||
: undefined,
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import type { SharepointGetListItemResponse, SharepointToolParams } from '@/tools/sharepoint/types'
|
||||
import { optionalTrim } from '@/tools/sharepoint/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const getListItemTool: ToolConfig<SharepointToolParams, SharepointGetListItemResponse> = {
|
||||
id: 'sharepoint_get_list_item',
|
||||
name: 'Get SharePoint List Item',
|
||||
description: 'Get a single item (with field values) from a SharePoint list',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'sharepoint',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The access token for the SharePoint API',
|
||||
},
|
||||
siteSelector: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Select the SharePoint site',
|
||||
},
|
||||
siteId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'hidden',
|
||||
description: 'The ID of the SharePoint site (internal use)',
|
||||
},
|
||||
listId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The ID of the list containing the item. Example: b!abc123def456 or a GUID like 12345678-1234-1234-1234-123456789012',
|
||||
},
|
||||
itemId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the list item to retrieve. Example: 1, 42, or 123',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const siteId = optionalTrim(params.siteId) || optionalTrim(params.siteSelector) || 'root'
|
||||
const listId = optionalTrim(params.listId)
|
||||
const itemId = optionalTrim(params.itemId)
|
||||
if (!listId) throw new Error('listId must be provided')
|
||||
if (!itemId) throw new Error('itemId must be provided')
|
||||
const listSegment = encodeURIComponent(listId)
|
||||
const itemSegment = encodeURIComponent(itemId)
|
||||
const url = new URL(
|
||||
`https://graph.microsoft.com/v1.0/sites/${encodeURIComponent(siteId)}/lists/${listSegment}/items/${itemSegment}`
|
||||
)
|
||||
url.searchParams.set('$expand', 'fields')
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
Accept: 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data: Record<string, unknown> = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
item: {
|
||||
id: data.id as string,
|
||||
fields: data.fields as Record<string, unknown> | undefined,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
item: {
|
||||
type: 'object',
|
||||
description: 'SharePoint list item with field values',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Item ID' },
|
||||
fields: { type: 'object', description: 'Field values for the item' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,19 +1,35 @@
|
||||
import { addListItemTool } from '@/tools/sharepoint/add_list_items'
|
||||
import { createListTool } from '@/tools/sharepoint/create_list'
|
||||
import { createPageTool } from '@/tools/sharepoint/create_page'
|
||||
import { deleteFileTool } from '@/tools/sharepoint/delete_file'
|
||||
import { deleteListItemTool } from '@/tools/sharepoint/delete_list_item'
|
||||
import { deletePageTool } from '@/tools/sharepoint/delete_page'
|
||||
import { downloadFileTool } from '@/tools/sharepoint/download_file'
|
||||
import { getDriveItemTool } from '@/tools/sharepoint/get_drive_item'
|
||||
import { getListTool } from '@/tools/sharepoint/get_list'
|
||||
import { getListItemTool } from '@/tools/sharepoint/get_list_item'
|
||||
import { listSitesTool } from '@/tools/sharepoint/list_sites'
|
||||
import { publishPageTool } from '@/tools/sharepoint/publish_page'
|
||||
import { readPageTool } from '@/tools/sharepoint/read_page'
|
||||
import { updateListItemTool } from '@/tools/sharepoint/update_list'
|
||||
import { updatePageTool } from '@/tools/sharepoint/update_page'
|
||||
import { uploadFileTool } from '@/tools/sharepoint/upload_file'
|
||||
|
||||
export const sharepointAddListItemTool = addListItemTool
|
||||
export const sharepointCreatePageTool = createPageTool
|
||||
export const sharepointCreateListTool = createListTool
|
||||
export const sharepointDeleteFileTool = deleteFileTool
|
||||
export const sharepointDeleteListItemTool = deleteListItemTool
|
||||
export const sharepointDeletePageTool = deletePageTool
|
||||
export const sharepointDownloadFileTool = downloadFileTool
|
||||
export const sharepointGetDriveItemTool = getDriveItemTool
|
||||
export const sharepointGetListTool = getListTool
|
||||
export const sharepointGetListItemTool = getListItemTool
|
||||
export const sharepointListSitesTool = listSitesTool
|
||||
export const sharepointPublishPageTool = publishPageTool
|
||||
export const sharepointReadPageTool = readPageTool
|
||||
export const sharepointUpdateListItemTool = updateListItemTool
|
||||
export const sharepointAddListItemTool = addListItemTool
|
||||
export const sharepointUpdatePageTool = updatePageTool
|
||||
export const sharepointUploadFileTool = uploadFileTool
|
||||
|
||||
export * from '@/tools/sharepoint/types'
|
||||
|
||||
@@ -62,9 +62,9 @@ export const listSitesTool: ToolConfig<SharepointToolParams, SharepointReadSiteR
|
||||
const siteId = optionalTrim(params.siteId) || optionalTrim(params.siteSelector)
|
||||
|
||||
if (groupId) {
|
||||
baseUrl = `https://graph.microsoft.com/v1.0/groups/${groupId}/sites/root`
|
||||
baseUrl = `https://graph.microsoft.com/v1.0/groups/${encodeURIComponent(groupId)}/sites/root`
|
||||
} else if (siteId) {
|
||||
baseUrl = `https://graph.microsoft.com/v1.0/sites/${siteId}`
|
||||
baseUrl = `https://graph.microsoft.com/v1.0/sites/${encodeURIComponent(siteId)}`
|
||||
} else {
|
||||
baseUrl = 'https://graph.microsoft.com/v1.0/sites?search=*'
|
||||
}
|
||||
@@ -140,9 +140,9 @@ export const listSitesTool: ToolConfig<SharepointToolParams, SharepointReadSiteR
|
||||
isPersonalSite: { type: 'boolean', description: 'Whether this is a personal site' },
|
||||
root: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
serverRelativeUrl: { type: 'string', description: 'Server relative URL' },
|
||||
},
|
||||
description:
|
||||
'Present (as an empty object) only when this site is the root of its site collection',
|
||||
optional: true,
|
||||
},
|
||||
siteCollection: {
|
||||
type: 'object',
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import type { SharepointPublishPageResponse, SharepointToolParams } from '@/tools/sharepoint/types'
|
||||
import { optionalTrim } from '@/tools/sharepoint/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const publishPageTool: ToolConfig<SharepointToolParams, SharepointPublishPageResponse> = {
|
||||
id: 'sharepoint_publish_page',
|
||||
name: 'Publish SharePoint Page',
|
||||
description: 'Publish the latest version of a SharePoint page, making it available to all users',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'sharepoint',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The access token for the SharePoint API',
|
||||
},
|
||||
siteSelector: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Select the SharePoint site',
|
||||
},
|
||||
siteId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'hidden',
|
||||
description: 'The ID of the SharePoint site (internal use)',
|
||||
},
|
||||
pageId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The ID of the page to publish. Example: a GUID like 12345678-1234-1234-1234-123456789012',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const siteId = optionalTrim(params.siteId) || optionalTrim(params.siteSelector) || 'root'
|
||||
const pageId = optionalTrim(params.pageId)
|
||||
if (!pageId) throw new Error('pageId must be provided')
|
||||
return `https://graph.microsoft.com/v1.0/sites/${encodeURIComponent(siteId)}/pages/${encodeURIComponent(pageId)}/microsoft.graph.sitePage/publish`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
Accept: 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (_response: Response, params) => {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
published: true,
|
||||
pageId: params?.pageId ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
published: { type: 'boolean', description: 'Whether the page was published' },
|
||||
pageId: { type: 'string', description: 'The ID of the published page' },
|
||||
},
|
||||
}
|
||||
@@ -85,12 +85,13 @@ export const readPageTool: ToolConfig<SharepointToolParams, SharepointReadPageRe
|
||||
|
||||
const siteId = optionalTrim(params.siteId) || optionalTrim(params.siteSelector) || 'root'
|
||||
const pageId = optionalTrim(params.pageId)
|
||||
const encodedSiteId = encodeURIComponent(siteId)
|
||||
|
||||
let baseUrl: string
|
||||
if (pageId) {
|
||||
baseUrl = `https://graph.microsoft.com/v1.0/sites/${siteId}/pages/${pageId}/microsoft.graph.sitePage`
|
||||
baseUrl = `https://graph.microsoft.com/v1.0/sites/${encodedSiteId}/pages/${encodeURIComponent(pageId)}/microsoft.graph.sitePage`
|
||||
} else {
|
||||
baseUrl = `https://graph.microsoft.com/v1.0/sites/${siteId}/pages/microsoft.graph.sitePage`
|
||||
baseUrl = `https://graph.microsoft.com/v1.0/sites/${encodedSiteId}/pages/microsoft.graph.sitePage`
|
||||
}
|
||||
|
||||
const url = new URL(baseUrl)
|
||||
@@ -156,7 +157,7 @@ export const readPageTool: ToolConfig<SharepointToolParams, SharepointReadPageRe
|
||||
const pageData = data
|
||||
const contentData = {
|
||||
content: extractTextFromCanvasLayout(data.canvasLayout),
|
||||
canvasLayout: data.canvasLayout as any,
|
||||
canvasLayout: data.canvasLayout ?? null,
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -180,7 +181,7 @@ export const readPageTool: ToolConfig<SharepointToolParams, SharepointReadPageRe
|
||||
if (!data.value || data.value.length === 0) {
|
||||
logger.info('No pages found', {
|
||||
searchName: params?.pageName,
|
||||
siteId: params?.siteId || params?.siteSelector || 'root',
|
||||
siteId: optionalTrim(params?.siteId) || optionalTrim(params?.siteSelector) || 'root',
|
||||
totalResults: data.value?.length || 0,
|
||||
})
|
||||
const message = params?.pageName
|
||||
@@ -199,14 +200,14 @@ export const readPageTool: ToolConfig<SharepointToolParams, SharepointReadPageRe
|
||||
|
||||
logger.info('Found pages', {
|
||||
searchName: params?.pageName,
|
||||
foundPages: data.value.map((p: any) => ({ id: p.id, name: p.name, title: p.title })),
|
||||
foundPages: data.value.map((p) => ({ id: p.id, name: p.name, title: p.title })),
|
||||
totalCount: data.value.length,
|
||||
})
|
||||
|
||||
if (params?.pageName) {
|
||||
const pageData = data.value[0]
|
||||
const siteId = params?.siteId || params?.siteSelector || 'root'
|
||||
const contentUrl = `https://graph.microsoft.com/v1.0/sites/${siteId}/pages/${pageData.id}/microsoft.graph.sitePage?$expand=canvasLayout`
|
||||
const siteId = optionalTrim(params?.siteId) || optionalTrim(params?.siteSelector) || 'root'
|
||||
const contentUrl = `https://graph.microsoft.com/v1.0/sites/${encodeURIComponent(siteId)}/pages/${encodeURIComponent(pageData.id)}/microsoft.graph.sitePage?$expand=canvasLayout`
|
||||
|
||||
logger.info('Making API call to get page content for searched page', {
|
||||
pageId: pageData.id,
|
||||
@@ -253,7 +254,7 @@ export const readPageTool: ToolConfig<SharepointToolParams, SharepointReadPageRe
|
||||
}
|
||||
}
|
||||
|
||||
const siteId = params?.siteId || params?.siteSelector || 'root'
|
||||
const siteId = optionalTrim(params?.siteId) || optionalTrim(params?.siteSelector) || 'root'
|
||||
const pagesWithContent = []
|
||||
const nextPageUrl = getGraphNextPageUrl(data)
|
||||
|
||||
@@ -262,8 +263,9 @@ export const readPageTool: ToolConfig<SharepointToolParams, SharepointReadPageRe
|
||||
siteId,
|
||||
})
|
||||
|
||||
const encodedSiteId = encodeURIComponent(siteId)
|
||||
for (const pageInfo of data.value) {
|
||||
const contentUrl = `https://graph.microsoft.com/v1.0/sites/${siteId}/pages/${pageInfo.id}/microsoft.graph.sitePage?$expand=canvasLayout`
|
||||
const contentUrl = `https://graph.microsoft.com/v1.0/sites/${encodedSiteId}/pages/${encodeURIComponent(pageInfo.id)}/microsoft.graph.sitePage?$expand=canvasLayout`
|
||||
|
||||
try {
|
||||
const contentResponse = await fetch(contentUrl, {
|
||||
|
||||
@@ -24,40 +24,12 @@ export interface SharepointPage {
|
||||
publishingState?: {
|
||||
level: string
|
||||
}
|
||||
canvasLayout?: {
|
||||
horizontalSections: Array<{
|
||||
layout: string
|
||||
id: string
|
||||
emphasis: string
|
||||
columns?: Array<{
|
||||
id: string
|
||||
width: number
|
||||
webparts: Array<{
|
||||
id: string
|
||||
innerHtml: string
|
||||
}>
|
||||
}>
|
||||
webparts?: Array<{
|
||||
id: string
|
||||
innerHtml: string
|
||||
}>
|
||||
}>
|
||||
}
|
||||
canvasLayout?: CanvasLayout
|
||||
}
|
||||
|
||||
export interface SharepointPageContent {
|
||||
content: string
|
||||
canvasLayout?: {
|
||||
horizontalSections: Array<{
|
||||
layout: string
|
||||
id: string
|
||||
emphasis: string
|
||||
webparts: Array<{
|
||||
id: string
|
||||
innerHtml: string
|
||||
}>
|
||||
}>
|
||||
} | null
|
||||
canvasLayout?: CanvasLayout | null
|
||||
}
|
||||
|
||||
interface SharepointColumn {
|
||||
@@ -132,9 +104,8 @@ export interface SharepointReadSiteResponse extends ToolResponse {
|
||||
createdDateTime?: string
|
||||
lastModifiedDateTime?: string
|
||||
isPersonalSite?: boolean
|
||||
root?: {
|
||||
serverRelativeUrl: string
|
||||
}
|
||||
// Graph returns an empty object marker (not a URL) when this site is the root of its site collection
|
||||
root?: Record<string, never>
|
||||
siteCollection?: {
|
||||
hostname: string
|
||||
}
|
||||
@@ -177,14 +148,15 @@ export interface SharepointToolParams {
|
||||
listDisplayName?: string
|
||||
listDescription?: string
|
||||
listTemplate?: string
|
||||
// Update List Item
|
||||
// Update List Item / Delete List Item / Get List Item
|
||||
itemId?: string
|
||||
listItemFields?: Record<string, unknown>
|
||||
// Upload File
|
||||
// Upload File / Download File / Delete File / Get Drive Item
|
||||
driveId?: string
|
||||
folderPath?: string
|
||||
fileName?: string
|
||||
files?: UserFile[]
|
||||
driveItemId?: string
|
||||
}
|
||||
|
||||
export interface GraphApiResponse {
|
||||
@@ -220,6 +192,8 @@ export interface CanvasLayout {
|
||||
id?: string
|
||||
emphasis?: string
|
||||
columns?: Array<{
|
||||
id?: string
|
||||
width?: number
|
||||
webparts?: Array<{
|
||||
id?: string
|
||||
innerHtml?: string
|
||||
@@ -242,6 +216,14 @@ export type SharepointResponse =
|
||||
| SharepointUpdateListItemResponse
|
||||
| SharepointAddListItemResponse
|
||||
| SharepointUploadFileResponse
|
||||
| SharepointDeleteListItemResponse
|
||||
| SharepointGetListItemResponse
|
||||
| SharepointDeletePageResponse
|
||||
| SharepointUpdatePageResponse
|
||||
| SharepointPublishPageResponse
|
||||
| SharepointDownloadFileResponse
|
||||
| SharepointDeleteFileResponse
|
||||
| SharepointGetDriveItemResponse
|
||||
|
||||
export interface SharepointGetListResponse extends ToolResponse {
|
||||
output: {
|
||||
@@ -307,3 +289,83 @@ export interface SharepointUploadFileResponse extends ToolResponse {
|
||||
errors?: SharepointUploadError[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface SharepointDeleteListItemResponse extends ToolResponse {
|
||||
output: {
|
||||
deleted: boolean
|
||||
itemId: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface SharepointGetListItemResponse extends ToolResponse {
|
||||
output: {
|
||||
item: {
|
||||
id: string
|
||||
fields?: Record<string, unknown>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface SharepointDeletePageResponse extends ToolResponse {
|
||||
output: {
|
||||
deleted: boolean
|
||||
pageId: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface SharepointUpdatePageResponse extends ToolResponse {
|
||||
output: {
|
||||
page: SharepointPage
|
||||
}
|
||||
}
|
||||
|
||||
export interface SharepointPublishPageResponse extends ToolResponse {
|
||||
output: {
|
||||
published: boolean
|
||||
pageId: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface SharepointDriveItem {
|
||||
id: string
|
||||
name: string
|
||||
webUrl?: string
|
||||
size?: number
|
||||
createdDateTime?: string
|
||||
lastModifiedDateTime?: string
|
||||
file?: {
|
||||
mimeType?: string
|
||||
} | null
|
||||
folder?: {
|
||||
childCount?: number
|
||||
} | null
|
||||
parentReference?: {
|
||||
id?: string
|
||||
driveId?: string
|
||||
path?: string
|
||||
} | null
|
||||
}
|
||||
|
||||
export interface SharepointDownloadFileResponse extends ToolResponse {
|
||||
output: {
|
||||
file: {
|
||||
name: string
|
||||
mimeType: string
|
||||
data: Buffer | string
|
||||
size: number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface SharepointDeleteFileResponse extends ToolResponse {
|
||||
output: {
|
||||
deleted: boolean
|
||||
itemId: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface SharepointGetDriveItemResponse extends ToolResponse {
|
||||
output: {
|
||||
driveItem: SharepointDriveItem
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
SharepointToolParams,
|
||||
SharepointUpdateListItemResponse,
|
||||
} from '@/tools/sharepoint/types'
|
||||
import { optionalTrim } from '@/tools/sharepoint/utils'
|
||||
import { optionalTrim, sanitizeListItemFields } from '@/tools/sharepoint/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('SharePointUpdateListItem')
|
||||
|
||||
export const updateListItemTool: ToolConfig<
|
||||
SharepointToolParams,
|
||||
SharepointUpdateListItemResponse
|
||||
@@ -72,7 +69,7 @@ export const updateListItemTool: ToolConfig<
|
||||
throw new Error('listId must be provided')
|
||||
}
|
||||
const listSegment = encodeURIComponent(listId)
|
||||
return `https://graph.microsoft.com/v1.0/sites/${siteId}/lists/${listSegment}/items/${encodeURIComponent(itemId)}/fields`
|
||||
return `https://graph.microsoft.com/v1.0/sites/${encodeURIComponent(siteId)}/lists/${listSegment}/items/${encodeURIComponent(itemId)}/fields`
|
||||
},
|
||||
method: 'PATCH',
|
||||
headers: (params) => ({
|
||||
@@ -85,53 +82,7 @@ export const updateListItemTool: ToolConfig<
|
||||
throw new Error('listItemFields must not be empty')
|
||||
}
|
||||
|
||||
// Filter out system/read-only fields that cannot be updated via Graph
|
||||
const readOnlyFields = new Set<string>([
|
||||
'Id',
|
||||
'id',
|
||||
'UniqueId',
|
||||
'GUID',
|
||||
'ContentTypeId',
|
||||
'Created',
|
||||
'Modified',
|
||||
'Author',
|
||||
'Editor',
|
||||
'CreatedBy',
|
||||
'ModifiedBy',
|
||||
'AuthorId',
|
||||
'EditorId',
|
||||
'_UIVersionString',
|
||||
'Attachments',
|
||||
'FileRef',
|
||||
'FileDirRef',
|
||||
'FileLeafRef',
|
||||
])
|
||||
|
||||
const entries = Object.entries(params.listItemFields)
|
||||
const updatableEntries = entries.filter(([key]) => !readOnlyFields.has(key))
|
||||
|
||||
if (updatableEntries.length !== entries.length) {
|
||||
const removed = entries.filter(([key]) => readOnlyFields.has(key)).map(([key]) => key)
|
||||
logger.warn('Removed read-only SharePoint fields from update', {
|
||||
removed,
|
||||
})
|
||||
}
|
||||
|
||||
if (updatableEntries.length === 0) {
|
||||
const requestedKeys = Object.keys(params.listItemFields)
|
||||
throw new Error(
|
||||
`All provided fields are read-only and cannot be updated: ${requestedKeys.join(', ')}`
|
||||
)
|
||||
}
|
||||
|
||||
const sanitizedFields = Object.fromEntries(updatableEntries)
|
||||
|
||||
logger.info('Updating SharePoint list item fields', {
|
||||
listItemId: params.itemId,
|
||||
listId: params.listId,
|
||||
fieldsKeys: Object.keys(sanitizedFields),
|
||||
})
|
||||
return sanitizedFields
|
||||
return sanitizeListItemFields(params.listItemFields, { action: 'update' })
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
CanvasLayout,
|
||||
SharepointToolParams,
|
||||
SharepointUpdatePageResponse,
|
||||
} from '@/tools/sharepoint/types'
|
||||
import { escapeHtml, optionalTrim } from '@/tools/sharepoint/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('SharePointUpdatePage')
|
||||
|
||||
export const updatePageTool: ToolConfig<SharepointToolParams, SharepointUpdatePageResponse> = {
|
||||
id: 'sharepoint_update_page',
|
||||
name: 'Update SharePoint Page',
|
||||
description: 'Update the title and/or content of a SharePoint page',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'sharepoint',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The access token for the SharePoint API',
|
||||
},
|
||||
siteId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'hidden',
|
||||
description: 'The ID of the SharePoint site (internal use)',
|
||||
},
|
||||
siteSelector: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Select the SharePoint site',
|
||||
},
|
||||
pageId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The ID of the page to update. Example: a GUID like 12345678-1234-1234-1234-123456789012',
|
||||
},
|
||||
pageTitle: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'The new title of the page',
|
||||
},
|
||||
pageContent: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'The new text content of the page. Replaces the entire canvas layout of the page.',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const siteId = optionalTrim(params.siteId) || optionalTrim(params.siteSelector) || 'root'
|
||||
const pageId = optionalTrim(params.pageId)
|
||||
if (!pageId) throw new Error('pageId must be provided')
|
||||
return `https://graph.microsoft.com/v1.0/sites/${encodeURIComponent(siteId)}/pages/${encodeURIComponent(pageId)}/microsoft.graph.sitePage`
|
||||
},
|
||||
method: 'PATCH',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const pageTitle = optionalTrim(params.pageTitle)
|
||||
const pageContent = typeof params.pageContent === 'string' ? params.pageContent : undefined
|
||||
|
||||
if (!pageTitle && !pageContent) {
|
||||
throw new Error('At least one of pageTitle or pageContent must be provided')
|
||||
}
|
||||
|
||||
const pageData: {
|
||||
'@odata.type': string
|
||||
title?: string
|
||||
canvasLayout?: CanvasLayout
|
||||
} = {
|
||||
'@odata.type': '#microsoft.graph.sitePage',
|
||||
}
|
||||
if (pageTitle) pageData.title = pageTitle
|
||||
|
||||
if (pageContent) {
|
||||
pageData.canvasLayout = {
|
||||
horizontalSections: [
|
||||
{
|
||||
layout: 'oneColumn',
|
||||
id: '1',
|
||||
emphasis: 'none',
|
||||
columns: [
|
||||
{
|
||||
id: '1',
|
||||
width: 12,
|
||||
webparts: [
|
||||
{
|
||||
id: '6f9230af-2a98-4952-b205-9ede4f9ef548',
|
||||
innerHtml: `<p>${escapeHtml(pageContent)}</p>`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('Updating SharePoint page', {
|
||||
pageId: params.pageId,
|
||||
hasTitle: !!pageTitle,
|
||||
hasContent: !!pageContent,
|
||||
})
|
||||
|
||||
return pageData
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
logger.info('SharePoint page updated successfully', {
|
||||
pageId: data.id,
|
||||
pageName: data.name,
|
||||
pageTitle: data.title,
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
page: {
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
title: data.title || data.name,
|
||||
webUrl: data.webUrl,
|
||||
pageLayout: data.pageLayout,
|
||||
createdDateTime: data.createdDateTime,
|
||||
lastModifiedDateTime: data.lastModifiedDateTime,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
page: {
|
||||
type: 'object',
|
||||
description: 'Updated SharePoint page information',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'The unique ID of the page' },
|
||||
name: { type: 'string', description: 'The name of the page' },
|
||||
title: { type: 'string', description: 'The title of the page' },
|
||||
webUrl: { type: 'string', description: 'The URL to access the page' },
|
||||
pageLayout: { type: 'string', description: 'The layout type of the page' },
|
||||
createdDateTime: { type: 'string', description: 'When the page was created' },
|
||||
lastModifiedDateTime: { type: 'string', description: 'When the page was last modified' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -13,6 +13,15 @@ export function escapeODataString(value: string): string {
|
||||
return value.replace(/'/g, "''")
|
||||
}
|
||||
|
||||
export function escapeHtml(value: string): string {
|
||||
return value
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
export function getGraphNextPageUrl(data: object): string | undefined {
|
||||
const nextLink = (data as Record<string, unknown>)['@odata.nextLink']
|
||||
return typeof nextLink === 'string' ? nextLink : undefined
|
||||
@@ -102,6 +111,57 @@ export function extractTextFromCanvasLayout(canvasLayout: CanvasLayout | null |
|
||||
return finalContent
|
||||
}
|
||||
|
||||
/** SharePoint list item fields that are system-managed and cannot be set via the Graph API. */
|
||||
export const READ_ONLY_LIST_ITEM_FIELDS = new Set<string>([
|
||||
'Id',
|
||||
'id',
|
||||
'UniqueId',
|
||||
'GUID',
|
||||
'ContentTypeId',
|
||||
'Created',
|
||||
'Modified',
|
||||
'Author',
|
||||
'Editor',
|
||||
'CreatedBy',
|
||||
'ModifiedBy',
|
||||
'AuthorId',
|
||||
'EditorId',
|
||||
'_UIVersionString',
|
||||
'Attachments',
|
||||
'FileRef',
|
||||
'FileDirRef',
|
||||
'FileLeafRef',
|
||||
])
|
||||
|
||||
/**
|
||||
* Removes read-only/system-managed fields from a SharePoint list item field set, logging any
|
||||
* fields that were stripped. Throws if no updatable fields remain.
|
||||
*/
|
||||
export function sanitizeListItemFields(
|
||||
fields: Record<string, unknown>,
|
||||
context: { action: 'update' | 'create' }
|
||||
): Record<string, unknown> {
|
||||
const entries = Object.entries(fields)
|
||||
const updatableEntries = entries.filter(([key]) => !READ_ONLY_LIST_ITEM_FIELDS.has(key))
|
||||
|
||||
if (updatableEntries.length !== entries.length) {
|
||||
const removed = entries
|
||||
.filter(([key]) => READ_ONLY_LIST_ITEM_FIELDS.has(key))
|
||||
.map(([key]) => key)
|
||||
logger.warn(`Removed read-only SharePoint fields from ${context.action}`, { removed })
|
||||
}
|
||||
|
||||
if (updatableEntries.length === 0) {
|
||||
const requestedKeys = Object.keys(fields)
|
||||
const verb = context.action === 'update' ? 'updated' : 'set'
|
||||
throw new Error(
|
||||
`All provided fields are read-only and cannot be ${verb}: ${requestedKeys.join(', ')}`
|
||||
)
|
||||
}
|
||||
|
||||
return Object.fromEntries(updatableEntries)
|
||||
}
|
||||
|
||||
export function cleanODataMetadata<T>(obj: T): T {
|
||||
if (!obj || typeof obj !== 'object') return obj
|
||||
|
||||
|
||||
Reference in New Issue
Block a user