diff --git a/apps/sim/blocks/blocks/onedrive.ts b/apps/sim/blocks/blocks/onedrive.ts index 30f8d82a428..e87f0212674 100644 --- a/apps/sim/blocks/blocks/onedrive.ts +++ b/apps/sim/blocks/blocks/onedrive.ts @@ -12,10 +12,10 @@ const logger = createLogger('OneDriveBlock') export const OneDriveBlock: BlockConfig = { type: 'onedrive', name: 'OneDrive', - description: 'Create, upload, download, list, and delete files', + description: 'Create, upload, download, search, move, copy, share, and delete files', authMode: AuthMode.OAuth, longDescription: - 'Integrate OneDrive into the workflow. Can create text and Excel files, upload files, download files, list files, and delete files or folders.', + 'Integrate OneDrive into the workflow. Can create text and Excel files, upload files, download files, list and search files, move or rename files, copy files, create sharing links, and delete files or folders.', docsLink: 'https://docs.sim.ai/integrations/onedrive', category: 'tools', integrationType: IntegrationType.Documents, @@ -33,6 +33,12 @@ export const OneDriveBlock: BlockConfig = { { label: 'Upload File', id: 'upload' }, { label: 'Download File', id: 'download' }, { label: 'List Files', id: 'list' }, + { label: 'Search Files', id: 'search' }, + { label: 'Get Item Info', id: 'get_item' }, + { label: 'Get Drive Info', id: 'get_drive_info' }, + { label: 'Move/Rename File', id: 'move' }, + { label: 'Copy File', id: 'copy' }, + { label: 'Create Sharing Link', id: 'create_share_link' }, { label: 'Delete File', id: 'delete' }, ], }, @@ -231,14 +237,45 @@ export const OneDriveBlock: BlockConfig = { title: 'Search Query', type: 'short-input', placeholder: 'Search for specific files (e.g., name contains "report")', - condition: { field: 'operation', value: 'list' }, + condition: { field: 'operation', value: ['list', 'search'] }, }, { id: 'pageSize', title: 'Results Per Page', type: 'short-input', placeholder: 'Number of results (default: 100, max: 1000)', - condition: { field: 'operation', value: 'list' }, + condition: { field: 'operation', value: ['list', 'search'] }, + }, + { + id: 'pageToken', + title: 'Page Token', + type: 'short-input', + placeholder: 'Continuation URL from a previous response (nextPageToken)', + mode: 'advanced', + condition: { field: 'operation', value: ['list', 'search'] }, + }, + // Get Item Info Fields - File Selector (basic mode) + { + id: 'getItemFileSelector', + title: 'Select File or Folder', + type: 'file-selector', + canonicalParamId: 'getItemFileId', + serviceId: 'onedrive', + selectorKey: 'onedrive.files', + requiredScopes: getScopesForService('onedrive'), + placeholder: 'Select a file or folder (leave empty for the drive root)', + mode: 'basic', + dependsOn: ['credential'], + condition: { field: 'operation', value: 'get_item' }, + }, + { + id: 'getItemManualFileId', + title: 'File or Folder ID', + type: 'short-input', + canonicalParamId: 'getItemFileId', + placeholder: 'Enter file or folder ID (leave empty for the drive root)', + mode: 'advanced', + condition: { field: 'operation', value: 'get_item' }, }, // Download File Fields - File Selector (basic mode) { @@ -274,6 +311,170 @@ export const OneDriveBlock: BlockConfig = { placeholder: 'Optional: Override the filename', condition: { field: 'operation', value: 'download' }, }, + // Move/Rename File Fields - File Selector (basic mode) + { + id: 'moveFileSelector', + title: 'Select File or Folder to Move', + type: 'file-selector', + canonicalParamId: 'moveFileId', + serviceId: 'onedrive', + selectorKey: 'onedrive.files', + requiredScopes: getScopesForService('onedrive'), + placeholder: 'Select a file or folder to move or rename', + mode: 'basic', + dependsOn: ['credential'], + condition: { field: 'operation', value: 'move' }, + required: true, + }, + { + id: 'moveManualFileId', + title: 'File or Folder ID', + type: 'short-input', + canonicalParamId: 'moveFileId', + placeholder: 'Enter file or folder ID to move or rename', + mode: 'advanced', + condition: { field: 'operation', value: 'move' }, + required: true, + }, + { + id: 'moveDestinationFolderSelector', + title: 'Select Destination Folder', + type: 'file-selector', + canonicalParamId: 'moveDestinationFolderId', + serviceId: 'onedrive', + selectorKey: 'onedrive.folders', + requiredScopes: getScopesForService('onedrive'), + mimeType: 'application/vnd.microsoft.graph.folder', + placeholder: 'Select a destination folder (leave empty to only rename)', + dependsOn: ['credential'], + mode: 'basic', + condition: { field: 'operation', value: 'move' }, + }, + { + id: 'moveDestinationManualFolderId', + title: 'Destination Folder ID', + type: 'short-input', + canonicalParamId: 'moveDestinationFolderId', + placeholder: 'Enter destination folder ID (leave empty to only rename)', + dependsOn: ['credential'], + mode: 'advanced', + condition: { field: 'operation', value: 'move' }, + }, + { + id: 'newName', + title: 'New Name', + type: 'short-input', + placeholder: 'New name for the file or folder (leave empty to only move)', + condition: { field: 'operation', value: 'move' }, + }, + // Copy File Fields - File Selector (basic mode) + { + id: 'copyFileSelector', + title: 'Select File or Folder to Copy', + type: 'file-selector', + canonicalParamId: 'copyFileId', + serviceId: 'onedrive', + selectorKey: 'onedrive.files', + requiredScopes: getScopesForService('onedrive'), + placeholder: 'Select a file or folder to copy', + mode: 'basic', + dependsOn: ['credential'], + condition: { field: 'operation', value: 'copy' }, + required: true, + }, + { + id: 'copyManualFileId', + title: 'File or Folder ID', + type: 'short-input', + canonicalParamId: 'copyFileId', + placeholder: 'Enter file or folder ID to copy', + mode: 'advanced', + condition: { field: 'operation', value: 'copy' }, + required: true, + }, + { + id: 'copyDestinationFolderSelector', + title: 'Select Destination Folder', + type: 'file-selector', + canonicalParamId: 'copyDestinationFolderId', + serviceId: 'onedrive', + selectorKey: 'onedrive.folders', + requiredScopes: getScopesForService('onedrive'), + mimeType: 'application/vnd.microsoft.graph.folder', + placeholder: 'Select a destination folder', + dependsOn: ['credential'], + mode: 'basic', + condition: { field: 'operation', value: 'copy' }, + required: true, + }, + { + id: 'copyDestinationManualFolderId', + title: 'Destination Folder ID', + type: 'short-input', + canonicalParamId: 'copyDestinationFolderId', + placeholder: 'Enter destination folder ID', + dependsOn: ['credential'], + mode: 'advanced', + condition: { field: 'operation', value: 'copy' }, + required: true, + }, + { + id: 'destinationFileName', + title: 'New Name', + type: 'short-input', + placeholder: 'Optional name for the copy (defaults to the original name)', + condition: { field: 'operation', value: 'copy' }, + }, + // Create Sharing Link Fields - File Selector (basic mode) + { + id: 'shareLinkFileSelector', + title: 'Select File or Folder to Share', + type: 'file-selector', + canonicalParamId: 'shareLinkFileId', + serviceId: 'onedrive', + selectorKey: 'onedrive.files', + requiredScopes: getScopesForService('onedrive'), + placeholder: 'Select a file or folder to share', + mode: 'basic', + dependsOn: ['credential'], + condition: { field: 'operation', value: 'create_share_link' }, + required: true, + }, + { + id: 'shareLinkManualFileId', + title: 'File or Folder ID', + type: 'short-input', + canonicalParamId: 'shareLinkFileId', + placeholder: 'Enter file or folder ID to share', + mode: 'advanced', + condition: { field: 'operation', value: 'create_share_link' }, + required: true, + }, + { + id: 'linkType', + title: 'Link Type', + type: 'dropdown', + options: [ + { label: 'View (read-only)', id: 'view' }, + { label: 'Edit (read-write)', id: 'edit' }, + { label: 'Embed', id: 'embed' }, + ], + placeholder: 'Select link type', + condition: { field: 'operation', value: 'create_share_link' }, + required: true, + }, + { + id: 'linkScope', + title: 'Link Scope', + type: 'dropdown', + options: [ + { label: 'Anyone with the link', id: 'anonymous' }, + { label: 'People in my organization', id: 'organization' }, + { label: 'Specific people', id: 'users' }, + ], + placeholder: 'Select who can use the link', + condition: { field: 'operation', value: 'create_share_link' }, + }, // Delete File Fields - File Selector (basic mode) { id: 'deleteFileSelector', @@ -308,6 +509,12 @@ export const OneDriveBlock: BlockConfig = { 'onedrive_create_folder', 'onedrive_download', 'onedrive_list', + 'onedrive_search', + 'onedrive_get_item', + 'onedrive_get_drive_info', + 'onedrive_move', + 'onedrive_copy', + 'onedrive_create_share_link', 'onedrive_delete', ], config: { @@ -322,6 +529,18 @@ export const OneDriveBlock: BlockConfig = { return 'onedrive_download' case 'list': return 'onedrive_list' + case 'search': + return 'onedrive_search' + case 'get_item': + return 'onedrive_get_item' + case 'get_drive_info': + return 'onedrive_get_drive_info' + case 'move': + return 'onedrive_move' + case 'copy': + return 'onedrive_copy' + case 'create_share_link': + return 'onedrive_create_share_link' case 'delete': return 'onedrive_delete' default: @@ -335,9 +554,15 @@ export const OneDriveBlock: BlockConfig = { uploadFolderId, createFolderParentId, listFolderId, + moveDestinationFolderId, + copyDestinationFolderId, // File canonical params (per-operation) downloadFileId, deleteFileId, + moveFileId, + copyFileId, + shareLinkFileId, + getItemFileId, mimeType, values, downloadFileName, @@ -377,6 +602,29 @@ export const OneDriveBlock: BlockConfig = { case 'delete': resolvedFileId = deleteFileId?.trim() || undefined break + case 'move': + resolvedFileId = moveFileId?.trim() || undefined + break + case 'copy': + resolvedFileId = copyFileId?.trim() || undefined + break + case 'create_share_link': + resolvedFileId = shareLinkFileId?.trim() || undefined + break + case 'get_item': + resolvedFileId = getItemFileId?.trim() || undefined + break + } + + // Resolve destinationFolderId based on operation + let resolvedDestinationFolderId: string | undefined + switch (params.operation) { + case 'move': + resolvedDestinationFolderId = moveDestinationFolderId?.trim() || undefined + break + case 'copy': + resolvedDestinationFolderId = copyDestinationFolderId?.trim() || undefined + break } return { @@ -386,6 +634,7 @@ export const OneDriveBlock: BlockConfig = { file: normalizedFile, folderId: resolvedFolderId, fileId: resolvedFileId, + destinationFolderId: resolvedDestinationFolderId, pageSize: rest.pageSize ? Number.parseInt(rest.pageSize as string, 10) : undefined, mimeType: mimeType, ...(downloadFileName && { fileName: downloadFileName }), @@ -411,9 +660,33 @@ export const OneDriveBlock: BlockConfig = { deleteFileId: { type: 'string', description: 'File to delete' }, downloadFileName: { type: 'string', description: 'File name override for download' }, folderName: { type: 'string', description: 'Folder name for create_folder' }, - // List operation inputs + // List / Search operation inputs query: { type: 'string', description: 'Search query' }, pageSize: { type: 'number', description: 'Results per page' }, + pageToken: { + type: 'string', + description: 'Continuation URL for the next page of list or search results', + }, + // Move operation inputs + moveFileId: { type: 'string', description: 'File or folder to move or rename' }, + moveDestinationFolderId: { type: 'string', description: 'Destination folder for move' }, + newName: { type: 'string', description: 'New name for move/rename' }, + // Copy operation inputs + copyFileId: { type: 'string', description: 'File or folder to copy' }, + copyDestinationFolderId: { type: 'string', description: 'Destination folder for copy' }, + destinationFileName: { type: 'string', description: 'Optional name for the copy' }, + // Create sharing link operation inputs + shareLinkFileId: { type: 'string', description: 'File or folder to share' }, + linkType: { type: 'string', description: 'Type of sharing link: view, edit, or embed' }, + linkScope: { + type: 'string', + description: 'Who can use the link: anonymous, organization, or users', + }, + // Get item operation inputs + getItemFileId: { + type: 'string', + description: 'File or folder to retrieve metadata for (empty for drive root)', + }, }, outputs: { success: { type: 'boolean', description: 'Whether the operation was successful' }, @@ -428,6 +701,37 @@ export const OneDriveBlock: BlockConfig = { description: 'An array of OneDrive file objects, each containing details such as id, name, size, and more.', }, + nextPageToken: { + type: 'string', + description: 'Token for retrieving the next page of list/search results, if any', + }, + sourceFileId: { + type: 'string', + description: 'The ID of the file or folder that was copied', + }, + name: { + type: 'string', + description: 'The requested name for the copy, if provided', + }, + monitorUrl: { + type: 'string', + description: 'URL to poll for the status of an asynchronous copy operation', + }, + link: { + type: 'json', + description: 'The created sharing link, including its type, scope, and URL', + }, + driveId: { type: 'string', description: 'The ID of the drive' }, + driveType: { + type: 'string', + description: 'The type of drive (e.g., "personal", "business")', + }, + webUrl: { type: 'string', description: 'URL to the drive in the browser' }, + owner: { type: 'string', description: 'Display name of the drive owner' }, + quota: { + type: 'json', + description: 'Drive storage quota information (total, used, remaining, deleted, state)', + }, }, } diff --git a/apps/sim/tools/onedrive/copy.ts b/apps/sim/tools/onedrive/copy.ts new file mode 100644 index 00000000000..7353ac0fc4b --- /dev/null +++ b/apps/sim/tools/onedrive/copy.ts @@ -0,0 +1,97 @@ +import { createLogger } from '@sim/logger' +import type { OneDriveCopyResponse, OneDriveToolParams } from '@/tools/onedrive/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('OneDriveCopyTool') + +/** + * Microsoft Graph processes driveItem copies asynchronously: a successful request returns + * `202 Accepted` with a `Location` header pointing to a monitor URL, not the copied item itself. + * See https://learn.microsoft.com/en-us/graph/api/driveitem-copy + */ +export const copyTool: ToolConfig = { + id: 'onedrive_copy', + name: 'Copy OneDrive File', + description: 'Copy a file or folder to another location within OneDrive', + version: '1.0', + + oauth: { + required: true, + provider: 'onedrive', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the OneDrive API', + }, + fileId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the file or folder to copy', + }, + destinationFolderId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the destination parent folder', + }, + destinationFileName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional new name for the copy (defaults to the original name)', + }, + }, + + request: { + url: (params) => + `https://graph.microsoft.com/v1.0/me/drive/items/${encodeURIComponent(params.fileId || '')}/copy`, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => ({ + parentReference: { id: params.destinationFolderId }, + ...(params.destinationFileName && { name: params.destinationFileName }), + }), + }, + + transformResponse: async (response: Response, params?: OneDriveToolParams) => { + if (response.status !== 202) { + const data = await response.json().catch(() => ({})) + throw new Error(data.error?.message || 'Failed to start OneDrive copy') + } + + const monitorUrl = response.headers.get('location') || undefined + + logger.info('OneDrive copy accepted for async processing', { + fileId: params?.fileId, + monitorUrl, + }) + + return { + success: true, + output: { + sourceFileId: params?.fileId || '', + name: params?.destinationFileName, + monitorUrl, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the copy request was accepted' }, + sourceFileId: { type: 'string', description: 'The ID of the file or folder that was copied' }, + name: { type: 'string', description: 'The requested name for the copy, if provided' }, + monitorUrl: { + type: 'string', + description: + 'URL to poll for the status of the asynchronous copy operation (copy completes in the background)', + }, + }, +} diff --git a/apps/sim/tools/onedrive/create_folder.ts b/apps/sim/tools/onedrive/create_folder.ts index 1aa799084b1..3c250ca05d8 100644 --- a/apps/sim/tools/onedrive/create_folder.ts +++ b/apps/sim/tools/onedrive/create_folder.ts @@ -25,25 +25,19 @@ export const createFolderTool: ToolConfig { // Use specific parent folder URL if parentId is provided - const parentFolderId = params.manualFolderId || params.folderSelector + const parentFolderId = params.folderId?.trim() if (parentFolderId) { return `https://graph.microsoft.com/v1.0/me/drive/items/${encodeURIComponent(parentFolderId)}/children` } diff --git a/apps/sim/tools/onedrive/create_share_link.ts b/apps/sim/tools/onedrive/create_share_link.ts new file mode 100644 index 00000000000..c1c8c508855 --- /dev/null +++ b/apps/sim/tools/onedrive/create_share_link.ts @@ -0,0 +1,80 @@ +import type { OneDriveShareLinkResponse, OneDriveToolParams } from '@/tools/onedrive/types' +import type { ToolConfig } from '@/tools/types' + +export const createShareLinkTool: ToolConfig = { + id: 'onedrive_create_share_link', + name: 'Create OneDrive Sharing Link', + description: 'Create a view or edit sharing link for a OneDrive file or folder', + version: '1.0', + + oauth: { + required: true, + provider: 'onedrive', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the OneDrive API', + }, + fileId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the file or folder to share', + }, + linkType: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Type of link to create: "view" (read-only), "edit" (read-write), or "embed"', + }, + linkScope: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Who can use the link: "anonymous" (anyone), "organization" (tenant members), or "users" (specific people)', + }, + }, + + request: { + url: (params) => + `https://graph.microsoft.com/v1.0/me/drive/items/${encodeURIComponent(params.fileId || '')}/createLink`, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => ({ + type: params.linkType || 'view', + ...(params.linkScope && { scope: params.linkScope }), + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + link: { + type: data.link?.type, + scope: data.link?.scope, + webUrl: data.link?.webUrl, + webHtml: data.link?.webHtml, + }, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the sharing link was created successfully' }, + link: { + type: 'object', + description: 'The created sharing link, including its type, scope, and URL', + }, + }, +} diff --git a/apps/sim/tools/onedrive/get_drive_info.ts b/apps/sim/tools/onedrive/get_drive_info.ts new file mode 100644 index 00000000000..6d282208282 --- /dev/null +++ b/apps/sim/tools/onedrive/get_drive_info.ts @@ -0,0 +1,64 @@ +import type { OneDriveGetDriveInfoResponse, OneDriveToolParams } from '@/tools/onedrive/types' +import type { ToolConfig } from '@/tools/types' + +export const getDriveInfoTool: ToolConfig = { + id: 'onedrive_get_drive_info', + name: 'Get OneDrive Info', + description: 'Get information about the OneDrive drive, including storage quota', + version: '1.0', + + oauth: { + required: true, + provider: 'onedrive', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the OneDrive API', + }, + }, + + request: { + url: () => 'https://graph.microsoft.com/v1.0/me/drive', + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + driveId: data.id, + driveType: data.driveType, + webUrl: data.webUrl, + owner: data.owner?.user?.displayName ?? null, + quota: { + total: data.quota?.total ?? 0, + used: data.quota?.used ?? 0, + remaining: data.quota?.remaining ?? 0, + deleted: data.quota?.deleted ?? 0, + state: data.quota?.state ?? 'normal', + }, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the drive info was retrieved' }, + driveId: { type: 'string', description: 'The ID of the drive' }, + driveType: { type: 'string', description: 'The type of drive (e.g., "personal", "business")' }, + webUrl: { type: 'string', description: 'URL to the drive in the browser' }, + owner: { type: 'string', description: 'Display name of the drive owner', optional: true }, + quota: { + type: 'object', + description: 'Storage quota information in bytes (total, used, remaining, deleted, state)', + }, + }, +} diff --git a/apps/sim/tools/onedrive/get_item.ts b/apps/sim/tools/onedrive/get_item.ts new file mode 100644 index 00000000000..ab0a9436b91 --- /dev/null +++ b/apps/sim/tools/onedrive/get_item.ts @@ -0,0 +1,84 @@ +import type { + MicrosoftGraphDriveItem, + OneDriveGetItemResponse, + OneDriveToolParams, +} from '@/tools/onedrive/types' +import type { ToolConfig } from '@/tools/types' + +export const getItemTool: ToolConfig = { + id: 'onedrive_get_item', + name: 'Get OneDrive Item Metadata', + description: 'Get metadata for a specific OneDrive file or folder by ID, or the drive root', + version: '1.0', + + oauth: { + required: true, + provider: 'onedrive', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the OneDrive API', + }, + fileId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'The ID of the file or folder to retrieve (e.g., "01BYE5RZ6QN3ZWBTUFOFD3GSPGOHDJD36M"). Leave empty to get the drive root folder', + }, + }, + + request: { + url: (params) => { + const fileId = params.fileId?.trim() + const baseUrl = fileId + ? `https://graph.microsoft.com/v1.0/me/drive/items/${encodeURIComponent(fileId)}` + : 'https://graph.microsoft.com/v1.0/me/drive/root' + + const url = new URL(baseUrl) + url.searchParams.append( + '$select', + 'id,name,file,folder,webUrl,size,createdDateTime,lastModifiedDateTime,parentReference,@microsoft.graph.downloadUrl' + ) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response: Response) => { + const data: MicrosoftGraphDriveItem = await response.json() + + return { + success: true, + output: { + file: { + id: data.id, + name: data.name, + mimeType: data.file?.mimeType || (data.folder ? 'application/folder' : 'unknown'), + webViewLink: data.webUrl, + webContentLink: data['@microsoft.graph.downloadUrl'], + size: data.size?.toString() || '0', + createdTime: data.createdDateTime, + modifiedTime: data.lastModifiedDateTime, + parents: data.parentReference ? [data.parentReference.id] : [], + }, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the item metadata was retrieved' }, + file: { + type: 'object', + description: + 'The file or folder metadata, including id, name, webViewLink, size, and timestamps', + }, + }, +} diff --git a/apps/sim/tools/onedrive/index.ts b/apps/sim/tools/onedrive/index.ts index f64a78aaf28..b7b184678bc 100644 --- a/apps/sim/tools/onedrive/index.ts +++ b/apps/sim/tools/onedrive/index.ts @@ -1,11 +1,23 @@ +import { copyTool } from '@/tools/onedrive/copy' import { createFolderTool } from '@/tools/onedrive/create_folder' +import { createShareLinkTool } from '@/tools/onedrive/create_share_link' import { deleteTool } from '@/tools/onedrive/delete' import { downloadTool } from '@/tools/onedrive/download' +import { getDriveInfoTool } from '@/tools/onedrive/get_drive_info' +import { getItemTool } from '@/tools/onedrive/get_item' import { listTool } from '@/tools/onedrive/list' +import { moveTool } from '@/tools/onedrive/move' +import { searchTool } from '@/tools/onedrive/search' import { uploadTool } from '@/tools/onedrive/upload' +export const onedriveCopyTool = copyTool export const onedriveCreateFolderTool = createFolderTool +export const onedriveCreateShareLinkTool = createShareLinkTool export const onedriveDeleteTool = deleteTool export const onedriveDownloadTool = downloadTool +export const onedriveGetDriveInfoTool = getDriveInfoTool +export const onedriveGetItemTool = getItemTool export const onedriveListTool = listTool +export const onedriveMoveTool = moveTool +export const onedriveSearchTool = searchTool export const onedriveUploadTool = uploadTool diff --git a/apps/sim/tools/onedrive/list.ts b/apps/sim/tools/onedrive/list.ts index 2113acd2ccb..6b79fff7de6 100644 --- a/apps/sim/tools/onedrive/list.ts +++ b/apps/sim/tools/onedrive/list.ts @@ -3,6 +3,8 @@ import type { OneDriveListResponse, OneDriveToolParams, } from '@/tools/onedrive/types' +import { escapeODataStringLiteral } from '@/tools/onedrive/utils' +import { assertGraphNextPageUrl, getGraphNextPageUrl } from '@/tools/sharepoint/utils' import type { ToolConfig } from '@/tools/types' export const listTool: ToolConfig = { @@ -23,18 +25,12 @@ export const listTool: ToolConfig = { visibility: 'hidden', description: 'The access token for the OneDrive API', }, - folderSelector: { + folderId: { type: 'string', required: false, visibility: 'user-or-llm', description: 'Folder ID to list files from (e.g., "01BYE5RZ6QN3ZWBTUFOFD3GSPGOHDJD36M")', }, - manualFolderId: { - type: 'string', - required: false, - visibility: 'hidden', - description: 'The manually entered folder ID (advanced mode)', - }, query: { type: 'string', required: false, @@ -47,12 +43,24 @@ export const listTool: ToolConfig = { visibility: 'user-or-llm', description: 'Maximum number of files to return (e.g., 10, 50, 100)', }, + pageToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + "Continuation URL from a previous response's nextPageToken, used to fetch the next page", + }, }, request: { url: (params) => { + const pageToken = params.pageToken?.trim() + if (pageToken) { + return assertGraphNextPageUrl(pageToken) + } + // Use specific folder if provided, otherwise use root - const folderId = params.manualFolderId || params.folderSelector + const folderId = params.folderId?.trim() const encodedFolderId = folderId ? encodeURIComponent(folderId) : '' const baseUrl = encodedFolderId ? `https://graph.microsoft.com/v1.0/me/drive/items/${encodedFolderId}/children` @@ -63,12 +71,15 @@ export const listTool: ToolConfig = { // Use Microsoft Graph $select parameter url.searchParams.append( '$select', - 'id,name,file,folder,webUrl,size,createdDateTime,lastModifiedDateTime,parentReference' + 'id,name,file,folder,webUrl,size,createdDateTime,lastModifiedDateTime,parentReference,@microsoft.graph.downloadUrl' ) // Add name filter if query provided if (params.query) { - url.searchParams.append('$filter', `startswith(name,'${params.query}')`) + url.searchParams.append( + '$filter', + `startswith(name,'${escapeODataStringLiteral(params.query)}')` + ) } // Add pagination @@ -102,7 +113,7 @@ export const listTool: ToolConfig = { parents: item.parentReference ? [item.parentReference.id] : [], })), // Use the actual @odata.nextLink URL as the continuation token - nextPageToken: data['@odata.nextLink'] || undefined, + nextPageToken: getGraphNextPageUrl(data), }, } }, diff --git a/apps/sim/tools/onedrive/move.ts b/apps/sim/tools/onedrive/move.ts new file mode 100644 index 00000000000..389cf18e15a --- /dev/null +++ b/apps/sim/tools/onedrive/move.ts @@ -0,0 +1,99 @@ +import { createLogger } from '@sim/logger' +import type { OneDriveMoveResponse, OneDriveToolParams } from '@/tools/onedrive/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('OneDriveMoveTool') + +export const moveTool: ToolConfig = { + id: 'onedrive_move', + name: 'Move or Rename OneDrive File', + description: 'Move a file or folder to a new parent folder, rename it, or both', + version: '1.0', + + oauth: { + required: true, + provider: 'onedrive', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the OneDrive API', + }, + fileId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the file or folder to move or rename', + }, + destinationFolderId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'The ID of the destination parent folder (omit to only rename in place)', + }, + newName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'The new name for the file or folder (omit to only move)', + }, + }, + + request: { + url: (params) => + `https://graph.microsoft.com/v1.0/me/drive/items/${encodeURIComponent(params.fileId || '')}`, + method: 'PATCH', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => { + if (!params.destinationFolderId && !params.newName) { + throw new Error('Provide a destination folder, a new name, or both') + } + + return { + ...(params.destinationFolderId && { + parentReference: { id: params.destinationFolderId }, + }), + ...(params.newName && { name: params.newName }), + } + }, + }, + + transformResponse: async (response: Response, params?: OneDriveToolParams) => { + const data = await response.json() + + logger.info('Successfully moved/renamed OneDrive item', { + fileId: params?.fileId, + newName: data.name, + }) + + return { + success: true, + output: { + file: { + id: data.id, + name: data.name, + mimeType: data.file?.mimeType || (data.folder ? 'application/folder' : 'unknown'), + webViewLink: data.webUrl, + size: data.size?.toString(), + createdTime: data.createdDateTime, + modifiedTime: data.lastModifiedDateTime, + parents: data.parentReference ? [data.parentReference.id] : [], + }, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the move or rename was successful' }, + file: { + type: 'object', + description: 'The updated file object with its new name and/or parent folder', + }, + }, +} diff --git a/apps/sim/tools/onedrive/search.ts b/apps/sim/tools/onedrive/search.ts new file mode 100644 index 00000000000..f16850acd03 --- /dev/null +++ b/apps/sim/tools/onedrive/search.ts @@ -0,0 +1,114 @@ +import type { + MicrosoftGraphDriveItem, + OneDriveSearchResponse, + OneDriveToolParams, +} from '@/tools/onedrive/types' +import { escapeODataStringLiteral } from '@/tools/onedrive/utils' +import { assertGraphNextPageUrl, getGraphNextPageUrl } from '@/tools/sharepoint/utils' +import type { ToolConfig } from '@/tools/types' + +export const searchTool: ToolConfig = { + id: 'onedrive_search', + name: 'Search OneDrive Files', + description: + 'Search for files and folders across OneDrive by name, metadata, or content (recursive)', + version: '1.0', + + oauth: { + required: true, + provider: 'onedrive', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the OneDrive API', + }, + query: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Search text matched against file name, metadata, and content. Not required when paginating with pageToken', + }, + pageSize: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of results to return (e.g., 10, 50, 100)', + }, + pageToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + "Continuation URL from a previous response's nextPageToken, used to fetch the next page", + }, + }, + + request: { + url: (params) => { + const pageToken = params.pageToken?.trim() + if (pageToken) { + return assertGraphNextPageUrl(pageToken) + } + + const query = params.query?.trim() + if (!query) { + throw new Error('A search query is required') + } + + const url = new URL( + `https://graph.microsoft.com/v1.0/me/drive/root/search(q='${encodeURIComponent(escapeODataStringLiteral(query))}')` + ) + url.searchParams.append( + '$select', + 'id,name,file,folder,webUrl,size,createdDateTime,lastModifiedDateTime,parentReference,@microsoft.graph.downloadUrl' + ) + if (params.pageSize) { + url.searchParams.append('$top', Number(params.pageSize).toString()) + } + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + files: (data.value || []).map((item: MicrosoftGraphDriveItem) => ({ + id: item.id, + name: item.name, + mimeType: item.file?.mimeType || (item.folder ? 'application/folder' : 'unknown'), + webViewLink: item.webUrl, + webContentLink: item['@microsoft.graph.downloadUrl'], + size: item.size?.toString() || '0', + createdTime: item.createdDateTime, + modifiedTime: item.lastModifiedDateTime, + parents: item.parentReference ? [item.parentReference.id] : [], + })), + nextPageToken: getGraphNextPageUrl(data), + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the search completed successfully' }, + files: { + type: 'array', + description: 'Array of file and folder objects matching the search query', + }, + nextPageToken: { + type: 'string', + description: 'Token for retrieving the next page of results (optional)', + }, + }, +} diff --git a/apps/sim/tools/onedrive/types.ts b/apps/sim/tools/onedrive/types.ts index 0f7a67a6212..dd3d3e4d464 100644 --- a/apps/sim/tools/onedrive/types.ts +++ b/apps/sim/tools/onedrive/types.ts @@ -85,10 +85,63 @@ export interface OneDriveDeleteResponse extends ToolResponse { } } +export interface OneDriveSearchResponse extends ToolResponse { + output: { + files: OneDriveFile[] + nextPageToken?: string + } +} + +export interface OneDriveMoveResponse extends ToolResponse { + output: { + file: OneDriveFile + } +} + +export interface OneDriveCopyResponse extends ToolResponse { + output: { + sourceFileId: string + name?: string + monitorUrl?: string + } +} + +export interface OneDriveShareLinkResponse extends ToolResponse { + output: { + link: { + type: string + scope?: string + webUrl: string + webHtml?: string + } + } +} + +export interface OneDriveGetItemResponse extends ToolResponse { + output: { + file: OneDriveFile + } +} + +export interface OneDriveGetDriveInfoResponse extends ToolResponse { + output: { + driveId: string + driveType: string + webUrl: string + owner: string | null + quota: { + total: number + used: number + remaining: number + deleted: number + state: string + } + } +} + export interface OneDriveToolParams { accessToken: string - folderSelector?: string - manualFolderId?: string + folderId?: string folderName?: string fileId?: string fileName?: string @@ -103,6 +156,14 @@ export interface OneDriveToolParams { values?: | (string | number | boolean | null)[][] | Array> + // Move/rename parameters + destinationFolderId?: string + newName?: string + // Copy parameters + destinationFileName?: string + // Sharing link parameters + linkType?: 'view' | 'edit' | 'embed' + linkScope?: 'anonymous' | 'organization' | 'users' } export type OneDriveResponse = @@ -110,3 +171,9 @@ export type OneDriveResponse = | OneDriveDownloadResponse | OneDriveListResponse | OneDriveDeleteResponse + | OneDriveSearchResponse + | OneDriveMoveResponse + | OneDriveCopyResponse + | OneDriveShareLinkResponse + | OneDriveGetItemResponse + | OneDriveGetDriveInfoResponse diff --git a/apps/sim/tools/onedrive/upload.ts b/apps/sim/tools/onedrive/upload.ts index 09090e2868e..9478fdca285 100644 --- a/apps/sim/tools/onedrive/upload.ts +++ b/apps/sim/tools/onedrive/upload.ts @@ -47,18 +47,12 @@ export const uploadTool: ToolConfig description: 'The MIME type of the file to create (e.g., text/plain for .txt, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet for .xlsx)', }, - folderSelector: { + folderId: { type: 'string', required: false, visibility: 'user-or-llm', description: 'Folder ID to upload the file to (e.g., "01BYE5RZ6QN3ZWBTUFOFD3GSPGOHDJD36M")', }, - manualFolderId: { - type: 'string', - required: false, - visibility: 'hidden', - description: 'Manually entered folder ID (advanced mode)', - }, }, request: { @@ -75,8 +69,8 @@ export const uploadTool: ToolConfig fileName = `${fileName.replace(/\.[^.]*$/, '')}.txt` } - const parentFolderId = params.manualFolderId || params.folderSelector - if (parentFolderId && parentFolderId.trim() !== '') { + const parentFolderId = params.folderId?.trim() + if (parentFolderId) { return `https://graph.microsoft.com/v1.0/me/drive/items/${encodeURIComponent(parentFolderId)}:/${fileName}:/content` } return `https://graph.microsoft.com/v1.0/me/drive/root:/${fileName}:/content` @@ -108,7 +102,7 @@ export const uploadTool: ToolConfig accessToken: params.accessToken, fileName: params.fileName, file: params.file, - folderId: params.manualFolderId || params.folderSelector, + folderId: params.folderId, ...(params.mimeType && { mimeType: params.mimeType }), ...(params.values && { values: params.values }), } diff --git a/apps/sim/tools/onedrive/utils.ts b/apps/sim/tools/onedrive/utils.ts index e1b595f944b..9d3760bef65 100644 --- a/apps/sim/tools/onedrive/utils.ts +++ b/apps/sim/tools/onedrive/utils.ts @@ -1,5 +1,14 @@ import type { OneDriveToolParams } from '@/tools/onedrive/types' +/** + * Escapes a value for embedding in an OData string literal (e.g. search(q='...'), + * $filter startswith(name,'...')). OData V4 escapes a literal single quote by doubling + * it — encodeURIComponent does not touch `'`, so this must run before URL-encoding. + */ +export function escapeODataStringLiteral(value: string): string { + return value.replace(/'/g, "''") +} + export type ExcelCell = string | number | boolean | null export type ExcelArrayValues = ExcelCell[][] export type ExcelObjectValues = Array> diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index bb6174de162..93790d0ff96 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -2481,10 +2481,16 @@ import { oktaUpdateUserTool, } from '@/tools/okta' import { + onedriveCopyTool, onedriveCreateFolderTool, + onedriveCreateShareLinkTool, onedriveDeleteTool, onedriveDownloadTool, + onedriveGetDriveInfoTool, + onedriveGetItemTool, onedriveListTool, + onedriveMoveTool, + onedriveSearchTool, onedriveUploadTool, } from '@/tools/onedrive' import { @@ -7552,10 +7558,16 @@ export const tools: Record = { shopify_create_fulfillment: shopifyCreateFulfillmentTool, shopify_list_collections: shopifyListCollectionsTool, shopify_get_collection: shopifyGetCollectionTool, + onedrive_copy: onedriveCopyTool, onedrive_create_folder: onedriveCreateFolderTool, + onedrive_create_share_link: onedriveCreateShareLinkTool, onedrive_delete: onedriveDeleteTool, onedrive_download: onedriveDownloadTool, + onedrive_get_drive_info: onedriveGetDriveInfoTool, + onedrive_get_item: onedriveGetItemTool, onedrive_list: onedriveListTool, + onedrive_move: onedriveMoveTool, + onedrive_search: onedriveSearchTool, onedrive_upload: onedriveUploadTool, microsoft_dataverse_associate: dataverseAssociateTool, microsoft_dataverse_create_multiple: dataverseCreateMultipleTool,