diff --git a/apps/docs/content/docs/en/tools/sharepoint.mdx b/apps/docs/content/docs/en/tools/sharepoint.mdx index 9b42322de..b32b84a32 100644 --- a/apps/docs/content/docs/en/tools/sharepoint.mdx +++ b/apps/docs/content/docs/en/tools/sharepoint.mdx @@ -1,6 +1,6 @@ --- title: Sharepoint -description: Read and create pages +description: Work with pages and lists --- import { BlockInfoCard } from "@/components/ui/block-info-card" @@ -61,7 +61,7 @@ In Sim, the SharePoint integration empowers your agents to create and access Sha ## Usage Instructions -Integrate Sharepoint into the workflow. Can read and create pages, and list sites. Requires OAuth. +Integrate SharePoint into the workflow. Read/create pages, list sites, and work with lists (read, create, update items). Requires OAuth. @@ -124,6 +124,65 @@ List details of all SharePoint sites | --------- | ---- | ----------- | | `site` | object | Information about the current SharePoint site | +### `sharepoint_create_list` + +Create a new list in a SharePoint site + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `siteId` | string | No | The ID of the SharePoint site \(internal use\) | +| `siteSelector` | string | No | Select the SharePoint site | +| `listDisplayName` | string | Yes | Display name of the list to create | +| `listDescription` | string | No | Description of the list | +| `listTemplate` | string | No | List template name \(e.g., 'genericList'\) | +| `pageContent` | string | No | Optional JSON of columns. Either a top-level array of column definitions or an object with \{ columns: \[...\] \}. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `list` | object | Created SharePoint list information | + +### `sharepoint_get_list` + +Get metadata (and optionally columns/items) for a SharePoint list + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `siteSelector` | string | No | Select the SharePoint site | +| `siteId` | string | No | The ID of the SharePoint site \(internal use\) | +| `listId` | string | No | The ID of the list to retrieve | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `list` | object | Information about the SharePoint list | + +### `sharepoint_update_list` + +Update the properties (fields) on a SharePoint list item + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `siteSelector` | string | No | Select the SharePoint site | +| `siteId` | string | No | The ID of the SharePoint site \(internal use\) | +| `listId` | string | No | The ID of the list containing the item | +| `itemId` | string | Yes | The ID of the list item to update | +| `listItemFields` | object | Yes | Field values to update on the list item | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `item` | object | Updated SharePoint list item | + ## Notes diff --git a/apps/sim/blocks/blocks/sharepoint.ts b/apps/sim/blocks/blocks/sharepoint.ts index 11212c9d7..176446bb6 100644 --- a/apps/sim/blocks/blocks/sharepoint.ts +++ b/apps/sim/blocks/blocks/sharepoint.ts @@ -1,13 +1,16 @@ import { MicrosoftSharepointIcon } from '@/components/icons' +import { createLogger } from '@/lib/logs/console/logger' import type { BlockConfig } from '@/blocks/types' import type { SharepointResponse } from '@/tools/sharepoint/types' +const logger = createLogger('SharepointBlock') + export const SharepointBlock: BlockConfig = { type: 'sharepoint', name: 'Sharepoint', - description: 'Read and create pages', + description: 'Work with pages and lists', longDescription: - 'Integrate Sharepoint into the workflow. Can read and create pages, and list sites. Requires OAuth.', + 'Integrate SharePoint into the workflow. Read/create pages, list sites, and work with lists (read, create, update items). Requires OAuth.', docsLink: 'https://docs.sim.ai/tools/sharepoint', category: 'tools', bgColor: '#E0E0E0', @@ -23,6 +26,9 @@ export const SharepointBlock: BlockConfig = { { label: 'Create Page', id: 'create_page' }, { label: 'Read Page', id: 'read_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' }, ], }, // Sharepoint Credentials @@ -39,6 +45,8 @@ export const SharepointBlock: BlockConfig = { 'email', 'Files.Read', 'Files.ReadWrite', + 'Sites.Read.All', + 'Sites.ReadWrite.All', 'offline_access', ], placeholder: 'Select Microsoft account', @@ -64,7 +72,17 @@ export const SharepointBlock: BlockConfig = { placeholder: 'Select a site', dependsOn: ['credential'], mode: 'basic', - condition: { field: 'operation', value: ['create_page', 'read_page', 'list_sites'] }, + condition: { + field: 'operation', + value: [ + 'create_page', + 'read_page', + 'list_sites', + 'create_list', + 'read_list', + 'update_list', + ], + }, }, { @@ -86,13 +104,59 @@ export const SharepointBlock: BlockConfig = { mode: 'advanced', }, + { + id: 'listId', + title: 'List ID', + type: 'short-input', + layout: 'full', + placeholder: 'Enter list ID (GUID). Required for Update; optional for Read.', + canonicalParamId: 'listId', + condition: { field: 'operation', value: ['read_list', 'update_list'] }, + }, + + { + id: 'listItemId', + title: 'Item ID', + type: 'short-input', + layout: 'full', + placeholder: 'Enter item ID', + canonicalParamId: 'itemId', + condition: { field: 'operation', value: ['update_list'] }, + }, + + { + id: 'listDisplayName', + title: 'List Display Name', + type: 'short-input', + layout: 'full', + placeholder: 'Name of the list', + condition: { field: 'operation', value: 'create_list' }, + }, + + { + id: 'listTemplate', + title: 'List Template', + type: 'short-input', + layout: 'full', + placeholder: "Template (e.g., 'genericList')", + condition: { field: 'operation', value: 'create_list' }, + }, + { id: 'pageContent', title: 'Page Content', type: 'long-input', layout: 'full', - placeholder: 'Content of the page', - condition: { field: 'operation', value: 'create_page' }, + placeholder: 'Provide page content', + condition: { field: 'operation', value: ['create_list'] }, + }, + { + id: 'listDescription', + title: 'List Description', + type: 'long-input', + layout: 'full', + placeholder: 'Optional description', + condition: { field: 'operation', value: 'create_list' }, }, { @@ -106,9 +170,26 @@ export const SharepointBlock: BlockConfig = { mode: 'advanced', condition: { field: 'operation', value: 'create_page' }, }, + + { + id: 'listItemFields', + title: 'List Item Fields', + type: 'long-input', + layout: 'full', + placeholder: 'Enter list item fields', + canonicalParamId: 'listItemFields', + condition: { field: 'operation', value: 'update_list' }, + }, ], tools: { - access: ['sharepoint_create_page', 'sharepoint_read_page', 'sharepoint_list_sites'], + access: [ + 'sharepoint_create_page', + 'sharepoint_read_page', + 'sharepoint_list_sites', + 'sharepoint_create_list', + 'sharepoint_get_list', + 'sharepoint_update_list', + ], config: { tool: (params) => { switch (params.operation) { @@ -118,6 +199,12 @@ export const SharepointBlock: BlockConfig = { return 'sharepoint_read_page' case 'list_sites': return 'sharepoint_list_sites' + case 'create_list': + return 'sharepoint_create_list' + case 'read_list': + return 'sharepoint_get_list' + case 'update_list': + return 'sharepoint_update_list' default: throw new Error(`Invalid Sharepoint operation: ${params.operation}`) } @@ -128,12 +215,71 @@ export const SharepointBlock: BlockConfig = { // Use siteSelector if provided, otherwise use manualSiteId const effectiveSiteId = (siteSelector || manualSiteId || '').trim() + const { + itemId: providedItemId, + listItemId, + listItemFields, + includeColumns, + includeItems, + ...others + } = rest as any + + let parsedItemFields: any = listItemFields + if (typeof listItemFields === 'string' && listItemFields.trim()) { + try { + parsedItemFields = JSON.parse(listItemFields) + } catch (error) { + logger.error('Failed to parse listItemFields JSON', { + error: error instanceof Error ? error.message : String(error), + }) + } + } + // Ensure listItemFields is an object for the tool schema + if (typeof parsedItemFields !== 'object' || parsedItemFields === null) { + parsedItemFields = undefined + } + + // Sanitize item ID (required by tool) + const rawItemId = providedItemId ?? listItemId + const sanitizedItemId = + rawItemId === undefined || rawItemId === null + ? undefined + : String(rawItemId).trim() || undefined + + const coerceBoolean = (value: any) => { + if (typeof value === 'boolean') return value + if (typeof value === 'string') return value.toLowerCase() === 'true' + return undefined + } + + // Debug logging for update_list param mapping + if (others.operation === 'update_list') { + try { + logger.info('SharepointBlock update_list param check', { + siteId: effectiveSiteId || undefined, + listId: (others as any)?.listId, + listTitle: (others as any)?.listTitle, + itemId: sanitizedItemId, + hasItemFields: !!parsedItemFields && typeof parsedItemFields === 'object', + itemFieldKeys: + parsedItemFields && typeof parsedItemFields === 'object' + ? Object.keys(parsedItemFields) + : [], + }) + } catch {} + } + return { credential, siteId: effectiveSiteId || undefined, - pageSize: rest.pageSize ? Number.parseInt(rest.pageSize as string, 10) : undefined, + pageSize: others.pageSize ? Number.parseInt(others.pageSize as string, 10) : undefined, mimeType: mimeType, - ...rest, + ...others, + // Map to tool param names + itemId: sanitizedItemId, + listItemFields: parsedItemFields, + includeColumns: coerceBoolean(includeColumns), + includeItems: coerceBoolean(includeItems), } }, }, @@ -151,6 +297,18 @@ export const SharepointBlock: BlockConfig = { siteSelector: { type: 'string', description: 'Site selector' }, manualSiteId: { type: 'string', description: 'Manual site ID' }, pageSize: { type: 'number', description: 'Results per page' }, + // Create List operation inputs + listDisplayName: { type: 'string', description: 'List display name' }, + listDescription: { type: 'string', description: 'List description' }, + listTemplate: { type: 'string', description: 'List template' }, + // Read List operation inputs + 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' }, + // Update List Item operation inputs + listItemId: { type: 'string', description: 'List item ID' }, + listItemFields: { type: 'string', description: 'List item fields' }, }, outputs: { sites: { @@ -158,5 +316,25 @@ export const SharepointBlock: BlockConfig = { description: 'An array of SharePoint site objects, each containing details such as id, name, and more.', }, + list: { + type: 'json', + description: 'SharePoint list object (id, displayName, name, webUrl, etc.)', + }, + item: { + type: 'json', + description: 'SharePoint list item with fields', + }, + items: { + type: 'json', + description: 'Array of SharePoint list items with fields', + }, + success: { + type: 'boolean', + description: 'Success status', + }, + error: { + type: 'string', + description: 'Error message', + }, }, } diff --git a/apps/sim/lib/auth.ts b/apps/sim/lib/auth.ts index 8ffdd3de4..e5183db57 100644 --- a/apps/sim/lib/auth.ts +++ b/apps/sim/lib/auth.ts @@ -565,6 +565,7 @@ export const auth = betterAuth({ 'email', 'Sites.Read.All', 'Sites.ReadWrite.All', + 'Sites.Manage.All', 'offline_access', ], responseType: 'code', diff --git a/apps/sim/lib/oauth/oauth.ts b/apps/sim/lib/oauth/oauth.ts index fe190e044..7774ba1d5 100644 --- a/apps/sim/lib/oauth/oauth.ts +++ b/apps/sim/lib/oauth/oauth.ts @@ -260,6 +260,7 @@ export const OAUTH_PROVIDERS: Record = { 'email', 'Sites.Read.All', 'Sites.ReadWrite.All', + 'Sites.Manage.All', 'offline_access', ], }, diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index bb8615f8e..a682c678c 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -145,9 +145,12 @@ import { redditGetCommentsTool, redditGetPostsTool, redditHotPostsTool } from '@ import { s3GetObjectTool } from '@/tools/s3' import { searchTool as serperSearch } from '@/tools/serper' import { + sharepointCreateListTool, sharepointCreatePageTool, + sharepointGetListTool, sharepointListSitesTool, sharepointReadPageTool, + sharepointUpdateListItemTool, } from '@/tools/sharepoint' import { slackCanvasTool, slackMessageReaderTool, slackMessageTool } from '@/tools/slack' import { smsSendTool } from '@/tools/sms' @@ -364,6 +367,9 @@ export const tools: Record = { sharepoint_create_page: sharepointCreatePageTool, sharepoint_read_page: sharepointReadPageTool, sharepoint_list_sites: sharepointListSitesTool, + sharepoint_get_list: sharepointGetListTool, + sharepoint_create_list: sharepointCreateListTool, + sharepoint_update_list: sharepointUpdateListItemTool, // Provider chat tools // Provider chat tools - handled separately in agent blocks } diff --git a/apps/sim/tools/sharepoint/create_list.ts b/apps/sim/tools/sharepoint/create_list.ts new file mode 100644 index 000000000..8f2546844 --- /dev/null +++ b/apps/sim/tools/sharepoint/create_list.ts @@ -0,0 +1,165 @@ +import { createLogger } from '@/lib/logs/console/logger' +import type { + SharepointCreateListResponse, + SharepointList, + SharepointToolParams, +} from '@/tools/sharepoint/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('SharePointCreateList') + +export const createListTool: ToolConfig = { + id: 'sharepoint_create_list', + name: 'Create SharePoint List', + description: 'Create a new list in a SharePoint site', + version: '1.0', + + oauth: { + required: true, + provider: 'sharepoint', + additionalScopes: ['openid', 'profile', 'email', 'Sites.ReadWrite.All', 'offline_access'], + }, + + 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', + }, + listDisplayName: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Display name of the list to create', + }, + listDescription: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Description of the list', + }, + listTemplate: { + type: 'string', + required: false, + visibility: 'user-only', + description: "List template name (e.g., 'genericList')", + }, + pageContent: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Optional JSON of columns. Either a top-level array of column definitions or an object with { columns: [...] }.', + }, + }, + + request: { + url: (params) => { + const siteId = params.siteSelector || params.siteId || 'root' + return `https://graph.microsoft.com/v1.0/sites/${siteId}/lists` + }, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + Accept: 'application/json', + }), + body: (params) => { + if (!params.listDisplayName) { + throw new Error('listDisplayName is required') + } + + // Derive columns from pageContent JSON (object or string) or top-level array + let columns: unknown[] | undefined + if (params.pageContent) { + if (typeof params.pageContent === 'string') { + try { + const parsed = JSON.parse(params.pageContent) + if (Array.isArray(parsed)) columns = parsed + else if (parsed && Array.isArray((parsed as any).columns)) + columns = (parsed as any).columns + } catch (error) { + logger.warn('Invalid JSON in pageContent for create list; ignoring', { + error: error instanceof Error ? error.message : String(error), + }) + } + } else if (typeof params.pageContent === 'object') { + const pc: any = params.pageContent + if (Array.isArray(pc)) columns = pc + else if (pc && Array.isArray(pc.columns)) columns = pc.columns + } + } + + const payload: any = { + displayName: params.listDisplayName, + description: params.listDescription, + list: { template: params.listTemplate || 'genericList' }, + } + if (columns && columns.length > 0) payload.columns = columns + + logger.info('Creating SharePoint list', { + displayName: payload.displayName, + template: payload.list.template, + hasDescription: !!payload.description, + }) + + return payload + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + 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, + } + + logger.info('SharePoint list created successfully', { + listId: list.id, + displayName: list.displayName, + }) + + return { + success: true, + output: { list }, + } + }, + + outputs: { + list: { + type: 'object', + description: 'Created SharePoint list information', + properties: { + id: { type: 'string', description: 'The unique ID of the list' }, + displayName: { type: 'string', description: 'The display name of the list' }, + name: { type: 'string', description: 'The internal name of the list' }, + webUrl: { type: 'string', description: 'The web URL of the list' }, + createdDateTime: { type: 'string', description: 'When the list was created' }, + lastModifiedDateTime: { + type: 'string', + description: 'When the list was last modified', + }, + list: { type: 'object', description: 'List properties (e.g., template)' }, + }, + }, + }, +} diff --git a/apps/sim/tools/sharepoint/get_list.ts b/apps/sim/tools/sharepoint/get_list.ts new file mode 100644 index 000000000..f8290d968 --- /dev/null +++ b/apps/sim/tools/sharepoint/get_list.ts @@ -0,0 +1,243 @@ +import { createLogger } from '@/lib/logs/console/logger' +import type { + SharepointGetListResponse, + SharepointList, + SharepointToolParams, +} from '@/tools/sharepoint/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('SharePointGetList') + +export const getListTool: ToolConfig = { + id: 'sharepoint_get_list', + name: 'Get SharePoint List', + description: 'Get metadata (and optionally columns/items) for a SharePoint list', + version: '1.0', + + oauth: { + required: true, + provider: 'sharepoint', + additionalScopes: ['openid', 'profile', 'email', 'Sites.Read.All', 'offline_access'], + }, + + 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: false, + visibility: 'user-only', + description: 'The ID of the list to retrieve', + }, + }, + + request: { + url: (params) => { + const siteId = params.siteId || params.siteSelector || 'root' + + // If neither listId nor listTitle provided, list all lists in the site + if (!params.listId) { + const baseUrl = `https://graph.microsoft.com/v1.0/sites/${siteId}/lists` + const url = new URL(baseUrl) + const finalUrl = url.toString() + logger.info('SharePoint List All Lists URL', { finalUrl, siteId }) + return finalUrl + } + + const listSegment = params.listId + // Default to returning items when targeting a specific list unless explicitly disabled + const wantsItems = typeof params.includeItems === 'boolean' ? params.includeItems : true + + // If caller wants items for a specific list, prefer the items endpoint (no columns) + if (wantsItems && !params.includeColumns) { + const itemsUrl = new URL( + `https://graph.microsoft.com/v1.0/sites/${siteId}/lists/${listSegment}/items` + ) + itemsUrl.searchParams.set('$expand', 'fields') + const finalItemsUrl = itemsUrl.toString() + logger.info('SharePoint Get List Items URL', { + finalUrl: finalItemsUrl, + siteId, + listId: params.listId, + }) + return finalItemsUrl + } + + // Otherwise, fetch list metadata (optionally with columns/items via $expand) + const baseUrl = `https://graph.microsoft.com/v1.0/sites/${siteId}/lists/${listSegment}` + const url = new URL(baseUrl) + const expandParts: string[] = [] + if (params.includeColumns) expandParts.push('columns') + if (wantsItems) expandParts.push('items($expand=fields)') + if (expandParts.length > 0) url.searchParams.append('$expand', expandParts.join(',')) + + const finalUrl = url.toString() + logger.info('SharePoint Get List URL', { + finalUrl, + siteId, + listId: params.listId, + includeColumns: !!params.includeColumns, + includeItems: wantsItems, + }) + return finalUrl + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + Accept: 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + // 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] + ) { + const items = (data as any).value.map((i: any) => ({ + id: i.id, + fields: i.fields as Record, + })) + + const nextLink: string | undefined = (data as any)['@odata.nextLink'] + const nextPageToken = nextLink + ? (() => { + try { + const u = new URL(nextLink) + return u.searchParams.get('$skiptoken') || u.searchParams.get('$skip') || undefined + } catch { + return undefined + } + })() + : undefined + + return { + success: true, + output: { list: { items } as SharepointList, nextPageToken }, + } + } + + // If this is a collection of lists (site-level) + 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, + })) + + const nextLink: string | undefined = (data as any)['@odata.nextLink'] + const nextPageToken = nextLink + ? (() => { + try { + const u = new URL(nextLink) + return u.searchParams.get('$skiptoken') || u.searchParams.get('$skip') || undefined + } catch { + return undefined + } + })() + : undefined + + return { + success: true, + output: { lists, nextPageToken }, + } + } + + // Single list response (with optional expands) + 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, + 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, + })) + : undefined, + items: Array.isArray(data.items) + ? data.items.map((i: any) => ({ id: i.id, fields: i.fields as Record })) + : undefined, + } + + return { + success: true, + output: { list }, + } + }, + + outputs: { + list: { + type: 'object', + description: 'Information about the SharePoint list', + properties: { + id: { type: 'string', description: 'The unique ID of the list' }, + displayName: { type: 'string', description: 'The display name of the list' }, + name: { type: 'string', description: 'The internal name of the list' }, + webUrl: { type: 'string', description: 'The web URL of the list' }, + createdDateTime: { type: 'string', description: 'When the list was created' }, + lastModifiedDateTime: { + type: 'string', + description: 'When the list was last modified', + }, + list: { type: 'object', description: 'List properties (e.g., template)' }, + columns: { + type: 'array', + description: 'List column definitions', + items: { type: 'object' }, + }, + items: { + type: 'array', + description: 'List items (with fields when expanded)', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Item ID' }, + fields: { type: 'object', description: 'Field values for the item' }, + }, + }, + }, + }, + }, + lists: { + type: 'array', + description: 'All lists in the site when no listId/title provided', + items: { type: 'object' }, + }, + }, +} diff --git a/apps/sim/tools/sharepoint/index.ts b/apps/sim/tools/sharepoint/index.ts index 702d29aec..fbfcd80a3 100644 --- a/apps/sim/tools/sharepoint/index.ts +++ b/apps/sim/tools/sharepoint/index.ts @@ -1,7 +1,13 @@ +import { createListTool } from '@/tools/sharepoint/create_list' import { createPageTool } from '@/tools/sharepoint/create_page' +import { getListTool } from '@/tools/sharepoint/get_list' import { listSitesTool } from '@/tools/sharepoint/list_sites' import { readPageTool } from '@/tools/sharepoint/read_page' +import { updateListItemTool } from '@/tools/sharepoint/update_list' export const sharepointCreatePageTool = createPageTool +export const sharepointCreateListTool = createListTool +export const sharepointGetListTool = getListTool export const sharepointListSitesTool = listSitesTool export const sharepointReadPageTool = readPageTool +export const sharepointUpdateListItemTool = updateListItemTool diff --git a/apps/sim/tools/sharepoint/types.ts b/apps/sim/tools/sharepoint/types.ts index 6ecddf4ff..4ce4847c0 100644 --- a/apps/sim/tools/sharepoint/types.ts +++ b/apps/sim/tools/sharepoint/types.ts @@ -58,6 +58,39 @@ export interface SharepointPageContent { } | null } +export interface SharepointColumn { + id?: string + name?: string + displayName?: string + description?: string + indexed?: boolean + enforcedUniqueValues?: boolean + hidden?: boolean + readOnly?: boolean + required?: boolean + columnGroup?: string + [key: string]: unknown +} + +export interface SharepointListItem { + id: string + fields?: Record +} + +export interface SharepointList { + id: string + displayName?: string + name?: string + webUrl?: string + createdDateTime?: string + lastModifiedDateTime?: string + list?: { + template?: string + } + columns?: SharepointColumn[] + items?: SharepointListItem[] +} + export interface SharepointListSitesResponse extends ToolResponse { output: { sites: SharepointSite[] @@ -131,6 +164,18 @@ export interface SharepointToolParams { serverRelativePath?: string groupId?: string maxPages?: number + // Lists + listId?: string + listTitle?: string + includeColumns?: boolean + includeItems?: boolean + // Create List + listDisplayName?: string + listDescription?: string + listTemplate?: string + // Update List Item + itemId?: string + listItemFields?: Record } export interface GraphApiResponse { @@ -211,3 +256,29 @@ export type SharepointResponse = | SharepointCreatePageResponse | SharepointReadPageResponse | SharepointReadSiteResponse + | SharepointGetListResponse + | SharepointCreateListResponse + | SharepointUpdateListItemResponse + +export interface SharepointGetListResponse extends ToolResponse { + output: { + list?: SharepointList + lists?: SharepointList[] + nextPageToken?: string + } +} + +export interface SharepointCreateListResponse extends ToolResponse { + output: { + list: SharepointList + } +} + +export interface SharepointUpdateListItemResponse extends ToolResponse { + output: { + item: { + id: string + fields?: Record + } + } +} diff --git a/apps/sim/tools/sharepoint/update_list.ts b/apps/sim/tools/sharepoint/update_list.ts new file mode 100644 index 000000000..c9b66513a --- /dev/null +++ b/apps/sim/tools/sharepoint/update_list.ts @@ -0,0 +1,169 @@ +import { createLogger } from '@/lib/logs/console/logger' +import type { + SharepointToolParams, + SharepointUpdateListItemResponse, +} from '@/tools/sharepoint/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('SharePointUpdateListItem') + +export const updateListItemTool: ToolConfig< + SharepointToolParams, + SharepointUpdateListItemResponse +> = { + id: 'sharepoint_update_list', + name: 'Update SharePoint List Item', + description: 'Update the properties (fields) on a SharePoint list item', + version: '1.0', + + oauth: { + required: true, + provider: 'sharepoint', + additionalScopes: ['openid', 'profile', 'email', 'Sites.ReadWrite.All', 'offline_access'], + }, + + 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: false, + visibility: 'user-only', + description: 'The ID of the list containing the item', + }, + itemId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ID of the list item to update', + }, + listItemFields: { + type: 'object', + required: true, + visibility: 'user-only', + description: 'Field values to update on the list item', + }, + }, + + request: { + url: (params) => { + const siteId = params.siteId || params.siteSelector || 'root' + if (!params.itemId) throw new Error('itemId is required') + if (!params.listId) { + throw new Error('listId must be provided') + } + const listSegment = params.listId + return `https://graph.microsoft.com/v1.0/sites/${siteId}/lists/${listSegment}/items/${params.itemId}/fields` + }, + method: 'PATCH', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + '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') + } + + // Filter out system/read-only fields that cannot be updated via Graph + const readOnlyFields = new Set([ + '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 + }, + }, + + transformResponse: async (response: Response, params) => { + let fields: Record | undefined + if (response.status !== 204) { + try { + fields = await response.json() + } catch { + // Fall back to submitted fields if no body is returned + fields = params?.listItemFields + } + } else { + fields = params?.listItemFields + } + + return { + success: true, + output: { + item: { + id: params?.itemId!, + fields, + }, + }, + } + }, + + outputs: { + item: { + type: 'object', + description: 'Updated SharePoint list item', + properties: { + id: { type: 'string', description: 'Item ID' }, + fields: { type: 'object', description: 'Updated field values' }, + }, + }, + }, +}