Feature/Add teams, gmail, outlook tools (#4577)

* add teams, gmail, outlook tools

* update docs link

* update credentials for oauth2

* add jira tool

* add google drive, google calendar, google sheets tools, powerpoint, excel, word doc loader

* update jira logo

* Refactor Gmail and Outlook tools to remove maxOutputLength parameter and enhance request handling. Update response formatting to include parameters in the output. Adjust Google Drive tools to simplify success messages by removing unnecessary parameter details.
This commit is contained in:
Henry Heng
2025-06-06 19:52:04 +01:00
committed by GitHub
parent 6dcb65cedb
commit 30c4180d97
62 changed files with 16832 additions and 144 deletions
@@ -0,0 +1,63 @@
import { INodeParams, INodeCredential } from '../src/Interface'
const scopes = [
'https://www.googleapis.com/auth/gmail.readonly',
'https://www.googleapis.com/auth/gmail.compose',
'https://www.googleapis.com/auth/gmail.modify',
'https://www.googleapis.com/auth/gmail.labels'
]
class GmailOAuth2 implements INodeCredential {
label: string
name: string
version: number
inputs: INodeParams[]
description: string
constructor() {
this.label = 'Gmail OAuth2'
this.name = 'gmailOAuth2'
this.version = 1.0
this.description =
'You can find the setup instructions <a target="_blank" href="https://docs.flowiseai.com/integrations/langchain/tools/gmail">here</a>'
this.inputs = [
{
label: 'Authorization URL',
name: 'authorizationUrl',
type: 'string',
default: 'https://accounts.google.com/o/oauth2/v2/auth'
},
{
label: 'Access Token URL',
name: 'accessTokenUrl',
type: 'string',
default: 'https://oauth2.googleapis.com/token'
},
{
label: 'Client ID',
name: 'clientId',
type: 'string'
},
{
label: 'Client Secret',
name: 'clientSecret',
type: 'password'
},
{
label: 'Additional Parameters',
name: 'additionalParameters',
type: 'string',
default: 'access_type=offline&prompt=consent',
hidden: true
},
{
label: 'Scope',
name: 'scope',
type: 'string',
hidden: true,
default: scopes.join(' ')
}
]
}
}
module.exports = { credClass: GmailOAuth2 }
@@ -0,0 +1,58 @@
import { INodeParams, INodeCredential } from '../src/Interface'
const scopes = ['https://www.googleapis.com/auth/calendar', 'https://www.googleapis.com/auth/calendar.events']
class GoogleCalendarOAuth2 implements INodeCredential {
label: string
name: string
version: number
inputs: INodeParams[]
description: string
constructor() {
this.label = 'Google Calendar OAuth2'
this.name = 'googleCalendarOAuth2'
this.version = 1.0
this.description =
'You can find the setup instructions <a target="_blank" href="https://docs.flowiseai.com/integrations/langchain/tools/google-calendar">here</a>'
this.inputs = [
{
label: 'Authorization URL',
name: 'authorizationUrl',
type: 'string',
default: 'https://accounts.google.com/o/oauth2/v2/auth'
},
{
label: 'Access Token URL',
name: 'accessTokenUrl',
type: 'string',
default: 'https://oauth2.googleapis.com/token'
},
{
label: 'Client ID',
name: 'clientId',
type: 'string'
},
{
label: 'Client Secret',
name: 'clientSecret',
type: 'password'
},
{
label: 'Additional Parameters',
name: 'additionalParameters',
type: 'string',
default: 'access_type=offline&prompt=consent',
hidden: true
},
{
label: 'Scope',
name: 'scope',
type: 'string',
hidden: true,
default: scopes.join(' ')
}
]
}
}
module.exports = { credClass: GoogleCalendarOAuth2 }
@@ -0,0 +1,62 @@
import { INodeParams, INodeCredential } from '../src/Interface'
const scopes = [
'https://www.googleapis.com/auth/drive',
'https://www.googleapis.com/auth/drive.appdata',
'https://www.googleapis.com/auth/drive.photos.readonly'
]
class GoogleDriveOAuth2 implements INodeCredential {
label: string
name: string
version: number
inputs: INodeParams[]
description: string
constructor() {
this.label = 'Google Drive OAuth2'
this.name = 'googleDriveOAuth2'
this.version = 1.0
this.description =
'You can find the setup instructions <a target="_blank" href="https://docs.flowiseai.com/integrations/langchain/tools/google-drive">here</a>'
this.inputs = [
{
label: 'Authorization URL',
name: 'authorizationUrl',
type: 'string',
default: 'https://accounts.google.com/o/oauth2/v2/auth'
},
{
label: 'Access Token URL',
name: 'accessTokenUrl',
type: 'string',
default: 'https://oauth2.googleapis.com/token'
},
{
label: 'Client ID',
name: 'clientId',
type: 'string'
},
{
label: 'Client Secret',
name: 'clientSecret',
type: 'password'
},
{
label: 'Additional Parameters',
name: 'additionalParameters',
type: 'string',
default: 'access_type=offline&prompt=consent',
hidden: true
},
{
label: 'Scope',
name: 'scope',
type: 'string',
hidden: true,
default: scopes.join(' ')
}
]
}
}
module.exports = { credClass: GoogleDriveOAuth2 }
@@ -0,0 +1,62 @@
import { INodeParams, INodeCredential } from '../src/Interface'
const scopes = [
'https://www.googleapis.com/auth/drive.file',
'https://www.googleapis.com/auth/spreadsheets',
'https://www.googleapis.com/auth/drive.metadata'
]
class GoogleSheetsOAuth2 implements INodeCredential {
label: string
name: string
version: number
inputs: INodeParams[]
description: string
constructor() {
this.label = 'Google Sheets OAuth2'
this.name = 'googleSheetsOAuth2'
this.version = 1.0
this.description =
'You can find the setup instructions <a target="_blank" href="https://docs.flowiseai.com/integrations/langchain/tools/google-sheets">here</a>'
this.inputs = [
{
label: 'Authorization URL',
name: 'authorizationUrl',
type: 'string',
default: 'https://accounts.google.com/o/oauth2/v2/auth'
},
{
label: 'Access Token URL',
name: 'accessTokenUrl',
type: 'string',
default: 'https://oauth2.googleapis.com/token'
},
{
label: 'Client ID',
name: 'clientId',
type: 'string'
},
{
label: 'Client Secret',
name: 'clientSecret',
type: 'password'
},
{
label: 'Additional Parameters',
name: 'additionalParameters',
type: 'string',
default: 'access_type=offline&prompt=consent',
hidden: true
},
{
label: 'Scope',
name: 'scope',
type: 'string',
hidden: true,
default: scopes.join(' ')
}
]
}
}
module.exports = { credClass: GoogleSheetsOAuth2 }
@@ -0,0 +1,66 @@
import { INodeParams, INodeCredential } from '../src/Interface'
const scopes = [
'openid',
'offline_access',
'Contacts.Read',
'Contacts.ReadWrite',
'Calendars.Read',
'Calendars.Read.Shared',
'Calendars.ReadWrite',
'Mail.Read',
'Mail.ReadWrite',
'Mail.ReadWrite.Shared',
'Mail.Send',
'Mail.Send.Shared',
'MailboxSettings.Read'
]
class MsoftOutlookOAuth2 implements INodeCredential {
label: string
name: string
version: number
description: string
inputs: INodeParams[]
constructor() {
this.label = 'Microsoft Outlook OAuth2'
this.name = 'microsoftOutlookOAuth2'
this.version = 1.0
this.description =
'You can find the setup instructions <a target="_blank" href="https://docs.flowiseai.com/integrations/langchain/tools/microsoft-outlook">here</a>'
this.inputs = [
{
label: 'Authorization URL',
name: 'authorizationUrl',
type: 'string',
default: 'https://login.microsoftonline.com/<tenantId>/oauth2/v2.0/authorize'
},
{
label: 'Access Token URL',
name: 'accessTokenUrl',
type: 'string',
default: 'https://login.microsoftonline.com/<tenantId>/oauth2/v2.0/token'
},
{
label: 'Client ID',
name: 'clientId',
type: 'string'
},
{
label: 'Client Secret',
name: 'clientSecret',
type: 'password'
},
{
label: 'Scope',
name: 'scope',
type: 'string',
hidden: true,
default: scopes.join(' ')
}
]
}
}
module.exports = { credClass: MsoftOutlookOAuth2 }
@@ -0,0 +1,87 @@
import { INodeParams, INodeCredential } from '../src/Interface'
// Comprehensive scopes for Microsoft Teams operations
const scopes = [
// Basic authentication
'openid',
'offline_access',
// User permissions
'User.Read',
'User.ReadWrite.All',
// Teams and Groups
'Group.ReadWrite.All',
'Team.ReadBasic.All',
'Team.Create',
'TeamMember.ReadWrite.All',
// Channels
'Channel.ReadBasic.All',
'Channel.Create',
'Channel.Delete.All',
'ChannelMember.ReadWrite.All',
// Chat operations
'Chat.ReadWrite',
'Chat.Create',
'ChatMember.ReadWrite',
// Messages
'ChatMessage.Send',
'ChatMessage.Read',
'ChannelMessage.Send',
'ChannelMessage.Read.All',
// Reactions and advanced features
'TeamsActivity.Send'
]
class MsoftTeamsOAuth2 implements INodeCredential {
label: string
name: string
version: number
inputs: INodeParams[]
description: string
constructor() {
this.label = 'Microsoft Teams OAuth2'
this.name = 'microsoftTeamsOAuth2'
this.version = 1.0
this.description =
'You can find the setup instructions <a target="_blank" href="https://docs.flowiseai.com/integrations/langchain/tools/microsoft-teams">here</a>'
this.inputs = [
{
label: 'Authorization URL',
name: 'authorizationUrl',
type: 'string',
default: 'https://login.microsoftonline.com/<tenantId>/oauth2/v2.0/authorize'
},
{
label: 'Access Token URL',
name: 'accessTokenUrl',
type: 'string',
default: 'https://login.microsoftonline.com/<tenantId>/oauth2/v2.0/token'
},
{
label: 'Client ID',
name: 'clientId',
type: 'string'
},
{
label: 'Client Secret',
name: 'clientSecret',
type: 'password'
},
{
label: 'Scope',
name: 'scope',
type: 'string',
hidden: true,
default: scopes.join(' ')
}
]
}
}
module.exports = { credClass: MsoftTeamsOAuth2 }
@@ -15,7 +15,7 @@ import { AnalyticHandler } from '../../../src/handler'
import { DEFAULT_SUMMARIZER_TEMPLATE } from '../prompt'
import { ILLMMessage } from '../Interface.Agentflow'
import { Tool } from '@langchain/core/tools'
import { ARTIFACTS_PREFIX, SOURCE_DOCUMENTS_PREFIX } from '../../../src/agents'
import { ARTIFACTS_PREFIX, SOURCE_DOCUMENTS_PREFIX, TOOL_ARGS_PREFIX } from '../../../src/agents'
import { flatten } from 'lodash'
import zodToJsonSchema from 'zod-to-json-schema'
import { getErrorMessage } from '../../../src/error'
@@ -1429,6 +1429,17 @@ class Agent_Agentflow implements INode {
}
}
let toolInput
if (typeof toolOutput === 'string' && toolOutput.includes(TOOL_ARGS_PREFIX)) {
const [output, args] = toolOutput.split(TOOL_ARGS_PREFIX)
toolOutput = output
try {
toolInput = JSON.parse(args)
} catch (e) {
console.error('Error parsing tool input from tool:', e)
}
}
// Add tool message to conversation
messages.push({
role: 'tool',
@@ -1444,7 +1455,7 @@ class Agent_Agentflow implements INode {
// Track used tools
usedTools.push({
tool: toolCall.name,
toolInput: toolCall.args,
toolInput: toolInput ?? toolCall.args,
toolOutput
})
} catch (e) {
@@ -1667,6 +1678,17 @@ class Agent_Agentflow implements INode {
}
}
let toolInput
if (typeof toolOutput === 'string' && toolOutput.includes(TOOL_ARGS_PREFIX)) {
const [output, args] = toolOutput.split(TOOL_ARGS_PREFIX)
toolOutput = output
try {
toolInput = JSON.parse(args)
} catch (e) {
console.error('Error parsing tool input from tool:', e)
}
}
// Add tool message to conversation
messages.push({
role: 'tool',
@@ -1682,7 +1704,7 @@ class Agent_Agentflow implements INode {
// Track used tools
usedTools.push({
tool: toolCall.name,
toolInput: toolCall.args,
toolInput: toolInput ?? toolCall.args,
toolOutput
})
} catch (e) {
@@ -1,7 +1,7 @@
import { ICommonObject, INode, INodeData, INodeOptionsValue, INodeParams, IServerSideEventStreamer } from '../../../src/Interface'
import { updateFlowState } from '../utils'
import { Tool } from '@langchain/core/tools'
import { ARTIFACTS_PREFIX } from '../../../src/agents'
import { ARTIFACTS_PREFIX, TOOL_ARGS_PREFIX } from '../../../src/agents'
import zodToJsonSchema from 'zod-to-json-schema'
interface IToolInputArgs {
@@ -268,6 +268,17 @@ class Tool_Agentflow implements INode {
}
}
let toolInput
if (typeof toolOutput === 'string' && toolOutput.includes(TOOL_ARGS_PREFIX)) {
const [output, args] = toolOutput.split(TOOL_ARGS_PREFIX)
toolOutput = output
try {
toolInput = JSON.parse(args)
} catch (e) {
console.error('Error parsing tool input from tool:', e)
}
}
if (typeof toolOutput === 'object') {
toolOutput = JSON.stringify(toolOutput, null, 2)
}
@@ -290,7 +301,7 @@ class Tool_Agentflow implements INode {
id: nodeData.id,
name: this.name,
input: {
toolInputArgs: toolInputArgs,
toolInputArgs: toolInput ?? toolInputArgs,
selectedTool: selectedTool
},
output: {
@@ -7,6 +7,8 @@ import { CSVLoader } from '@langchain/community/document_loaders/fs/csv'
import { PDFLoader } from '@langchain/community/document_loaders/fs/pdf'
import { DocxLoader } from '@langchain/community/document_loaders/fs/docx'
import { BaseDocumentLoader } from 'langchain/document_loaders/base'
import { LoadOfSheet } from '../MicrosoftExcel/ExcelLoader'
import { PowerpointLoader } from '../MicrosoftPowerpoint/PowerpointLoader'
import { Document } from '@langchain/core/documents'
import { getFileFromStorage } from '../../../src/storageUtils'
import { handleEscapeCharacters, mapMimeTypeToExt } from '../../../src/utils'
@@ -213,10 +215,14 @@ class File_DocumentLoaders implements INode {
jsonl: (blob) => new JSONLinesLoader(blob, '/' + pointerName.trim()),
txt: (blob) => new TextLoader(blob),
csv: (blob) => new CSVLoader(blob),
xls: (blob) => new CSVLoader(blob),
xlsx: (blob) => new CSVLoader(blob),
xls: (blob) => new LoadOfSheet(blob),
xlsx: (blob) => new LoadOfSheet(blob),
xlsm: (blob) => new LoadOfSheet(blob),
xlsb: (blob) => new LoadOfSheet(blob),
docx: (blob) => new DocxLoader(blob),
doc: (blob) => new DocxLoader(blob),
ppt: (blob) => new PowerpointLoader(blob),
pptx: (blob) => new PowerpointLoader(blob),
pdf: (blob) =>
pdfUsage === 'perFile'
? // @ts-ignore
@@ -7,6 +7,8 @@ import { JSONLinesLoader, JSONLoader } from 'langchain/document_loaders/fs/json'
import { CSVLoader } from '@langchain/community/document_loaders/fs/csv'
import { PDFLoader } from '@langchain/community/document_loaders/fs/pdf'
import { DocxLoader } from '@langchain/community/document_loaders/fs/docx'
import { LoadOfSheet } from '../MicrosoftExcel/ExcelLoader'
import { PowerpointLoader } from '../MicrosoftPowerpoint/PowerpointLoader'
import { handleEscapeCharacters } from '../../../src/utils'
class Folder_DocumentLoaders implements INode {
@@ -135,10 +137,14 @@ class Folder_DocumentLoaders implements INode {
'.jsonl': (blob) => new JSONLinesLoader(blob, '/' + pointerName.trim()),
'.txt': (path) => new TextLoader(path),
'.csv': (path) => new CSVLoader(path),
'.xls': (path) => new CSVLoader(path),
'.xlsx': (path) => new CSVLoader(path),
'.xls': (path) => new LoadOfSheet(path),
'.xlsx': (path) => new LoadOfSheet(path),
'.xlsm': (path) => new LoadOfSheet(path),
'.xlsb': (path) => new LoadOfSheet(path),
'.doc': (path) => new DocxLoader(path),
'.docx': (path) => new DocxLoader(path),
'.ppt': (path) => new PowerpointLoader(path),
'.pptx': (path) => new PowerpointLoader(path),
'.pdf': (path) =>
pdfUsage === 'perFile'
? // @ts-ignore
@@ -0,0 +1,828 @@
import { omit } from 'lodash'
import { ICommonObject, IDocument, INode, INodeData, INodeParams, INodeOptionsValue } from '../../../src/Interface'
import { TextSplitter } from 'langchain/text_splitter'
import {
convertMultiOptionsToStringArray,
getCredentialData,
getCredentialParam,
handleEscapeCharacters,
INodeOutputsValue,
refreshOAuth2Token
} from '../../../src'
import { PDFLoader } from '@langchain/community/document_loaders/fs/pdf'
import { DocxLoader } from '@langchain/community/document_loaders/fs/docx'
import { CSVLoader } from '@langchain/community/document_loaders/fs/csv'
import * as fs from 'fs'
import * as path from 'path'
import * as os from 'os'
import { LoadOfSheet } from '../MicrosoftExcel/ExcelLoader'
import { PowerpointLoader } from '../MicrosoftPowerpoint/PowerpointLoader'
// Helper function to get human-readable MIME type labels
const getMimeTypeLabel = (mimeType: string): string | undefined => {
const mimeTypeLabels: { [key: string]: string } = {
'application/vnd.google-apps.document': 'Google Doc',
'application/vnd.google-apps.spreadsheet': 'Google Sheet',
'application/vnd.google-apps.presentation': 'Google Slides',
'application/pdf': 'PDF',
'text/plain': 'Text File',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'Word Doc',
'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'PowerPoint',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'Excel File'
}
return mimeTypeLabels[mimeType] || undefined
}
class GoogleDrive_DocumentLoaders implements INode {
label: string
name: string
version: number
description: string
type: string
icon: string
category: string
baseClasses: string[]
credential: INodeParams
inputs: INodeParams[]
outputs: INodeOutputsValue[]
constructor() {
this.label = 'Google Drive'
this.name = 'googleDrive'
this.version = 1.0
this.type = 'Document'
this.icon = 'google-drive.svg'
this.category = 'Document Loaders'
this.description = `Load documents from Google Drive files`
this.baseClasses = [this.type]
this.credential = {
label: 'Connect Credential',
name: 'credential',
type: 'credential',
description: 'Google Drive OAuth2 Credential',
credentialNames: ['googleDriveOAuth2']
}
this.inputs = [
{
label: 'Select Files',
name: 'selectedFiles',
type: 'asyncMultiOptions',
loadMethod: 'listFiles',
description: 'Select files from your Google Drive',
refresh: true
},
{
label: 'Folder ID',
name: 'folderId',
type: 'string',
description: 'Google Drive folder ID to load all files from (alternative to selecting specific files)',
placeholder: '1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms',
optional: true
},
{
label: 'File Types',
name: 'fileTypes',
type: 'multiOptions',
description: 'Types of files to load',
options: [
{
label: 'Google Docs',
name: 'application/vnd.google-apps.document'
},
{
label: 'Google Sheets',
name: 'application/vnd.google-apps.spreadsheet'
},
{
label: 'Google Slides',
name: 'application/vnd.google-apps.presentation'
},
{
label: 'PDF Files',
name: 'application/pdf'
},
{
label: 'Text Files',
name: 'text/plain'
},
{
label: 'Word Documents',
name: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
},
{
label: 'PowerPoint',
name: 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
},
{
label: 'Excel Files',
name: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
}
],
default: [
'application/vnd.google-apps.document',
'application/vnd.google-apps.spreadsheet',
'application/vnd.google-apps.presentation',
'text/plain',
'application/pdf',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
],
optional: true
},
{
label: 'Include Subfolders',
name: 'includeSubfolders',
type: 'boolean',
description: 'Whether to include files from subfolders when loading from a folder',
default: false,
optional: true
},
{
label: 'Include Shared Drives',
name: 'includeSharedDrives',
type: 'boolean',
description: 'Whether to include files from shared drives (Team Drives) that you have access to',
default: false,
optional: true
},
{
label: 'Max Files',
name: 'maxFiles',
type: 'number',
description: 'Maximum number of files to load (default: 50)',
default: 50,
optional: true
},
{
label: 'Text Splitter',
name: 'textSplitter',
type: 'TextSplitter',
optional: true
},
{
label: 'Additional Metadata',
name: 'metadata',
type: 'json',
description: 'Additional metadata to be added to the extracted documents',
optional: true,
additionalParams: true
},
{
label: 'Omit Metadata Keys',
name: 'omitMetadataKeys',
type: 'string',
rows: 4,
description:
'Each document loader comes with a default set of metadata keys that are extracted from the document. You can use this field to omit some of the default metadata keys. The value should be a list of keys, seperated by comma. Use * to omit all metadata keys execept the ones you specify in the Additional Metadata field',
placeholder: 'key1, key2, key3.nestedKey1',
optional: true,
additionalParams: true
}
]
this.outputs = [
{
label: 'Document',
name: 'document',
description: 'Array of document objects containing metadata and pageContent',
baseClasses: [...this.baseClasses, 'json']
},
{
label: 'Text',
name: 'text',
description: 'Concatenated string from pageContent of documents',
baseClasses: ['string', 'json']
}
]
}
//@ts-ignore
loadMethods = {
async listFiles(nodeData: INodeData, options: ICommonObject): Promise<INodeOptionsValue[]> {
const returnData: INodeOptionsValue[] = []
try {
let credentialData = await getCredentialData(nodeData.credential ?? '', options)
credentialData = await refreshOAuth2Token(nodeData.credential ?? '', credentialData, options)
const accessToken = getCredentialParam('access_token', credentialData, nodeData)
if (!accessToken) {
return returnData
}
// Get file types from input to filter
const fileTypes = convertMultiOptionsToStringArray(nodeData.inputs?.fileTypes)
const includeSharedDrives = nodeData.inputs?.includeSharedDrives as boolean
const maxFiles = (nodeData.inputs?.maxFiles as number) || 100
let query = 'trashed = false'
// Add file type filter if specified
if (fileTypes && fileTypes.length > 0) {
const mimeTypeQuery = fileTypes.map((type) => `mimeType='${type}'`).join(' or ')
query += ` and (${mimeTypeQuery})`
}
const url = new URL('https://www.googleapis.com/drive/v3/files')
url.searchParams.append('q', query)
url.searchParams.append('pageSize', Math.min(maxFiles, 1000).toString())
url.searchParams.append('fields', 'files(id, name, mimeType, size, createdTime, modifiedTime, webViewLink, driveId)')
url.searchParams.append('orderBy', 'modifiedTime desc')
// Add shared drives support if requested
if (includeSharedDrives) {
url.searchParams.append('supportsAllDrives', 'true')
url.searchParams.append('includeItemsFromAllDrives', 'true')
}
const response = await fetch(url.toString(), {
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json'
}
})
if (!response.ok) {
console.error(`Failed to list files: ${response.statusText}`)
return returnData
}
const data = await response.json()
for (const file of data.files) {
const mimeTypeLabel = getMimeTypeLabel(file.mimeType)
if (!mimeTypeLabel) {
continue
}
// Add drive context to description
const driveContext = file.driveId ? ' (Shared Drive)' : ' (My Drive)'
const obj: INodeOptionsValue = {
name: file.id,
label: file.name,
description: `Type: ${mimeTypeLabel}${driveContext} | Modified: ${new Date(file.modifiedTime).toLocaleDateString()}`
}
returnData.push(obj)
}
} catch (error) {
console.error('Error listing Google Drive files:', error)
}
return returnData
}
}
async init(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
const selectedFiles = nodeData.inputs?.selectedFiles as string
const folderId = nodeData.inputs?.folderId as string
const fileTypes = nodeData.inputs?.fileTypes as string[]
const includeSubfolders = nodeData.inputs?.includeSubfolders as boolean
const includeSharedDrives = nodeData.inputs?.includeSharedDrives as boolean
const maxFiles = (nodeData.inputs?.maxFiles as number) || 50
const textSplitter = nodeData.inputs?.textSplitter as TextSplitter
const metadata = nodeData.inputs?.metadata
const _omitMetadataKeys = nodeData.inputs?.omitMetadataKeys as string
const output = nodeData.outputs?.output as string
let omitMetadataKeys: string[] = []
if (_omitMetadataKeys) {
omitMetadataKeys = _omitMetadataKeys.split(',').map((key) => key.trim())
}
if (!selectedFiles && !folderId) {
throw new Error('Either selected files or Folder ID is required')
}
let credentialData = await getCredentialData(nodeData.credential ?? '', options)
credentialData = await refreshOAuth2Token(nodeData.credential ?? '', credentialData, options)
const accessToken = getCredentialParam('access_token', credentialData, nodeData)
if (!accessToken) {
throw new Error('No access token found in credential')
}
let docs: IDocument[] = []
try {
let filesToProcess: any[] = []
if (selectedFiles) {
// Load selected files (selectedFiles can be a single ID or comma-separated IDs)
let ids: string[] = []
if (typeof selectedFiles === 'string' && selectedFiles.startsWith('[') && selectedFiles.endsWith(']')) {
ids = convertMultiOptionsToStringArray(selectedFiles)
} else if (typeof selectedFiles === 'string') {
ids = [selectedFiles]
} else if (Array.isArray(selectedFiles)) {
ids = selectedFiles
}
for (const id of ids) {
const fileInfo = await this.getFileInfo(id, accessToken, includeSharedDrives)
if (fileInfo && this.shouldProcessFile(fileInfo, fileTypes)) {
filesToProcess.push(fileInfo)
}
}
} else if (folderId) {
// Load files from folder
filesToProcess = await this.getFilesFromFolder(
folderId,
accessToken,
fileTypes,
includeSubfolders,
includeSharedDrives,
maxFiles
)
}
// Process each file
for (const fileInfo of filesToProcess) {
try {
const doc = await this.processFile(fileInfo, accessToken)
if (doc.length > 0) {
docs.push(...doc)
}
} catch (error) {
console.warn(`Failed to process file ${fileInfo.name}: ${error.message}`)
}
}
// Apply text splitter if provided
if (textSplitter && docs.length > 0) {
docs = await textSplitter.splitDocuments(docs)
}
// Apply metadata transformations
if (metadata) {
const parsedMetadata = typeof metadata === 'object' ? metadata : JSON.parse(metadata)
docs = docs.map((doc) => ({
...doc,
metadata:
_omitMetadataKeys === '*'
? {
...parsedMetadata
}
: omit(
{
...doc.metadata,
...parsedMetadata
},
omitMetadataKeys
)
}))
} else {
docs = docs.map((doc) => ({
...doc,
metadata:
_omitMetadataKeys === '*'
? {}
: omit(
{
...doc.metadata
},
omitMetadataKeys
)
}))
}
} catch (error) {
throw new Error(`Failed to load Google Drive documents: ${error.message}`)
}
if (output === 'document') {
return docs
} else {
let finaltext = ''
for (const doc of docs) {
finaltext += `${doc.pageContent}\n`
}
return handleEscapeCharacters(finaltext, false)
}
}
private async getFileInfo(fileId: string, accessToken: string, includeSharedDrives: boolean): Promise<any> {
const url = new URL(`https://www.googleapis.com/drive/v3/files/${encodeURIComponent(fileId)}`)
url.searchParams.append('fields', 'id, name, mimeType, size, createdTime, modifiedTime, parents, webViewLink, driveId')
// Add shared drives support if requested
if (includeSharedDrives) {
url.searchParams.append('supportsAllDrives', 'true')
}
const response = await fetch(url.toString(), {
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json'
}
})
if (!response.ok) {
throw new Error(`Failed to get file info: ${response.statusText}`)
}
const fileInfo = await response.json()
// Add drive context to description
const driveContext = fileInfo.driveId ? ' (Shared Drive)' : ' (My Drive)'
return {
...fileInfo,
driveContext
}
}
private async getFilesFromFolder(
folderId: string,
accessToken: string,
fileTypes: string[] | undefined,
includeSubfolders: boolean,
includeSharedDrives: boolean,
maxFiles: number
): Promise<any[]> {
const files: any[] = []
let nextPageToken: string | undefined
do {
let query = `'${folderId}' in parents and trashed = false`
// Add file type filter if specified
if (fileTypes && fileTypes.length > 0) {
const mimeTypeQuery = fileTypes.map((type) => `mimeType='${type}'`).join(' or ')
query += ` and (${mimeTypeQuery})`
}
const url = new URL('https://www.googleapis.com/drive/v3/files')
url.searchParams.append('q', query)
url.searchParams.append('pageSize', Math.min(maxFiles - files.length, 1000).toString())
url.searchParams.append(
'fields',
'nextPageToken, files(id, name, mimeType, size, createdTime, modifiedTime, parents, webViewLink, driveId)'
)
// Add shared drives support if requested
if (includeSharedDrives) {
url.searchParams.append('supportsAllDrives', 'true')
url.searchParams.append('includeItemsFromAllDrives', 'true')
}
if (nextPageToken) {
url.searchParams.append('pageToken', nextPageToken)
}
const response = await fetch(url.toString(), {
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json'
}
})
if (!response.ok) {
throw new Error(`Failed to list files: ${response.statusText}`)
}
const data = await response.json()
// Add drive context to each file
const filesWithContext = data.files.map((file: any) => ({
...file,
driveContext: file.driveId ? ' (Shared Drive)' : ' (My Drive)'
}))
files.push(...filesWithContext)
nextPageToken = data.nextPageToken
// If includeSubfolders is true, also get files from subfolders
if (includeSubfolders) {
for (const file of data.files) {
if (file.mimeType === 'application/vnd.google-apps.folder') {
const subfolderFiles = await this.getFilesFromFolder(
file.id,
accessToken,
fileTypes,
includeSubfolders,
includeSharedDrives,
maxFiles - files.length
)
files.push(...subfolderFiles)
}
}
}
} while (nextPageToken && files.length < maxFiles)
return files.slice(0, maxFiles)
}
private shouldProcessFile(fileInfo: any, fileTypes: string[] | undefined): boolean {
if (!fileTypes || fileTypes.length === 0) {
return true
}
return fileTypes.includes(fileInfo.mimeType)
}
private async processFile(fileInfo: any, accessToken: string): Promise<IDocument[]> {
let content = ''
try {
// Handle different file types
if (this.isTextBasedFile(fileInfo.mimeType)) {
// Download regular text files
content = await this.downloadFile(fileInfo.id, accessToken)
// Create document with metadata
return [
{
pageContent: content,
metadata: {
source: fileInfo.webViewLink || `https://drive.google.com/file/d/${fileInfo.id}/view`,
fileId: fileInfo.id,
fileName: fileInfo.name,
mimeType: fileInfo.mimeType,
size: fileInfo.size ? parseInt(fileInfo.size) : undefined,
createdTime: fileInfo.createdTime,
modifiedTime: fileInfo.modifiedTime,
parents: fileInfo.parents,
driveId: fileInfo.driveId,
driveContext: fileInfo.driveContext || (fileInfo.driveId ? ' (Shared Drive)' : ' (My Drive)')
}
}
]
} else if (this.isSupportedBinaryFile(fileInfo.mimeType) || this.isGoogleWorkspaceFile(fileInfo.mimeType)) {
// Process binary files and Google Workspace files using loaders
return await this.processBinaryFile(fileInfo, accessToken)
} else {
console.warn(`Unsupported file type ${fileInfo.mimeType} for file ${fileInfo.name}`)
return []
}
} catch (error) {
console.warn(`Failed to process file ${fileInfo.name}: ${error.message}`)
return []
}
}
private isSupportedBinaryFile(mimeType: string): boolean {
const supportedBinaryTypes = [
'application/pdf',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/msword',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.ms-excel',
'text/csv'
]
return supportedBinaryTypes.includes(mimeType)
}
private async processBinaryFile(fileInfo: any, accessToken: string): Promise<IDocument[]> {
let tempFilePath: string | null = null
try {
let buffer: Buffer
let processedMimeType: string
let processedFileName: string
if (this.isGoogleWorkspaceFile(fileInfo.mimeType)) {
// Handle Google Workspace files by exporting to appropriate format
const exportResult = await this.exportGoogleWorkspaceFileAsBuffer(fileInfo.id, fileInfo.mimeType, accessToken)
buffer = exportResult.buffer
processedMimeType = exportResult.mimeType
processedFileName = exportResult.fileName
} else {
// Handle regular binary files
buffer = await this.downloadBinaryFile(fileInfo.id, accessToken)
processedMimeType = fileInfo.mimeType
processedFileName = fileInfo.name
}
// Download file to temporary location
tempFilePath = await this.createTempFile(buffer, processedFileName, processedMimeType)
let docs: IDocument[] = []
const mimeType = processedMimeType.toLowerCase()
switch (mimeType) {
case 'application/pdf': {
const pdfLoader = new PDFLoader(tempFilePath, {
// @ts-ignore
pdfjs: () => import('pdf-parse/lib/pdf.js/v1.10.100/build/pdf.js')
})
docs = await pdfLoader.load()
break
}
case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
case 'application/msword': {
const docxLoader = new DocxLoader(tempFilePath)
docs = await docxLoader.load()
break
}
case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
case 'application/vnd.ms-excel': {
const excelLoader = new LoadOfSheet(tempFilePath)
docs = await excelLoader.load()
break
}
case 'application/vnd.openxmlformats-officedocument.presentationml.presentation':
case 'application/vnd.ms-powerpoint': {
const pptxLoader = new PowerpointLoader(tempFilePath)
docs = await pptxLoader.load()
break
}
case 'text/csv': {
const csvLoader = new CSVLoader(tempFilePath)
docs = await csvLoader.load()
break
}
default:
throw new Error(`Unsupported binary file type: ${mimeType}`)
}
// Add Google Drive metadata to each document
if (docs.length > 0) {
const googleDriveMetadata = {
source: fileInfo.webViewLink || `https://drive.google.com/file/d/${fileInfo.id}/view`,
fileId: fileInfo.id,
fileName: fileInfo.name,
mimeType: fileInfo.mimeType,
size: fileInfo.size ? parseInt(fileInfo.size) : undefined,
createdTime: fileInfo.createdTime,
modifiedTime: fileInfo.modifiedTime,
parents: fileInfo.parents,
totalPages: docs.length // Total number of pages/sheets in the file
}
return docs.map((doc, index) => ({
...doc,
metadata: {
...doc.metadata, // Keep original loader metadata (page numbers, etc.)
...googleDriveMetadata, // Add Google Drive metadata
pageIndex: index, // Add page/sheet index
driveId: fileInfo.driveId,
driveContext: fileInfo.driveContext || (fileInfo.driveId ? ' (Shared Drive)' : ' (My Drive)')
}
}))
}
return []
} catch (error) {
throw new Error(`Failed to process binary file: ${error.message}`)
} finally {
// Clean up temporary file
if (tempFilePath && fs.existsSync(tempFilePath)) {
try {
fs.unlinkSync(tempFilePath)
} catch (e) {
console.warn(`Failed to delete temporary file: ${tempFilePath}`)
}
}
}
}
private async createTempFile(buffer: Buffer, fileName: string, mimeType: string): Promise<string> {
// Get appropriate file extension
let extension = path.extname(fileName)
if (!extension) {
const extensionMap: { [key: string]: string } = {
'application/pdf': '.pdf',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx',
'application/msword': '.doc',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx',
'application/vnd.ms-excel': '.xls',
'application/vnd.openxmlformats-officedocument.presentationml.presentation': '.pptx',
'application/vnd.ms-powerpoint': '.ppt',
'text/csv': '.csv'
}
extension = extensionMap[mimeType] || '.tmp'
}
// Create temporary file
const tempDir = os.tmpdir()
const tempFileName = `gdrive_${Date.now()}_${Math.random().toString(36).substring(7)}${extension}`
const tempFilePath = path.join(tempDir, tempFileName)
fs.writeFileSync(tempFilePath, buffer)
return tempFilePath
}
private async downloadBinaryFile(fileId: string, accessToken: string): Promise<Buffer> {
const url = `https://www.googleapis.com/drive/v3/files/${encodeURIComponent(fileId)}?alt=media`
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${accessToken}`
}
})
if (!response.ok) {
throw new Error(`Failed to download file: ${response.statusText}`)
}
const arrayBuffer = await response.arrayBuffer()
return Buffer.from(arrayBuffer)
}
private async downloadFile(fileId: string, accessToken: string): Promise<string> {
const url = `https://www.googleapis.com/drive/v3/files/${encodeURIComponent(fileId)}?alt=media`
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${accessToken}`
}
})
if (!response.ok) {
throw new Error(`Failed to download file: ${response.statusText}`)
}
// Only call response.text() for text-based files
const contentType = response.headers.get('content-type') || ''
if (!contentType.startsWith('text/') && !contentType.includes('json') && !contentType.includes('xml')) {
throw new Error(`Cannot process binary file with content-type: ${contentType}`)
}
return await response.text()
}
private isGoogleWorkspaceFile(mimeType: string): boolean {
const googleWorkspaceMimeTypes = [
'application/vnd.google-apps.document',
'application/vnd.google-apps.spreadsheet',
'application/vnd.google-apps.presentation',
'application/vnd.google-apps.drawing'
]
return googleWorkspaceMimeTypes.includes(mimeType)
}
private isTextBasedFile(mimeType: string): boolean {
const textBasedMimeTypes = [
'text/plain',
'text/html',
'text/css',
'text/javascript',
'text/csv',
'text/xml',
'application/json',
'application/xml',
'text/markdown',
'text/x-markdown'
]
return textBasedMimeTypes.includes(mimeType)
}
private async exportGoogleWorkspaceFileAsBuffer(
fileId: string,
mimeType: string,
accessToken: string
): Promise<{ buffer: Buffer; mimeType: string; fileName: string }> {
// Automatic mapping of Google Workspace MIME types to export formats
let exportMimeType: string
let fileExtension: string
switch (mimeType) {
case 'application/vnd.google-apps.document':
exportMimeType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
fileExtension = '.docx'
break
case 'application/vnd.google-apps.spreadsheet':
exportMimeType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
fileExtension = '.xlsx'
break
case 'application/vnd.google-apps.presentation':
exportMimeType = 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
fileExtension = '.pptx'
break
case 'application/vnd.google-apps.drawing':
exportMimeType = 'application/pdf'
fileExtension = '.pdf'
break
default:
// Fallback to DOCX for any other Google Workspace file
exportMimeType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
fileExtension = '.docx'
break
}
const url = `https://www.googleapis.com/drive/v3/files/${encodeURIComponent(fileId)}/export?mimeType=${encodeURIComponent(
exportMimeType
)}`
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${accessToken}`
}
})
if (!response.ok) {
throw new Error(`Failed to export file: ${response.statusText}`)
}
const arrayBuffer = await response.arrayBuffer()
const buffer = Buffer.from(arrayBuffer)
return {
buffer,
mimeType: exportMimeType,
fileName: `exported_file${fileExtension}`
}
}
}
module.exports = { nodeClass: GoogleDrive_DocumentLoaders }
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="96px" height="96px"><path fill="#1e88e5" d="M38.59,39c-0.535,0.93-0.298,1.68-1.195,2.197C36.498,41.715,35.465,42,34.39,42H13.61 c-1.074,0-2.106-0.285-3.004-0.802C9.708,40.681,9.945,39.93,9.41,39l7.67-9h13.84L38.59,39z"/><path fill="#fbc02d" d="M27.463,6.999c1.073-0.002,2.104-0.716,3.001-0.198c0.897,0.519,1.66,1.27,2.197,2.201l10.39,17.996 c0.537,0.93,0.807,1.967,0.808,3.002c0.001,1.037-1.267,2.073-1.806,3.001l-11.127-3.005l-6.924-11.993L27.463,6.999z"/><path fill="#e53935" d="M43.86,30c0,1.04-0.27,2.07-0.81,3l-3.67,6.35c-0.53,0.78-1.21,1.4-1.99,1.85L30.92,30H43.86z"/><path fill="#4caf50" d="M5.947,33.001c-0.538-0.928-1.806-1.964-1.806-3c0.001-1.036,0.27-2.073,0.808-3.004l10.39-17.996 c0.537-0.93,1.3-1.682,2.196-2.2c0.897-0.519,1.929,0.195,3.002,0.197l3.459,11.009l-6.922,11.989L5.947,33.001z"/><path fill="#1565c0" d="M17.08,30l-6.47,11.2c-0.78-0.45-1.46-1.07-1.99-1.85L4.95,33c-0.54-0.93-0.81-1.96-0.81-3H17.08z"/><path fill="#2e7d32" d="M30.46,6.8L24,18L17.53,6.8c0.78-0.45,1.66-0.73,2.6-0.79L27.46,6C28.54,6,29.57,6.28,30.46,6.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,429 @@
import { omit } from 'lodash'
import { ICommonObject, IDocument, INode, INodeData, INodeParams, INodeOptionsValue } from '../../../src/Interface'
import { TextSplitter } from 'langchain/text_splitter'
import {
convertMultiOptionsToStringArray,
getCredentialData,
getCredentialParam,
handleEscapeCharacters,
INodeOutputsValue,
refreshOAuth2Token
} from '../../../src'
class GoogleSheets_DocumentLoaders implements INode {
label: string
name: string
version: number
description: string
type: string
icon: string
category: string
baseClasses: string[]
credential: INodeParams
inputs: INodeParams[]
outputs: INodeOutputsValue[]
constructor() {
this.label = 'Google Sheets'
this.name = 'googleSheets'
this.version = 1.0
this.type = 'Document'
this.icon = 'google-sheets.svg'
this.category = 'Document Loaders'
this.description = `Load data from Google Sheets as documents`
this.baseClasses = [this.type]
this.credential = {
label: 'Connect Credential',
name: 'credential',
type: 'credential',
description: 'Google Sheets OAuth2 Credential',
credentialNames: ['googleSheetsOAuth2']
}
this.inputs = [
{
label: 'Select Spreadsheet',
name: 'spreadsheetIds',
type: 'asyncMultiOptions',
loadMethod: 'listSpreadsheets',
description: 'Select spreadsheet from your Google Drive',
refresh: true
},
{
label: 'Sheet Names',
name: 'sheetNames',
type: 'string',
description: 'Comma-separated list of sheet names to load. If empty, loads all sheets.',
placeholder: 'Sheet1, Sheet2',
optional: true
},
{
label: 'Range',
name: 'range',
type: 'string',
description: 'Range to load (e.g., A1:E10). If empty, loads entire sheet.',
placeholder: 'A1:E10',
optional: true
},
{
label: 'Include Headers',
name: 'includeHeaders',
type: 'boolean',
description: 'Whether to include the first row as headers',
default: true
},
{
label: 'Value Render Option',
name: 'valueRenderOption',
type: 'options',
description: 'How values should be represented in the output',
options: [
{
label: 'Formatted Value',
name: 'FORMATTED_VALUE'
},
{
label: 'Unformatted Value',
name: 'UNFORMATTED_VALUE'
},
{
label: 'Formula',
name: 'FORMULA'
}
],
default: 'FORMATTED_VALUE',
optional: true
},
{
label: 'Text Splitter',
name: 'textSplitter',
type: 'TextSplitter',
optional: true
},
{
label: 'Additional Metadata',
name: 'metadata',
type: 'json',
description: 'Additional metadata to be added to the extracted documents',
optional: true,
additionalParams: true
},
{
label: 'Omit Metadata Keys',
name: 'omitMetadataKeys',
type: 'string',
rows: 4,
description:
'Each document loader comes with a default set of metadata keys that are extracted from the document. You can use this field to omit some of the default metadata keys. The value should be a list of keys, seperated by comma. Use * to omit all metadata keys execept the ones you specify in the Additional Metadata field',
placeholder: 'key1, key2, key3.nestedKey1',
optional: true,
additionalParams: true
}
]
this.outputs = [
{
label: 'Document',
name: 'document',
description: 'Array of document objects containing metadata and pageContent',
baseClasses: [...this.baseClasses, 'json']
},
{
label: 'Text',
name: 'text',
description: 'Concatenated string from pageContent of documents',
baseClasses: ['string', 'json']
}
]
}
//@ts-ignore
loadMethods = {
async listSpreadsheets(nodeData: INodeData, options: ICommonObject): Promise<INodeOptionsValue[]> {
const returnData: INodeOptionsValue[] = []
try {
let credentialData = await getCredentialData(nodeData.credential ?? '', options)
credentialData = await refreshOAuth2Token(nodeData.credential ?? '', credentialData, options)
const accessToken = getCredentialParam('access_token', credentialData, nodeData)
if (!accessToken) {
return returnData
}
// Query for Google Sheets files specifically
const query = "mimeType='application/vnd.google-apps.spreadsheet' and trashed = false"
const url = new URL('https://www.googleapis.com/drive/v3/files')
url.searchParams.append('q', query)
url.searchParams.append('pageSize', '100')
url.searchParams.append('fields', 'files(id, name, modifiedTime, webViewLink)')
url.searchParams.append('orderBy', 'modifiedTime desc')
const response = await fetch(url.toString(), {
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json'
}
})
if (!response.ok) {
console.error(`Failed to list spreadsheets: ${response.statusText}`)
return returnData
}
const data = await response.json()
for (const file of data.files) {
const obj: INodeOptionsValue = {
name: file.id,
label: file.name,
description: `Modified: ${new Date(file.modifiedTime).toLocaleDateString()}`
}
returnData.push(obj)
}
} catch (error) {
console.error('Error listing Google Sheets:', error)
}
return returnData
}
}
async init(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
const _spreadsheetIds = nodeData.inputs?.spreadsheetIds as string
const sheetNames = nodeData.inputs?.sheetNames as string
const range = nodeData.inputs?.range as string
const includeHeaders = nodeData.inputs?.includeHeaders as boolean
const valueRenderOption = (nodeData.inputs?.valueRenderOption as string) || 'FORMATTED_VALUE'
const textSplitter = nodeData.inputs?.textSplitter as TextSplitter
const metadata = nodeData.inputs?.metadata
const _omitMetadataKeys = nodeData.inputs?.omitMetadataKeys as string
const output = nodeData.outputs?.output as string
let omitMetadataKeys: string[] = []
if (_omitMetadataKeys) {
omitMetadataKeys = _omitMetadataKeys.split(',').map((key) => key.trim())
}
if (!_spreadsheetIds) {
throw new Error('At least one spreadsheet is required')
}
let spreadsheetIds = convertMultiOptionsToStringArray(_spreadsheetIds)
let credentialData = await getCredentialData(nodeData.credential ?? '', options)
credentialData = await refreshOAuth2Token(nodeData.credential ?? '', credentialData, options)
const accessToken = getCredentialParam('access_token', credentialData, nodeData)
if (!accessToken) {
throw new Error('No access token found in credential')
}
let docs: IDocument[] = []
try {
// Process each spreadsheet
for (const spreadsheetId of spreadsheetIds) {
try {
// Get spreadsheet metadata first
const spreadsheetMetadata = await this.getSpreadsheetMetadata(spreadsheetId, accessToken)
// Determine which sheets to load
let sheetsToLoad: string[] = []
if (sheetNames) {
sheetsToLoad = sheetNames.split(',').map((name) => name.trim())
} else {
// Get all sheet names from metadata
sheetsToLoad = spreadsheetMetadata.sheets?.map((sheet: any) => sheet.properties.title) || []
}
// Load data from each sheet
for (const sheetName of sheetsToLoad) {
const sheetRange = range ? `${sheetName}!${range}` : sheetName
const sheetData = await this.getSheetData(spreadsheetId, sheetRange, valueRenderOption, accessToken)
if (sheetData.values && sheetData.values.length > 0) {
const sheetDoc = this.convertSheetToDocument(
sheetData,
sheetName,
spreadsheetId,
spreadsheetMetadata,
includeHeaders
)
docs.push(sheetDoc)
}
}
} catch (error) {
console.warn(`Failed to process spreadsheet ${spreadsheetId}: ${error.message}`)
// Continue processing other spreadsheets even if one fails
}
}
// Apply text splitter if provided
if (textSplitter && docs.length > 0) {
docs = await textSplitter.splitDocuments(docs)
}
// Apply metadata transformations
if (metadata) {
const parsedMetadata = typeof metadata === 'object' ? metadata : JSON.parse(metadata)
docs = docs.map((doc) => ({
...doc,
metadata:
_omitMetadataKeys === '*'
? {
...parsedMetadata
}
: omit(
{
...doc.metadata,
...parsedMetadata
},
omitMetadataKeys
)
}))
} else {
docs = docs.map((doc) => ({
...doc,
metadata:
_omitMetadataKeys === '*'
? {}
: omit(
{
...doc.metadata
},
omitMetadataKeys
)
}))
}
} catch (error) {
throw new Error(`Failed to load Google Sheets data: ${error.message}`)
}
if (output === 'document') {
return docs
} else {
let finaltext = ''
for (const doc of docs) {
finaltext += `${doc.pageContent}\n`
}
return handleEscapeCharacters(finaltext, false)
}
}
private async getSpreadsheetMetadata(spreadsheetId: string, accessToken: string): Promise<any> {
const url = `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}`
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json'
}
})
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Failed to get spreadsheet metadata: ${response.status} ${response.statusText} - ${errorText}`)
}
return response.json()
}
private async getSheetData(spreadsheetId: string, range: string, valueRenderOption: string, accessToken: string): Promise<any> {
const url = `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values/${encodeURIComponent(range)}`
const params = new URLSearchParams({
valueRenderOption,
dateTimeRenderOption: 'FORMATTED_STRING',
majorDimension: 'ROWS'
})
const response = await fetch(`${url}?${params}`, {
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json'
}
})
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Failed to get sheet data: ${response.status} ${response.statusText} - ${errorText}`)
}
return response.json()
}
private convertSheetToDocument(
sheetData: any,
sheetName: string,
spreadsheetId: string,
spreadsheetMetadata: any,
includeHeaders: boolean
): IDocument {
const values = sheetData.values || []
if (values.length === 0) {
return {
pageContent: '',
metadata: {
source: `Google Sheets: ${spreadsheetMetadata.properties?.title || 'Unknown'} - ${sheetName}`,
spreadsheetId,
sheetName,
spreadsheetTitle: spreadsheetMetadata.properties?.title,
range: sheetData.range,
rowCount: 0,
columnCount: 0
}
}
}
let headers: string[] = []
let dataRows: string[][] = []
if (includeHeaders && values.length > 0) {
headers = values[0] || []
dataRows = values.slice(1)
} else {
// Generate default headers like A, B, C, etc.
const maxColumns = Math.max(...values.map((row: any[]) => row.length))
headers = Array.from({ length: maxColumns }, (_, i) => String.fromCharCode(65 + i))
dataRows = values
}
// Convert to markdown table format
let content = ''
if (headers.length > 0) {
// Create header row
content += '| ' + headers.join(' | ') + ' |\n'
// Create separator row
content += '| ' + headers.map(() => '---').join(' | ') + ' |\n'
// Add data rows
for (const row of dataRows) {
const paddedRow = [...row]
// Pad row to match header length
while (paddedRow.length < headers.length) {
paddedRow.push('')
}
content += '| ' + paddedRow.join(' | ') + ' |\n'
}
}
return {
pageContent: content,
metadata: {
source: `Google Sheets: ${spreadsheetMetadata.properties?.title || 'Unknown'} - ${sheetName}`,
spreadsheetId,
sheetName,
spreadsheetTitle: spreadsheetMetadata.properties?.title,
spreadsheetUrl: `https://docs.google.com/spreadsheets/d/${spreadsheetId}`,
range: sheetData.range,
rowCount: values.length,
columnCount: headers.length,
headers: includeHeaders ? headers : undefined,
totalDataRows: dataRows.length
}
}
}
}
module.exports = { nodeClass: GoogleSheets_DocumentLoaders }
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="96px" height="96px"><path fill="#43a047" d="M37,45H11c-1.657,0-3-1.343-3-3V6c0-1.657,1.343-3,3-3h19l10,10v29C40,43.657,38.657,45,37,45z"/><path fill="#c8e6c9" d="M40 13L30 13 30 3z"/><path fill="#2e7d32" d="M30 13L40 23 40 13z"/><path fill="#e8f5e9" d="M31,23H17h-2v2v2v2v2v2v2v2h18v-2v-2v-2v-2v-2v-2v-2H31z M17,25h4v2h-4V25z M17,29h4v2h-4V29z M17,33h4v2h-4V33z M31,35h-8v-2h8V35z M31,31h-8v-2h8V31z M31,27h-8v-2h8V27z"/></svg>

After

Width:  |  Height:  |  Size: 495 B

@@ -1,2 +1 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg"><defs><style>.a{fill:none;stroke:#000000;stroke-linecap:round;stroke-linejoin:round;}</style></defs><path class="a" d="M5.5,22.9722h0a8.7361,8.7361,0,0,0,8.7361,8.7361h2.0556v2.0556A8.7361,8.7361,0,0,0,25.0278,42.5h0V22.9722Z"/><path class="a" d="M14.2361,14.2361h0a8.7361,8.7361,0,0,0,8.7361,8.7361h2.0556v2.0556a8.7361,8.7361,0,0,0,8.7361,8.7361h0V14.2361Z"/><path class="a" d="M22.9722,5.5h0a8.7361,8.7361,0,0,0,8.7361,8.7361h2.0556v2.0556A8.7361,8.7361,0,0,0,42.5,25.0278h0V5.5Z"/></svg>
<svg height="2500" preserveAspectRatio="xMidYMid" width="2500" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 -30.632388516510233 255.324 285.95638851651023"><linearGradient id="a"><stop offset=".18" stop-color="#0052cc"/><stop offset="1" stop-color="#2684ff"/></linearGradient><linearGradient id="b" x1="98.031%" x2="58.888%" xlink:href="#a" y1=".161%" y2="40.766%"/><linearGradient id="c" x1="100.665%" x2="55.402%" xlink:href="#a" y1=".455%" y2="44.727%"/><path d="M244.658 0H121.707a55.502 55.502 0 0 0 55.502 55.502h22.649V77.37c.02 30.625 24.841 55.447 55.466 55.467V10.666C255.324 4.777 250.55 0 244.658 0z" fill="#2684ff"/><path d="M183.822 61.262H60.872c.019 30.625 24.84 55.447 55.466 55.467h22.649v21.938c.039 30.625 24.877 55.43 55.502 55.43V71.93c0-5.891-4.776-10.667-10.667-10.667z" fill="url(#b)"/><path d="M122.951 122.489H0c0 30.653 24.85 55.502 55.502 55.502h22.72v21.867c.02 30.597 24.798 55.408 55.396 55.466V133.156c0-5.891-4.776-10.667-10.667-10.667z" fill="url(#c)"/></svg>

Before

Width:  |  Height:  |  Size: 699 B

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -0,0 +1,72 @@
import { Document } from '@langchain/core/documents'
import { BufferLoader } from 'langchain/document_loaders/fs/buffer'
import { read, utils } from 'xlsx'
/**
* Document loader that uses SheetJS to load documents.
*
* Each worksheet is parsed into an array of row objects using the SheetJS
* `sheet_to_json` method and projected to a `Document`. Metadata includes
* original sheet name, row data, and row index
*/
export class LoadOfSheet extends BufferLoader {
attributes: { name: string; description: string; type: string }[] = []
constructor(filePathOrBlob: string | Blob) {
super(filePathOrBlob)
this.attributes = []
}
/**
* Parse document
*
* NOTE: column labels in multiple sheets are not disambiguated!
*
* @param raw Raw data Buffer
* @param metadata Document metadata
* @returns Array of Documents
*/
async parse(raw: Buffer, metadata: Document['metadata']): Promise<Document[]> {
const result: Document[] = []
this.attributes = [
{ name: 'worksheet', description: 'Sheet or Worksheet Name', type: 'string' },
{ name: 'rowNum', description: 'Row index', type: 'number' }
]
const wb = read(raw, { type: 'buffer' })
for (let name of wb.SheetNames) {
const fields: Record<string, Record<string, boolean>> = {}
const ws = wb.Sheets[name]
if (!ws) continue
const aoo = utils.sheet_to_json(ws) as Record<string, unknown>[]
aoo.forEach((row) => {
result.push({
pageContent:
Object.entries(row)
.map((kv) => `- ${kv[0]}: ${kv[1]}`)
.join('\n') + '\n',
metadata: {
worksheet: name,
rowNum: row['__rowNum__'],
...metadata,
...row
}
})
Object.entries(row).forEach(([k, v]) => {
if (v != null) (fields[k] || (fields[k] = {}))[v instanceof Date ? 'date' : typeof v] = true
})
})
Object.entries(fields).forEach(([k, v]) =>
this.attributes.push({
name: k,
description: k,
type: Object.keys(v).join(' or ')
})
)
}
return result
}
}
@@ -0,0 +1,142 @@
import { TextSplitter } from 'langchain/text_splitter'
import { LoadOfSheet } from './ExcelLoader'
import { getFileFromStorage, handleDocumentLoaderDocuments, handleDocumentLoaderMetadata, handleDocumentLoaderOutput } from '../../../src'
import { ICommonObject, IDocument, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface'
class MicrosoftExcel_DocumentLoaders implements INode {
label: string
name: string
version: number
description: string
type: string
icon: string
category: string
baseClasses: string[]
inputs: INodeParams[]
outputs: INodeOutputsValue[]
constructor() {
this.label = 'Microsoft Excel'
this.name = 'microsoftExcel'
this.version = 1.0
this.type = 'Document'
this.icon = 'excel.svg'
this.category = 'Document Loaders'
this.description = `Load data from Microsoft Excel files`
this.baseClasses = [this.type]
this.inputs = [
{
label: 'Excel File',
name: 'excelFile',
type: 'file',
fileType: '.xlsx, .xls, .xlsm, .xlsb'
},
{
label: 'Text Splitter',
name: 'textSplitter',
type: 'TextSplitter',
optional: true
},
{
label: 'Additional Metadata',
name: 'metadata',
type: 'json',
description: 'Additional metadata to be added to the extracted documents',
optional: true,
additionalParams: true
},
{
label: 'Omit Metadata Keys',
name: 'omitMetadataKeys',
type: 'string',
rows: 4,
description:
'Each document loader comes with a default set of metadata keys that are extracted from the document. You can use this field to omit some of the default metadata keys. The value should be a list of keys, seperated by comma. Use * to omit all metadata keys execept the ones you specify in the Additional Metadata field',
placeholder: 'key1, key2, key3.nestedKey1',
optional: true,
additionalParams: true
}
]
this.outputs = [
{
label: 'Document',
name: 'document',
description: 'Array of document objects containing metadata and pageContent',
baseClasses: [...this.baseClasses, 'json']
},
{
label: 'Text',
name: 'text',
description: 'Concatenated string from pageContent of documents',
baseClasses: ['string', 'json']
}
]
}
getFiles(nodeData: INodeData) {
const excelFileBase64 = nodeData.inputs?.excelFile as string
let files: string[] = []
let fromStorage: boolean = true
if (excelFileBase64.startsWith('FILE-STORAGE::')) {
const fileName = excelFileBase64.replace('FILE-STORAGE::', '')
if (fileName.startsWith('[') && fileName.endsWith(']')) {
files = JSON.parse(fileName)
} else {
files = [fileName]
}
} else {
if (excelFileBase64.startsWith('[') && excelFileBase64.endsWith(']')) {
files = JSON.parse(excelFileBase64)
} else {
files = [excelFileBase64]
}
fromStorage = false
}
return { files, fromStorage }
}
async getFileData(file: string, { orgId, chatflowid }: { orgId: string; chatflowid: string }, fromStorage?: boolean) {
if (fromStorage) {
return getFileFromStorage(file, orgId, chatflowid)
} else {
const splitDataURI = file.split(',')
splitDataURI.pop()
return Buffer.from(splitDataURI.pop() || '', 'base64')
}
}
async init(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
const textSplitter = nodeData.inputs?.textSplitter as TextSplitter
const metadata = nodeData.inputs?.metadata
const output = nodeData.outputs?.output as string
const _omitMetadataKeys = nodeData.inputs?.omitMetadataKeys as string
let docs: IDocument[] = []
const orgId = options.orgId
const chatflowid = options.chatflowid
const { files, fromStorage } = this.getFiles(nodeData)
for (const file of files) {
if (!file) continue
const fileData = await this.getFileData(file, { orgId, chatflowid }, fromStorage)
const blob = new Blob([fileData])
const loader = new LoadOfSheet(blob)
// use spread instead of push, because it raises RangeError: Maximum call stack size exceeded when too many docs
docs = [...docs, ...(await handleDocumentLoaderDocuments(loader, textSplitter))]
}
docs = handleDocumentLoaderMetadata(docs, _omitMetadataKeys, metadata)
return handleDocumentLoaderOutput(docs, output)
}
}
module.exports = { nodeClass: MicrosoftExcel_DocumentLoaders }
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="96px" height="96px"><path fill="#169154" d="M29,6H15.744C14.781,6,14,6.781,14,7.744v7.259h15V6z"/><path fill="#18482a" d="M14,33.054v7.202C14,41.219,14.781,42,15.743,42H29v-8.946H14z"/><path fill="#0c8045" d="M14 15.003H29V24.005000000000003H14z"/><path fill="#17472a" d="M14 24.005H29V33.055H14z"/><g><path fill="#29c27f" d="M42.256,6H29v9.003h15V7.744C44,6.781,43.219,6,42.256,6z"/><path fill="#27663f" d="M29,33.054V42h13.257C43.219,42,44,41.219,44,40.257v-7.202H29z"/><path fill="#19ac65" d="M29 15.003H44V24.005000000000003H29z"/><path fill="#129652" d="M29 24.005H44V33.055H29z"/></g><path fill="#0c7238" d="M22.319,34H5.681C4.753,34,4,33.247,4,32.319V15.681C4,14.753,4.753,14,5.681,14h16.638 C23.247,14,24,14.753,24,15.681v16.638C24,33.247,23.247,34,22.319,34z"/><path fill="#fff" d="M9.807 19L12.193 19 14.129 22.754 16.175 19 18.404 19 15.333 24 18.474 29 16.123 29 14.013 25.07 11.912 29 9.526 29 12.719 23.982z"/></svg>

After

Width:  |  Height:  |  Size: 998 B

@@ -0,0 +1,142 @@
import { TextSplitter } from 'langchain/text_splitter'
import { PowerpointLoader } from './PowerpointLoader'
import { getFileFromStorage, handleDocumentLoaderDocuments, handleDocumentLoaderMetadata, handleDocumentLoaderOutput } from '../../../src'
import { ICommonObject, IDocument, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface'
class MicrosoftPowerpoint_DocumentLoaders implements INode {
label: string
name: string
version: number
description: string
type: string
icon: string
category: string
baseClasses: string[]
inputs: INodeParams[]
outputs: INodeOutputsValue[]
constructor() {
this.label = 'Microsoft PowerPoint'
this.name = 'microsoftPowerpoint'
this.version = 1.0
this.type = 'Document'
this.icon = 'powerpoint.svg'
this.category = 'Document Loaders'
this.description = `Load data from Microsoft PowerPoint files`
this.baseClasses = [this.type]
this.inputs = [
{
label: 'PowerPoint File',
name: 'powerpointFile',
type: 'file',
fileType: '.pptx, .ppt'
},
{
label: 'Text Splitter',
name: 'textSplitter',
type: 'TextSplitter',
optional: true
},
{
label: 'Additional Metadata',
name: 'metadata',
type: 'json',
description: 'Additional metadata to be added to the extracted documents',
optional: true,
additionalParams: true
},
{
label: 'Omit Metadata Keys',
name: 'omitMetadataKeys',
type: 'string',
rows: 4,
description:
'Each document loader comes with a default set of metadata keys that are extracted from the document. You can use this field to omit some of the default metadata keys. The value should be a list of keys, seperated by comma. Use * to omit all metadata keys execept the ones you specify in the Additional Metadata field',
placeholder: 'key1, key2, key3.nestedKey1',
optional: true,
additionalParams: true
}
]
this.outputs = [
{
label: 'Document',
name: 'document',
description: 'Array of document objects containing metadata and pageContent',
baseClasses: [...this.baseClasses, 'json']
},
{
label: 'Text',
name: 'text',
description: 'Concatenated string from pageContent of documents',
baseClasses: ['string', 'json']
}
]
}
getFiles(nodeData: INodeData) {
const powerpointFileBase64 = nodeData.inputs?.powerpointFile as string
let files: string[] = []
let fromStorage: boolean = true
if (powerpointFileBase64.startsWith('FILE-STORAGE::')) {
const fileName = powerpointFileBase64.replace('FILE-STORAGE::', '')
if (fileName.startsWith('[') && fileName.endsWith(']')) {
files = JSON.parse(fileName)
} else {
files = [fileName]
}
} else {
if (powerpointFileBase64.startsWith('[') && powerpointFileBase64.endsWith(']')) {
files = JSON.parse(powerpointFileBase64)
} else {
files = [powerpointFileBase64]
}
fromStorage = false
}
return { files, fromStorage }
}
async getFileData(file: string, { orgId, chatflowid }: { orgId: string; chatflowid: string }, fromStorage?: boolean) {
if (fromStorage) {
return getFileFromStorage(file, orgId, chatflowid)
} else {
const splitDataURI = file.split(',')
splitDataURI.pop()
return Buffer.from(splitDataURI.pop() || '', 'base64')
}
}
async init(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
const textSplitter = nodeData.inputs?.textSplitter as TextSplitter
const metadata = nodeData.inputs?.metadata
const output = nodeData.outputs?.output as string
const _omitMetadataKeys = nodeData.inputs?.omitMetadataKeys as string
let docs: IDocument[] = []
const orgId = options.orgId
const chatflowid = options.chatflowid
const { files, fromStorage } = this.getFiles(nodeData)
for (const file of files) {
if (!file) continue
const fileData = await this.getFileData(file, { orgId, chatflowid }, fromStorage)
const blob = new Blob([fileData])
const loader = new PowerpointLoader(blob)
// use spread instead of push, because it raises RangeError: Maximum call stack size exceeded when too many docs
docs = [...docs, ...(await handleDocumentLoaderDocuments(loader, textSplitter))]
}
docs = handleDocumentLoaderMetadata(docs, _omitMetadataKeys, metadata)
return handleDocumentLoaderOutput(docs, output)
}
}
module.exports = { nodeClass: MicrosoftPowerpoint_DocumentLoaders }
@@ -0,0 +1,101 @@
import { Document } from '@langchain/core/documents'
import { BufferLoader } from 'langchain/document_loaders/fs/buffer'
import { parseOfficeAsync } from 'officeparser'
/**
* Document loader that uses officeparser to load PowerPoint documents.
*
* Each slide is parsed into a separate Document with metadata including
* slide number and extracted text content.
*/
export class PowerpointLoader extends BufferLoader {
attributes: { name: string; description: string; type: string }[] = []
constructor(filePathOrBlob: string | Blob) {
super(filePathOrBlob)
this.attributes = []
}
/**
* Parse PowerPoint document
*
* @param raw Raw data Buffer
* @param metadata Document metadata
* @returns Array of Documents
*/
async parse(raw: Buffer, metadata: Document['metadata']): Promise<Document[]> {
const result: Document[] = []
this.attributes = [
{ name: 'slideNumber', description: 'Slide number', type: 'number' },
{ name: 'documentType', description: 'Type of document', type: 'string' }
]
try {
// Use officeparser to extract text from PowerPoint
const data = await parseOfficeAsync(raw)
if (typeof data === 'string' && data.trim()) {
// Split content by common slide separators or use the entire content as one document
const slides = this.splitIntoSlides(data)
slides.forEach((slideContent, index) => {
if (slideContent.trim()) {
result.push({
pageContent: slideContent.trim(),
metadata: {
slideNumber: index + 1,
documentType: 'powerpoint',
...metadata
}
})
}
})
}
} catch (error) {
console.error('Error parsing PowerPoint file:', error)
throw new Error(`Failed to parse PowerPoint file: ${error instanceof Error ? error.message : 'Unknown error'}`)
}
return result
}
/**
* Split content into slides based on common patterns
* This is a heuristic approach since officeparser returns plain text
*/
private splitIntoSlides(content: string): string[] {
// Try to split by common slide patterns
const slidePatterns = [
/\n\s*Slide\s+\d+/gi,
/\n\s*Page\s+\d+/gi,
/\n\s*\d+\s*\/\s*\d+/gi,
/\n\s*_{3,}/g, // Underscores as separators
/\n\s*-{3,}/g // Dashes as separators
]
let slides: string[] = []
// Try each pattern and use the one that creates the most reasonable splits
for (const pattern of slidePatterns) {
const potentialSlides = content.split(pattern)
if (potentialSlides.length > 1 && potentialSlides.length < 100) {
// Reasonable number of slides
slides = potentialSlides
break
}
}
// If no good pattern found, split by double newlines as a fallback
if (slides.length === 0) {
slides = content.split(/\n\s*\n\s*\n/)
}
// If still no good split, treat entire content as one slide
if (slides.length === 0 || slides.every((slide) => slide.trim().length < 10)) {
slides = [content]
}
return slides.filter((slide) => slide.trim().length > 0)
}
}
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="96px" height="96px"><path fill="#d35230" d="M8,24c0,9.941,8.059,18,18,18s18-8.059,18-18H26H8z"/><path fill="#ff8f6b" d="M26,6v18h18C44,14.059,35.941,6,26,6z"/><path fill="#ed6c47" d="M26,6C16.059,6,8,14.059,8,24h18V6z"/><path d="M26,16.681C26,14.648,24.352,13,22.319,13H11.774C9.417,16.044,8,19.852,8,24 c0,5.116,2.145,9.723,5.571,13h8.747C24.352,37,26,35.352,26,33.319V16.681z" opacity=".05"/><path d="M22.213,13.333H11.525C9.32,16.321,8,20.002,8,24c0,4.617,1.753,8.814,4.611,12h9.602 c1.724,0,3.121-1.397,3.121-3.121V16.454C25.333,14.731,23.936,13.333,22.213,13.333z" opacity=".07"/><path d="M22.106,13.667H11.276C9.218,16.593,8,20.151,8,24c0,4.148,1.417,7.956,3.774,11h10.332 c1.414,0,2.56-1.146,2.56-2.56V16.227C24.667,14.813,23.52,13.667,22.106,13.667z" opacity=".09"/><linearGradient id="N~uyq1CljjkKMh72IFt0Fa" x1="4.586" x2="22.77" y1="14.586" y2="32.77" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ca4e2a"/><stop offset="1" stop-color="#b63016"/></linearGradient><path fill="url(#N~uyq1CljjkKMh72IFt0Fa)" d="M22,34H6c-1.105,0-2-0.895-2-2V16c0-1.105,0.895-2,2-2h16c1.105,0,2,0.895,2,2v16 C24,33.105,23.105,34,22,34z"/><path fill="#fff" d="M14.673,19.012H10v10h2.024v-3.521H14.3c1.876,0,3.397-1.521,3.397-3.397v-0.058 C17.697,20.366,16.343,19.012,14.673,19.012z M15.57,22.358c0,0.859-0.697,1.556-1.556,1.556h-1.99v-3.325h1.99 c0.859,0,1.556,0.697,1.556,1.556V22.358z"/></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,142 @@
import { TextSplitter } from 'langchain/text_splitter'
import { WordLoader } from './WordLoader'
import { getFileFromStorage, handleDocumentLoaderDocuments, handleDocumentLoaderMetadata, handleDocumentLoaderOutput } from '../../../src'
import { ICommonObject, IDocument, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface'
class MicrosoftWord_DocumentLoaders implements INode {
label: string
name: string
version: number
description: string
type: string
icon: string
category: string
baseClasses: string[]
inputs: INodeParams[]
outputs: INodeOutputsValue[]
constructor() {
this.label = 'Microsoft Word'
this.name = 'microsoftWord'
this.version = 1.0
this.type = 'Document'
this.icon = 'word.svg'
this.category = 'Document Loaders'
this.description = `Load data from Microsoft Word files`
this.baseClasses = [this.type]
this.inputs = [
{
label: 'Word File',
name: 'docxFile',
type: 'file',
fileType: '.docx, .doc'
},
{
label: 'Text Splitter',
name: 'textSplitter',
type: 'TextSplitter',
optional: true
},
{
label: 'Additional Metadata',
name: 'metadata',
type: 'json',
description: 'Additional metadata to be added to the extracted documents',
optional: true,
additionalParams: true
},
{
label: 'Omit Metadata Keys',
name: 'omitMetadataKeys',
type: 'string',
rows: 4,
description:
'Each document loader comes with a default set of metadata keys that are extracted from the document. You can use this field to omit some of the default metadata keys. The value should be a list of keys, seperated by comma. Use * to omit all metadata keys execept the ones you specify in the Additional Metadata field',
placeholder: 'key1, key2, key3.nestedKey1',
optional: true,
additionalParams: true
}
]
this.outputs = [
{
label: 'Document',
name: 'document',
description: 'Array of document objects containing metadata and pageContent',
baseClasses: [...this.baseClasses, 'json']
},
{
label: 'Text',
name: 'text',
description: 'Concatenated string from pageContent of documents',
baseClasses: ['string', 'json']
}
]
}
getFiles(nodeData: INodeData) {
const docxFileBase64 = nodeData.inputs?.docxFile as string
let files: string[] = []
let fromStorage: boolean = true
if (docxFileBase64.startsWith('FILE-STORAGE::')) {
const fileName = docxFileBase64.replace('FILE-STORAGE::', '')
if (fileName.startsWith('[') && fileName.endsWith(']')) {
files = JSON.parse(fileName)
} else {
files = [fileName]
}
} else {
if (docxFileBase64.startsWith('[') && docxFileBase64.endsWith(']')) {
files = JSON.parse(docxFileBase64)
} else {
files = [docxFileBase64]
}
fromStorage = false
}
return { files, fromStorage }
}
async getFileData(file: string, { orgId, chatflowid }: { orgId: string; chatflowid: string }, fromStorage?: boolean) {
if (fromStorage) {
return getFileFromStorage(file, orgId, chatflowid)
} else {
const splitDataURI = file.split(',')
splitDataURI.pop()
return Buffer.from(splitDataURI.pop() || '', 'base64')
}
}
async init(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
const textSplitter = nodeData.inputs?.textSplitter as TextSplitter
const metadata = nodeData.inputs?.metadata
const output = nodeData.outputs?.output as string
const _omitMetadataKeys = nodeData.inputs?.omitMetadataKeys as string
let docs: IDocument[] = []
const orgId = options.orgId
const chatflowid = options.chatflowid
const { files, fromStorage } = this.getFiles(nodeData)
for (const file of files) {
if (!file) continue
const fileData = await this.getFileData(file, { orgId, chatflowid }, fromStorage)
const blob = new Blob([fileData])
const loader = new WordLoader(blob)
// use spread instead of push, because it raises RangeError: Maximum call stack size exceeded when too many docs
docs = [...docs, ...(await handleDocumentLoaderDocuments(loader, textSplitter))]
}
docs = handleDocumentLoaderMetadata(docs, _omitMetadataKeys, metadata)
return handleDocumentLoaderOutput(docs, output)
}
}
module.exports = { nodeClass: MicrosoftWord_DocumentLoaders }
@@ -0,0 +1,108 @@
import { Document } from '@langchain/core/documents'
import { BufferLoader } from 'langchain/document_loaders/fs/buffer'
import { parseOfficeAsync } from 'officeparser'
/**
* Document loader that uses officeparser to load Word documents.
*
* The document is parsed into a single Document with metadata including
* document type and extracted text content.
*/
export class WordLoader extends BufferLoader {
attributes: { name: string; description: string; type: string }[] = []
constructor(filePathOrBlob: string | Blob) {
super(filePathOrBlob)
this.attributes = []
}
/**
* Parse Word document
*
* @param raw Raw data Buffer
* @param metadata Document metadata
* @returns Array of Documents
*/
async parse(raw: Buffer, metadata: Document['metadata']): Promise<Document[]> {
const result: Document[] = []
this.attributes = [
{ name: 'documentType', description: 'Type of document', type: 'string' },
{ name: 'pageCount', description: 'Number of pages/sections', type: 'number' }
]
try {
// Use officeparser to extract text from Word document
const data = await parseOfficeAsync(raw)
if (typeof data === 'string' && data.trim()) {
// Split content by common page/section separators
const sections = this.splitIntoSections(data)
sections.forEach((sectionContent, index) => {
if (sectionContent.trim()) {
result.push({
pageContent: sectionContent.trim(),
metadata: {
documentType: 'word',
pageNumber: index + 1,
...metadata
}
})
}
})
}
} catch (error) {
console.error('Error parsing Word file:', error)
throw new Error(`Failed to parse Word file: ${error instanceof Error ? error.message : 'Unknown error'}`)
}
return result
}
/**
* Split content into sections based on common patterns
* This is a heuristic approach since officeparser returns plain text
*/
private splitIntoSections(content: string): string[] {
// Try to split by common section patterns
const sectionPatterns = [
/\n\s*Page\s+\d+/gi,
/\n\s*Section\s+\d+/gi,
/\n\s*Chapter\s+\d+/gi,
/\n\s*\d+\.\s+/gi, // Numbered sections like "1. ", "2. "
/\n\s*[A-Z][A-Z\s]{2,}\n/g, // ALL CAPS headings
/\n\s*_{5,}/g, // Long underscores as separators
/\n\s*-{5,}/g // Long dashes as separators
]
let sections: string[] = []
// Try each pattern and use the one that creates the most reasonable splits
for (const pattern of sectionPatterns) {
const potentialSections = content.split(pattern)
if (potentialSections.length > 1 && potentialSections.length < 50) {
// Reasonable number of sections
sections = potentialSections
break
}
}
// If no good pattern found, split by multiple newlines as a fallback
if (sections.length === 0) {
sections = content.split(/\n\s*\n\s*\n\s*\n/)
}
// If still no good split, split by double newlines
if (sections.length === 0 || sections.every((section) => section.trim().length < 20)) {
sections = content.split(/\n\s*\n\s*\n/)
}
// If still no good split, treat entire content as one section
if (sections.length === 0 || sections.every((section) => section.trim().length < 10)) {
sections = [content]
}
return sections.filter((section) => section.trim().length > 0)
}
}
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="96px" height="96px"><linearGradient id="Q7XamDf1hnh~bz~vAO7C6a" x1="28" x2="28" y1="14.966" y2="6.45" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#42a3f2"/><stop offset="1" stop-color="#42a4eb"/></linearGradient><path fill="url(#Q7XamDf1hnh~bz~vAO7C6a)" d="M42,6H14c-1.105,0-2,0.895-2,2v7.003h32V8C44,6.895,43.105,6,42,6z"/><linearGradient id="Q7XamDf1hnh~bz~vAO7C6b" x1="28" x2="28" y1="42" y2="33.054" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#11408a"/><stop offset="1" stop-color="#103f8f"/></linearGradient><path fill="url(#Q7XamDf1hnh~bz~vAO7C6b)" d="M12,33.054V40c0,1.105,0.895,2,2,2h28c1.105,0,2-0.895,2-2v-6.946H12z"/><linearGradient id="Q7XamDf1hnh~bz~vAO7C6c" x1="28" x2="28" y1="-15.46" y2="-15.521" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#3079d6"/><stop offset="1" stop-color="#297cd2"/></linearGradient><path fill="url(#Q7XamDf1hnh~bz~vAO7C6c)" d="M12,15.003h32v9.002H12V15.003z"/><linearGradient id="Q7XamDf1hnh~bz~vAO7C6d" x1="12" x2="44" y1="28.53" y2="28.53" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#1d59b3"/><stop offset="1" stop-color="#195bbc"/></linearGradient><path fill="url(#Q7XamDf1hnh~bz~vAO7C6d)" d="M12,24.005h32v9.05H12V24.005z"/><path d="M22.319,13H12v24h10.319C24.352,37,26,35.352,26,33.319V16.681C26,14.648,24.352,13,22.319,13z" opacity=".05"/><path d="M22.213,36H12V13.333h10.213c1.724,0,3.121,1.397,3.121,3.121v16.425 C25.333,34.603,23.936,36,22.213,36z" opacity=".07"/><path d="M22.106,35H12V13.667h10.106c1.414,0,2.56,1.146,2.56,2.56V32.44C24.667,33.854,23.52,35,22.106,35z" opacity=".09"/><linearGradient id="Q7XamDf1hnh~bz~vAO7C6e" x1="4.744" x2="23.494" y1="14.744" y2="33.493" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#256ac2"/><stop offset="1" stop-color="#1247ad"/></linearGradient><path fill="url(#Q7XamDf1hnh~bz~vAO7C6e)" d="M22,34H6c-1.105,0-2-0.895-2-2V16c0-1.105,0.895-2,2-2h16c1.105,0,2,0.895,2,2v16 C24,33.105,23.105,34,22,34z"/><path fill="#fff" d="M18.403,19l-1.546,7.264L15.144,19h-2.187l-1.767,7.489L9.597,19H7.641l2.344,10h2.352l1.713-7.689 L15.764,29h2.251l2.344-10H18.403z"/></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

@@ -19,9 +19,9 @@ import { PDFLoader } from '@langchain/community/document_loaders/fs/pdf'
import { DocxLoader } from '@langchain/community/document_loaders/fs/docx'
import { TextLoader } from 'langchain/document_loaders/fs/text'
import { TextSplitter } from 'langchain/text_splitter'
import { CSVLoader } from '../Csv/CsvLoader'
import { LoadOfSheet } from '../MicrosoftExcel/ExcelLoader'
import { PowerpointLoader } from '../MicrosoftPowerpoint/PowerpointLoader'
class S3_DocumentLoaders implements INode {
label: string
name: string
@@ -240,7 +240,13 @@ class S3_DocumentLoaders implements INode {
'.json': (path) => new JSONLoader(path),
'.txt': (path) => new TextLoader(path),
'.csv': (path) => new CSVLoader(path),
'.xls': (path) => new LoadOfSheet(path),
'.xlsx': (path) => new LoadOfSheet(path),
'.xlsm': (path) => new LoadOfSheet(path),
'.xlsb': (path) => new LoadOfSheet(path),
'.docx': (path) => new DocxLoader(path),
'.ppt': (path) => new PowerpointLoader(path),
'.pptx': (path) => new PowerpointLoader(path),
'.pdf': (path) =>
new PDFLoader(path, {
splitPages: pdfUsage !== 'perFile',
@@ -14,12 +14,21 @@ import {
handleDocumentLoaderMetadata,
handleDocumentLoaderOutput
} from '../../../src/utils'
import { S3Client, GetObjectCommand, S3ClientConfig } from '@aws-sdk/client-s3'
import { S3Client, GetObjectCommand, HeadObjectCommand, S3ClientConfig } from '@aws-sdk/client-s3'
import { getRegions, MODEL_TYPE } from '../../../src/modelLoader'
import { Readable } from 'node:stream'
import * as fsDefault from 'node:fs'
import * as path from 'node:path'
import * as os from 'node:os'
import { PDFLoader } from '@langchain/community/document_loaders/fs/pdf'
import { DocxLoader } from '@langchain/community/document_loaders/fs/docx'
import { CSVLoader } from '@langchain/community/document_loaders/fs/csv'
import { LoadOfSheet } from '../MicrosoftExcel/ExcelLoader'
import { PowerpointLoader } from '../MicrosoftPowerpoint/PowerpointLoader'
import { TextSplitter } from 'langchain/text_splitter'
import { IDocument } from '../../../src/Interface'
import { omit } from 'lodash'
import { handleEscapeCharacters } from '../../../src'
class S3_DocumentLoaders implements INode {
label: string
@@ -37,7 +46,7 @@ class S3_DocumentLoaders implements INode {
constructor() {
this.label = 'S3'
this.name = 'S3'
this.version = 4.0
this.version = 5.0
this.type = 'Document'
this.icon = 's3.svg'
this.category = 'Document Loaders'
@@ -70,6 +79,52 @@ class S3_DocumentLoaders implements INode {
loadMethod: 'listRegions',
default: 'us-east-1'
},
{
label: 'File Processing Method',
name: 'fileProcessingMethod',
type: 'options',
options: [
{
label: 'Built In Loaders',
name: 'builtIn',
description: 'Use the built in loaders to process the file.'
},
{
label: 'Unstructured',
name: 'unstructured',
description: 'Use the Unstructured API to process the file.'
}
],
default: 'builtIn'
},
{
label: 'Text Splitter',
name: 'textSplitter',
type: 'TextSplitter',
optional: true,
show: {
fileProcessingMethod: 'builtIn'
}
},
{
label: 'Additional Metadata',
name: 'metadata',
type: 'json',
description: 'Additional metadata to be added to the extracted documents',
optional: true,
additionalParams: true
},
{
label: 'Omit Metadata Keys',
name: 'omitMetadataKeys',
type: 'string',
rows: 4,
description:
'Each document loader comes with a default set of metadata keys that are extracted from the document. You can use this field to omit some of the default metadata keys. The value should be a list of keys, seperated by comma. Use * to omit all metadata keys execept the ones you specify in the Additional Metadata field',
placeholder: 'key1, key2, key3.nestedKey1',
optional: true,
additionalParams: true
},
{
label: 'Unstructured API URL',
name: 'unstructuredAPIUrl',
@@ -77,13 +132,21 @@ class S3_DocumentLoaders implements INode {
'Your Unstructured.io URL. Read <a target="_blank" href="https://unstructured-io.github.io/unstructured/introduction.html#getting-started">more</a> on how to get started',
type: 'string',
placeholder: process.env.UNSTRUCTURED_API_URL || 'http://localhost:8000/general/v0/general',
optional: !!process.env.UNSTRUCTURED_API_URL
optional: !!process.env.UNSTRUCTURED_API_URL,
additionalParams: true,
show: {
fileProcessingMethod: 'unstructured'
}
},
{
label: 'Unstructured API KEY',
name: 'unstructuredAPIKey',
type: 'password',
optional: true
optional: true,
additionalParams: true,
show: {
fileProcessingMethod: 'unstructured'
}
},
{
label: 'Strategy',
@@ -110,7 +173,10 @@ class S3_DocumentLoaders implements INode {
],
optional: true,
additionalParams: true,
default: 'auto'
default: 'auto',
show: {
fileProcessingMethod: 'unstructured'
}
},
{
label: 'Encoding',
@@ -119,7 +185,10 @@ class S3_DocumentLoaders implements INode {
type: 'string',
optional: true,
additionalParams: true,
default: 'utf-8'
default: 'utf-8',
show: {
fileProcessingMethod: 'unstructured'
}
},
{
label: 'Skip Infer Table Types',
@@ -214,7 +283,10 @@ class S3_DocumentLoaders implements INode {
],
optional: true,
additionalParams: true,
default: '["pdf", "jpg", "png"]'
default: '["pdf", "jpg", "png"]',
show: {
fileProcessingMethod: 'unstructured'
}
},
{
label: 'Hi-Res Model Name',
@@ -247,7 +319,10 @@ class S3_DocumentLoaders implements INode {
],
optional: true,
additionalParams: true,
default: 'detectron2_onnx'
default: 'detectron2_onnx',
show: {
fileProcessingMethod: 'unstructured'
}
},
{
label: 'Chunking Strategy',
@@ -267,7 +342,10 @@ class S3_DocumentLoaders implements INode {
],
optional: true,
additionalParams: true,
default: 'by_title'
default: 'by_title',
show: {
fileProcessingMethod: 'unstructured'
}
},
{
label: 'OCR Languages',
@@ -337,7 +415,10 @@ class S3_DocumentLoaders implements INode {
}
],
optional: true,
additionalParams: true
additionalParams: true,
show: {
fileProcessingMethod: 'unstructured'
}
},
{
label: 'Source ID Key',
@@ -348,7 +429,10 @@ class S3_DocumentLoaders implements INode {
default: 'source',
placeholder: 'source',
optional: true,
additionalParams: true
additionalParams: true,
show: {
fileProcessingMethod: 'unstructured'
}
},
{
label: 'Coordinates',
@@ -357,7 +441,10 @@ class S3_DocumentLoaders implements INode {
description: 'If true, return coordinates for each element. Default: false.',
optional: true,
additionalParams: true,
default: false
default: false,
show: {
fileProcessingMethod: 'unstructured'
}
},
{
label: 'XML Keep Tags',
@@ -366,7 +453,10 @@ class S3_DocumentLoaders implements INode {
'If True, will retain the XML tags in the output. Otherwise it will simply extract the text from within the tags. Only applies to partition_xml.',
type: 'boolean',
optional: true,
additionalParams: true
additionalParams: true,
show: {
fileProcessingMethod: 'unstructured'
}
},
{
label: 'Include Page Breaks',
@@ -374,15 +464,10 @@ class S3_DocumentLoaders implements INode {
description: 'When true, the output will include page break elements when the filetype supports it.',
type: 'boolean',
optional: true,
additionalParams: true
},
{
label: 'XML Keep Tags',
name: 'xmlKeepTags',
description: 'Whether to keep XML tags in the output.',
type: 'boolean',
optional: true,
additionalParams: true
additionalParams: true,
show: {
fileProcessingMethod: 'unstructured'
}
},
{
label: 'Multi-Page Sections',
@@ -390,7 +475,10 @@ class S3_DocumentLoaders implements INode {
description: 'Whether to treat multi-page documents as separate sections.',
type: 'boolean',
optional: true,
additionalParams: true
additionalParams: true,
show: {
fileProcessingMethod: 'unstructured'
}
},
{
label: 'Combine Under N Chars',
@@ -399,7 +487,10 @@ class S3_DocumentLoaders implements INode {
"If chunking strategy is set, combine elements until a section reaches a length of n chars. Default: value of max_characters. Can't exceed value of max_characters.",
type: 'number',
optional: true,
additionalParams: true
additionalParams: true,
show: {
fileProcessingMethod: 'unstructured'
}
},
{
label: 'New After N Chars',
@@ -408,7 +499,10 @@ class S3_DocumentLoaders implements INode {
"If chunking strategy is set, cut off new sections after reaching a length of n chars (soft max). value of max_characters. Can't exceed value of max_characters.",
type: 'number',
optional: true,
additionalParams: true
additionalParams: true,
show: {
fileProcessingMethod: 'unstructured'
}
},
{
label: 'Max Characters',
@@ -418,7 +512,10 @@ class S3_DocumentLoaders implements INode {
type: 'number',
optional: true,
additionalParams: true,
default: '500'
default: '500',
show: {
fileProcessingMethod: 'unstructured'
}
},
{
label: 'Additional Metadata',
@@ -426,7 +523,10 @@ class S3_DocumentLoaders implements INode {
type: 'json',
description: 'Additional metadata to be added to the extracted documents',
optional: true,
additionalParams: true
additionalParams: true,
show: {
fileProcessingMethod: 'unstructured'
}
},
{
label: 'Omit Metadata Keys',
@@ -437,7 +537,10 @@ class S3_DocumentLoaders implements INode {
'Each document loader comes with a default set of metadata keys that are extracted from the document. You can use this field to omit some of the default metadata keys. The value should be a list of keys, seperated by comma. Use * to omit all metadata keys execept the ones you specify in the Additional Metadata field',
placeholder: 'key1, key2, key3.nestedKey1',
optional: true,
additionalParams: true
additionalParams: true,
show: {
fileProcessingMethod: 'unstructured'
}
}
]
this.outputs = [
@@ -466,6 +569,171 @@ class S3_DocumentLoaders implements INode {
const bucketName = nodeData.inputs?.bucketName as string
const keyName = nodeData.inputs?.keyName as string
const region = nodeData.inputs?.region as string
const fileProcessingMethod = nodeData.inputs?.fileProcessingMethod as string
const textSplitter = nodeData.inputs?.textSplitter as TextSplitter
const metadata = nodeData.inputs?.metadata
const _omitMetadataKeys = nodeData.inputs?.omitMetadataKeys as string
const output = nodeData.outputs?.output as string
let omitMetadataKeys: string[] = []
if (_omitMetadataKeys) {
omitMetadataKeys = _omitMetadataKeys.split(',').map((key) => key.trim())
}
let credentials: S3ClientConfig['credentials'] | undefined
if (nodeData.credential) {
const credentialData = await getCredentialData(nodeData.credential, options)
const accessKeyId = getCredentialParam('awsKey', credentialData, nodeData)
const secretAccessKey = getCredentialParam('awsSecret', credentialData, nodeData)
if (accessKeyId && secretAccessKey) {
credentials = {
accessKeyId,
secretAccessKey
}
}
}
const s3Config: S3ClientConfig = {
region,
credentials
}
if (fileProcessingMethod === 'builtIn') {
return await this.processWithBuiltInLoaders(
bucketName,
keyName,
s3Config,
textSplitter,
metadata,
omitMetadataKeys,
_omitMetadataKeys,
output
)
} else {
return await this.processWithUnstructured(nodeData, options, bucketName, keyName, s3Config)
}
}
private async processWithBuiltInLoaders(
bucketName: string,
keyName: string,
s3Config: S3ClientConfig,
textSplitter: TextSplitter,
metadata: any,
omitMetadataKeys: string[],
_omitMetadataKeys: string,
output: string
): Promise<any> {
let docs: IDocument[] = []
try {
const s3Client = new S3Client(s3Config)
// Get file metadata to determine content type
const headCommand = new HeadObjectCommand({
Bucket: bucketName,
Key: keyName
})
const headResponse = await s3Client.send(headCommand)
const contentType = headResponse.ContentType || this.getMimeTypeFromExtension(keyName)
// Download the file
const getObjectCommand = new GetObjectCommand({
Bucket: bucketName,
Key: keyName
})
const response = await s3Client.send(getObjectCommand)
const objectData = await new Promise<Buffer>((resolve, reject) => {
const chunks: Buffer[] = []
if (response.Body instanceof Readable) {
response.Body.on('data', (chunk: Buffer) => chunks.push(chunk))
response.Body.on('end', () => resolve(Buffer.concat(chunks)))
response.Body.on('error', reject)
} else {
reject(new Error('Response body is not a readable stream.'))
}
})
// Process the file based on content type
const fileInfo = {
id: keyName,
name: path.basename(keyName),
mimeType: contentType,
size: objectData.length,
webViewLink: `s3://${bucketName}/${keyName}`,
bucketName: bucketName,
key: keyName,
lastModified: headResponse.LastModified,
etag: headResponse.ETag
}
docs = await this.processFile(fileInfo, objectData)
// Apply text splitter if provided
if (textSplitter && docs.length > 0) {
docs = await textSplitter.splitDocuments(docs)
}
// Apply metadata transformations
if (metadata) {
const parsedMetadata = typeof metadata === 'object' ? metadata : JSON.parse(metadata)
docs = docs.map((doc) => ({
...doc,
metadata:
_omitMetadataKeys === '*'
? {
...parsedMetadata
}
: omit(
{
...doc.metadata,
...parsedMetadata
},
omitMetadataKeys
)
}))
} else {
docs = docs.map((doc) => ({
...doc,
metadata:
_omitMetadataKeys === '*'
? {}
: omit(
{
...doc.metadata
},
omitMetadataKeys
)
}))
}
} catch (error) {
throw new Error(`Failed to load S3 document: ${error.message}`)
}
if (output === 'document') {
return docs
} else {
let finaltext = ''
for (const doc of docs) {
finaltext += `${doc.pageContent}\n`
}
return handleEscapeCharacters(finaltext, false)
}
}
private async processWithUnstructured(
nodeData: INodeData,
options: ICommonObject,
bucketName: string,
keyName: string,
s3Config: S3ClientConfig
): Promise<any> {
const unstructuredAPIUrl = nodeData.inputs?.unstructuredAPIUrl as string
const unstructuredAPIKey = nodeData.inputs?.unstructuredAPIKey as string
const strategy = nodeData.inputs?.strategy as UnstructuredLoaderStrategy
@@ -488,26 +756,6 @@ class S3_DocumentLoaders implements INode {
const _omitMetadataKeys = nodeData.inputs?.omitMetadataKeys as string
const output = nodeData.outputs?.output as string
let credentials: S3ClientConfig['credentials'] | undefined
if (nodeData.credential) {
const credentialData = await getCredentialData(nodeData.credential, options)
const accessKeyId = getCredentialParam('awsKey', credentialData, nodeData)
const secretAccessKey = getCredentialParam('awsSecret', credentialData, nodeData)
if (accessKeyId && secretAccessKey) {
credentials = {
accessKeyId,
secretAccessKey
}
}
}
const s3Config: S3ClientConfig = {
region,
credentials
}
const loader = new S3Loader({
bucket: bucketName,
key: keyName,
@@ -586,5 +834,202 @@ class S3_DocumentLoaders implements INode {
return loader.load()
}
private getMimeTypeFromExtension(fileName: string): string {
const extension = path.extname(fileName).toLowerCase()
const mimeTypeMap: { [key: string]: string } = {
'.pdf': 'application/pdf',
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'.doc': 'application/msword',
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'.xls': 'application/vnd.ms-excel',
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'.ppt': 'application/vnd.ms-powerpoint',
'.txt': 'text/plain',
'.csv': 'text/csv',
'.html': 'text/html',
'.htm': 'text/html',
'.json': 'application/json',
'.xml': 'application/xml',
'.md': 'text/markdown'
}
return mimeTypeMap[extension] || 'application/octet-stream'
}
private async processFile(fileInfo: any, buffer: Buffer): Promise<IDocument[]> {
try {
// Handle different file types
if (this.isTextBasedFile(fileInfo.mimeType)) {
// Process text files directly from buffer
const content = buffer.toString('utf-8')
// Create document with metadata
return [
{
pageContent: content,
metadata: {
source: fileInfo.webViewLink,
fileId: fileInfo.key,
fileName: fileInfo.name,
mimeType: fileInfo.mimeType,
size: fileInfo.size,
lastModified: fileInfo.lastModified,
etag: fileInfo.etag,
bucketName: fileInfo.bucketName
}
}
]
} else if (this.isSupportedBinaryFile(fileInfo.mimeType)) {
// Process binary files using loaders
return await this.processBinaryFile(fileInfo, buffer)
} else {
console.warn(`Unsupported file type ${fileInfo.mimeType} for file ${fileInfo.name}`)
return []
}
} catch (error) {
console.warn(`Failed to process file ${fileInfo.name}: ${error.message}`)
return []
}
}
private isTextBasedFile(mimeType: string): boolean {
const textBasedMimeTypes = [
'text/plain',
'text/html',
'text/css',
'text/javascript',
'text/csv',
'text/xml',
'application/json',
'application/xml',
'text/markdown',
'text/x-markdown'
]
return textBasedMimeTypes.includes(mimeType)
}
private isSupportedBinaryFile(mimeType: string): boolean {
const supportedBinaryTypes = [
'application/pdf',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/msword',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'application/vnd.ms-powerpoint'
]
return supportedBinaryTypes.includes(mimeType)
}
private async processBinaryFile(fileInfo: any, buffer: Buffer): Promise<IDocument[]> {
let tempFilePath: string | null = null
try {
// Create temporary file
tempFilePath = await this.createTempFile(buffer, fileInfo.name, fileInfo.mimeType)
let docs: IDocument[] = []
const mimeType = fileInfo.mimeType.toLowerCase()
switch (mimeType) {
case 'application/pdf': {
const pdfLoader = new PDFLoader(tempFilePath, {
// @ts-ignore
pdfjs: () => import('pdf-parse/lib/pdf.js/v1.10.100/build/pdf.js')
})
docs = await pdfLoader.load()
break
}
case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
case 'application/msword': {
const docxLoader = new DocxLoader(tempFilePath)
docs = await docxLoader.load()
break
}
case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
case 'application/vnd.ms-excel': {
const excelLoader = new LoadOfSheet(tempFilePath)
docs = await excelLoader.load()
break
}
case 'application/vnd.openxmlformats-officedocument.presentationml.presentation':
case 'application/vnd.ms-powerpoint': {
const pptxLoader = new PowerpointLoader(tempFilePath)
docs = await pptxLoader.load()
break
}
case 'text/csv': {
const csvLoader = new CSVLoader(tempFilePath)
docs = await csvLoader.load()
break
}
default:
throw new Error(`Unsupported binary file type: ${mimeType}`)
}
// Add S3 metadata to each document
if (docs.length > 0) {
const s3Metadata = {
source: fileInfo.webViewLink,
fileId: fileInfo.key,
fileName: fileInfo.name,
mimeType: fileInfo.mimeType,
size: fileInfo.size,
lastModified: fileInfo.lastModified,
etag: fileInfo.etag,
bucketName: fileInfo.bucketName,
totalPages: docs.length // Total number of pages/sheets in the file
}
return docs.map((doc, index) => ({
...doc,
metadata: {
...doc.metadata, // Keep original loader metadata (page numbers, etc.)
...s3Metadata, // Add S3 metadata
pageIndex: index // Add page/sheet index
}
}))
}
return []
} catch (error) {
throw new Error(`Failed to process binary file: ${error.message}`)
} finally {
// Clean up temporary file
if (tempFilePath && fsDefault.existsSync(tempFilePath)) {
try {
fsDefault.unlinkSync(tempFilePath)
} catch (e) {
console.warn(`Failed to delete temporary file: ${tempFilePath}`)
}
}
}
}
private async createTempFile(buffer: Buffer, fileName: string, mimeType: string): Promise<string> {
// Get appropriate file extension
let extension = path.extname(fileName)
if (!extension) {
const extensionMap: { [key: string]: string } = {
'application/pdf': '.pdf',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx',
'application/msword': '.doc',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx',
'application/vnd.ms-excel': '.xls',
'application/vnd.openxmlformats-officedocument.presentationml.presentation': '.pptx',
'application/vnd.ms-powerpoint': '.ppt',
'text/csv': '.csv'
}
extension = extensionMap[mimeType] || '.tmp'
}
// Create temporary file
const tempDir = os.tmpdir()
const tempFileName = `s3_${Date.now()}_${Math.random().toString(36).substring(7)}${extension}`
const tempFilePath = path.join(tempDir, tempFileName)
fsDefault.writeFileSync(tempFilePath, buffer)
return tempFilePath
}
}
module.exports = { nodeClass: S3_DocumentLoaders }
@@ -22,7 +22,13 @@ import {
IStateWithMessages,
ConversationHistorySelection
} from '../../../src/Interface'
import { ToolCallingAgentOutputParser, AgentExecutor, SOURCE_DOCUMENTS_PREFIX, ARTIFACTS_PREFIX } from '../../../src/agents'
import {
ToolCallingAgentOutputParser,
AgentExecutor,
SOURCE_DOCUMENTS_PREFIX,
ARTIFACTS_PREFIX,
TOOL_ARGS_PREFIX
} from '../../../src/agents'
import {
extractOutputFromArray,
getInputVariables,
@@ -1041,6 +1047,17 @@ class ToolNode<T extends BaseMessage[] | MessagesState> extends RunnableCallable
}
}
let toolInput
if (typeof output === 'string' && output.includes(TOOL_ARGS_PREFIX)) {
const outputArray = output.split(TOOL_ARGS_PREFIX)
output = outputArray[0]
try {
toolInput = JSON.parse(outputArray[1])
} catch (e) {
console.error('Error parsing tool input from tool')
}
}
return new ToolMessage({
name: tool.name,
content: typeof output === 'string' ? output : JSON.stringify(output),
@@ -1048,11 +1065,11 @@ class ToolNode<T extends BaseMessage[] | MessagesState> extends RunnableCallable
additional_kwargs: {
sourceDocuments,
artifacts,
args: call.args,
args: toolInput ?? call.args,
usedTools: [
{
tool: tool.name ?? '',
toolInput: call.args,
toolInput: toolInput ?? call.args,
toolOutput: output
}
]
@@ -12,7 +12,7 @@ import {
import { AIMessage, AIMessageChunk, BaseMessage, ToolMessage } from '@langchain/core/messages'
import { StructuredTool } from '@langchain/core/tools'
import { RunnableConfig } from '@langchain/core/runnables'
import { ARTIFACTS_PREFIX, SOURCE_DOCUMENTS_PREFIX } from '../../../src/agents'
import { ARTIFACTS_PREFIX, SOURCE_DOCUMENTS_PREFIX, TOOL_ARGS_PREFIX } from '../../../src/agents'
import { Document } from '@langchain/core/documents'
import { DataSource } from 'typeorm'
import { MessagesState, RunnableCallable, customGet, getVM } from '../commonUtils'
@@ -448,6 +448,17 @@ class ToolNode<T extends IStateWithMessages | BaseMessage[] | MessagesState> ext
}
}
let toolInput
if (typeof output === 'string' && output.includes(TOOL_ARGS_PREFIX)) {
const outputArray = output.split(TOOL_ARGS_PREFIX)
output = outputArray[0]
try {
toolInput = JSON.parse(outputArray[1])
} catch (e) {
console.error('Error parsing tool input from tool')
}
}
return new ToolMessage({
name: tool.name,
content: typeof output === 'string' ? output : JSON.stringify(output),
@@ -455,11 +466,11 @@ class ToolNode<T extends IStateWithMessages | BaseMessage[] | MessagesState> ext
additional_kwargs: {
sourceDocuments,
artifacts,
args: call.args,
args: toolInput ?? call.args,
usedTools: [
{
tool: tool.name ?? '',
toolInput: call.args,
toolInput: toolInput ?? call.args,
toolOutput: output
}
]
@@ -0,0 +1,698 @@
import { convertMultiOptionsToStringArray, getCredentialData, getCredentialParam, refreshOAuth2Token } from '../../../src/utils'
import { createGmailTools } from './core'
import type { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
class Gmail_Tools implements INode {
label: string
name: string
version: number
type: string
icon: string
category: string
description: string
baseClasses: string[]
credential: INodeParams
inputs: INodeParams[]
constructor() {
this.label = 'Gmail'
this.name = 'gmail'
this.version = 1.0
this.type = 'Gmail'
this.icon = 'gmail.svg'
this.category = 'Tools'
this.description = 'Perform Gmail operations for drafts, messages, labels, and threads'
this.baseClasses = [this.type, 'Tool']
this.credential = {
label: 'Connect Credential',
name: 'credential',
type: 'credential',
credentialNames: ['gmailOAuth2']
}
this.inputs = [
{
label: 'Type',
name: 'gmailType',
type: 'options',
options: [
{
label: 'Drafts',
name: 'drafts'
},
{
label: 'Messages',
name: 'messages'
},
{
label: 'Labels',
name: 'labels'
},
{
label: 'Threads',
name: 'threads'
}
]
},
// Draft Actions
{
label: 'Draft Actions',
name: 'draftActions',
type: 'multiOptions',
options: [
{
label: 'List Drafts',
name: 'listDrafts'
},
{
label: 'Create Draft',
name: 'createDraft'
},
{
label: 'Get Draft',
name: 'getDraft'
},
{
label: 'Update Draft',
name: 'updateDraft'
},
{
label: 'Send Draft',
name: 'sendDraft'
},
{
label: 'Delete Draft',
name: 'deleteDraft'
}
],
show: {
gmailType: ['drafts']
}
},
// Message Actions
{
label: 'Message Actions',
name: 'messageActions',
type: 'multiOptions',
options: [
{
label: 'List Messages',
name: 'listMessages'
},
{
label: 'Get Message',
name: 'getMessage'
},
{
label: 'Send Message',
name: 'sendMessage'
},
{
label: 'Modify Message',
name: 'modifyMessage'
},
{
label: 'Trash Message',
name: 'trashMessage'
},
{
label: 'Untrash Message',
name: 'untrashMessage'
},
{
label: 'Delete Message',
name: 'deleteMessage'
}
],
show: {
gmailType: ['messages']
}
},
// Label Actions
{
label: 'Label Actions',
name: 'labelActions',
type: 'multiOptions',
options: [
{
label: 'List Labels',
name: 'listLabels'
},
{
label: 'Get Label',
name: 'getLabel'
},
{
label: 'Create Label',
name: 'createLabel'
},
{
label: 'Update Label',
name: 'updateLabel'
},
{
label: 'Delete Label',
name: 'deleteLabel'
}
],
show: {
gmailType: ['labels']
}
},
// Thread Actions
{
label: 'Thread Actions',
name: 'threadActions',
type: 'multiOptions',
options: [
{
label: 'List Threads',
name: 'listThreads'
},
{
label: 'Get Thread',
name: 'getThread'
},
{
label: 'Modify Thread',
name: 'modifyThread'
},
{
label: 'Trash Thread',
name: 'trashThread'
},
{
label: 'Untrash Thread',
name: 'untrashThread'
},
{
label: 'Delete Thread',
name: 'deleteThread'
}
],
show: {
gmailType: ['threads']
}
},
// DRAFT PARAMETERS
// List Drafts Parameters
{
label: 'Max Results',
name: 'draftMaxResults',
type: 'number',
description: 'Maximum number of drafts to return',
default: 100,
show: {
draftActions: ['listDrafts']
},
additionalParams: true,
optional: true
},
// Create Draft Parameters
{
label: 'To',
name: 'draftTo',
type: 'string',
description: 'Recipient email address(es), comma-separated',
placeholder: 'user1@example.com,user2@example.com',
show: {
draftActions: ['createDraft']
},
additionalParams: true,
optional: true
},
{
label: 'Subject',
name: 'draftSubject',
type: 'string',
description: 'Email subject',
placeholder: 'Email Subject',
show: {
draftActions: ['createDraft']
},
additionalParams: true,
optional: true
},
{
label: 'Body',
name: 'draftBody',
type: 'string',
description: 'Email body content',
placeholder: 'Email content',
rows: 4,
show: {
draftActions: ['createDraft']
},
additionalParams: true,
optional: true
},
{
label: 'CC',
name: 'draftCc',
type: 'string',
description: 'CC email address(es), comma-separated',
placeholder: 'cc1@example.com,cc2@example.com',
show: {
draftActions: ['createDraft']
},
additionalParams: true,
optional: true
},
{
label: 'BCC',
name: 'draftBcc',
type: 'string',
description: 'BCC email address(es), comma-separated',
placeholder: 'bcc1@example.com,bcc2@example.com',
show: {
draftActions: ['createDraft']
},
additionalParams: true,
optional: true
},
// Draft ID for Get/Update/Send/Delete
{
label: 'Draft ID',
name: 'draftId',
type: 'string',
description: 'ID of the draft',
show: {
draftActions: ['getDraft', 'updateDraft', 'sendDraft', 'deleteDraft']
},
additionalParams: true,
optional: true
},
// Update Draft Parameters
{
label: 'To (Update)',
name: 'draftUpdateTo',
type: 'string',
description: 'Recipient email address(es), comma-separated',
placeholder: 'user1@example.com,user2@example.com',
show: {
draftActions: ['updateDraft']
},
additionalParams: true,
optional: true
},
{
label: 'Subject (Update)',
name: 'draftUpdateSubject',
type: 'string',
description: 'Email subject',
placeholder: 'Email Subject',
show: {
draftActions: ['updateDraft']
},
additionalParams: true,
optional: true
},
{
label: 'Body (Update)',
name: 'draftUpdateBody',
type: 'string',
description: 'Email body content',
placeholder: 'Email content',
rows: 4,
show: {
draftActions: ['updateDraft']
},
additionalParams: true,
optional: true
},
// MESSAGE PARAMETERS
// List Messages Parameters
{
label: 'Max Results',
name: 'messageMaxResults',
type: 'number',
description: 'Maximum number of messages to return',
default: 100,
show: {
messageActions: ['listMessages']
},
additionalParams: true,
optional: true
},
{
label: 'Query',
name: 'messageQuery',
type: 'string',
description: 'Query string for filtering results (Gmail search syntax)',
placeholder: 'is:unread from:example@gmail.com',
show: {
messageActions: ['listMessages']
},
additionalParams: true,
optional: true
},
// Send Message Parameters
{
label: 'To',
name: 'messageTo',
type: 'string',
description: 'Recipient email address(es), comma-separated',
placeholder: 'user1@example.com,user2@example.com',
show: {
messageActions: ['sendMessage']
},
additionalParams: true,
optional: true
},
{
label: 'Subject',
name: 'messageSubject',
type: 'string',
description: 'Email subject',
placeholder: 'Email Subject',
show: {
messageActions: ['sendMessage']
},
additionalParams: true,
optional: true
},
{
label: 'Body',
name: 'messageBody',
type: 'string',
description: 'Email body content',
placeholder: 'Email content',
rows: 4,
show: {
messageActions: ['sendMessage']
},
additionalParams: true,
optional: true
},
{
label: 'CC',
name: 'messageCc',
type: 'string',
description: 'CC email address(es), comma-separated',
placeholder: 'cc1@example.com,cc2@example.com',
show: {
messageActions: ['sendMessage']
},
additionalParams: true,
optional: true
},
{
label: 'BCC',
name: 'messageBcc',
type: 'string',
description: 'BCC email address(es), comma-separated',
placeholder: 'bcc1@example.com,bcc2@example.com',
show: {
messageActions: ['sendMessage']
},
additionalParams: true,
optional: true
},
// Message ID for Get/Modify/Trash/Untrash/Delete
{
label: 'Message ID',
name: 'messageId',
type: 'string',
description: 'ID of the message',
show: {
messageActions: ['getMessage', 'modifyMessage', 'trashMessage', 'untrashMessage', 'deleteMessage']
},
additionalParams: true,
optional: true
},
// Message Label Modification
{
label: 'Add Label IDs',
name: 'messageAddLabelIds',
type: 'string',
description: 'Comma-separated label IDs to add',
placeholder: 'INBOX,STARRED',
show: {
messageActions: ['modifyMessage']
},
additionalParams: true,
optional: true
},
{
label: 'Remove Label IDs',
name: 'messageRemoveLabelIds',
type: 'string',
description: 'Comma-separated label IDs to remove',
placeholder: 'UNREAD,SPAM',
show: {
messageActions: ['modifyMessage']
},
additionalParams: true,
optional: true
},
// LABEL PARAMETERS
// Create Label Parameters
{
label: 'Label Name',
name: 'labelName',
type: 'string',
description: 'Name of the label',
placeholder: 'Important',
show: {
labelActions: ['createLabel', 'updateLabel']
},
additionalParams: true,
optional: true
},
{
label: 'Label Color',
name: 'labelColor',
type: 'string',
description: 'Color of the label (hex color code)',
placeholder: '#ff0000',
show: {
labelActions: ['createLabel', 'updateLabel']
},
additionalParams: true,
optional: true
},
// Label ID for Get/Update/Delete
{
label: 'Label ID',
name: 'labelId',
type: 'string',
description: 'ID of the label',
show: {
labelActions: ['getLabel', 'updateLabel', 'deleteLabel']
},
additionalParams: true,
optional: true
},
// THREAD PARAMETERS
// List Threads Parameters
{
label: 'Max Results',
name: 'threadMaxResults',
type: 'number',
description: 'Maximum number of threads to return',
default: 100,
show: {
threadActions: ['listThreads']
},
additionalParams: true,
optional: true
},
{
label: 'Query',
name: 'threadQuery',
type: 'string',
description: 'Query string for filtering results (Gmail search syntax)',
placeholder: 'is:unread from:example@gmail.com',
show: {
threadActions: ['listThreads']
},
additionalParams: true,
optional: true
},
// Thread ID for Get/Modify/Trash/Untrash/Delete
{
label: 'Thread ID',
name: 'threadId',
type: 'string',
description: 'ID of the thread',
show: {
threadActions: ['getThread', 'modifyThread', 'trashThread', 'untrashThread', 'deleteThread']
},
additionalParams: true,
optional: true
},
// Thread Label Modification
{
label: 'Add Label IDs',
name: 'threadAddLabelIds',
type: 'string',
description: 'Comma-separated label IDs to add',
placeholder: 'INBOX,STARRED',
show: {
threadActions: ['modifyThread']
},
additionalParams: true,
optional: true
},
{
label: 'Remove Label IDs',
name: 'threadRemoveLabelIds',
type: 'string',
description: 'Comma-separated label IDs to remove',
placeholder: 'UNREAD,SPAM',
show: {
threadActions: ['modifyThread']
},
additionalParams: true,
optional: true
}
]
}
async init(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
let credentialData = await getCredentialData(nodeData.credential ?? '', options)
credentialData = await refreshOAuth2Token(nodeData.credential ?? '', credentialData, options)
const accessToken = getCredentialParam('access_token', credentialData, nodeData)
if (!accessToken) {
throw new Error('No access token found in credential')
}
// Get all actions based on type
const gmailType = nodeData.inputs?.gmailType as string
let actions: string[] = []
if (gmailType === 'drafts') {
actions = convertMultiOptionsToStringArray(nodeData.inputs?.draftActions)
} else if (gmailType === 'messages') {
actions = convertMultiOptionsToStringArray(nodeData.inputs?.messageActions)
} else if (gmailType === 'labels') {
actions = convertMultiOptionsToStringArray(nodeData.inputs?.labelActions)
} else if (gmailType === 'threads') {
actions = convertMultiOptionsToStringArray(nodeData.inputs?.threadActions)
}
// Prepare default parameters for each action
const defaultParams: ICommonObject = {}
// Draft parameters
const draftMaxResults = nodeData.inputs?.draftMaxResults
const draftTo = nodeData.inputs?.draftTo
const draftSubject = nodeData.inputs?.draftSubject
const draftBody = nodeData.inputs?.draftBody
const draftCc = nodeData.inputs?.draftCc
const draftBcc = nodeData.inputs?.draftBcc
const draftId = nodeData.inputs?.draftId
const draftUpdateTo = nodeData.inputs?.draftUpdateTo
const draftUpdateSubject = nodeData.inputs?.draftUpdateSubject
const draftUpdateBody = nodeData.inputs?.draftUpdateBody
// Message parameters
const messageMaxResults = nodeData.inputs?.messageMaxResults
const messageQuery = nodeData.inputs?.messageQuery
const messageTo = nodeData.inputs?.messageTo
const messageSubject = nodeData.inputs?.messageSubject
const messageBody = nodeData.inputs?.messageBody
const messageCc = nodeData.inputs?.messageCc
const messageBcc = nodeData.inputs?.messageBcc
const messageId = nodeData.inputs?.messageId
const messageAddLabelIds = nodeData.inputs?.messageAddLabelIds
const messageRemoveLabelIds = nodeData.inputs?.messageRemoveLabelIds
// Label parameters
const labelName = nodeData.inputs?.labelName
const labelColor = nodeData.inputs?.labelColor
const labelId = nodeData.inputs?.labelId
// Thread parameters
const threadMaxResults = nodeData.inputs?.threadMaxResults
const threadQuery = nodeData.inputs?.threadQuery
const threadId = nodeData.inputs?.threadId
const threadAddLabelIds = nodeData.inputs?.threadAddLabelIds
const threadRemoveLabelIds = nodeData.inputs?.threadRemoveLabelIds
// Set default parameters based on actions
actions.forEach((action) => {
const params: ICommonObject = {}
// Draft action parameters
if (action.startsWith('list') && draftMaxResults) params.maxResults = draftMaxResults
if (action === 'createDraft') {
if (draftTo) params.to = draftTo
if (draftSubject) params.subject = draftSubject
if (draftBody) params.body = draftBody
if (draftCc) params.cc = draftCc
if (draftBcc) params.bcc = draftBcc
}
if (action === 'updateDraft') {
if (draftId) params.draftId = draftId
if (draftUpdateTo) params.to = draftUpdateTo
if (draftUpdateSubject) params.subject = draftUpdateSubject
if (draftUpdateBody) params.body = draftUpdateBody
}
if (['getDraft', 'sendDraft', 'deleteDraft'].includes(action) && draftId) {
params.draftId = draftId
}
// Message action parameters
if (action === 'listMessages') {
if (messageMaxResults) params.maxResults = messageMaxResults
if (messageQuery) params.query = messageQuery
}
if (action === 'sendMessage') {
if (messageTo) params.to = messageTo
if (messageSubject) params.subject = messageSubject
if (messageBody) params.body = messageBody
if (messageCc) params.cc = messageCc
if (messageBcc) params.bcc = messageBcc
}
if (['getMessage', 'trashMessage', 'untrashMessage', 'deleteMessage'].includes(action) && messageId) {
params.messageId = messageId
}
if (action === 'modifyMessage') {
if (messageId) params.messageId = messageId
if (messageAddLabelIds) params.addLabelIds = messageAddLabelIds.split(',').map((id: string) => id.trim())
if (messageRemoveLabelIds) params.removeLabelIds = messageRemoveLabelIds.split(',').map((id: string) => id.trim())
}
// Label action parameters
if (action === 'createLabel') {
if (labelName) params.labelName = labelName
if (labelColor) params.labelColor = labelColor
}
if (['getLabel', 'updateLabel', 'deleteLabel'].includes(action) && labelId) {
params.labelId = labelId
}
if (action === 'updateLabel') {
if (labelName) params.labelName = labelName
if (labelColor) params.labelColor = labelColor
}
// Thread action parameters
if (action === 'listThreads') {
if (threadMaxResults) params.maxResults = threadMaxResults
if (threadQuery) params.query = threadQuery
}
if (['getThread', 'trashThread', 'untrashThread', 'deleteThread'].includes(action) && threadId) {
params.threadId = threadId
}
if (action === 'modifyThread') {
if (threadId) params.threadId = threadId
if (threadAddLabelIds) params.addLabelIds = threadAddLabelIds.split(',').map((id: string) => id.trim())
if (threadRemoveLabelIds) params.removeLabelIds = threadRemoveLabelIds.split(',').map((id: string) => id.trim())
}
defaultParams[action] = params
})
// Create and return tools based on selected actions
const tools = createGmailTools({
actions,
accessToken,
defaultParams
})
return tools
}
}
module.exports = { nodeClass: Gmail_Tools }
File diff suppressed because it is too large Load Diff
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="96px" height="96px"><path fill="#4caf50" d="M45,16.2l-5,2.75l-5,4.75L35,40h7c1.657,0,3-1.343,3-3V16.2z"/><path fill="#1e88e5" d="M3,16.2l3.614,1.71L13,23.7V40H6c-1.657,0-3-1.343-3-3V16.2z"/><polygon fill="#e53935" points="35,11.2 24,19.45 13,11.2 12,17 13,23.7 24,31.95 35,23.7 36,17"/><path fill="#c62828" d="M3,12.298V16.2l10,7.5V11.2L9.876,8.859C9.132,8.301,8.228,8,7.298,8h0C4.924,8,3,9.924,3,12.298z"/><path fill="#fbc02d" d="M45,12.298V16.2l-10,7.5V11.2l3.124-2.341C38.868,8.301,39.772,8,40.702,8h0 C43.076,8,45,9.924,45,12.298z"/></svg>

After

Width:  |  Height:  |  Size: 611 B

@@ -0,0 +1,655 @@
import { convertMultiOptionsToStringArray, getCredentialData, getCredentialParam, refreshOAuth2Token } from '../../../src/utils'
import { createGoogleCalendarTools } from './core'
import type { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
class GoogleCalendar_Tools implements INode {
label: string
name: string
version: number
type: string
icon: string
category: string
description: string
baseClasses: string[]
credential: INodeParams
inputs: INodeParams[]
constructor() {
this.label = 'Google Calendar'
this.name = 'googleCalendarTool'
this.version = 1.0
this.type = 'GoogleCalendar'
this.icon = 'google-calendar.svg'
this.category = 'Tools'
this.description = 'Perform Google Calendar operations such as managing events, calendars, and checking availability'
this.baseClasses = ['Tool']
this.credential = {
label: 'Connect Credential',
name: 'credential',
type: 'credential',
credentialNames: ['googleCalendarOAuth2']
}
this.inputs = [
{
label: 'Type',
name: 'calendarType',
type: 'options',
description: 'Type of Google Calendar operation',
options: [
{
label: 'Event',
name: 'event'
},
{
label: 'Calendar',
name: 'calendar'
},
{
label: 'Freebusy',
name: 'freebusy'
}
]
},
// Event Actions
{
label: 'Event Actions',
name: 'eventActions',
type: 'multiOptions',
description: 'Actions to perform',
options: [
{
label: 'List Events',
name: 'listEvents'
},
{
label: 'Create Event',
name: 'createEvent'
},
{
label: 'Get Event',
name: 'getEvent'
},
{
label: 'Update Event',
name: 'updateEvent'
},
{
label: 'Delete Event',
name: 'deleteEvent'
},
{
label: 'Quick Add Event',
name: 'quickAddEvent'
}
],
show: {
calendarType: ['event']
}
},
// Calendar Actions
{
label: 'Calendar Actions',
name: 'calendarActions',
type: 'multiOptions',
description: 'Actions to perform',
options: [
{
label: 'List Calendars',
name: 'listCalendars'
},
{
label: 'Create Calendar',
name: 'createCalendar'
},
{
label: 'Get Calendar',
name: 'getCalendar'
},
{
label: 'Update Calendar',
name: 'updateCalendar'
},
{
label: 'Delete Calendar',
name: 'deleteCalendar'
},
{
label: 'Clear Calendar',
name: 'clearCalendar'
}
],
show: {
calendarType: ['calendar']
}
},
// Freebusy Actions
{
label: 'Freebusy Actions',
name: 'freebusyActions',
type: 'multiOptions',
description: 'Actions to perform',
options: [
{
label: 'Query Freebusy',
name: 'queryFreebusy'
}
],
show: {
calendarType: ['freebusy']
}
},
// Event Parameters
{
label: 'Calendar ID',
name: 'calendarId',
type: 'string',
description: 'Calendar ID (use "primary" for primary calendar)',
default: 'primary',
show: {
calendarType: ['event']
},
additionalParams: true,
optional: true
},
{
label: 'Event ID',
name: 'eventId',
type: 'string',
description: 'Event ID for operations on specific events',
show: {
eventActions: ['getEvent', 'updateEvent', 'deleteEvent']
},
additionalParams: true,
optional: true
},
{
label: 'Summary',
name: 'summary',
type: 'string',
description: 'Event title/summary',
show: {
eventActions: ['createEvent', 'updateEvent']
},
additionalParams: true,
optional: true
},
{
label: 'Description',
name: 'description',
type: 'string',
description: 'Event description',
show: {
eventActions: ['createEvent', 'updateEvent']
},
additionalParams: true,
optional: true
},
{
label: 'Location',
name: 'location',
type: 'string',
description: 'Event location',
show: {
eventActions: ['createEvent', 'updateEvent']
},
additionalParams: true,
optional: true
},
{
label: 'Start Date Time',
name: 'startDateTime',
type: 'string',
description: 'Event start time (ISO 8601 format: 2023-12-25T10:00:00)',
show: {
eventActions: ['createEvent', 'updateEvent']
},
additionalParams: true,
optional: true
},
{
label: 'End Date Time',
name: 'endDateTime',
type: 'string',
description: 'Event end time (ISO 8601 format: 2023-12-25T11:00:00)',
show: {
eventActions: ['createEvent', 'updateEvent']
},
additionalParams: true,
optional: true
},
{
label: 'Time Zone',
name: 'timeZone',
type: 'string',
description: 'Time zone (e.g., America/New_York)',
show: {
eventActions: ['createEvent', 'updateEvent']
},
additionalParams: true,
optional: true
},
{
label: 'All Day Event',
name: 'allDay',
type: 'boolean',
description: 'Whether this is an all-day event',
show: {
eventActions: ['createEvent', 'updateEvent']
},
additionalParams: true,
optional: true
},
{
label: 'Start Date',
name: 'startDate',
type: 'string',
description: 'Start date for all-day events (YYYY-MM-DD format)',
show: {
eventActions: ['createEvent', 'updateEvent']
},
additionalParams: true,
optional: true
},
{
label: 'End Date',
name: 'endDate',
type: 'string',
description: 'End date for all-day events (YYYY-MM-DD format)',
show: {
eventActions: ['createEvent', 'updateEvent']
},
additionalParams: true,
optional: true
},
{
label: 'Attendees',
name: 'attendees',
type: 'string',
description: 'Comma-separated list of attendee emails',
show: {
eventActions: ['createEvent', 'updateEvent']
},
additionalParams: true,
optional: true
},
{
label: 'Recurrence Rules',
name: 'recurrence',
type: 'string',
description: 'Recurrence rules (RRULE format)',
show: {
eventActions: ['createEvent', 'updateEvent']
},
additionalParams: true,
optional: true
},
{
label: 'Reminder Minutes',
name: 'reminderMinutes',
type: 'number',
description: 'Minutes before event to send reminder',
show: {
eventActions: ['createEvent', 'updateEvent']
},
additionalParams: true,
optional: true
},
{
label: 'Visibility',
name: 'visibility',
type: 'options',
description: 'Event visibility',
options: [
{ label: 'Default', name: 'default' },
{ label: 'Public', name: 'public' },
{ label: 'Private', name: 'private' },
{ label: 'Confidential', name: 'confidential' }
],
show: {
eventActions: ['createEvent', 'updateEvent']
},
additionalParams: true,
optional: true
},
{
label: 'Quick Add Text',
name: 'quickAddText',
type: 'string',
description: 'Natural language text for quick event creation (e.g., "Lunch with John tomorrow at 12pm")',
show: {
eventActions: ['quickAddEvent']
},
additionalParams: true,
optional: true
},
{
label: 'Time Min',
name: 'timeMin',
type: 'string',
description: 'Lower bound for event search (ISO 8601 format)',
show: {
eventActions: ['listEvents']
},
additionalParams: true,
optional: true
},
{
label: 'Time Max',
name: 'timeMax',
type: 'string',
description: 'Upper bound for event search (ISO 8601 format)',
show: {
eventActions: ['listEvents']
},
additionalParams: true,
optional: true
},
{
label: 'Max Results',
name: 'maxResults',
type: 'number',
description: 'Maximum number of events to return',
default: 250,
show: {
eventActions: ['listEvents']
},
additionalParams: true,
optional: true
},
{
label: 'Single Events',
name: 'singleEvents',
type: 'boolean',
description: 'Whether to expand recurring events into instances',
default: true,
show: {
eventActions: ['listEvents']
},
additionalParams: true,
optional: true
},
{
label: 'Order By',
name: 'orderBy',
type: 'options',
description: 'Order of events returned',
options: [
{ label: 'Start Time', name: 'startTime' },
{ label: 'Updated', name: 'updated' }
],
show: {
eventActions: ['listEvents']
},
additionalParams: true,
optional: true
},
{
label: 'Query',
name: 'query',
type: 'string',
description: 'Free text search terms',
show: {
eventActions: ['listEvents']
},
additionalParams: true,
optional: true
},
// Calendar Parameters
{
label: 'Calendar ID',
name: 'calendarIdForCalendar',
type: 'string',
description: 'Calendar ID for operations on specific calendars',
show: {
calendarActions: ['getCalendar', 'updateCalendar', 'deleteCalendar', 'clearCalendar']
},
additionalParams: true,
optional: true
},
{
label: 'Calendar Summary',
name: 'calendarSummary',
type: 'string',
description: 'Calendar title/name',
show: {
calendarActions: ['createCalendar', 'updateCalendar']
},
additionalParams: true,
optional: true
},
{
label: 'Calendar Description',
name: 'calendarDescription',
type: 'string',
description: 'Calendar description',
show: {
calendarActions: ['createCalendar', 'updateCalendar']
},
additionalParams: true,
optional: true
},
{
label: 'Calendar Location',
name: 'calendarLocation',
type: 'string',
description: 'Calendar location',
show: {
calendarActions: ['createCalendar', 'updateCalendar']
},
additionalParams: true,
optional: true
},
{
label: 'Calendar Time Zone',
name: 'calendarTimeZone',
type: 'string',
description: 'Calendar time zone (e.g., America/New_York)',
show: {
calendarActions: ['createCalendar', 'updateCalendar']
},
additionalParams: true,
optional: true
},
{
label: 'Show Hidden',
name: 'showHidden',
type: 'boolean',
description: 'Whether to show hidden calendars',
show: {
calendarActions: ['listCalendars']
},
additionalParams: true,
optional: true
},
{
label: 'Min Access Role',
name: 'minAccessRole',
type: 'options',
description: 'Minimum access role for calendar list',
options: [
{ label: 'Free/Busy Reader', name: 'freeBusyReader' },
{ label: 'Reader', name: 'reader' },
{ label: 'Writer', name: 'writer' },
{ label: 'Owner', name: 'owner' }
],
show: {
calendarActions: ['listCalendars']
},
additionalParams: true,
optional: true
},
// Freebusy Parameters
{
label: 'Time Min',
name: 'freebusyTimeMin',
type: 'string',
description: 'Lower bound for freebusy query (ISO 8601 format)',
show: {
freebusyActions: ['queryFreebusy']
},
additionalParams: true,
optional: true
},
{
label: 'Time Max',
name: 'freebusyTimeMax',
type: 'string',
description: 'Upper bound for freebusy query (ISO 8601 format)',
show: {
freebusyActions: ['queryFreebusy']
},
additionalParams: true,
optional: true
},
{
label: 'Calendar IDs',
name: 'calendarIds',
type: 'string',
description: 'Comma-separated list of calendar IDs to check for free/busy info',
show: {
freebusyActions: ['queryFreebusy']
},
additionalParams: true,
optional: true
},
{
label: 'Group Expansion Max',
name: 'groupExpansionMax',
type: 'number',
description: 'Maximum number of calendars for which FreeBusy information is to be provided',
show: {
freebusyActions: ['queryFreebusy']
},
additionalParams: true,
optional: true
},
{
label: 'Calendar Expansion Max',
name: 'calendarExpansionMax',
type: 'number',
description: 'Maximum number of events that can be expanded for each calendar',
show: {
freebusyActions: ['queryFreebusy']
},
additionalParams: true,
optional: true
}
]
}
async init(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
const calendarType = nodeData.inputs?.calendarType as string
let credentialData = await getCredentialData(nodeData.credential ?? '', options)
credentialData = await refreshOAuth2Token(nodeData.credential ?? '', credentialData, options)
const accessToken = getCredentialParam('access_token', credentialData, nodeData)
if (!accessToken) {
throw new Error('No access token found in credential')
}
// Get all actions based on type
let actions: string[] = []
if (calendarType === 'event') {
actions = convertMultiOptionsToStringArray(nodeData.inputs?.eventActions)
} else if (calendarType === 'calendar') {
actions = convertMultiOptionsToStringArray(nodeData.inputs?.calendarActions)
} else if (calendarType === 'freebusy') {
actions = convertMultiOptionsToStringArray(nodeData.inputs?.freebusyActions)
}
// Create default params object based on inputs
const defaultParams: any = {}
// Event-specific default params
if (calendarType === 'event') {
actions.forEach((action) => {
const params: any = {}
if (nodeData.inputs?.calendarId) params.calendarId = nodeData.inputs.calendarId
if (action === 'getEvent' || action === 'updateEvent' || action === 'deleteEvent') {
if (nodeData.inputs?.eventId) params.eventId = nodeData.inputs.eventId
}
if (action === 'createEvent' || action === 'updateEvent') {
if (nodeData.inputs?.summary) params.summary = nodeData.inputs.summary
if (nodeData.inputs?.description) params.description = nodeData.inputs.description
if (nodeData.inputs?.location) params.location = nodeData.inputs.location
if (nodeData.inputs?.startDateTime) params.startDateTime = nodeData.inputs.startDateTime
if (nodeData.inputs?.endDateTime) params.endDateTime = nodeData.inputs.endDateTime
if (nodeData.inputs?.timeZone) params.timeZone = nodeData.inputs.timeZone
if (nodeData.inputs?.allDay !== undefined) params.allDay = nodeData.inputs.allDay
if (nodeData.inputs?.startDate) params.startDate = nodeData.inputs.startDate
if (nodeData.inputs?.endDate) params.endDate = nodeData.inputs.endDate
if (nodeData.inputs?.attendees) params.attendees = nodeData.inputs.attendees
if (nodeData.inputs?.recurrence) params.recurrence = nodeData.inputs.recurrence
if (nodeData.inputs?.reminderMinutes) params.reminderMinutes = nodeData.inputs.reminderMinutes
if (nodeData.inputs?.visibility) params.visibility = nodeData.inputs.visibility
}
if (action === 'quickAddEvent') {
if (nodeData.inputs?.quickAddText) params.quickAddText = nodeData.inputs.quickAddText
}
if (action === 'listEvents') {
if (nodeData.inputs?.timeMin) params.timeMin = nodeData.inputs.timeMin
if (nodeData.inputs?.timeMax) params.timeMax = nodeData.inputs.timeMax
if (nodeData.inputs?.maxResults) params.maxResults = nodeData.inputs.maxResults
if (nodeData.inputs?.singleEvents !== undefined) params.singleEvents = nodeData.inputs.singleEvents
if (nodeData.inputs?.orderBy) params.orderBy = nodeData.inputs.orderBy
if (nodeData.inputs?.query) params.query = nodeData.inputs.query
}
defaultParams[action] = params
})
}
// Calendar-specific default params
if (calendarType === 'calendar') {
actions.forEach((action) => {
const params: any = {}
if (['getCalendar', 'updateCalendar', 'deleteCalendar', 'clearCalendar'].includes(action)) {
if (nodeData.inputs?.calendarIdForCalendar) params.calendarId = nodeData.inputs.calendarIdForCalendar
}
if (action === 'createCalendar' || action === 'updateCalendar') {
if (nodeData.inputs?.calendarSummary) params.summary = nodeData.inputs.calendarSummary
if (nodeData.inputs?.calendarDescription) params.description = nodeData.inputs.calendarDescription
if (nodeData.inputs?.calendarLocation) params.location = nodeData.inputs.calendarLocation
if (nodeData.inputs?.calendarTimeZone) params.timeZone = nodeData.inputs.calendarTimeZone
}
if (action === 'listCalendars') {
if (nodeData.inputs?.showHidden !== undefined) params.showHidden = nodeData.inputs.showHidden
if (nodeData.inputs?.minAccessRole) params.minAccessRole = nodeData.inputs.minAccessRole
}
defaultParams[action] = params
})
}
// Freebusy-specific default params
if (calendarType === 'freebusy') {
actions.forEach((action) => {
const params: any = {}
if (action === 'queryFreebusy') {
if (nodeData.inputs?.freebusyTimeMin) params.timeMin = nodeData.inputs.freebusyTimeMin
if (nodeData.inputs?.freebusyTimeMax) params.timeMax = nodeData.inputs.freebusyTimeMax
if (nodeData.inputs?.calendarIds) params.calendarIds = nodeData.inputs.calendarIds
if (nodeData.inputs?.groupExpansionMax) params.groupExpansionMax = nodeData.inputs.groupExpansionMax
if (nodeData.inputs?.calendarExpansionMax) params.calendarExpansionMax = nodeData.inputs.calendarExpansionMax
}
defaultParams[action] = params
})
}
const tools = createGoogleCalendarTools({
accessToken,
actions,
defaultParams
})
return tools
}
}
module.exports = { nodeClass: GoogleCalendar_Tools }
@@ -0,0 +1,864 @@
import { z } from 'zod'
import fetch from 'node-fetch'
import { DynamicStructuredTool } from '../OpenAPIToolkit/core'
import { TOOL_ARGS_PREFIX } from '../../../src/agents'
export const desc = `Use this when you want to access Google Calendar API for managing events and calendars`
export interface Headers {
[key: string]: string
}
export interface Body {
[key: string]: any
}
export interface RequestParameters {
headers?: Headers
body?: Body
url?: string
description?: string
name?: string
actions?: string[]
accessToken?: string
defaultParams?: any
}
// Define schemas for different Google Calendar operations
// Event Schemas
const ListEventsSchema = z.object({
calendarId: z.string().default('primary').describe('Calendar ID (use "primary" for primary calendar)'),
timeMin: z.string().optional().describe('Lower bound for event search (RFC3339 timestamp)'),
timeMax: z.string().optional().describe('Upper bound for event search (RFC3339 timestamp)'),
maxResults: z.number().optional().default(250).describe('Maximum number of events to return'),
singleEvents: z.boolean().optional().default(true).describe('Whether to expand recurring events into instances'),
orderBy: z.enum(['startTime', 'updated']).optional().describe('Order of events returned'),
query: z.string().optional().describe('Free text search terms')
})
const CreateEventSchema = z.object({
calendarId: z.string().default('primary').describe('Calendar ID where the event will be created'),
summary: z.string().describe('Event title/summary'),
description: z.string().optional().describe('Event description'),
location: z.string().optional().describe('Event location'),
startDateTime: z.string().optional().describe('Event start time (ISO 8601 format)'),
endDateTime: z.string().optional().describe('Event end time (ISO 8601 format)'),
startDate: z.string().optional().describe('Start date for all-day events (YYYY-MM-DD)'),
endDate: z.string().optional().describe('End date for all-day events (YYYY-MM-DD)'),
timeZone: z.string().optional().describe('Time zone (e.g., America/New_York)'),
attendees: z.string().optional().describe('Comma-separated list of attendee emails'),
recurrence: z.string().optional().describe('Recurrence rules (RRULE format)'),
reminderMinutes: z.number().optional().describe('Minutes before event to send reminder'),
visibility: z.enum(['default', 'public', 'private', 'confidential']).optional().describe('Event visibility')
})
const GetEventSchema = z.object({
calendarId: z.string().default('primary').describe('Calendar ID'),
eventId: z.string().describe('Event ID')
})
const UpdateEventSchema = z.object({
calendarId: z.string().default('primary').describe('Calendar ID'),
eventId: z.string().describe('Event ID'),
summary: z.string().optional().describe('Updated event title/summary'),
description: z.string().optional().describe('Updated event description'),
location: z.string().optional().describe('Updated event location'),
startDateTime: z.string().optional().describe('Updated event start time (ISO 8601 format)'),
endDateTime: z.string().optional().describe('Updated event end time (ISO 8601 format)'),
startDate: z.string().optional().describe('Updated start date for all-day events (YYYY-MM-DD)'),
endDate: z.string().optional().describe('Updated end date for all-day events (YYYY-MM-DD)'),
timeZone: z.string().optional().describe('Updated time zone'),
attendees: z.string().optional().describe('Updated comma-separated list of attendee emails'),
recurrence: z.string().optional().describe('Updated recurrence rules'),
reminderMinutes: z.number().optional().describe('Updated reminder minutes'),
visibility: z.enum(['default', 'public', 'private', 'confidential']).optional().describe('Updated event visibility')
})
const DeleteEventSchema = z.object({
calendarId: z.string().default('primary').describe('Calendar ID'),
eventId: z.string().describe('Event ID to delete')
})
const QuickAddEventSchema = z.object({
calendarId: z.string().default('primary').describe('Calendar ID'),
quickAddText: z.string().describe('Natural language text for quick event creation')
})
// Calendar Schemas
const ListCalendarsSchema = z.object({
showHidden: z.boolean().optional().describe('Whether to show hidden calendars'),
minAccessRole: z.enum(['freeBusyReader', 'reader', 'writer', 'owner']).optional().describe('Minimum access role')
})
const CreateCalendarSchema = z.object({
summary: z.string().describe('Calendar title/name'),
description: z.string().optional().describe('Calendar description'),
location: z.string().optional().describe('Calendar location'),
timeZone: z.string().optional().describe('Calendar time zone (e.g., America/New_York)')
})
const GetCalendarSchema = z.object({
calendarId: z.string().describe('Calendar ID')
})
const UpdateCalendarSchema = z.object({
calendarId: z.string().describe('Calendar ID'),
summary: z.string().optional().describe('Updated calendar title/name'),
description: z.string().optional().describe('Updated calendar description'),
location: z.string().optional().describe('Updated calendar location'),
timeZone: z.string().optional().describe('Updated calendar time zone')
})
const DeleteCalendarSchema = z.object({
calendarId: z.string().describe('Calendar ID to delete')
})
const ClearCalendarSchema = z.object({
calendarId: z.string().describe('Calendar ID to clear (removes all events)')
})
// Freebusy Schemas
const QueryFreebusySchema = z.object({
timeMin: z.string().describe('Lower bound for freebusy query (RFC3339 timestamp)'),
timeMax: z.string().describe('Upper bound for freebusy query (RFC3339 timestamp)'),
calendarIds: z.string().describe('Comma-separated list of calendar IDs to check for free/busy info'),
groupExpansionMax: z.number().optional().describe('Maximum number of calendars for which FreeBusy information is to be provided'),
calendarExpansionMax: z.number().optional().describe('Maximum number of events that can be expanded for each calendar')
})
class BaseGoogleCalendarTool extends DynamicStructuredTool {
protected accessToken: string = ''
constructor(args: any) {
super(args)
this.accessToken = args.accessToken ?? ''
}
async makeGoogleCalendarRequest({
endpoint,
method = 'GET',
body,
params
}: {
endpoint: string
method?: string
body?: any
params?: any
}): Promise<string> {
const url = `https://www.googleapis.com/calendar/v3/${endpoint}`
const headers = {
Authorization: `Bearer ${this.accessToken}`,
'Content-Type': 'application/json',
Accept: 'application/json',
...this.headers
}
const response = await fetch(url, {
method,
headers,
body: body ? JSON.stringify(body) : undefined
})
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Google Calendar API Error ${response.status}: ${response.statusText} - ${errorText}`)
}
const data = await response.text()
return data + TOOL_ARGS_PREFIX + JSON.stringify(params)
}
}
// Event Tools
class ListEventsTool extends BaseGoogleCalendarTool {
defaultParams: any
constructor(args: any) {
const toolInput = {
name: 'list_events',
description: 'List events from Google Calendar',
schema: ListEventsSchema,
baseUrl: '',
method: 'GET',
headers: {}
}
super({
...toolInput,
accessToken: args.accessToken
})
this.defaultParams = args.defaultParams || {}
}
async _call(arg: any): Promise<string> {
const params = { ...arg, ...this.defaultParams }
const queryParams = new URLSearchParams()
if (params.timeMin) queryParams.append('timeMin', params.timeMin)
if (params.timeMax) queryParams.append('timeMax', params.timeMax)
if (params.maxResults) queryParams.append('maxResults', params.maxResults.toString())
if (params.singleEvents !== undefined) queryParams.append('singleEvents', params.singleEvents.toString())
if (params.orderBy) queryParams.append('orderBy', params.orderBy)
if (params.query) queryParams.append('q', params.query)
const endpoint = `calendars/${encodeURIComponent(params.calendarId)}/events?${queryParams.toString()}`
try {
const response = await this.makeGoogleCalendarRequest({ endpoint, params })
return response
} catch (error) {
return `Error listing events: ${error}`
}
}
}
class CreateEventTool extends BaseGoogleCalendarTool {
defaultParams: any
constructor(args: any) {
const toolInput = {
name: 'create_event',
description: 'Create a new event in Google Calendar',
schema: CreateEventSchema,
baseUrl: '',
method: 'POST',
headers: {}
}
super({
...toolInput,
accessToken: args.accessToken
})
this.defaultParams = args.defaultParams || {}
}
async _call(arg: any): Promise<string> {
const params = { ...arg, ...this.defaultParams }
try {
const eventData: any = {
summary: params.summary
}
if (params.description) eventData.description = params.description
if (params.location) eventData.location = params.location
// Handle date/time
if (params.startDate && params.endDate) {
// All-day event
eventData.start = { date: params.startDate }
eventData.end = { date: params.endDate }
} else if (params.startDateTime && params.endDateTime) {
// Timed event
eventData.start = {
dateTime: params.startDateTime,
timeZone: params.timeZone || 'UTC'
}
eventData.end = {
dateTime: params.endDateTime,
timeZone: params.timeZone || 'UTC'
}
}
// Handle attendees
if (params.attendees) {
eventData.attendees = params.attendees.split(',').map((email: string) => ({
email: email.trim()
}))
}
// Handle recurrence
if (params.recurrence) {
eventData.recurrence = [params.recurrence]
}
// Handle reminders
if (params.reminderMinutes !== undefined) {
eventData.reminders = {
useDefault: false,
overrides: [
{
method: 'popup',
minutes: params.reminderMinutes
}
]
}
}
if (params.visibility) eventData.visibility = params.visibility
const endpoint = `calendars/${encodeURIComponent(params.calendarId)}/events`
const response = await this.makeGoogleCalendarRequest({ endpoint, method: 'POST', body: eventData, params })
return response
} catch (error) {
return `Error creating event: ${error}`
}
}
}
class GetEventTool extends BaseGoogleCalendarTool {
defaultParams: any
constructor(args: any) {
const toolInput = {
name: 'get_event',
description: 'Get a specific event from Google Calendar',
schema: GetEventSchema,
baseUrl: '',
method: 'GET',
headers: {}
}
super({
...toolInput,
accessToken: args.accessToken
})
this.defaultParams = args.defaultParams || {}
}
async _call(arg: any): Promise<string> {
const params = { ...arg, ...this.defaultParams }
try {
const endpoint = `calendars/${encodeURIComponent(params.calendarId)}/events/${encodeURIComponent(params.eventId)}`
const response = await this.makeGoogleCalendarRequest({ endpoint, params })
return response
} catch (error) {
return `Error getting event: ${error}`
}
}
}
class UpdateEventTool extends BaseGoogleCalendarTool {
defaultParams: any
constructor(args: any) {
const toolInput = {
name: 'update_event',
description: 'Update an existing event in Google Calendar',
schema: UpdateEventSchema,
baseUrl: '',
method: 'PUT',
headers: {}
}
super({
...toolInput,
accessToken: args.accessToken
})
this.defaultParams = args.defaultParams || {}
}
async _call(arg: any): Promise<string> {
const params = { ...arg, ...this.defaultParams }
try {
const updateData: any = {}
if (params.summary) updateData.summary = params.summary
if (params.description) updateData.description = params.description
if (params.location) updateData.location = params.location
// Handle date/time updates
if (params.startDate && params.endDate) {
updateData.start = { date: params.startDate }
updateData.end = { date: params.endDate }
} else if (params.startDateTime && params.endDateTime) {
updateData.start = {
dateTime: params.startDateTime,
timeZone: params.timeZone || 'UTC'
}
updateData.end = {
dateTime: params.endDateTime,
timeZone: params.timeZone || 'UTC'
}
}
if (params.attendees) {
updateData.attendees = params.attendees.split(',').map((email: string) => ({
email: email.trim()
}))
}
if (params.recurrence) {
updateData.recurrence = [params.recurrence]
}
if (params.reminderMinutes !== undefined) {
updateData.reminders = {
useDefault: false,
overrides: [
{
method: 'popup',
minutes: params.reminderMinutes
}
]
}
}
if (params.visibility) updateData.visibility = params.visibility
const endpoint = `calendars/${encodeURIComponent(params.calendarId)}/events/${encodeURIComponent(params.eventId)}`
const response = await this.makeGoogleCalendarRequest({ endpoint, method: 'PUT', body: updateData, params })
return response
} catch (error) {
return `Error updating event: ${error}`
}
}
}
class DeleteEventTool extends BaseGoogleCalendarTool {
defaultParams: any
constructor(args: any) {
const toolInput = {
name: 'delete_event',
description: 'Delete an event from Google Calendar',
schema: DeleteEventSchema,
baseUrl: '',
method: 'DELETE',
headers: {}
}
super({
...toolInput,
accessToken: args.accessToken
})
this.defaultParams = args.defaultParams || {}
}
async _call(arg: any): Promise<string> {
const params = { ...arg, ...this.defaultParams }
try {
const endpoint = `calendars/${encodeURIComponent(params.calendarId)}/events/${encodeURIComponent(params.eventId)}`
const response = await this.makeGoogleCalendarRequest({ endpoint, method: 'DELETE', params })
return response || 'Event deleted successfully'
} catch (error) {
return `Error deleting event: ${error}`
}
}
}
class QuickAddEventTool extends BaseGoogleCalendarTool {
defaultParams: any
constructor(args: any) {
const toolInput = {
name: 'quick_add_event',
description: 'Quick add event to Google Calendar using natural language',
schema: QuickAddEventSchema,
baseUrl: '',
method: 'POST',
headers: {}
}
super({
...toolInput,
accessToken: args.accessToken
})
this.defaultParams = args.defaultParams || {}
}
async _call(arg: any): Promise<string> {
const params = { ...arg, ...this.defaultParams }
try {
const queryParams = new URLSearchParams()
queryParams.append('text', params.quickAddText)
const endpoint = `calendars/${encodeURIComponent(params.calendarId)}/events/quickAdd?${queryParams.toString()}`
const response = await this.makeGoogleCalendarRequest({ endpoint, method: 'POST', params })
return response
} catch (error) {
return `Error quick adding event: ${error}`
}
}
}
// Calendar Tools
class ListCalendarsTool extends BaseGoogleCalendarTool {
defaultParams: any
constructor(args: any) {
const toolInput = {
name: 'list_calendars',
description: 'List calendars from Google Calendar',
schema: ListCalendarsSchema,
baseUrl: '',
method: 'GET',
headers: {}
}
super({
...toolInput,
accessToken: args.accessToken
})
this.defaultParams = args.defaultParams || {}
}
async _call(arg: any): Promise<string> {
const params = { ...arg, ...this.defaultParams }
const queryParams = new URLSearchParams()
if (params.showHidden !== undefined) queryParams.append('showHidden', params.showHidden.toString())
if (params.minAccessRole) queryParams.append('minAccessRole', params.minAccessRole)
const endpoint = `users/me/calendarList?${queryParams.toString()}`
try {
const response = await this.makeGoogleCalendarRequest({ endpoint, params })
return response
} catch (error) {
return `Error listing calendars: ${error}`
}
}
}
class CreateCalendarTool extends BaseGoogleCalendarTool {
defaultParams: any
constructor(args: any) {
const toolInput = {
name: 'create_calendar',
description: 'Create a new calendar in Google Calendar',
schema: CreateCalendarSchema,
baseUrl: '',
method: 'POST',
headers: {}
}
super({
...toolInput,
accessToken: args.accessToken
})
this.defaultParams = args.defaultParams || {}
}
async _call(arg: any): Promise<string> {
const params = { ...arg, ...this.defaultParams }
try {
const calendarData: any = {
summary: params.summary
}
if (params.description) calendarData.description = params.description
if (params.location) calendarData.location = params.location
if (params.timeZone) calendarData.timeZone = params.timeZone
const endpoint = 'calendars'
const response = await this.makeGoogleCalendarRequest({ endpoint, method: 'POST', body: calendarData, params })
return response
} catch (error) {
return `Error creating calendar: ${error}`
}
}
}
class GetCalendarTool extends BaseGoogleCalendarTool {
defaultParams: any
constructor(args: any) {
const toolInput = {
name: 'get_calendar',
description: 'Get a specific calendar from Google Calendar',
schema: GetCalendarSchema,
baseUrl: '',
method: 'GET',
headers: {}
}
super({
...toolInput,
accessToken: args.accessToken
})
this.defaultParams = args.defaultParams || {}
}
async _call(arg: any): Promise<string> {
const params = { ...arg, ...this.defaultParams }
try {
const endpoint = `calendars/${encodeURIComponent(params.calendarId)}`
const response = await this.makeGoogleCalendarRequest({ endpoint, params })
return response
} catch (error) {
return `Error getting calendar: ${error}`
}
}
}
class UpdateCalendarTool extends BaseGoogleCalendarTool {
defaultParams: any
constructor(args: any) {
const toolInput = {
name: 'update_calendar',
description: 'Update an existing calendar in Google Calendar',
schema: UpdateCalendarSchema,
baseUrl: '',
method: 'PUT',
headers: {}
}
super({
...toolInput,
accessToken: args.accessToken
})
this.defaultParams = args.defaultParams || {}
}
async _call(arg: any): Promise<string> {
const params = { ...arg, ...this.defaultParams }
try {
const updateData: any = {}
if (params.summary) updateData.summary = params.summary
if (params.description) updateData.description = params.description
if (params.location) updateData.location = params.location
if (params.timeZone) updateData.timeZone = params.timeZone
const endpoint = `calendars/${encodeURIComponent(params.calendarId)}`
const response = await this.makeGoogleCalendarRequest({ endpoint, method: 'PUT', body: updateData, params })
return response
} catch (error) {
return `Error updating calendar: ${error}`
}
}
}
class DeleteCalendarTool extends BaseGoogleCalendarTool {
defaultParams: any
constructor(args: any) {
const toolInput = {
name: 'delete_calendar',
description: 'Delete a calendar from Google Calendar',
schema: DeleteCalendarSchema,
baseUrl: '',
method: 'DELETE',
headers: {}
}
super({
...toolInput,
accessToken: args.accessToken
})
this.defaultParams = args.defaultParams || {}
}
async _call(arg: any): Promise<string> {
const params = { ...arg, ...this.defaultParams }
try {
const endpoint = `calendars/${encodeURIComponent(params.calendarId)}`
const response = await this.makeGoogleCalendarRequest({ endpoint, method: 'DELETE', params })
return response || 'Calendar deleted successfully'
} catch (error) {
return `Error deleting calendar: ${error}`
}
}
}
class ClearCalendarTool extends BaseGoogleCalendarTool {
defaultParams: any
constructor(args: any) {
const toolInput = {
name: 'clear_calendar',
description: 'Clear all events from a Google Calendar',
schema: ClearCalendarSchema,
baseUrl: '',
method: 'POST',
headers: {}
}
super({
...toolInput,
accessToken: args.accessToken
})
this.defaultParams = args.defaultParams || {}
}
async _call(arg: any): Promise<string> {
const params = { ...arg, ...this.defaultParams }
try {
const endpoint = `calendars/${encodeURIComponent(params.calendarId)}/clear`
const response = await this.makeGoogleCalendarRequest({ endpoint, method: 'POST', params })
return response || 'Calendar cleared successfully'
} catch (error) {
return `Error clearing calendar: ${error}`
}
}
}
// Freebusy Tools
class QueryFreebusyTool extends BaseGoogleCalendarTool {
defaultParams: any
constructor(args: any) {
const toolInput = {
name: 'query_freebusy',
description: 'Query free/busy information for a set of calendars',
schema: QueryFreebusySchema,
baseUrl: '',
method: 'POST',
headers: {}
}
super({
...toolInput,
accessToken: args.accessToken
})
this.defaultParams = args.defaultParams || {}
}
async _call(arg: any): Promise<string> {
const params = { ...arg, ...this.defaultParams }
try {
const freebusyData: any = {
timeMin: params.timeMin,
timeMax: params.timeMax,
items: params.calendarIds.split(',').map((id: string) => ({
id: id.trim()
}))
}
if (params.groupExpansionMax !== undefined) {
freebusyData.groupExpansionMax = params.groupExpansionMax
}
if (params.calendarExpansionMax !== undefined) {
freebusyData.calendarExpansionMax = params.calendarExpansionMax
}
const endpoint = 'freeBusy'
const response = await this.makeGoogleCalendarRequest({ endpoint, method: 'POST', body: freebusyData, params })
return response
} catch (error) {
return `Error querying freebusy: ${error}`
}
}
}
export const createGoogleCalendarTools = (args?: RequestParameters): DynamicStructuredTool[] => {
const tools: DynamicStructuredTool[] = []
const actions = args?.actions || []
const accessToken = args?.accessToken || ''
const defaultParams = args?.defaultParams || {}
// Event tools
if (actions.includes('listEvents')) {
tools.push(
new ListEventsTool({
accessToken,
defaultParams: defaultParams.listEvents
})
)
}
if (actions.includes('createEvent')) {
tools.push(
new CreateEventTool({
accessToken,
defaultParams: defaultParams.createEvent
})
)
}
if (actions.includes('getEvent')) {
tools.push(
new GetEventTool({
accessToken,
defaultParams: defaultParams.getEvent
})
)
}
if (actions.includes('updateEvent')) {
tools.push(
new UpdateEventTool({
accessToken,
defaultParams: defaultParams.updateEvent
})
)
}
if (actions.includes('deleteEvent')) {
tools.push(
new DeleteEventTool({
accessToken,
defaultParams: defaultParams.deleteEvent
})
)
}
if (actions.includes('quickAddEvent')) {
tools.push(
new QuickAddEventTool({
accessToken,
defaultParams: defaultParams.quickAddEvent
})
)
}
// Calendar tools
if (actions.includes('listCalendars')) {
tools.push(
new ListCalendarsTool({
accessToken,
defaultParams: defaultParams.listCalendars
})
)
}
if (actions.includes('createCalendar')) {
tools.push(
new CreateCalendarTool({
accessToken,
defaultParams: defaultParams.createCalendar
})
)
}
if (actions.includes('getCalendar')) {
tools.push(
new GetCalendarTool({
accessToken,
defaultParams: defaultParams.getCalendar
})
)
}
if (actions.includes('updateCalendar')) {
tools.push(
new UpdateCalendarTool({
accessToken,
defaultParams: defaultParams.updateCalendar
})
)
}
if (actions.includes('deleteCalendar')) {
tools.push(
new DeleteCalendarTool({
accessToken,
defaultParams: defaultParams.deleteCalendar
})
)
}
if (actions.includes('clearCalendar')) {
tools.push(
new ClearCalendarTool({
accessToken,
defaultParams: defaultParams.clearCalendar
})
)
}
// Freebusy tools
if (actions.includes('queryFreebusy')) {
tools.push(
new QueryFreebusyTool({
accessToken,
defaultParams: defaultParams.queryFreebusy
})
)
}
return tools
}
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="96px" height="96px"><rect width="22" height="22" x="13" y="13" fill="#fff"/><polygon fill="#1e88e5" points="25.68,20.92 26.688,22.36 28.272,21.208 28.272,29.56 30,29.56 30,18.616 28.56,18.616"/><path fill="#1e88e5" d="M22.943,23.745c0.625-0.574,1.013-1.37,1.013-2.249c0-1.747-1.533-3.168-3.417-3.168 c-1.602,0-2.972,1.009-3.33,2.453l1.657,0.421c0.165-0.664,0.868-1.146,1.673-1.146c0.942,0,1.709,0.646,1.709,1.44 c0,0.794-0.767,1.44-1.709,1.44h-0.997v1.728h0.997c1.081,0,1.993,0.751,1.993,1.64c0,0.904-0.866,1.64-1.931,1.64 c-0.962,0-1.784-0.61-1.914-1.418L17,26.802c0.262,1.636,1.81,2.87,3.6,2.87c2.007,0,3.64-1.511,3.64-3.368 C24.24,25.281,23.736,24.363,22.943,23.745z"/><polygon fill="#fbc02d" points="34,42 14,42 13,38 14,34 34,34 35,38"/><polygon fill="#4caf50" points="38,35 42,34 42,14 38,13 34,14 34,34"/><path fill="#1e88e5" d="M34,14l1-4l-1-4H9C7.343,6,6,7.343,6,9v25l4,1l4-1V14H34z"/><polygon fill="#e53935" points="34,34 34,42 42,34"/><path fill="#1565c0" d="M39,6h-5v8h8V9C42,7.343,40.657,6,39,6z"/><path fill="#1565c0" d="M9,42h5v-8H6v5C6,40.657,7.343,42,9,42z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,657 @@
import { convertMultiOptionsToStringArray, getCredentialData, getCredentialParam, refreshOAuth2Token } from '../../../src/utils'
import { createGoogleDriveTools } from './core'
import type { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
class GoogleDrive_Tools implements INode {
label: string
name: string
version: number
type: string
icon: string
category: string
description: string
baseClasses: string[]
credential: INodeParams
inputs: INodeParams[]
constructor() {
this.label = 'Google Drive'
this.name = 'googleDriveTool'
this.version = 1.0
this.type = 'GoogleDrive'
this.icon = 'google-drive.svg'
this.category = 'Tools'
this.description = 'Perform Google Drive operations such as managing files, folders, sharing, and searching'
this.baseClasses = ['Tool']
this.credential = {
label: 'Connect Credential',
name: 'credential',
type: 'credential',
credentialNames: ['googleDriveOAuth2']
}
this.inputs = [
{
label: 'Type',
name: 'driveType',
type: 'options',
description: 'Type of Google Drive operation',
options: [
{
label: 'File',
name: 'file'
},
{
label: 'Folder',
name: 'folder'
},
{
label: 'Search',
name: 'search'
},
{
label: 'Share',
name: 'share'
}
]
},
// File Actions
{
label: 'File Actions',
name: 'fileActions',
type: 'multiOptions',
description: 'Actions to perform on files',
options: [
{
label: 'List Files',
name: 'listFiles'
},
{
label: 'Get File',
name: 'getFile'
},
{
label: 'Create File',
name: 'createFile'
},
{
label: 'Update File',
name: 'updateFile'
},
{
label: 'Delete File',
name: 'deleteFile'
},
{
label: 'Copy File',
name: 'copyFile'
},
{
label: 'Download File',
name: 'downloadFile'
}
],
show: {
driveType: ['file']
}
},
// Folder Actions
{
label: 'Folder Actions',
name: 'folderActions',
type: 'multiOptions',
description: 'Actions to perform on folders',
options: [
{
label: 'Create Folder',
name: 'createFolder'
},
{
label: 'List Folder Contents',
name: 'listFolderContents'
},
{
label: 'Delete Folder',
name: 'deleteFolder'
}
],
show: {
driveType: ['folder']
}
},
// Search Actions
{
label: 'Search Actions',
name: 'searchActions',
type: 'multiOptions',
description: 'Search operations',
options: [
{
label: 'Search Files',
name: 'searchFiles'
}
],
show: {
driveType: ['search']
}
},
// Share Actions
{
label: 'Share Actions',
name: 'shareActions',
type: 'multiOptions',
description: 'Sharing operations',
options: [
{
label: 'Share File',
name: 'shareFile'
},
{
label: 'Get Permissions',
name: 'getPermissions'
},
{
label: 'Remove Permission',
name: 'removePermission'
}
],
show: {
driveType: ['share']
}
},
// File Parameters
{
label: 'File ID',
name: 'fileId',
type: 'string',
description: 'File ID for file operations',
show: {
fileActions: ['getFile', 'updateFile', 'deleteFile', 'copyFile', 'downloadFile']
},
additionalParams: true,
optional: true
},
{
label: 'File ID',
name: 'fileId',
type: 'string',
description: 'File ID for sharing operations',
show: {
shareActions: ['shareFile', 'getPermissions', 'removePermission']
},
additionalParams: true,
optional: true
},
{
label: 'Folder ID',
name: 'folderId',
type: 'string',
description: 'Folder ID for folder operations',
show: {
folderActions: ['listFolderContents', 'deleteFolder']
},
additionalParams: true,
optional: true
},
{
label: 'Permission ID',
name: 'permissionId',
type: 'string',
description: 'Permission ID to remove',
show: {
shareActions: ['removePermission']
},
additionalParams: true,
optional: true
},
{
label: 'File Name',
name: 'fileName',
type: 'string',
description: 'Name of the file',
show: {
fileActions: ['createFile', 'copyFile']
},
additionalParams: true,
optional: true
},
{
label: 'Folder Name',
name: 'fileName',
type: 'string',
description: 'Name of the folder',
show: {
folderActions: ['createFolder']
},
additionalParams: true,
optional: true
},
{
label: 'File Content',
name: 'fileContent',
type: 'string',
description: 'Content of the file (for text files)',
show: {
fileActions: ['createFile']
},
additionalParams: true,
optional: true
},
{
label: 'MIME Type',
name: 'mimeType',
type: 'string',
description: 'MIME type of the file (e.g., text/plain, application/pdf)',
show: {
fileActions: ['createFile']
},
additionalParams: true,
optional: true
},
{
label: 'Parent Folder ID',
name: 'parentFolderId',
type: 'string',
description: 'ID of the parent folder (comma-separated for multiple parents)',
show: {
fileActions: ['createFile', 'copyFile']
},
additionalParams: true,
optional: true
},
{
label: 'Parent Folder ID',
name: 'parentFolderId',
type: 'string',
description: 'ID of the parent folder for the new folder',
show: {
folderActions: ['createFolder']
},
additionalParams: true,
optional: true
},
{
label: 'File Description',
name: 'description',
type: 'string',
description: 'File description',
show: {
fileActions: ['createFile', 'updateFile']
},
additionalParams: true,
optional: true
},
{
label: 'Folder Description',
name: 'description',
type: 'string',
description: 'Folder description',
show: {
folderActions: ['createFolder']
},
additionalParams: true,
optional: true
},
// Search Parameters
{
label: 'Search Query',
name: 'searchQuery',
type: 'string',
description: 'Search query using Google Drive search syntax',
show: {
searchActions: ['searchFiles']
},
additionalParams: true,
optional: true
},
{
label: 'Max Results',
name: 'maxResults',
type: 'number',
description: 'Maximum number of results to return (1-1000)',
default: 10,
show: {
fileActions: ['listFiles']
},
additionalParams: true,
optional: true
},
{
label: 'Max Results',
name: 'maxResults',
type: 'number',
description: 'Maximum number of results to return (1-1000)',
default: 10,
show: {
searchActions: ['searchFiles']
},
additionalParams: true,
optional: true
},
{
label: 'Order By',
name: 'orderBy',
type: 'options',
description: 'Sort order for file results',
options: [
{
label: 'Name',
name: 'name'
},
{
label: 'Created Time',
name: 'createdTime'
},
{
label: 'Modified Time',
name: 'modifiedTime'
},
{
label: 'Size',
name: 'quotaBytesUsed'
},
{
label: 'Folder',
name: 'folder'
}
],
show: {
fileActions: ['listFiles']
},
additionalParams: true,
optional: true
},
{
label: 'Order By',
name: 'orderBy',
type: 'options',
description: 'Sort order for search results',
options: [
{
label: 'Name',
name: 'name'
},
{
label: 'Created Time',
name: 'createdTime'
},
{
label: 'Modified Time',
name: 'modifiedTime'
},
{
label: 'Size',
name: 'quotaBytesUsed'
},
{
label: 'Folder',
name: 'folder'
}
],
show: {
searchActions: ['searchFiles']
},
additionalParams: true,
optional: true
},
// Share Parameters
{
label: 'Share Role',
name: 'shareRole',
type: 'options',
description: 'Permission role for sharing',
options: [
{
label: 'Reader',
name: 'reader'
},
{
label: 'Writer',
name: 'writer'
},
{
label: 'Commenter',
name: 'commenter'
},
{
label: 'Owner',
name: 'owner'
}
],
show: {
shareActions: ['shareFile']
},
additionalParams: true,
optional: true
},
{
label: 'Share Type',
name: 'shareType',
type: 'options',
description: 'Type of permission',
options: [
{
label: 'User',
name: 'user'
},
{
label: 'Group',
name: 'group'
},
{
label: 'Domain',
name: 'domain'
},
{
label: 'Anyone',
name: 'anyone'
}
],
show: {
shareActions: ['shareFile']
},
additionalParams: true,
optional: true
},
{
label: 'Email Address',
name: 'emailAddress',
type: 'string',
description: 'Email address for user/group sharing',
show: {
shareActions: ['shareFile']
},
additionalParams: true,
optional: true
},
{
label: 'Domain Name',
name: 'domainName',
type: 'string',
description: 'Domain name for domain sharing',
show: {
shareActions: ['shareFile']
},
additionalParams: true,
optional: true
},
{
label: 'Send Notification Email',
name: 'sendNotificationEmail',
type: 'boolean',
description: 'Whether to send notification emails when sharing',
default: true,
show: {
shareActions: ['shareFile']
},
additionalParams: true,
optional: true
},
{
label: 'Email Message',
name: 'emailMessage',
type: 'string',
description: 'Custom message to include in notification email',
show: {
shareActions: ['shareFile']
},
additionalParams: true,
optional: true
},
// Advanced Parameters for File Actions
{
label: 'Include Items From All Drives',
name: 'includeItemsFromAllDrives',
type: 'boolean',
description: 'Include items from all drives (shared drives)',
show: {
fileActions: ['listFiles']
},
additionalParams: true,
optional: true
},
{
label: 'Include Items From All Drives',
name: 'includeItemsFromAllDrives',
type: 'boolean',
description: 'Include items from all drives (shared drives)',
show: {
searchActions: ['searchFiles']
},
additionalParams: true,
optional: true
},
{
label: 'Supports All Drives',
name: 'supportsAllDrives',
type: 'boolean',
description: 'Whether the application supports both My Drives and shared drives',
show: {
fileActions: ['listFiles', 'getFile', 'createFile', 'updateFile', 'deleteFile', 'copyFile', 'downloadFile']
},
additionalParams: true,
optional: true
},
{
label: 'Supports All Drives',
name: 'supportsAllDrives',
type: 'boolean',
description: 'Whether the application supports both My Drives and shared drives',
show: {
folderActions: ['createFolder', 'listFolderContents', 'deleteFolder']
},
additionalParams: true,
optional: true
},
{
label: 'Supports All Drives',
name: 'supportsAllDrives',
type: 'boolean',
description: 'Whether the application supports both My Drives and shared drives',
show: {
searchActions: ['searchFiles']
},
additionalParams: true,
optional: true
},
{
label: 'Supports All Drives',
name: 'supportsAllDrives',
type: 'boolean',
description: 'Whether the application supports both My Drives and shared drives',
show: {
shareActions: ['shareFile', 'getPermissions', 'removePermission']
},
additionalParams: true,
optional: true
},
{
label: 'Fields',
name: 'fields',
type: 'string',
description: 'Specific fields to include in response (e.g., "files(id,name,mimeType)")',
show: {
fileActions: ['listFiles', 'getFile']
},
additionalParams: true,
optional: true
},
{
label: 'Acknowledge Abuse',
name: 'acknowledgeAbuse',
type: 'boolean',
description: 'Acknowledge the risk of downloading known malware or abusive files',
show: {
fileActions: ['getFile', 'downloadFile']
},
additionalParams: true,
optional: true
}
]
}
async init(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
let credentialData = await getCredentialData(nodeData.credential ?? '', options)
credentialData = await refreshOAuth2Token(nodeData.credential ?? '', credentialData, options)
const accessToken = getCredentialParam('access_token', credentialData, nodeData)
if (!accessToken) {
throw new Error('No access token found in credential')
}
const driveType = nodeData.inputs?.driveType as string
const fileActions = convertMultiOptionsToStringArray(nodeData.inputs?.fileActions)
const folderActions = convertMultiOptionsToStringArray(nodeData.inputs?.folderActions)
const searchActions = convertMultiOptionsToStringArray(nodeData.inputs?.searchActions)
const shareActions = convertMultiOptionsToStringArray(nodeData.inputs?.shareActions)
// Combine all actions based on type
let actions: string[] = []
if (driveType === 'file') {
actions = fileActions
} else if (driveType === 'folder') {
actions = folderActions
} else if (driveType === 'search') {
actions = searchActions
} else if (driveType === 'share') {
actions = shareActions
}
// Collect default parameters from inputs
const defaultParams: any = {}
// Add parameters based on the inputs provided
if (nodeData.inputs?.fileId) defaultParams.fileId = nodeData.inputs.fileId
if (nodeData.inputs?.folderId) defaultParams.folderId = nodeData.inputs.folderId
if (nodeData.inputs?.permissionId) defaultParams.permissionId = nodeData.inputs.permissionId
if (nodeData.inputs?.fileName) defaultParams.name = nodeData.inputs.fileName
if (nodeData.inputs?.fileContent) defaultParams.content = nodeData.inputs.fileContent
if (nodeData.inputs?.mimeType) defaultParams.mimeType = nodeData.inputs.mimeType
if (nodeData.inputs?.parentFolderId) defaultParams.parents = nodeData.inputs.parentFolderId
if (nodeData.inputs?.description) defaultParams.description = nodeData.inputs.description
if (nodeData.inputs?.searchQuery) defaultParams.query = nodeData.inputs.searchQuery
if (nodeData.inputs?.maxResults) defaultParams.pageSize = nodeData.inputs.maxResults
if (nodeData.inputs?.orderBy) defaultParams.orderBy = nodeData.inputs.orderBy
if (nodeData.inputs?.shareRole) defaultParams.role = nodeData.inputs.shareRole
if (nodeData.inputs?.shareType) defaultParams.type = nodeData.inputs.shareType
if (nodeData.inputs?.emailAddress) defaultParams.emailAddress = nodeData.inputs.emailAddress
if (nodeData.inputs?.domainName) defaultParams.domain = nodeData.inputs.domainName
if (nodeData.inputs?.sendNotificationEmail !== undefined)
defaultParams.sendNotificationEmail = nodeData.inputs.sendNotificationEmail
if (nodeData.inputs?.emailMessage) defaultParams.emailMessage = nodeData.inputs.emailMessage
if (nodeData.inputs?.includeItemsFromAllDrives !== undefined)
defaultParams.includeItemsFromAllDrives = nodeData.inputs.includeItemsFromAllDrives
if (nodeData.inputs?.supportsAllDrives !== undefined) defaultParams.supportsAllDrives = nodeData.inputs.supportsAllDrives
if (nodeData.inputs?.fields) defaultParams.fields = nodeData.inputs.fields
if (nodeData.inputs?.acknowledgeAbuse !== undefined) defaultParams.acknowledgeAbuse = nodeData.inputs.acknowledgeAbuse
const tools = createGoogleDriveTools({
accessToken,
actions,
defaultParams
})
return tools
}
}
module.exports = { nodeClass: GoogleDrive_Tools }
@@ -0,0 +1,982 @@
import { z } from 'zod'
import fetch from 'node-fetch'
import { DynamicStructuredTool } from '../OpenAPIToolkit/core'
import { TOOL_ARGS_PREFIX } from '../../../src/agents'
export const desc = `Use this when you want to access Google Drive API for managing files and folders`
export interface Headers {
[key: string]: string
}
export interface Body {
[key: string]: any
}
export interface RequestParameters {
headers?: Headers
body?: Body
url?: string
description?: string
name?: string
actions?: string[]
accessToken?: string
defaultParams?: any
}
// Define schemas for different Google Drive operations
// File Schemas
const ListFilesSchema = z.object({
pageSize: z.number().optional().default(10).describe('Maximum number of files to return (1-1000)'),
pageToken: z.string().optional().describe('Token for next page of results'),
orderBy: z.string().optional().describe('Sort order (name, folder, createdTime, modifiedTime, etc.)'),
query: z.string().optional().describe('Search query (e.g., "name contains \'hello\'")'),
spaces: z.string().optional().default('drive').describe('Spaces to search (drive, appDataFolder, photos)'),
fields: z.string().optional().describe('Fields to include in response'),
includeItemsFromAllDrives: z.boolean().optional().describe('Include items from all drives'),
supportsAllDrives: z.boolean().optional().describe('Whether the requesting application supports both My Drives and shared drives')
})
const GetFileSchema = z.object({
fileId: z.string().describe('File ID'),
fields: z.string().optional().describe('Fields to include in response'),
supportsAllDrives: z.boolean().optional().describe('Whether the requesting application supports both My Drives and shared drives'),
acknowledgeAbuse: z
.boolean()
.optional()
.describe('Whether the user is acknowledging the risk of downloading known malware or other abusive files')
})
const CreateFileSchema = z.object({
name: z.string().describe('File name'),
parents: z.string().optional().describe('Comma-separated list of parent folder IDs'),
mimeType: z.string().optional().describe('MIME type of the file'),
description: z.string().optional().describe('File description'),
content: z.string().optional().describe('File content (for text files)'),
supportsAllDrives: z.boolean().optional().describe('Whether the requesting application supports both My Drives and shared drives')
})
const UpdateFileSchema = z.object({
fileId: z.string().describe('File ID to update'),
name: z.string().optional().describe('New file name'),
description: z.string().optional().describe('New file description'),
starred: z.boolean().optional().describe('Whether the file is starred'),
trashed: z.boolean().optional().describe('Whether the file is trashed'),
parents: z.string().optional().describe('Comma-separated list of new parent folder IDs'),
supportsAllDrives: z.boolean().optional().describe('Whether the requesting application supports both My Drives and shared drives')
})
const DeleteFileSchema = z.object({
fileId: z.string().describe('File ID to delete'),
supportsAllDrives: z.boolean().optional().describe('Whether the requesting application supports both My Drives and shared drives')
})
const CopyFileSchema = z.object({
fileId: z.string().describe('File ID to copy'),
name: z.string().describe('Name for the copied file'),
parents: z.string().optional().describe('Comma-separated list of parent folder IDs for the copy'),
supportsAllDrives: z.boolean().optional().describe('Whether the requesting application supports both My Drives and shared drives')
})
const DownloadFileSchema = z.object({
fileId: z.string().describe('File ID to download'),
acknowledgeAbuse: z
.boolean()
.optional()
.describe('Whether the user is acknowledging the risk of downloading known malware or other abusive files'),
supportsAllDrives: z.boolean().optional().describe('Whether the requesting application supports both My Drives and shared drives')
})
const CreateFolderSchema = z.object({
name: z.string().describe('Folder name'),
parents: z.string().optional().describe('Comma-separated list of parent folder IDs'),
description: z.string().optional().describe('Folder description'),
supportsAllDrives: z.boolean().optional().describe('Whether the requesting application supports both My Drives and shared drives')
})
const SearchFilesSchema = z.object({
query: z.string().describe('Search query using Google Drive search syntax'),
pageSize: z.number().optional().default(10).describe('Maximum number of files to return'),
orderBy: z.string().optional().describe('Sort order'),
includeItemsFromAllDrives: z.boolean().optional().describe('Include items from all drives'),
supportsAllDrives: z.boolean().optional().describe('Whether the requesting application supports both My Drives and shared drives')
})
const ShareFileSchema = z.object({
fileId: z.string().describe('File ID to share'),
role: z.enum(['reader', 'writer', 'commenter', 'owner']).describe('Permission role'),
type: z.enum(['user', 'group', 'domain', 'anyone']).describe('Permission type'),
emailAddress: z.string().optional().describe('Email address (required for user/group types)'),
domain: z.string().optional().describe('Domain name (required for domain type)'),
allowFileDiscovery: z.boolean().optional().describe('Whether the file can be discovered by search'),
sendNotificationEmail: z.boolean().optional().default(true).describe('Whether to send notification emails'),
emailMessage: z.string().optional().describe('Custom message to include in notification email'),
supportsAllDrives: z.boolean().optional().describe('Whether the requesting application supports both My Drives and shared drives')
})
class BaseGoogleDriveTool extends DynamicStructuredTool {
protected accessToken: string = ''
constructor(args: any) {
super(args)
this.accessToken = args.accessToken ?? ''
}
async makeGoogleDriveRequest({
endpoint,
method = 'GET',
body,
params
}: {
endpoint: string
method?: string
body?: any
params?: any
}): Promise<string> {
const baseUrl = 'https://www.googleapis.com/drive/v3'
const url = `${baseUrl}/${endpoint}`
const headers: { [key: string]: string } = {
Authorization: `Bearer ${this.accessToken}`,
Accept: 'application/json',
...this.headers
}
if (method !== 'GET' && body) {
headers['Content-Type'] = 'application/json'
}
const response = await fetch(url, {
method,
headers,
body: body ? (typeof body === 'string' ? body : JSON.stringify(body)) : undefined
})
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Google Drive API Error ${response.status}: ${response.statusText} - ${errorText}`)
}
const data = await response.text()
return data + TOOL_ARGS_PREFIX + JSON.stringify(params)
}
}
// File Tools
class ListFilesTool extends BaseGoogleDriveTool {
defaultParams: any
constructor(args: any) {
const toolInput = {
name: 'list_files',
description: 'List files and folders from Google Drive',
schema: ListFilesSchema,
baseUrl: '',
method: 'GET',
headers: {}
}
super({
...toolInput,
accessToken: args.accessToken
})
this.defaultParams = args.defaultParams || {}
}
async _call(arg: any): Promise<string> {
const params = { ...arg, ...this.defaultParams }
const queryParams = new URLSearchParams()
if (params.pageSize) queryParams.append('pageSize', params.pageSize.toString())
if (params.pageToken) queryParams.append('pageToken', params.pageToken)
if (params.orderBy) queryParams.append('orderBy', params.orderBy)
if (params.query) queryParams.append('q', params.query)
if (params.spaces) queryParams.append('spaces', params.spaces)
if (params.fields) queryParams.append('fields', params.fields)
if (params.includeItemsFromAllDrives) queryParams.append('includeItemsFromAllDrives', params.includeItemsFromAllDrives.toString())
if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString())
const endpoint = `files?${queryParams.toString()}`
try {
const response = await this.makeGoogleDriveRequest({ endpoint, params })
return response
} catch (error) {
return `Error listing files: ${error}`
}
}
}
class GetFileTool extends BaseGoogleDriveTool {
defaultParams: any
constructor(args: any) {
const toolInput = {
name: 'get_file',
description: 'Get file metadata from Google Drive',
schema: GetFileSchema,
baseUrl: '',
method: 'GET',
headers: {}
}
super({
...toolInput,
accessToken: args.accessToken
})
this.defaultParams = args.defaultParams || {}
}
async _call(arg: any): Promise<string> {
const params = { ...arg, ...this.defaultParams }
const queryParams = new URLSearchParams()
if (params.fields) queryParams.append('fields', params.fields)
if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString())
if (params.acknowledgeAbuse) queryParams.append('acknowledgeAbuse', params.acknowledgeAbuse.toString())
const endpoint = `files/${encodeURIComponent(params.fileId)}?${queryParams.toString()}`
try {
const response = await this.makeGoogleDriveRequest({ endpoint, params })
return response
} catch (error) {
return `Error getting file: ${error}`
}
}
}
class CreateFileTool extends BaseGoogleDriveTool {
defaultParams: any
constructor(args: any) {
const toolInput = {
name: 'create_file',
description: 'Create a new file in Google Drive',
schema: CreateFileSchema,
baseUrl: '',
method: 'POST',
headers: {}
}
super({
...toolInput,
accessToken: args.accessToken
})
this.defaultParams = args.defaultParams || {}
}
async _call(arg: any): Promise<string> {
const params = { ...arg, ...this.defaultParams }
try {
// Validate required parameters
if (!params.name) {
throw new Error('File name is required')
}
const queryParams = new URLSearchParams()
if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString())
// Prepare metadata
const fileMetadata: any = {
name: params.name
}
if (params.parents) {
// Validate parent folder IDs format
const parentIds = params.parents
.split(',')
.map((p: string) => p.trim())
.filter((p: string) => p.length > 0)
if (parentIds.length > 0) {
fileMetadata.parents = parentIds
}
}
if (params.mimeType) fileMetadata.mimeType = params.mimeType
if (params.description) fileMetadata.description = params.description
// Determine upload type based on content and metadata
if (!params.content) {
// Metadata-only upload (no file content) - standard endpoint
const endpoint = `files?${queryParams.toString()}`
const response = await this.makeGoogleDriveRequest({
endpoint,
method: 'POST',
body: fileMetadata,
params
})
return response
} else {
// Validate content
if (typeof params.content !== 'string') {
throw new Error('File content must be a string')
}
// Check if we have metadata beyond just the name
const hasAdditionalMetadata = params.parents || params.description || params.mimeType
if (!hasAdditionalMetadata) {
// Simple upload (uploadType=media) - only file content, basic metadata
return await this.performSimpleUpload(params, queryParams)
} else {
// Multipart upload (uploadType=multipart) - file content + metadata
return await this.performMultipartUpload(params, fileMetadata, queryParams)
}
}
} catch (error) {
return `Error creating file: ${error}`
}
}
private async performSimpleUpload(params: any, queryParams: URLSearchParams): Promise<string> {
// Simple upload: POST https://www.googleapis.com/upload/drive/v3/files?uploadType=media
queryParams.append('uploadType', 'media')
const url = `https://www.googleapis.com/upload/drive/v3/files?${queryParams.toString()}`
const headers: { [key: string]: string } = {
Authorization: `Bearer ${this.accessToken}`,
'Content-Type': params.mimeType || 'application/octet-stream',
'Content-Length': Buffer.byteLength(params.content, 'utf8').toString()
}
const response = await fetch(url, {
method: 'POST',
headers,
body: params.content
})
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Google Drive API Error ${response.status}: ${response.statusText} - ${errorText}`)
}
const data = await response.text()
return data + TOOL_ARGS_PREFIX + JSON.stringify(params)
}
private async performMultipartUpload(params: any, fileMetadata: any, queryParams: URLSearchParams): Promise<string> {
// Multipart upload: POST https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart
queryParams.append('uploadType', 'multipart')
const url = `https://www.googleapis.com/upload/drive/v3/files?${queryParams.toString()}`
// Create multipart/related body according to RFC 2387
const boundary = '-------314159265358979323846'
// Build multipart body - RFC 2387 format
let body = `--${boundary}\r\n`
// Part 1: Metadata (application/json; charset=UTF-8)
body += 'Content-Type: application/json; charset=UTF-8\r\n\r\n'
body += JSON.stringify(fileMetadata) + '\r\n'
// Part 2: Media content (any MIME type)
body += `--${boundary}\r\n`
body += `Content-Type: ${params.mimeType || 'application/octet-stream'}\r\n\r\n`
body += params.content + '\r\n'
// Close boundary
body += `--${boundary}--`
const headers: { [key: string]: string } = {
Authorization: `Bearer ${this.accessToken}`,
'Content-Type': `multipart/related; boundary="${boundary}"`,
'Content-Length': Buffer.byteLength(body, 'utf8').toString()
}
try {
const response = await fetch(url, {
method: 'POST',
headers,
body: body
})
if (!response.ok) {
const errorText = await response.text()
console.error('Multipart upload failed:', {
url,
headers: { ...headers, Authorization: '[REDACTED]' },
metadata: fileMetadata,
contentLength: params.content?.length || 0,
error: errorText
})
throw new Error(`Google Drive API Error ${response.status}: ${response.statusText} - ${errorText}`)
}
const data = await response.text()
return data + TOOL_ARGS_PREFIX + JSON.stringify(params)
} catch (error) {
throw new Error(`Multipart upload failed: ${error}`)
}
}
}
class UpdateFileTool extends BaseGoogleDriveTool {
defaultParams: any
constructor(args: any) {
const toolInput = {
name: 'update_file',
description: 'Update file metadata in Google Drive',
schema: UpdateFileSchema,
baseUrl: '',
method: 'PATCH',
headers: {}
}
super({
...toolInput,
accessToken: args.accessToken
})
this.defaultParams = args.defaultParams || {}
}
async _call(arg: any): Promise<string> {
const params = { ...arg, ...this.defaultParams }
try {
const updateData: any = {}
if (params.name) updateData.name = params.name
if (params.description) updateData.description = params.description
if (params.starred !== undefined) updateData.starred = params.starred
if (params.trashed !== undefined) updateData.trashed = params.trashed
const queryParams = new URLSearchParams()
if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString())
const endpoint = `files/${encodeURIComponent(params.fileId)}?${queryParams.toString()}`
const response = await this.makeGoogleDriveRequest({
endpoint,
method: 'PATCH',
body: updateData,
params
})
return response
} catch (error) {
return `Error updating file: ${error}`
}
}
}
class DeleteFileTool extends BaseGoogleDriveTool {
defaultParams: any
constructor(args: any) {
const toolInput = {
name: 'delete_file',
description: 'Delete a file from Google Drive',
schema: DeleteFileSchema,
baseUrl: '',
method: 'DELETE',
headers: {}
}
super({
...toolInput,
accessToken: args.accessToken
})
this.defaultParams = args.defaultParams || {}
}
async _call(arg: any): Promise<string> {
const params = { ...arg, ...this.defaultParams }
try {
const queryParams = new URLSearchParams()
if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString())
const endpoint = `files/${encodeURIComponent(params.fileId)}?${queryParams.toString()}`
await this.makeGoogleDriveRequest({
endpoint,
method: 'DELETE',
params
})
return `File deleted successfully`
} catch (error) {
return `Error deleting file: ${error}`
}
}
}
class CopyFileTool extends BaseGoogleDriveTool {
defaultParams: any
constructor(args: any) {
const toolInput = {
name: 'copy_file',
description: 'Copy a file in Google Drive',
schema: CopyFileSchema,
baseUrl: '',
method: 'POST',
headers: {}
}
super({
...toolInput,
accessToken: args.accessToken
})
this.defaultParams = args.defaultParams || {}
}
async _call(arg: any): Promise<string> {
const params = { ...arg, ...this.defaultParams }
try {
const copyData: any = {
name: params.name
}
if (params.parents) {
copyData.parents = params.parents.split(',').map((p: string) => p.trim())
}
const queryParams = new URLSearchParams()
if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString())
const endpoint = `files/${encodeURIComponent(params.fileId)}/copy?${queryParams.toString()}`
const response = await this.makeGoogleDriveRequest({
endpoint,
method: 'POST',
body: copyData,
params
})
return response
} catch (error) {
return `Error copying file: ${error}`
}
}
}
class DownloadFileTool extends BaseGoogleDriveTool {
defaultParams: any
constructor(args: any) {
const toolInput = {
name: 'download_file',
description: 'Download a file from Google Drive',
schema: DownloadFileSchema,
baseUrl: '',
method: 'GET',
headers: {}
}
super({
...toolInput,
accessToken: args.accessToken
})
this.defaultParams = args.defaultParams || {}
}
async _call(arg: any): Promise<string> {
const params = { ...arg, ...this.defaultParams }
try {
const queryParams = new URLSearchParams()
queryParams.append('alt', 'media')
if (params.acknowledgeAbuse) queryParams.append('acknowledgeAbuse', params.acknowledgeAbuse.toString())
if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString())
const endpoint = `files/${encodeURIComponent(params.fileId)}?${queryParams.toString()}`
const response = await this.makeGoogleDriveRequest({ endpoint, params })
return response
} catch (error) {
return `Error downloading file: ${error}`
}
}
}
class CreateFolderTool extends BaseGoogleDriveTool {
defaultParams: any
constructor(args: any) {
const toolInput = {
name: 'create_folder',
description: 'Create a new folder in Google Drive',
schema: CreateFolderSchema,
baseUrl: '',
method: 'POST',
headers: {}
}
super({
...toolInput,
accessToken: args.accessToken
})
this.defaultParams = args.defaultParams || {}
}
async _call(arg: any): Promise<string> {
const params = { ...arg, ...this.defaultParams }
try {
const folderData: any = {
name: params.name,
mimeType: 'application/vnd.google-apps.folder'
}
if (params.parents) {
folderData.parents = params.parents.split(',').map((p: string) => p.trim())
}
if (params.description) folderData.description = params.description
const queryParams = new URLSearchParams()
if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString())
const endpoint = `files?${queryParams.toString()}`
const response = await this.makeGoogleDriveRequest({
endpoint,
method: 'POST',
body: folderData,
params
})
return response
} catch (error) {
return `Error creating folder: ${error}`
}
}
}
class SearchFilesTool extends BaseGoogleDriveTool {
defaultParams: any
constructor(args: any) {
const toolInput = {
name: 'search_files',
description: 'Search files in Google Drive',
schema: SearchFilesSchema,
baseUrl: '',
method: 'GET',
headers: {}
}
super({
...toolInput,
accessToken: args.accessToken
})
this.defaultParams = args.defaultParams || {}
}
async _call(arg: any): Promise<string> {
const params = { ...arg, ...this.defaultParams }
try {
const queryParams = new URLSearchParams()
queryParams.append('q', params.query)
if (params.pageSize) queryParams.append('pageSize', params.pageSize.toString())
if (params.orderBy) queryParams.append('orderBy', params.orderBy)
if (params.includeItemsFromAllDrives)
queryParams.append('includeItemsFromAllDrives', params.includeItemsFromAllDrives.toString())
if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString())
const endpoint = `files?${queryParams.toString()}`
const response = await this.makeGoogleDriveRequest({ endpoint, params })
return response
} catch (error) {
return `Error searching files: ${error}`
}
}
}
class ShareFileTool extends BaseGoogleDriveTool {
defaultParams: any
constructor(args: any) {
const toolInput = {
name: 'share_file',
description: 'Share a file in Google Drive',
schema: ShareFileSchema,
baseUrl: '',
method: 'POST',
headers: {}
}
super({
...toolInput,
accessToken: args.accessToken
})
this.defaultParams = args.defaultParams || {}
}
async _call(arg: any): Promise<string> {
const params = { ...arg, ...this.defaultParams }
try {
const permissionData: any = {
role: params.role,
type: params.type
}
if (params.emailAddress) permissionData.emailAddress = params.emailAddress
if (params.domain) permissionData.domain = params.domain
if (params.allowFileDiscovery !== undefined) permissionData.allowFileDiscovery = params.allowFileDiscovery
const queryParams = new URLSearchParams()
if (params.sendNotificationEmail !== undefined)
queryParams.append('sendNotificationEmail', params.sendNotificationEmail.toString())
if (params.emailMessage) queryParams.append('emailMessage', params.emailMessage)
if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString())
const endpoint = `files/${encodeURIComponent(params.fileId)}/permissions?${queryParams.toString()}`
const response = await this.makeGoogleDriveRequest({
endpoint,
method: 'POST',
body: permissionData,
params
})
return response
} catch (error) {
return `Error sharing file: ${error}`
}
}
}
class ListFolderContentsTool extends BaseGoogleDriveTool {
defaultParams: any
constructor(args: any) {
const toolInput = {
name: 'list_folder_contents',
description: 'List contents of a specific folder in Google Drive',
schema: z.object({
folderId: z.string().describe('Folder ID to list contents from'),
pageSize: z.number().optional().default(10).describe('Maximum number of files to return'),
orderBy: z.string().optional().describe('Sort order'),
includeItemsFromAllDrives: z.boolean().optional().describe('Include items from all drives'),
supportsAllDrives: z
.boolean()
.optional()
.describe('Whether the requesting application supports both My Drives and shared drives')
}),
baseUrl: '',
method: 'GET',
headers: {}
}
super({
...toolInput,
accessToken: args.accessToken
})
this.defaultParams = args.defaultParams || {}
}
async _call(arg: any): Promise<string> {
const params = { ...arg, ...this.defaultParams }
try {
const queryParams = new URLSearchParams()
queryParams.append('q', `'${params.folderId}' in parents`)
if (params.pageSize) queryParams.append('pageSize', params.pageSize.toString())
if (params.orderBy) queryParams.append('orderBy', params.orderBy)
if (params.includeItemsFromAllDrives)
queryParams.append('includeItemsFromAllDrives', params.includeItemsFromAllDrives.toString())
if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString())
const endpoint = `files?${queryParams.toString()}`
const response = await this.makeGoogleDriveRequest({ endpoint, params })
return response
} catch (error) {
return `Error listing folder contents: ${error}`
}
}
}
class DeleteFolderTool extends BaseGoogleDriveTool {
defaultParams: any
constructor(args: any) {
const toolInput = {
name: 'delete_folder',
description: 'Delete a folder from Google Drive',
schema: z.object({
folderId: z.string().describe('Folder ID to delete'),
supportsAllDrives: z
.boolean()
.optional()
.describe('Whether the requesting application supports both My Drives and shared drives')
}),
baseUrl: '',
method: 'DELETE',
headers: {}
}
super({
...toolInput,
accessToken: args.accessToken
})
this.defaultParams = args.defaultParams || {}
}
async _call(arg: any): Promise<string> {
const params = { ...arg, ...this.defaultParams }
try {
const queryParams = new URLSearchParams()
if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString())
const endpoint = `files/${encodeURIComponent(params.folderId)}?${queryParams.toString()}`
await this.makeGoogleDriveRequest({
endpoint,
method: 'DELETE',
params
})
return `Folder deleted successfully`
} catch (error) {
return `Error deleting folder: ${error}`
}
}
}
class GetPermissionsTool extends BaseGoogleDriveTool {
defaultParams: any
constructor(args: any) {
const toolInput = {
name: 'get_permissions',
description: 'Get permissions for a file in Google Drive',
schema: z.object({
fileId: z.string().describe('File ID to get permissions for'),
supportsAllDrives: z
.boolean()
.optional()
.describe('Whether the requesting application supports both My Drives and shared drives')
}),
baseUrl: '',
method: 'GET',
headers: {}
}
super({
...toolInput,
accessToken: args.accessToken
})
this.defaultParams = args.defaultParams || {}
}
async _call(arg: any): Promise<string> {
const params = { ...arg, ...this.defaultParams }
try {
const queryParams = new URLSearchParams()
if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString())
const endpoint = `files/${encodeURIComponent(params.fileId)}/permissions?${queryParams.toString()}`
const response = await this.makeGoogleDriveRequest({ endpoint, params })
return response
} catch (error) {
return `Error getting permissions: ${error}`
}
}
}
class RemovePermissionTool extends BaseGoogleDriveTool {
defaultParams: any
constructor(args: any) {
const toolInput = {
name: 'remove_permission',
description: 'Remove a permission from a file in Google Drive',
schema: z.object({
fileId: z.string().describe('File ID to remove permission from'),
permissionId: z.string().describe('Permission ID to remove'),
supportsAllDrives: z
.boolean()
.optional()
.describe('Whether the requesting application supports both My Drives and shared drives')
}),
baseUrl: '',
method: 'DELETE',
headers: {}
}
super({
...toolInput,
accessToken: args.accessToken
})
this.defaultParams = args.defaultParams || {}
}
async _call(arg: any): Promise<string> {
const params = { ...arg, ...this.defaultParams }
try {
const queryParams = new URLSearchParams()
if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString())
const endpoint = `files/${encodeURIComponent(params.fileId)}/permissions/${encodeURIComponent(
params.permissionId
)}?${queryParams.toString()}`
await this.makeGoogleDriveRequest({
endpoint,
method: 'DELETE',
params
})
return `Permission removed successfully`
} catch (error) {
return `Error removing permission: ${error}`
}
}
}
export const createGoogleDriveTools = (args?: RequestParameters): DynamicStructuredTool[] => {
const tools: DynamicStructuredTool[] = []
const actions = args?.actions || []
const accessToken = args?.accessToken || ''
const defaultParams = args?.defaultParams || {}
if (actions.includes('listFiles')) {
tools.push(new ListFilesTool({ accessToken, defaultParams }))
}
if (actions.includes('getFile')) {
tools.push(new GetFileTool({ accessToken, defaultParams }))
}
if (actions.includes('createFile')) {
tools.push(new CreateFileTool({ accessToken, defaultParams }))
}
if (actions.includes('updateFile')) {
tools.push(new UpdateFileTool({ accessToken, defaultParams }))
}
if (actions.includes('deleteFile')) {
tools.push(new DeleteFileTool({ accessToken, defaultParams }))
}
if (actions.includes('copyFile')) {
tools.push(new CopyFileTool({ accessToken, defaultParams }))
}
if (actions.includes('downloadFile')) {
tools.push(new DownloadFileTool({ accessToken, defaultParams }))
}
if (actions.includes('createFolder')) {
tools.push(new CreateFolderTool({ accessToken, defaultParams }))
}
if (actions.includes('listFolderContents')) {
tools.push(new ListFolderContentsTool({ accessToken, defaultParams }))
}
if (actions.includes('deleteFolder')) {
tools.push(new DeleteFolderTool({ accessToken, defaultParams }))
}
if (actions.includes('searchFiles')) {
tools.push(new SearchFilesTool({ accessToken, defaultParams }))
}
if (actions.includes('shareFile')) {
tools.push(new ShareFileTool({ accessToken, defaultParams }))
}
if (actions.includes('getPermissions')) {
tools.push(new GetPermissionsTool({ accessToken, defaultParams }))
}
if (actions.includes('removePermission')) {
tools.push(new RemovePermissionTool({ accessToken, defaultParams }))
}
return tools
}
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="96px" height="96px"><path fill="#1e88e5" d="M38.59,39c-0.535,0.93-0.298,1.68-1.195,2.197C36.498,41.715,35.465,42,34.39,42H13.61 c-1.074,0-2.106-0.285-3.004-0.802C9.708,40.681,9.945,39.93,9.41,39l7.67-9h13.84L38.59,39z"/><path fill="#fbc02d" d="M27.463,6.999c1.073-0.002,2.104-0.716,3.001-0.198c0.897,0.519,1.66,1.27,2.197,2.201l10.39,17.996 c0.537,0.93,0.807,1.967,0.808,3.002c0.001,1.037-1.267,2.073-1.806,3.001l-11.127-3.005l-6.924-11.993L27.463,6.999z"/><path fill="#e53935" d="M43.86,30c0,1.04-0.27,2.07-0.81,3l-3.67,6.35c-0.53,0.78-1.21,1.4-1.99,1.85L30.92,30H43.86z"/><path fill="#4caf50" d="M5.947,33.001c-0.538-0.928-1.806-1.964-1.806-3c0.001-1.036,0.27-2.073,0.808-3.004l10.39-17.996 c0.537-0.93,1.3-1.682,2.196-2.2c0.897-0.519,1.929,0.195,3.002,0.197l3.459,11.009l-6.922,11.989L5.947,33.001z"/><path fill="#1565c0" d="M17.08,30l-6.47,11.2c-0.78-0.45-1.46-1.07-1.99-1.85L4.95,33c-0.54-0.93-0.81-1.96-0.81-3H17.08z"/><path fill="#2e7d32" d="M30.46,6.8L24,18L17.53,6.8c0.78-0.45,1.66-0.73,2.6-0.79L27.46,6C28.54,6,29.57,6.28,30.46,6.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,424 @@
import { convertMultiOptionsToStringArray, getCredentialData, getCredentialParam, refreshOAuth2Token } from '../../../src/utils'
import { createGoogleSheetsTools } from './core'
import type { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
class GoogleSheets_Tools implements INode {
label: string
name: string
version: number
type: string
icon: string
category: string
description: string
baseClasses: string[]
credential: INodeParams
inputs: INodeParams[]
constructor() {
this.label = 'Google Sheets'
this.name = 'googleSheetsTool'
this.version = 1.0
this.type = 'GoogleSheets'
this.icon = 'google-sheets.svg'
this.category = 'Tools'
this.description = 'Perform Google Sheets operations such as managing spreadsheets, reading and writing values'
this.baseClasses = ['Tool']
this.credential = {
label: 'Connect Credential',
name: 'credential',
type: 'credential',
credentialNames: ['googleSheetsOAuth2']
}
this.inputs = [
{
label: 'Type',
name: 'sheetsType',
type: 'options',
description: 'Type of Google Sheets operation',
options: [
{
label: 'Spreadsheet',
name: 'spreadsheet'
},
{
label: 'Values',
name: 'values'
}
]
},
// Spreadsheet Actions
{
label: 'Spreadsheet Actions',
name: 'spreadsheetActions',
type: 'multiOptions',
description: 'Actions to perform on spreadsheets',
options: [
{
label: 'Create Spreadsheet',
name: 'createSpreadsheet'
},
{
label: 'Get Spreadsheet',
name: 'getSpreadsheet'
},
{
label: 'Update Spreadsheet',
name: 'updateSpreadsheet'
}
],
show: {
sheetsType: ['spreadsheet']
}
},
// Values Actions
{
label: 'Values Actions',
name: 'valuesActions',
type: 'multiOptions',
description: 'Actions to perform on sheet values',
options: [
{
label: 'Get Values',
name: 'getValues'
},
{
label: 'Update Values',
name: 'updateValues'
},
{
label: 'Append Values',
name: 'appendValues'
},
{
label: 'Clear Values',
name: 'clearValues'
},
{
label: 'Batch Get Values',
name: 'batchGetValues'
},
{
label: 'Batch Update Values',
name: 'batchUpdateValues'
},
{
label: 'Batch Clear Values',
name: 'batchClearValues'
}
],
show: {
sheetsType: ['values']
}
},
// Spreadsheet Parameters
{
label: 'Spreadsheet ID',
name: 'spreadsheetId',
type: 'string',
description: 'The ID of the spreadsheet',
show: {
sheetsType: ['spreadsheet', 'values']
},
additionalParams: true,
optional: true
},
{
label: 'Title',
name: 'title',
type: 'string',
description: 'The title of the spreadsheet',
show: {
spreadsheetActions: ['createSpreadsheet', 'updateSpreadsheet']
},
additionalParams: true,
optional: true
},
{
label: 'Sheet Count',
name: 'sheetCount',
type: 'number',
description: 'Number of sheets to create',
default: 1,
show: {
spreadsheetActions: ['createSpreadsheet']
},
additionalParams: true,
optional: true
},
// Values Parameters
{
label: 'Range',
name: 'range',
type: 'string',
description: 'The range to read/write (e.g., A1:B2, Sheet1!A1:C10)',
show: {
valuesActions: ['getValues', 'updateValues', 'clearValues']
},
additionalParams: true,
optional: true
},
{
label: 'Ranges',
name: 'ranges',
type: 'string',
description: 'Comma-separated list of ranges for batch operations',
show: {
valuesActions: ['batchGetValues', 'batchClearValues']
},
additionalParams: true,
optional: true
},
{
label: 'Values',
name: 'values',
type: 'string',
description: 'JSON array of values to write (e.g., [["A1", "B1"], ["A2", "B2"]])',
show: {
valuesActions: ['updateValues', 'appendValues', 'batchUpdateValues']
},
additionalParams: true,
optional: true
},
{
label: 'Value Input Option',
name: 'valueInputOption',
type: 'options',
description: 'How input data should be interpreted',
options: [
{
label: 'Raw',
name: 'RAW'
},
{
label: 'User Entered',
name: 'USER_ENTERED'
}
],
default: 'USER_ENTERED',
show: {
valuesActions: ['updateValues', 'appendValues', 'batchUpdateValues']
},
additionalParams: true,
optional: true
},
{
label: 'Value Render Option',
name: 'valueRenderOption',
type: 'options',
description: 'How values should be represented in the output',
options: [
{
label: 'Formatted Value',
name: 'FORMATTED_VALUE'
},
{
label: 'Unformatted Value',
name: 'UNFORMATTED_VALUE'
},
{
label: 'Formula',
name: 'FORMULA'
}
],
default: 'FORMATTED_VALUE',
show: {
valuesActions: ['getValues', 'batchGetValues']
},
additionalParams: true,
optional: true
},
{
label: 'Date Time Render Option',
name: 'dateTimeRenderOption',
type: 'options',
description: 'How dates, times, and durations should be represented',
options: [
{
label: 'Serial Number',
name: 'SERIAL_NUMBER'
},
{
label: 'Formatted String',
name: 'FORMATTED_STRING'
}
],
default: 'FORMATTED_STRING',
show: {
valuesActions: ['getValues', 'batchGetValues']
},
additionalParams: true,
optional: true
},
{
label: 'Insert Data Option',
name: 'insertDataOption',
type: 'options',
description: 'How data should be inserted',
options: [
{
label: 'Overwrite',
name: 'OVERWRITE'
},
{
label: 'Insert Rows',
name: 'INSERT_ROWS'
}
],
default: 'OVERWRITE',
show: {
valuesActions: ['appendValues']
},
additionalParams: true,
optional: true
},
{
label: 'Include Grid Data',
name: 'includeGridData',
type: 'boolean',
description: 'True if grid data should be returned',
default: false,
show: {
spreadsheetActions: ['getSpreadsheet']
},
additionalParams: true,
optional: true
},
{
label: 'Major Dimension',
name: 'majorDimension',
type: 'options',
description: 'The major dimension that results should use',
options: [
{
label: 'Rows',
name: 'ROWS'
},
{
label: 'Columns',
name: 'COLUMNS'
}
],
default: 'ROWS',
show: {
valuesActions: ['getValues', 'updateValues', 'appendValues', 'batchGetValues', 'batchUpdateValues']
},
additionalParams: true,
optional: true
}
]
}
async init(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
const sheetsType = nodeData.inputs?.sheetsType as string
let credentialData = await getCredentialData(nodeData.credential ?? '', options)
credentialData = await refreshOAuth2Token(nodeData.credential ?? '', credentialData, options)
const accessToken = getCredentialParam('access_token', credentialData, nodeData)
if (!accessToken) {
throw new Error('No access token found in credential')
}
// Get all actions based on type
let actions: string[] = []
if (sheetsType === 'spreadsheet') {
actions = convertMultiOptionsToStringArray(nodeData.inputs?.spreadsheetActions)
} else if (sheetsType === 'values') {
actions = convertMultiOptionsToStringArray(nodeData.inputs?.valuesActions)
}
// Create default params object based on inputs
const defaultParams: any = {}
// Spreadsheet-specific default params
if (sheetsType === 'spreadsheet') {
actions.forEach((action) => {
const params: any = {}
// Common spreadsheet parameters
if (nodeData.inputs?.spreadsheetId) params.spreadsheetId = nodeData.inputs.spreadsheetId
if (action === 'createSpreadsheet') {
if (nodeData.inputs?.title) params.title = nodeData.inputs.title
if (nodeData.inputs?.sheetCount) params.sheetCount = nodeData.inputs.sheetCount
}
if (action === 'getSpreadsheet') {
if (nodeData.inputs?.ranges) params.ranges = nodeData.inputs.ranges
if (nodeData.inputs?.includeGridData !== undefined) params.includeGridData = nodeData.inputs.includeGridData
}
if (action === 'updateSpreadsheet') {
if (nodeData.inputs?.title) params.title = nodeData.inputs.title
}
defaultParams[action] = params
})
}
// Values-specific default params
if (sheetsType === 'values') {
actions.forEach((action) => {
const params: any = {}
// Common values parameters
if (nodeData.inputs?.spreadsheetId) params.spreadsheetId = nodeData.inputs.spreadsheetId
if (action === 'getValues') {
if (nodeData.inputs?.range) params.range = nodeData.inputs.range
if (nodeData.inputs?.valueRenderOption) params.valueRenderOption = nodeData.inputs.valueRenderOption
if (nodeData.inputs?.dateTimeRenderOption) params.dateTimeRenderOption = nodeData.inputs.dateTimeRenderOption
if (nodeData.inputs?.majorDimension) params.majorDimension = nodeData.inputs.majorDimension
}
if (action === 'updateValues') {
if (nodeData.inputs?.range) params.range = nodeData.inputs.range
if (nodeData.inputs?.values) params.values = nodeData.inputs.values
if (nodeData.inputs?.valueInputOption) params.valueInputOption = nodeData.inputs.valueInputOption
if (nodeData.inputs?.majorDimension) params.majorDimension = nodeData.inputs.majorDimension
}
if (action === 'appendValues') {
if (nodeData.inputs?.range) params.range = nodeData.inputs.range
if (nodeData.inputs?.values) params.values = nodeData.inputs.values
if (nodeData.inputs?.valueInputOption) params.valueInputOption = nodeData.inputs.valueInputOption
if (nodeData.inputs?.insertDataOption) params.insertDataOption = nodeData.inputs.insertDataOption
if (nodeData.inputs?.majorDimension) params.majorDimension = nodeData.inputs.majorDimension
}
if (action === 'clearValues') {
if (nodeData.inputs?.range) params.range = nodeData.inputs.range
}
if (action === 'batchGetValues') {
if (nodeData.inputs?.ranges) params.ranges = nodeData.inputs.ranges
if (nodeData.inputs?.valueRenderOption) params.valueRenderOption = nodeData.inputs.valueRenderOption
if (nodeData.inputs?.dateTimeRenderOption) params.dateTimeRenderOption = nodeData.inputs.dateTimeRenderOption
if (nodeData.inputs?.majorDimension) params.majorDimension = nodeData.inputs.majorDimension
}
if (action === 'batchUpdateValues') {
if (nodeData.inputs?.values) params.values = nodeData.inputs.values
if (nodeData.inputs?.valueInputOption) params.valueInputOption = nodeData.inputs.valueInputOption
}
if (action === 'batchClearValues') {
if (nodeData.inputs?.ranges) params.ranges = nodeData.inputs.ranges
}
defaultParams[action] = params
})
}
const tools = createGoogleSheetsTools({
accessToken,
actions,
defaultParams
})
return tools
}
}
module.exports = { nodeClass: GoogleSheets_Tools }
@@ -0,0 +1,631 @@
import { z } from 'zod'
import fetch from 'node-fetch'
import { DynamicStructuredTool } from '../OpenAPIToolkit/core'
import { TOOL_ARGS_PREFIX } from '../../../src/agents'
export const desc = `Use this when you want to access Google Sheets API for managing spreadsheets and values`
export interface Headers {
[key: string]: string
}
export interface Body {
[key: string]: any
}
export interface RequestParameters {
headers?: Headers
body?: Body
url?: string
description?: string
name?: string
actions?: string[]
accessToken?: string
defaultParams?: any
}
// Define schemas for different Google Sheets operations
// Spreadsheet Schemas
const CreateSpreadsheetSchema = z.object({
title: z.string().describe('The title of the spreadsheet'),
sheetCount: z.number().optional().default(1).describe('Number of sheets to create'),
locale: z.string().optional().describe('The locale of the spreadsheet (e.g., en_US)'),
timeZone: z.string().optional().describe('The time zone of the spreadsheet (e.g., America/New_York)')
})
const GetSpreadsheetSchema = z.object({
spreadsheetId: z.string().describe('The ID of the spreadsheet to retrieve'),
ranges: z.string().optional().describe('Comma-separated list of ranges to retrieve'),
includeGridData: z.boolean().optional().default(false).describe('True if grid data should be returned')
})
const UpdateSpreadsheetSchema = z.object({
spreadsheetId: z.string().describe('The ID of the spreadsheet to update'),
title: z.string().optional().describe('New title for the spreadsheet'),
locale: z.string().optional().describe('New locale for the spreadsheet'),
timeZone: z.string().optional().describe('New time zone for the spreadsheet')
})
// Values Schemas
const GetValuesSchema = z.object({
spreadsheetId: z.string().describe('The ID of the spreadsheet'),
range: z.string().describe('The A1 notation of the range to retrieve values from'),
valueRenderOption: z
.enum(['FORMATTED_VALUE', 'UNFORMATTED_VALUE', 'FORMULA'])
.optional()
.default('FORMATTED_VALUE')
.describe('How values should be represented'),
dateTimeRenderOption: z
.enum(['SERIAL_NUMBER', 'FORMATTED_STRING'])
.optional()
.default('FORMATTED_STRING')
.describe('How dates should be represented'),
majorDimension: z.enum(['ROWS', 'COLUMNS']).optional().default('ROWS').describe('The major dimension that results should use')
})
const UpdateValuesSchema = z.object({
spreadsheetId: z.string().describe('The ID of the spreadsheet'),
range: z.string().describe('The A1 notation of the range to update'),
values: z.string().describe('JSON array of values to write (e.g., [["A1", "B1"], ["A2", "B2"]])'),
valueInputOption: z.enum(['RAW', 'USER_ENTERED']).optional().default('USER_ENTERED').describe('How input data should be interpreted'),
majorDimension: z.enum(['ROWS', 'COLUMNS']).optional().default('ROWS').describe('The major dimension of the values')
})
const AppendValuesSchema = z.object({
spreadsheetId: z.string().describe('The ID of the spreadsheet'),
range: z.string().describe('The A1 notation of the range to append to'),
values: z.string().describe('JSON array of values to append'),
valueInputOption: z.enum(['RAW', 'USER_ENTERED']).optional().default('USER_ENTERED').describe('How input data should be interpreted'),
insertDataOption: z.enum(['OVERWRITE', 'INSERT_ROWS']).optional().default('OVERWRITE').describe('How data should be inserted'),
majorDimension: z.enum(['ROWS', 'COLUMNS']).optional().default('ROWS').describe('The major dimension of the values')
})
const ClearValuesSchema = z.object({
spreadsheetId: z.string().describe('The ID of the spreadsheet'),
range: z.string().describe('The A1 notation of the range to clear')
})
const BatchGetValuesSchema = z.object({
spreadsheetId: z.string().describe('The ID of the spreadsheet'),
ranges: z.string().describe('Comma-separated list of ranges to retrieve'),
valueRenderOption: z
.enum(['FORMATTED_VALUE', 'UNFORMATTED_VALUE', 'FORMULA'])
.optional()
.default('FORMATTED_VALUE')
.describe('How values should be represented'),
dateTimeRenderOption: z
.enum(['SERIAL_NUMBER', 'FORMATTED_STRING'])
.optional()
.default('FORMATTED_STRING')
.describe('How dates should be represented'),
majorDimension: z.enum(['ROWS', 'COLUMNS']).optional().default('ROWS').describe('The major dimension that results should use')
})
const BatchUpdateValuesSchema = z.object({
spreadsheetId: z.string().describe('The ID of the spreadsheet'),
valueInputOption: z.enum(['RAW', 'USER_ENTERED']).optional().default('USER_ENTERED').describe('How input data should be interpreted'),
values: z
.string()
.describe('JSON array of value ranges to update (e.g., [{"range": "A1:B2", "values": [["A1", "B1"], ["A2", "B2"]]}])'),
includeValuesInResponse: z.boolean().optional().default(false).describe('Whether to return the updated values in the response')
})
const BatchClearValuesSchema = z.object({
spreadsheetId: z.string().describe('The ID of the spreadsheet'),
ranges: z.string().describe('Comma-separated list of ranges to clear')
})
class BaseGoogleSheetsTool extends DynamicStructuredTool {
protected accessToken: string = ''
constructor(args: any) {
super(args)
this.accessToken = args.accessToken ?? ''
}
async makeGoogleSheetsRequest({
endpoint,
method = 'GET',
body,
params
}: {
endpoint: string
method?: string
body?: any
params?: any
}): Promise<string> {
const url = `https://sheets.googleapis.com/v4/${endpoint}`
const headers = {
Authorization: `Bearer ${this.accessToken}`,
'Content-Type': 'application/json',
Accept: 'application/json',
...this.headers
}
const response = await fetch(url, {
method,
headers,
body: body ? JSON.stringify(body) : undefined
})
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Google Sheets API Error ${response.status}: ${response.statusText} - ${errorText}`)
}
const data = await response.text()
return data + TOOL_ARGS_PREFIX + JSON.stringify(params)
}
}
// Spreadsheet Tools
class CreateSpreadsheetTool extends BaseGoogleSheetsTool {
defaultParams: any
constructor(args: any) {
const toolInput = {
name: 'create_spreadsheet',
description: 'Create a new Google Spreadsheet',
schema: CreateSpreadsheetSchema,
baseUrl: '',
method: 'POST',
headers: {}
}
super({
...toolInput,
accessToken: args.accessToken
})
this.defaultParams = args.defaultParams || {}
}
async _call(arg: any): Promise<string> {
const params = { ...arg, ...this.defaultParams }
const body: any = {
properties: {
title: params.title
}
}
if (params.locale) body.properties.locale = params.locale
if (params.timeZone) body.properties.timeZone = params.timeZone
// Add sheets if specified
if (params.sheetCount && params.sheetCount > 1) {
body.sheets = []
for (let i = 0; i < params.sheetCount; i++) {
body.sheets.push({
properties: {
title: i === 0 ? 'Sheet1' : `Sheet${i + 1}`
}
})
}
}
return await this.makeGoogleSheetsRequest({
endpoint: 'spreadsheets',
method: 'POST',
body,
params
})
}
}
class GetSpreadsheetTool extends BaseGoogleSheetsTool {
defaultParams: any
constructor(args: any) {
const toolInput = {
name: 'get_spreadsheet',
description: 'Get a Google Spreadsheet by ID',
schema: GetSpreadsheetSchema,
baseUrl: '',
method: 'GET',
headers: {}
}
super({
...toolInput,
accessToken: args.accessToken
})
this.defaultParams = args.defaultParams || {}
}
async _call(arg: any): Promise<string> {
const params = { ...arg, ...this.defaultParams }
const queryParams = new URLSearchParams()
if (params.ranges) {
params.ranges.split(',').forEach((range: string) => {
queryParams.append('ranges', range.trim())
})
}
if (params.includeGridData) queryParams.append('includeGridData', 'true')
const queryString = queryParams.toString()
const endpoint = `spreadsheets/${params.spreadsheetId}${queryString ? `?${queryString}` : ''}`
return await this.makeGoogleSheetsRequest({
endpoint,
method: 'GET',
params
})
}
}
class UpdateSpreadsheetTool extends BaseGoogleSheetsTool {
defaultParams: any
constructor(args: any) {
const toolInput = {
name: 'update_spreadsheet',
description: 'Update a Google Spreadsheet properties',
schema: UpdateSpreadsheetSchema,
baseUrl: '',
method: 'POST',
headers: {}
}
super({
...toolInput,
accessToken: args.accessToken
})
this.defaultParams = args.defaultParams || {}
}
async _call(arg: any): Promise<string> {
const params = { ...arg, ...this.defaultParams }
const requests = []
if (params.title || params.locale || params.timeZone) {
const updateProperties: any = {}
if (params.title) updateProperties.title = params.title
if (params.locale) updateProperties.locale = params.locale
if (params.timeZone) updateProperties.timeZone = params.timeZone
requests.push({
updateSpreadsheetProperties: {
properties: updateProperties,
fields: Object.keys(updateProperties).join(',')
}
})
}
const body = { requests }
return await this.makeGoogleSheetsRequest({
endpoint: `spreadsheets/${params.spreadsheetId}:batchUpdate`,
method: 'POST',
body,
params
})
}
}
// Values Tools
class GetValuesTool extends BaseGoogleSheetsTool {
defaultParams: any
constructor(args: any) {
const toolInput = {
name: 'get_values',
description: 'Get values from a Google Spreadsheet range',
schema: GetValuesSchema,
baseUrl: '',
method: 'GET',
headers: {}
}
super({
...toolInput,
accessToken: args.accessToken
})
this.defaultParams = args.defaultParams || {}
}
async _call(arg: any): Promise<string> {
const params = { ...arg, ...this.defaultParams }
const queryParams = new URLSearchParams()
if (params.valueRenderOption) queryParams.append('valueRenderOption', params.valueRenderOption)
if (params.dateTimeRenderOption) queryParams.append('dateTimeRenderOption', params.dateTimeRenderOption)
if (params.majorDimension) queryParams.append('majorDimension', params.majorDimension)
const queryString = queryParams.toString()
const encodedRange = encodeURIComponent(params.range)
const endpoint = `spreadsheets/${params.spreadsheetId}/values/${encodedRange}${queryString ? `?${queryString}` : ''}`
return await this.makeGoogleSheetsRequest({
endpoint,
method: 'GET',
params
})
}
}
class UpdateValuesTool extends BaseGoogleSheetsTool {
defaultParams: any
constructor(args: any) {
const toolInput = {
name: 'update_values',
description: 'Update values in a Google Spreadsheet range',
schema: UpdateValuesSchema,
baseUrl: '',
method: 'PUT',
headers: {}
}
super({
...toolInput,
accessToken: args.accessToken
})
this.defaultParams = args.defaultParams || {}
}
async _call(arg: any): Promise<string> {
const params = { ...arg, ...this.defaultParams }
let values
try {
values = JSON.parse(params.values)
} catch (error) {
throw new Error('Values must be a valid JSON array')
}
const body = {
values,
majorDimension: params.majorDimension || 'ROWS'
}
const queryParams = new URLSearchParams()
queryParams.append('valueInputOption', params.valueInputOption || 'USER_ENTERED')
const encodedRange = encodeURIComponent(params.range)
const endpoint = `spreadsheets/${params.spreadsheetId}/values/${encodedRange}?${queryParams.toString()}`
return await this.makeGoogleSheetsRequest({
endpoint,
method: 'PUT',
body,
params
})
}
}
class AppendValuesTool extends BaseGoogleSheetsTool {
defaultParams: any
constructor(args: any) {
const toolInput = {
name: 'append_values',
description: 'Append values to a Google Spreadsheet range',
schema: AppendValuesSchema,
baseUrl: '',
method: 'POST',
headers: {}
}
super({
...toolInput,
accessToken: args.accessToken
})
this.defaultParams = args.defaultParams || {}
}
async _call(arg: any): Promise<string> {
const params = { ...arg, ...this.defaultParams }
let values
try {
values = JSON.parse(params.values)
} catch (error) {
throw new Error('Values must be a valid JSON array')
}
const body = {
values,
majorDimension: params.majorDimension || 'ROWS'
}
const queryParams = new URLSearchParams()
queryParams.append('valueInputOption', params.valueInputOption || 'USER_ENTERED')
queryParams.append('insertDataOption', params.insertDataOption || 'OVERWRITE')
const encodedRange = encodeURIComponent(params.range)
const endpoint = `spreadsheets/${params.spreadsheetId}/values/${encodedRange}:append?${queryParams.toString()}`
return await this.makeGoogleSheetsRequest({
endpoint,
method: 'POST',
body,
params
})
}
}
class ClearValuesTool extends BaseGoogleSheetsTool {
defaultParams: any
constructor(args: any) {
const toolInput = {
name: 'clear_values',
description: 'Clear values from a Google Spreadsheet range',
schema: ClearValuesSchema,
baseUrl: '',
method: 'POST',
headers: {}
}
super({
...toolInput,
accessToken: args.accessToken
})
this.defaultParams = args.defaultParams || {}
}
async _call(arg: any): Promise<string> {
const params = { ...arg, ...this.defaultParams }
const encodedRange = encodeURIComponent(params.range)
const endpoint = `spreadsheets/${params.spreadsheetId}/values/${encodedRange}:clear`
return await this.makeGoogleSheetsRequest({
endpoint,
method: 'POST',
body: {},
params
})
}
}
class BatchGetValuesTool extends BaseGoogleSheetsTool {
defaultParams: any
constructor(args: any) {
const toolInput = {
name: 'batch_get_values',
description: 'Get values from multiple Google Spreadsheet ranges',
schema: BatchGetValuesSchema,
baseUrl: '',
method: 'GET',
headers: {}
}
super({
...toolInput,
accessToken: args.accessToken
})
this.defaultParams = args.defaultParams || {}
}
async _call(arg: any): Promise<string> {
const params = { ...arg, ...this.defaultParams }
const queryParams = new URLSearchParams()
// Add ranges
params.ranges.split(',').forEach((range: string) => {
queryParams.append('ranges', range.trim())
})
if (params.valueRenderOption) queryParams.append('valueRenderOption', params.valueRenderOption)
if (params.dateTimeRenderOption) queryParams.append('dateTimeRenderOption', params.dateTimeRenderOption)
if (params.majorDimension) queryParams.append('majorDimension', params.majorDimension)
const endpoint = `spreadsheets/${params.spreadsheetId}/values:batchGet?${queryParams.toString()}`
return await this.makeGoogleSheetsRequest({
endpoint,
method: 'GET',
params
})
}
}
class BatchUpdateValuesTool extends BaseGoogleSheetsTool {
defaultParams: any
constructor(args: any) {
const toolInput = {
name: 'batch_update_values',
description: 'Update values in multiple Google Spreadsheet ranges',
schema: BatchUpdateValuesSchema,
baseUrl: '',
method: 'POST',
headers: {}
}
super({
...toolInput,
accessToken: args.accessToken
})
this.defaultParams = args.defaultParams || {}
}
async _call(arg: any): Promise<string> {
const params = { ...arg, ...this.defaultParams }
let valueRanges
try {
valueRanges = JSON.parse(params.values)
} catch (error) {
throw new Error('Values must be a valid JSON array of value ranges')
}
const body = {
valueInputOption: params.valueInputOption || 'USER_ENTERED',
data: valueRanges,
includeValuesInResponse: params.includeValuesInResponse || false
}
const endpoint = `spreadsheets/${params.spreadsheetId}/values:batchUpdate`
return await this.makeGoogleSheetsRequest({
endpoint,
method: 'POST',
body,
params
})
}
}
class BatchClearValuesTool extends BaseGoogleSheetsTool {
defaultParams: any
constructor(args: any) {
const toolInput = {
name: 'batch_clear_values',
description: 'Clear values from multiple Google Spreadsheet ranges',
schema: BatchClearValuesSchema,
baseUrl: '',
method: 'POST',
headers: {}
}
super({
...toolInput,
accessToken: args.accessToken
})
this.defaultParams = args.defaultParams || {}
}
async _call(arg: any): Promise<string> {
const params = { ...arg, ...this.defaultParams }
const ranges = params.ranges.split(',').map((range: string) => range.trim())
const body = { ranges }
const endpoint = `spreadsheets/${params.spreadsheetId}/values:batchClear`
return await this.makeGoogleSheetsRequest({
endpoint,
method: 'POST',
body,
params
})
}
}
export const createGoogleSheetsTools = (args?: RequestParameters): DynamicStructuredTool[] => {
const { actions = [], accessToken, defaultParams } = args || {}
const tools: DynamicStructuredTool[] = []
// Define all available tools
const toolClasses = {
// Spreadsheet tools
createSpreadsheet: CreateSpreadsheetTool,
getSpreadsheet: GetSpreadsheetTool,
updateSpreadsheet: UpdateSpreadsheetTool,
// Values tools
getValues: GetValuesTool,
updateValues: UpdateValuesTool,
appendValues: AppendValuesTool,
clearValues: ClearValuesTool,
batchGetValues: BatchGetValuesTool,
batchUpdateValues: BatchUpdateValuesTool,
batchClearValues: BatchClearValuesTool
}
// Create tools based on requested actions
actions.forEach((action) => {
const ToolClass = toolClasses[action as keyof typeof toolClasses]
if (ToolClass) {
tools.push(new ToolClass({ accessToken, defaultParams }))
}
})
return tools
}
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="96px" height="96px"><path fill="#43a047" d="M37,45H11c-1.657,0-3-1.343-3-3V6c0-1.657,1.343-3,3-3h19l10,10v29C40,43.657,38.657,45,37,45z"/><path fill="#c8e6c9" d="M40 13L30 13 30 3z"/><path fill="#2e7d32" d="M30 13L40 23 40 13z"/><path fill="#e8f5e9" d="M31,23H17h-2v2v2v2v2v2v2v2h18v-2v-2v-2v-2v-2v-2v-2H31z M17,25h4v2h-4V25z M17,29h4v2h-4V29z M17,33h4v2h-4V33z M31,35h-8v-2h8V35z M31,31h-8v-2h8V31z M31,27h-8v-2h8V27z"/></svg>

After

Width:  |  Height:  |  Size: 495 B

@@ -0,0 +1,504 @@
import { convertMultiOptionsToStringArray, getCredentialData, getCredentialParam } from '../../../src/utils'
import { createJiraTools } from './core'
import type { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
class Jira_Tools implements INode {
label: string
name: string
version: number
type: string
icon: string
category: string
description: string
baseClasses: string[]
credential: INodeParams
inputs: INodeParams[]
constructor() {
this.label = 'Jira'
this.name = 'jiraTool'
this.version = 1.0
this.type = 'Jira'
this.icon = 'jira.svg'
this.category = 'Tools'
this.description = 'Perform Jira operations for issues, comments, and users'
this.baseClasses = [this.type, 'Tool']
this.credential = {
label: 'Connect Credential',
name: 'credential',
type: 'credential',
credentialNames: ['jiraApi']
}
this.inputs = [
{
label: 'Host',
name: 'jiraHost',
type: 'string',
placeholder: 'https://example.atlassian.net'
},
{
label: 'Type',
name: 'jiraType',
type: 'options',
options: [
{
label: 'Issues',
name: 'issues'
},
{
label: 'Issue Comments',
name: 'comments'
},
{
label: 'Users',
name: 'users'
}
]
},
// Issue Actions
{
label: 'Issue Actions',
name: 'issueActions',
type: 'multiOptions',
options: [
{
label: 'List Issues',
name: 'listIssues'
},
{
label: 'Create Issue',
name: 'createIssue'
},
{
label: 'Get Issue',
name: 'getIssue'
},
{
label: 'Update Issue',
name: 'updateIssue'
},
{
label: 'Delete Issue',
name: 'deleteIssue'
},
{
label: 'Assign Issue',
name: 'assignIssue'
},
{
label: 'Transition Issue',
name: 'transitionIssue'
}
],
show: {
jiraType: ['issues']
}
},
// Comment Actions
{
label: 'Comment Actions',
name: 'commentActions',
type: 'multiOptions',
options: [
{
label: 'List Comments',
name: 'listComments'
},
{
label: 'Create Comment',
name: 'createComment'
},
{
label: 'Get Comment',
name: 'getComment'
},
{
label: 'Update Comment',
name: 'updateComment'
},
{
label: 'Delete Comment',
name: 'deleteComment'
}
],
show: {
jiraType: ['comments']
}
},
// User Actions
{
label: 'User Actions',
name: 'userActions',
type: 'multiOptions',
options: [
{
label: 'Search Users',
name: 'searchUsers'
},
{
label: 'Get User',
name: 'getUser'
},
{
label: 'Create User',
name: 'createUser'
},
{
label: 'Update User',
name: 'updateUser'
},
{
label: 'Delete User',
name: 'deleteUser'
}
],
show: {
jiraType: ['users']
}
},
// ISSUE PARAMETERS
{
label: 'Project Key',
name: 'projectKey',
type: 'string',
placeholder: 'PROJ',
description: 'Project key for the issue',
show: {
issueActions: ['listIssues', 'createIssue']
},
additionalParams: true,
optional: true
},
{
label: 'Issue Type',
name: 'issueType',
type: 'string',
placeholder: 'Bug, Task, Story',
description: 'Type of issue to create',
show: {
issueActions: ['createIssue']
},
additionalParams: true,
optional: true
},
{
label: 'Summary',
name: 'issueSummary',
type: 'string',
description: 'Issue summary/title',
show: {
issueActions: ['createIssue', 'updateIssue']
},
additionalParams: true,
optional: true
},
{
label: 'Description',
name: 'issueDescription',
type: 'string',
description: 'Issue description',
show: {
issueActions: ['createIssue', 'updateIssue']
},
additionalParams: true,
optional: true
},
{
label: 'Priority',
name: 'issuePriority',
type: 'string',
placeholder: 'Highest, High, Medium, Low, Lowest',
description: 'Issue priority',
show: {
issueActions: ['createIssue', 'updateIssue']
},
additionalParams: true,
optional: true
},
{
label: 'Issue Key',
name: 'issueKey',
type: 'string',
placeholder: 'PROJ-123',
description: 'Issue key (e.g., PROJ-123)',
show: {
issueActions: ['getIssue', 'updateIssue', 'deleteIssue', 'assignIssue', 'transitionIssue']
},
additionalParams: true,
optional: true
},
{
label: 'Assignee Account ID',
name: 'assigneeAccountId',
type: 'string',
description: 'Account ID of the user to assign',
show: {
issueActions: ['assignIssue', 'createIssue', 'updateIssue']
},
additionalParams: true,
optional: true
},
{
label: 'Transition ID',
name: 'transitionId',
type: 'string',
description: 'ID of the transition to execute',
show: {
issueActions: ['transitionIssue']
},
additionalParams: true,
optional: true
},
{
label: 'JQL Query',
name: 'jqlQuery',
type: 'string',
placeholder: 'project = PROJ AND status = "To Do"',
description: 'JQL query for filtering issues',
show: {
issueActions: ['listIssues']
},
additionalParams: true,
optional: true
},
{
label: 'Max Results',
name: 'issueMaxResults',
type: 'number',
default: 50,
description: 'Maximum number of issues to return',
show: {
issueActions: ['listIssues']
},
additionalParams: true,
optional: true
},
// COMMENT PARAMETERS
{
label: 'Issue Key (for Comments)',
name: 'commentIssueKey',
type: 'string',
placeholder: 'PROJ-123',
description: 'Issue key for comment operations',
show: {
commentActions: ['listComments', 'createComment']
},
additionalParams: true,
optional: true
},
{
label: 'Comment Text',
name: 'commentText',
type: 'string',
description: 'Comment content',
show: {
commentActions: ['createComment', 'updateComment']
},
additionalParams: true,
optional: true
},
{
label: 'Comment ID',
name: 'commentId',
type: 'string',
description: 'ID of the comment',
show: {
commentActions: ['getComment', 'updateComment', 'deleteComment']
},
additionalParams: true,
optional: true
},
// USER PARAMETERS
{
label: 'Search Query',
name: 'userQuery',
type: 'string',
placeholder: 'john.doe',
description: 'Query string for user search',
show: {
userActions: ['searchUsers']
},
additionalParams: true,
optional: true
},
{
label: 'Account ID',
name: 'userAccountId',
type: 'string',
description: 'User account ID',
show: {
userActions: ['getUser', 'updateUser', 'deleteUser']
},
additionalParams: true,
optional: true
},
{
label: 'Email Address',
name: 'userEmail',
type: 'string',
placeholder: 'user@example.com',
description: 'User email address',
show: {
userActions: ['createUser', 'updateUser']
},
additionalParams: true,
optional: true
},
{
label: 'Display Name',
name: 'userDisplayName',
type: 'string',
description: 'User display name',
show: {
userActions: ['createUser', 'updateUser']
},
additionalParams: true,
optional: true
},
{
label: 'User Max Results',
name: 'userMaxResults',
type: 'number',
default: 50,
description: 'Maximum number of users to return',
show: {
userActions: ['searchUsers']
},
additionalParams: true,
optional: true
}
]
}
async init(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
let credentialData = await getCredentialData(nodeData.credential ?? '', options)
const username = getCredentialParam('username', credentialData, nodeData)
const accessToken = getCredentialParam('accessToken', credentialData, nodeData)
const jiraHost = nodeData.inputs?.jiraHost as string
if (!username) {
throw new Error('No username found in credential')
}
if (!accessToken) {
throw new Error('No access token found in credential')
}
if (!jiraHost) {
throw new Error('No Jira host provided')
}
// Get all actions based on type
const jiraType = nodeData.inputs?.jiraType as string
let actions: string[] = []
if (jiraType === 'issues') {
actions = convertMultiOptionsToStringArray(nodeData.inputs?.issueActions)
} else if (jiraType === 'comments') {
actions = convertMultiOptionsToStringArray(nodeData.inputs?.commentActions)
} else if (jiraType === 'users') {
actions = convertMultiOptionsToStringArray(nodeData.inputs?.userActions)
}
// Prepare default parameters for each action
const defaultParams: ICommonObject = {}
// Issue parameters
const projectKey = nodeData.inputs?.projectKey
const issueType = nodeData.inputs?.issueType
const issueSummary = nodeData.inputs?.issueSummary
const issueDescription = nodeData.inputs?.issueDescription
const issuePriority = nodeData.inputs?.issuePriority
const issueKey = nodeData.inputs?.issueKey
const assigneeAccountId = nodeData.inputs?.assigneeAccountId
const transitionId = nodeData.inputs?.transitionId
const jqlQuery = nodeData.inputs?.jqlQuery
const issueMaxResults = nodeData.inputs?.issueMaxResults
// Comment parameters
const commentIssueKey = nodeData.inputs?.commentIssueKey
const commentText = nodeData.inputs?.commentText
const commentId = nodeData.inputs?.commentId
// User parameters
const userQuery = nodeData.inputs?.userQuery
const userAccountId = nodeData.inputs?.userAccountId
const userEmail = nodeData.inputs?.userEmail
const userDisplayName = nodeData.inputs?.userDisplayName
const userMaxResults = nodeData.inputs?.userMaxResults
// Set default parameters based on actions
actions.forEach((action) => {
const params: ICommonObject = {}
// Issue action parameters
if (action === 'listIssues') {
if (projectKey) params.projectKey = projectKey
if (jqlQuery) params.jql = jqlQuery
if (issueMaxResults) params.maxResults = issueMaxResults
}
if (action === 'createIssue') {
if (projectKey) params.projectKey = projectKey
if (issueType) params.issueType = issueType
if (issueSummary) params.summary = issueSummary
if (issueDescription) params.description = issueDescription
if (issuePriority) params.priority = issuePriority
if (assigneeAccountId) params.assigneeAccountId = assigneeAccountId
}
if (['getIssue', 'updateIssue', 'deleteIssue', 'assignIssue', 'transitionIssue'].includes(action)) {
if (issueKey) params.issueKey = issueKey
}
if (action === 'updateIssue') {
if (issueSummary) params.summary = issueSummary
if (issueDescription) params.description = issueDescription
if (issuePriority) params.priority = issuePriority
if (assigneeAccountId) params.assigneeAccountId = assigneeAccountId
}
if (action === 'assignIssue') {
if (assigneeAccountId) params.assigneeAccountId = assigneeAccountId
}
if (action === 'transitionIssue') {
if (transitionId) params.transitionId = transitionId
}
// Comment action parameters
if (['listComments', 'createComment'].includes(action) && commentIssueKey) {
params.issueKey = commentIssueKey
}
if (['createComment', 'updateComment'].includes(action) && commentText) {
params.text = commentText
}
if (['getComment', 'updateComment', 'deleteComment'].includes(action) && commentId) {
params.commentId = commentId
}
// User action parameters
if (action === 'searchUsers') {
if (userQuery) params.query = userQuery
if (userMaxResults) params.maxResults = userMaxResults
}
if (['getUser', 'updateUser', 'deleteUser'].includes(action) && userAccountId) {
params.accountId = userAccountId
}
if (['createUser', 'updateUser'].includes(action)) {
if (userEmail) params.emailAddress = userEmail
if (userDisplayName) params.displayName = userDisplayName
}
defaultParams[action] = params
})
// Create and return tools based on selected actions
const tools = createJiraTools({
actions,
username,
accessToken,
jiraHost,
defaultParams
})
return tools
}
}
module.exports = { nodeClass: Jira_Tools }
File diff suppressed because it is too large Load Diff
@@ -0,0 +1 @@
<svg height="2500" preserveAspectRatio="xMidYMid" width="2500" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 -30.632388516510233 255.324 285.95638851651023"><linearGradient id="a"><stop offset=".18" stop-color="#0052cc"/><stop offset="1" stop-color="#2684ff"/></linearGradient><linearGradient id="b" x1="98.031%" x2="58.888%" xlink:href="#a" y1=".161%" y2="40.766%"/><linearGradient id="c" x1="100.665%" x2="55.402%" xlink:href="#a" y1=".455%" y2="44.727%"/><path d="M244.658 0H121.707a55.502 55.502 0 0 0 55.502 55.502h22.649V77.37c.02 30.625 24.841 55.447 55.466 55.467V10.666C255.324 4.777 250.55 0 244.658 0z" fill="#2684ff"/><path d="M183.822 61.262H60.872c.019 30.625 24.84 55.447 55.466 55.467h22.649v21.938c.039 30.625 24.877 55.43 55.502 55.43V71.93c0-5.891-4.776-10.667-10.667-10.667z" fill="url(#b)"/><path d="M122.951 122.489H0c0 30.653 24.85 55.502 55.502 55.502h22.72v21.867c.02 30.597 24.798 55.408 55.396 55.466V133.156c0-5.891-4.776-10.667-10.667-10.667z" fill="url(#c)"/></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -0,0 +1,971 @@
import { convertMultiOptionsToStringArray, getCredentialData, getCredentialParam, refreshOAuth2Token } from '../../../src/utils'
import { createOutlookTools } from './core'
import type { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
class MicrosoftOutlook_Tools implements INode {
label: string
name: string
version: number
type: string
icon: string
category: string
description: string
baseClasses: string[]
credential: INodeParams
inputs: INodeParams[]
constructor() {
this.label = 'Microsoft Outlook'
this.name = 'microsoftOutlook'
this.version = 1.0
this.type = 'MicrosoftOutlook'
this.icon = 'outlook.svg'
this.category = 'Tools'
this.description = 'Perform Microsoft Outlook operations for calendars, events, and messages'
this.baseClasses = [this.type, 'Tool']
this.credential = {
label: 'Connect Credential',
name: 'credential',
type: 'credential',
credentialNames: ['microsoftOutlookOAuth2']
}
this.inputs = [
{
label: 'Type',
name: 'outlookType',
type: 'options',
options: [
{
label: 'Calendar',
name: 'calendar'
},
{
label: 'Message',
name: 'message'
}
]
},
// Calendar Actions
{
label: 'Calendar Actions',
name: 'calendarActions',
type: 'multiOptions',
options: [
{
label: 'List Calendars',
name: 'listCalendars'
},
{
label: 'Get Calendar',
name: 'getCalendar'
},
{
label: 'Create Calendar',
name: 'createCalendar'
},
{
label: 'Update Calendar',
name: 'updateCalendar'
},
{
label: 'Delete Calendar',
name: 'deleteCalendar'
},
{
label: 'List Events',
name: 'listEvents'
},
{
label: 'Get Event',
name: 'getEvent'
},
{
label: 'Create Event',
name: 'createEvent'
},
{
label: 'Update Event',
name: 'updateEvent'
},
{
label: 'Delete Event',
name: 'deleteEvent'
}
],
show: {
outlookType: ['calendar']
}
},
// Message Actions
{
label: 'Message Actions',
name: 'messageActions',
type: 'multiOptions',
options: [
{
label: 'List Messages',
name: 'listMessages'
},
{
label: 'Get Message',
name: 'getMessage'
},
{
label: 'Create Draft Message',
name: 'createDraftMessage'
},
{
label: 'Send Message',
name: 'sendMessage'
},
{
label: 'Update Message',
name: 'updateMessage'
},
{
label: 'Delete Message',
name: 'deleteMessage'
},
{
label: 'Copy Message',
name: 'copyMessage'
},
{
label: 'Move Message',
name: 'moveMessage'
},
{
label: 'Reply to Message',
name: 'replyMessage'
},
{
label: 'Forward Message',
name: 'forwardMessage'
}
],
show: {
outlookType: ['message']
}
},
// CALENDAR PARAMETERS
// List Calendars Parameters
{
label: 'Max Results [List Calendars]',
name: 'maxResultsListCalendars',
type: 'number',
description: 'Maximum number of calendars to return',
default: 50,
show: {
outlookType: ['calendar'],
calendarActions: ['listCalendars']
},
additionalParams: true,
optional: true
},
// Get Calendar Parameters
{
label: 'Calendar ID [Get Calendar]',
name: 'calendarIdGetCalendar',
type: 'string',
description: 'ID of the calendar to retrieve',
show: {
outlookType: ['calendar'],
calendarActions: ['getCalendar']
},
additionalParams: true,
optional: true
},
// Create Calendar Parameters
{
label: 'Calendar Name [Create Calendar]',
name: 'calendarNameCreateCalendar',
type: 'string',
description: 'Name of the calendar',
placeholder: 'My New Calendar',
show: {
outlookType: ['calendar'],
calendarActions: ['createCalendar']
},
additionalParams: true,
optional: true
},
// Update Calendar Parameters
{
label: 'Calendar ID [Update Calendar]',
name: 'calendarIdUpdateCalendar',
type: 'string',
description: 'ID of the calendar to update',
show: {
outlookType: ['calendar'],
calendarActions: ['updateCalendar']
},
additionalParams: true,
optional: true
},
{
label: 'Calendar Name [Update Calendar]',
name: 'calendarNameUpdateCalendar',
type: 'string',
description: 'New name of the calendar',
show: {
outlookType: ['calendar'],
calendarActions: ['updateCalendar']
},
additionalParams: true,
optional: true
},
// Delete Calendar Parameters
{
label: 'Calendar ID [Delete Calendar]',
name: 'calendarIdDeleteCalendar',
type: 'string',
description: 'ID of the calendar to delete',
show: {
outlookType: ['calendar'],
calendarActions: ['deleteCalendar']
},
additionalParams: true,
optional: true
},
// List Events Parameters
{
label: 'Calendar ID [List Events]',
name: 'calendarIdListEvents',
type: 'string',
description: 'ID of the calendar (leave empty for primary calendar)',
show: {
outlookType: ['calendar'],
calendarActions: ['listEvents']
},
additionalParams: true,
optional: true
},
{
label: 'Max Results [List Events]',
name: 'maxResultsListEvents',
type: 'number',
description: 'Maximum number of events to return',
default: 50,
show: {
outlookType: ['calendar'],
calendarActions: ['listEvents']
},
additionalParams: true,
optional: true
},
{
label: 'Start Date Time [List Events]',
name: 'startDateTimeListEvents',
type: 'string',
description: 'Start date time filter in ISO format',
placeholder: '2024-01-01T00:00:00Z',
show: {
outlookType: ['calendar'],
calendarActions: ['listEvents']
},
additionalParams: true,
optional: true
},
{
label: 'End Date Time [List Events]',
name: 'endDateTimeListEvents',
type: 'string',
description: 'End date time filter in ISO format',
placeholder: '2024-12-31T23:59:59Z',
show: {
outlookType: ['calendar'],
calendarActions: ['listEvents']
},
additionalParams: true,
optional: true
},
// Get Event Parameters
{
label: 'Event ID [Get Event]',
name: 'eventIdGetEvent',
type: 'string',
description: 'ID of the event to retrieve',
show: {
outlookType: ['calendar'],
calendarActions: ['getEvent']
},
additionalParams: true,
optional: true
},
// Create Event Parameters
{
label: 'Subject [Create Event]',
name: 'subjectCreateEvent',
type: 'string',
description: 'Subject/title of the event',
placeholder: 'Meeting Title',
show: {
outlookType: ['calendar'],
calendarActions: ['createEvent']
},
additionalParams: true,
optional: true
},
{
label: 'Body [Create Event]',
name: 'bodyCreateEvent',
type: 'string',
description: 'Body/description of the event',
placeholder: 'Meeting description',
rows: 3,
show: {
outlookType: ['calendar'],
calendarActions: ['createEvent']
},
additionalParams: true,
optional: true
},
{
label: 'Start Date Time [Create Event]',
name: 'startDateTimeCreateEvent',
type: 'string',
description: 'Start date and time in ISO format',
placeholder: '2024-01-15T10:00:00',
show: {
outlookType: ['calendar'],
calendarActions: ['createEvent']
},
additionalParams: true,
optional: true
},
{
label: 'End Date Time [Create Event]',
name: 'endDateTimeCreateEvent',
type: 'string',
description: 'End date and time in ISO format',
placeholder: '2024-01-15T11:00:00',
show: {
outlookType: ['calendar'],
calendarActions: ['createEvent']
},
additionalParams: true,
optional: true
},
{
label: 'Time Zone [Create Event]',
name: 'timeZoneCreateEvent',
type: 'string',
description: 'Time zone for the event',
placeholder: 'UTC',
default: 'UTC',
show: {
outlookType: ['calendar'],
calendarActions: ['createEvent']
},
additionalParams: true,
optional: true
},
{
label: 'Location [Create Event]',
name: 'locationCreateEvent',
type: 'string',
description: 'Location of the event',
placeholder: 'Conference Room A',
show: {
outlookType: ['calendar'],
calendarActions: ['createEvent']
},
additionalParams: true,
optional: true
},
{
label: 'Attendees [Create Event]',
name: 'attendeesCreateEvent',
type: 'string',
description: 'Comma-separated list of attendee email addresses',
placeholder: 'user1@example.com,user2@example.com',
show: {
outlookType: ['calendar'],
calendarActions: ['createEvent']
},
additionalParams: true,
optional: true
},
// Update Event Parameters
{
label: 'Event ID [Update Event]',
name: 'eventIdUpdateEvent',
type: 'string',
description: 'ID of the event to update',
show: {
outlookType: ['calendar'],
calendarActions: ['updateEvent']
},
additionalParams: true,
optional: true
},
{
label: 'Subject [Update Event]',
name: 'subjectUpdateEvent',
type: 'string',
description: 'New subject/title of the event',
show: {
outlookType: ['calendar'],
calendarActions: ['updateEvent']
},
additionalParams: true,
optional: true
},
// Delete Event Parameters
{
label: 'Event ID [Delete Event]',
name: 'eventIdDeleteEvent',
type: 'string',
description: 'ID of the event to delete',
show: {
outlookType: ['calendar'],
calendarActions: ['deleteEvent']
},
additionalParams: true,
optional: true
},
// MESSAGE PARAMETERS
// List Messages Parameters
{
label: 'Max Results [List Messages]',
name: 'maxResultsListMessages',
type: 'number',
description: 'Maximum number of messages to return',
default: 50,
show: {
outlookType: ['message'],
messageActions: ['listMessages']
},
additionalParams: true,
optional: true
},
{
label: 'Filter [List Messages]',
name: 'filterListMessages',
type: 'string',
description: 'Filter query (e.g., "isRead eq false")',
show: {
outlookType: ['message'],
messageActions: ['listMessages']
},
additionalParams: true,
optional: true
},
// Get Message Parameters
{
label: 'Message ID [Get Message]',
name: 'messageIdGetMessage',
type: 'string',
description: 'ID of the message to retrieve',
show: {
outlookType: ['message'],
messageActions: ['getMessage']
},
additionalParams: true,
optional: true
},
// Create Draft Message Parameters
{
label: 'To [Create Draft Message]',
name: 'toCreateDraftMessage',
type: 'string',
description: 'Recipient email address(es), comma-separated',
placeholder: 'user@example.com',
show: {
outlookType: ['message'],
messageActions: ['createDraftMessage']
},
additionalParams: true,
optional: true
},
{
label: 'Subject [Create Draft Message]',
name: 'subjectCreateDraftMessage',
type: 'string',
description: 'Subject of the message',
placeholder: 'Email Subject',
show: {
outlookType: ['message'],
messageActions: ['createDraftMessage']
},
additionalParams: true,
optional: true
},
{
label: 'Body [Create Draft Message]',
name: 'bodyCreateDraftMessage',
type: 'string',
description: 'Body content of the message',
placeholder: 'Email body content',
rows: 4,
show: {
outlookType: ['message'],
messageActions: ['createDraftMessage']
},
additionalParams: true,
optional: true
},
{
label: 'CC [Create Draft Message]',
name: 'ccCreateDraftMessage',
type: 'string',
description: 'CC email address(es), comma-separated',
placeholder: 'cc@example.com',
show: {
outlookType: ['message'],
messageActions: ['createDraftMessage']
},
additionalParams: true,
optional: true
},
{
label: 'BCC [Create Draft Message]',
name: 'bccCreateDraftMessage',
type: 'string',
description: 'BCC email address(es), comma-separated',
placeholder: 'bcc@example.com',
show: {
outlookType: ['message'],
messageActions: ['createDraftMessage']
},
additionalParams: true,
optional: true
},
// Send Message Parameters
{
label: 'To [Send Message]',
name: 'toSendMessage',
type: 'string',
description: 'Recipient email address(es), comma-separated',
placeholder: 'user@example.com',
show: {
outlookType: ['message'],
messageActions: ['sendMessage']
},
additionalParams: true,
optional: true
},
{
label: 'Subject [Send Message]',
name: 'subjectSendMessage',
type: 'string',
description: 'Subject of the message',
placeholder: 'Email Subject',
show: {
outlookType: ['message'],
messageActions: ['sendMessage']
},
additionalParams: true,
optional: true
},
{
label: 'Body [Send Message]',
name: 'bodySendMessage',
type: 'string',
description: 'Body content of the message',
placeholder: 'Email body content',
rows: 4,
show: {
outlookType: ['message'],
messageActions: ['sendMessage']
},
additionalParams: true,
optional: true
},
// Update Message Parameters
{
label: 'Message ID [Update Message]',
name: 'messageIdUpdateMessage',
type: 'string',
description: 'ID of the message to update',
show: {
outlookType: ['message'],
messageActions: ['updateMessage']
},
additionalParams: true,
optional: true
},
{
label: 'Is Read [Update Message]',
name: 'isReadUpdateMessage',
type: 'boolean',
description: 'Mark message as read/unread',
show: {
outlookType: ['message'],
messageActions: ['updateMessage']
},
additionalParams: true,
optional: true
},
// Delete Message Parameters
{
label: 'Message ID [Delete Message]',
name: 'messageIdDeleteMessage',
type: 'string',
description: 'ID of the message to delete',
show: {
outlookType: ['message'],
messageActions: ['deleteMessage']
},
additionalParams: true,
optional: true
},
// Copy Message Parameters
{
label: 'Message ID [Copy Message]',
name: 'messageIdCopyMessage',
type: 'string',
description: 'ID of the message to copy',
show: {
outlookType: ['message'],
messageActions: ['copyMessage']
},
additionalParams: true,
optional: true
},
{
label: 'Destination Folder ID [Copy Message]',
name: 'destinationFolderIdCopyMessage',
type: 'string',
description: 'ID of the destination folder',
show: {
outlookType: ['message'],
messageActions: ['copyMessage']
},
additionalParams: true,
optional: true
},
// Move Message Parameters
{
label: 'Message ID [Move Message]',
name: 'messageIdMoveMessage',
type: 'string',
description: 'ID of the message to move',
show: {
outlookType: ['message'],
messageActions: ['moveMessage']
},
additionalParams: true,
optional: true
},
{
label: 'Destination Folder ID [Move Message]',
name: 'destinationFolderIdMoveMessage',
type: 'string',
description: 'ID of the destination folder',
show: {
outlookType: ['message'],
messageActions: ['moveMessage']
},
additionalParams: true,
optional: true
},
// Reply Message Parameters
{
label: 'Message ID [Reply Message]',
name: 'messageIdReplyMessage',
type: 'string',
description: 'ID of the message to reply to',
show: {
outlookType: ['message'],
messageActions: ['replyMessage']
},
additionalParams: true,
optional: true
},
{
label: 'Reply Body [Reply Message]',
name: 'replyBodyReplyMessage',
type: 'string',
description: 'Reply message body',
rows: 4,
show: {
outlookType: ['message'],
messageActions: ['replyMessage']
},
additionalParams: true,
optional: true
},
// Forward Message Parameters
{
label: 'Message ID [Forward Message]',
name: 'messageIdForwardMessage',
type: 'string',
description: 'ID of the message to forward',
show: {
outlookType: ['message'],
messageActions: ['forwardMessage']
},
additionalParams: true,
optional: true
},
{
label: 'Forward To [Forward Message]',
name: 'forwardToForwardMessage',
type: 'string',
description: 'Email address(es) to forward to, comma-separated',
show: {
outlookType: ['message'],
messageActions: ['forwardMessage']
},
additionalParams: true,
optional: true
},
{
label: 'Forward Comment [Forward Message]',
name: 'forwardCommentForwardMessage',
type: 'string',
description: 'Additional comment to include with forward',
rows: 2,
show: {
outlookType: ['message'],
messageActions: ['forwardMessage']
},
additionalParams: true,
optional: true
}
]
}
async init(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
const outlookType = nodeData.inputs?.outlookType as string
const calendarActions = nodeData.inputs?.calendarActions as string
const messageActions = nodeData.inputs?.messageActions as string
let credentialData = await getCredentialData(nodeData.credential ?? '', options)
credentialData = await refreshOAuth2Token(nodeData.credential ?? '', credentialData, options)
const accessToken = getCredentialParam('access_token', credentialData, nodeData)
if (!accessToken) {
throw new Error('No access token found in credential')
}
let actions: string[] = []
if (outlookType === 'calendar') {
actions = convertMultiOptionsToStringArray(calendarActions)
} else if (outlookType === 'message') {
actions = convertMultiOptionsToStringArray(messageActions)
}
// Prepare default parameters for each action based on type
const defaultParams: ICommonObject = {}
if (outlookType === 'calendar') {
// Map calendar actions to their parameters
actions.forEach((action) => {
defaultParams[action] = {}
switch (action) {
case 'listCalendars':
if (nodeData.inputs?.maxResultsListCalendars) {
defaultParams[action].maxResults = nodeData.inputs.maxResultsListCalendars
}
break
case 'getCalendar':
if (nodeData.inputs?.calendarIdGetCalendar) {
defaultParams[action].calendarId = nodeData.inputs.calendarIdGetCalendar
}
break
case 'createCalendar':
if (nodeData.inputs?.calendarNameCreateCalendar) {
defaultParams[action].calendarName = nodeData.inputs.calendarNameCreateCalendar
}
break
case 'updateCalendar':
if (nodeData.inputs?.calendarIdUpdateCalendar) {
defaultParams[action].calendarId = nodeData.inputs.calendarIdUpdateCalendar
}
if (nodeData.inputs?.calendarNameUpdateCalendar) {
defaultParams[action].calendarName = nodeData.inputs.calendarNameUpdateCalendar
}
break
case 'deleteCalendar':
if (nodeData.inputs?.calendarIdDeleteCalendar) {
defaultParams[action].calendarId = nodeData.inputs.calendarIdDeleteCalendar
}
break
case 'listEvents':
if (nodeData.inputs?.calendarIdListEvents) {
defaultParams[action].calendarId = nodeData.inputs.calendarIdListEvents
}
if (nodeData.inputs?.maxResultsListEvents) {
defaultParams[action].maxResults = nodeData.inputs.maxResultsListEvents
}
if (nodeData.inputs?.startDateTimeListEvents) {
defaultParams[action].startDateTime = nodeData.inputs.startDateTimeListEvents
}
if (nodeData.inputs?.endDateTimeListEvents) {
defaultParams[action].endDateTime = nodeData.inputs.endDateTimeListEvents
}
break
case 'getEvent':
if (nodeData.inputs?.eventIdGetEvent) {
defaultParams[action].eventId = nodeData.inputs.eventIdGetEvent
}
break
case 'createEvent':
if (nodeData.inputs?.subjectCreateEvent) {
defaultParams[action].subject = nodeData.inputs.subjectCreateEvent
}
if (nodeData.inputs?.bodyCreateEvent) {
defaultParams[action].body = nodeData.inputs.bodyCreateEvent
}
if (nodeData.inputs?.startDateTimeCreateEvent) {
defaultParams[action].startDateTime = nodeData.inputs.startDateTimeCreateEvent
}
if (nodeData.inputs?.endDateTimeCreateEvent) {
defaultParams[action].endDateTime = nodeData.inputs.endDateTimeCreateEvent
}
if (nodeData.inputs?.timeZoneCreateEvent) {
defaultParams[action].timeZone = nodeData.inputs.timeZoneCreateEvent
}
if (nodeData.inputs?.locationCreateEvent) {
defaultParams[action].location = nodeData.inputs.locationCreateEvent
}
if (nodeData.inputs?.attendeesCreateEvent) {
defaultParams[action].attendees = nodeData.inputs.attendeesCreateEvent
}
break
case 'updateEvent':
if (nodeData.inputs?.eventIdUpdateEvent) {
defaultParams[action].eventId = nodeData.inputs.eventIdUpdateEvent
}
if (nodeData.inputs?.subjectUpdateEvent) {
defaultParams[action].subject = nodeData.inputs.subjectUpdateEvent
}
break
case 'deleteEvent':
if (nodeData.inputs?.eventIdDeleteEvent) {
defaultParams[action].eventId = nodeData.inputs.eventIdDeleteEvent
}
break
}
})
} else if (outlookType === 'message') {
// Map message actions to their parameters
actions.forEach((action) => {
defaultParams[action] = {}
switch (action) {
case 'listMessages':
if (nodeData.inputs?.maxResultsListMessages) {
defaultParams[action].maxResults = nodeData.inputs.maxResultsListMessages
}
if (nodeData.inputs?.filterListMessages) {
defaultParams[action].filter = nodeData.inputs.filterListMessages
}
break
case 'getMessage':
if (nodeData.inputs?.messageIdGetMessage) {
defaultParams[action].messageId = nodeData.inputs.messageIdGetMessage
}
break
case 'createDraftMessage':
if (nodeData.inputs?.toCreateDraftMessage) {
defaultParams[action].to = nodeData.inputs.toCreateDraftMessage
}
if (nodeData.inputs?.subjectCreateDraftMessage) {
defaultParams[action].subject = nodeData.inputs.subjectCreateDraftMessage
}
if (nodeData.inputs?.bodyCreateDraftMessage) {
defaultParams[action].body = nodeData.inputs.bodyCreateDraftMessage
}
if (nodeData.inputs?.ccCreateDraftMessage) {
defaultParams[action].cc = nodeData.inputs.ccCreateDraftMessage
}
if (nodeData.inputs?.bccCreateDraftMessage) {
defaultParams[action].bcc = nodeData.inputs.bccCreateDraftMessage
}
break
case 'sendMessage':
if (nodeData.inputs?.toSendMessage) {
defaultParams[action].to = nodeData.inputs.toSendMessage
}
if (nodeData.inputs?.subjectSendMessage) {
defaultParams[action].subject = nodeData.inputs.subjectSendMessage
}
if (nodeData.inputs?.bodySendMessage) {
defaultParams[action].body = nodeData.inputs.bodySendMessage
}
break
case 'updateMessage':
if (nodeData.inputs?.messageIdUpdateMessage) {
defaultParams[action].messageId = nodeData.inputs.messageIdUpdateMessage
}
if (nodeData.inputs?.isReadUpdateMessage !== undefined) {
defaultParams[action].isRead = nodeData.inputs.isReadUpdateMessage
}
break
case 'deleteMessage':
if (nodeData.inputs?.messageIdDeleteMessage) {
defaultParams[action].messageId = nodeData.inputs.messageIdDeleteMessage
}
break
case 'copyMessage':
if (nodeData.inputs?.messageIdCopyMessage) {
defaultParams[action].messageId = nodeData.inputs.messageIdCopyMessage
}
if (nodeData.inputs?.destinationFolderIdCopyMessage) {
defaultParams[action].destinationFolderId = nodeData.inputs.destinationFolderIdCopyMessage
}
break
case 'moveMessage':
if (nodeData.inputs?.messageIdMoveMessage) {
defaultParams[action].messageId = nodeData.inputs.messageIdMoveMessage
}
if (nodeData.inputs?.destinationFolderIdMoveMessage) {
defaultParams[action].destinationFolderId = nodeData.inputs.destinationFolderIdMoveMessage
}
break
case 'replyMessage':
if (nodeData.inputs?.messageIdReplyMessage) {
defaultParams[action].messageId = nodeData.inputs.messageIdReplyMessage
}
if (nodeData.inputs?.replyBodyReplyMessage) {
defaultParams[action].replyBody = nodeData.inputs.replyBodyReplyMessage
}
break
case 'forwardMessage':
if (nodeData.inputs?.messageIdForwardMessage) {
defaultParams[action].messageId = nodeData.inputs.messageIdForwardMessage
}
if (nodeData.inputs?.forwardToForwardMessage) {
defaultParams[action].forwardTo = nodeData.inputs.forwardToForwardMessage
}
if (nodeData.inputs?.forwardCommentForwardMessage) {
defaultParams[action].forwardComment = nodeData.inputs.forwardCommentForwardMessage
}
break
}
})
}
const outlookTools = createOutlookTools({
accessToken,
actions,
defaultParams
})
return outlookTools
}
}
module.exports = { nodeClass: MicrosoftOutlook_Tools }
File diff suppressed because it is too large Load Diff
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="96px" height="96px"><path fill="#03A9F4" d="M21,31c0,1.104,0.896,2,2,2h17c1.104,0,2-0.896,2-2V16c0-1.104-0.896-2-2-2H23c-1.104,0-2,0.896-2,2V31z"/><path fill="#B3E5FC" d="M42,16.975V16c0-0.428-0.137-0.823-0.367-1.148l-11.264,6.932l-7.542-4.656L22.125,19l8.459,5L42,16.975z"/><path fill="#0277BD" d="M27 41.46L6 37.46 6 9.46 27 5.46z"/><path fill="#FFF" d="M21.216,18.311c-1.098-1.275-2.546-1.913-4.328-1.913c-1.892,0-3.408,0.669-4.554,2.003c-1.144,1.337-1.719,3.088-1.719,5.246c0,2.045,0.564,3.714,1.69,4.986c1.126,1.273,2.592,1.91,4.378,1.91c1.84,0,3.331-0.652,4.474-1.975c1.143-1.313,1.712-3.043,1.712-5.199C22.869,21.281,22.318,19.595,21.216,18.311z M19.049,26.735c-0.568,0.769-1.339,1.152-2.313,1.152c-0.939,0-1.699-0.394-2.285-1.187c-0.581-0.785-0.87-1.861-0.87-3.211c0-1.336,0.289-2.414,0.87-3.225c0.586-0.81,1.368-1.211,2.355-1.211c0.962,0,1.718,0.393,2.267,1.178c0.555,0.795,0.833,1.895,0.833,3.31C19.907,24.906,19.618,25.968,19.049,26.735z"/></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="96px" height="96px"><path fill="#5c6bc0" d="M41.5 13A3.5 3.5 0 1 0 41.5 20 3.5 3.5 0 1 0 41.5 13zM4 40l23 4V4L4 8V40z"/><path fill="#fff" d="M21 16.27L21 19 17.01 19.18 16.99 31.04 14.01 30.95 14.01 19.29 10 19.45 10 16.94z"/><path fill="#5c6bc0" d="M36 14c0 2.21-1.79 4-4 4-1.2 0-2.27-.53-3-1.36v-5.28c.73-.83 1.8-1.36 3-1.36C34.21 10 36 11.79 36 14zM38 23v11c0 0 1.567 0 3.5 0 1.762 0 3.205-1.306 3.45-3H45v-8H38zM29 20v17c0 0 1.567 0 3.5 0 1.762 0 3.205-1.306 3.45-3H36V20H29z"/></svg>

After

Width:  |  Height:  |  Size: 556 B

+2
View File
@@ -120,6 +120,7 @@
"node-html-markdown": "^1.3.0",
"notion-to-md": "^3.1.1",
"object-hash": "^3.0.0",
"officeparser": "5.1.1",
"ollama": "^0.5.11",
"openai": "^4.96.0",
"papaparse": "^5.4.1",
@@ -138,6 +139,7 @@
"weaviate-ts-client": "^1.1.0",
"winston": "^3.9.0",
"ws": "^8.18.0",
"xlsx": "0.18.5",
"zod": "3.22.4",
"zod-to-json-schema": "^3.21.4"
},
+79 -34
View File
@@ -585,42 +585,87 @@ const _showHideOperation = (nodeData: Record<string, any>, inputParam: Record<st
if (path.includes('$index') && index) {
path = path.replace('$index', index.toString())
}
const groundValue = get(nodeData.inputs, path, '')
let groundValue = get(nodeData.inputs, path, '')
if (groundValue && typeof groundValue === 'string' && groundValue.startsWith('[') && groundValue.endsWith(']')) {
groundValue = JSON.parse(groundValue)
}
if (Array.isArray(comparisonValue)) {
if (displayType === 'show' && !comparisonValue.includes(groundValue)) {
inputParam.display = false
// Handle case where groundValue is an array
if (Array.isArray(groundValue)) {
if (Array.isArray(comparisonValue)) {
// Both are arrays - check if there's any intersection
const hasIntersection = comparisonValue.some((val) => groundValue.includes(val))
if (displayType === 'show' && !hasIntersection) {
inputParam.display = false
}
if (displayType === 'hide' && hasIntersection) {
inputParam.display = false
}
} else if (typeof comparisonValue === 'string') {
// comparisonValue is string, groundValue is array - check if array contains the string
const matchFound = groundValue.some((val) => comparisonValue === val || new RegExp(comparisonValue).test(val))
if (displayType === 'show' && !matchFound) {
inputParam.display = false
}
if (displayType === 'hide' && matchFound) {
inputParam.display = false
}
} else if (typeof comparisonValue === 'boolean' || typeof comparisonValue === 'number') {
// For boolean/number comparison with array, check if array contains the value
const matchFound = groundValue.includes(comparisonValue)
if (displayType === 'show' && !matchFound) {
inputParam.display = false
}
if (displayType === 'hide' && matchFound) {
inputParam.display = false
}
} else if (typeof comparisonValue === 'object') {
// For object comparison with array, use deep equality check
const matchFound = groundValue.some((val) => isEqual(comparisonValue, val))
if (displayType === 'show' && !matchFound) {
inputParam.display = false
}
if (displayType === 'hide' && matchFound) {
inputParam.display = false
}
}
if (displayType === 'hide' && comparisonValue.includes(groundValue)) {
inputParam.display = false
}
} else if (typeof comparisonValue === 'string') {
if (displayType === 'show' && !(comparisonValue === groundValue || new RegExp(comparisonValue).test(groundValue))) {
inputParam.display = false
}
if (displayType === 'hide' && (comparisonValue === groundValue || new RegExp(comparisonValue).test(groundValue))) {
inputParam.display = false
}
} else if (typeof comparisonValue === 'boolean') {
if (displayType === 'show' && comparisonValue !== groundValue) {
inputParam.display = false
}
if (displayType === 'hide' && comparisonValue === groundValue) {
inputParam.display = false
}
} else if (typeof comparisonValue === 'object') {
if (displayType === 'show' && !isEqual(comparisonValue, groundValue)) {
inputParam.display = false
}
if (displayType === 'hide' && isEqual(comparisonValue, groundValue)) {
inputParam.display = false
}
} else if (typeof comparisonValue === 'number') {
if (displayType === 'show' && comparisonValue !== groundValue) {
inputParam.display = false
}
if (displayType === 'hide' && comparisonValue === groundValue) {
inputParam.display = false
} else {
// Original logic for non-array groundValue
if (Array.isArray(comparisonValue)) {
if (displayType === 'show' && !comparisonValue.includes(groundValue)) {
inputParam.display = false
}
if (displayType === 'hide' && comparisonValue.includes(groundValue)) {
inputParam.display = false
}
} else if (typeof comparisonValue === 'string') {
if (displayType === 'show' && !(comparisonValue === groundValue || new RegExp(comparisonValue).test(groundValue))) {
inputParam.display = false
}
if (displayType === 'hide' && (comparisonValue === groundValue || new RegExp(comparisonValue).test(groundValue))) {
inputParam.display = false
}
} else if (typeof comparisonValue === 'boolean') {
if (displayType === 'show' && comparisonValue !== groundValue) {
inputParam.display = false
}
if (displayType === 'hide' && comparisonValue === groundValue) {
inputParam.display = false
}
} else if (typeof comparisonValue === 'object') {
if (displayType === 'show' && !isEqual(comparisonValue, groundValue)) {
inputParam.display = false
}
if (displayType === 'hide' && isEqual(comparisonValue, groundValue)) {
inputParam.display = false
}
} else if (typeof comparisonValue === 'number') {
if (displayType === 'show' && comparisonValue !== groundValue) {
inputParam.display = false
}
if (displayType === 'hide' && comparisonValue === groundValue) {
inputParam.display = false
}
}
}
})
+20 -1
View File
@@ -28,6 +28,7 @@ import { getErrorMessage } from './error'
export const SOURCE_DOCUMENTS_PREFIX = '\n\n----FLOWISE_SOURCE_DOCUMENTS----\n\n'
export const ARTIFACTS_PREFIX = '\n\n----FLOWISE_ARTIFACTS----\n\n'
export const TOOL_ARGS_PREFIX = '\n\n----FLOWISE_TOOL_ARGS----\n\n'
export type AgentFinish = {
returnValues: Record<string, any>
@@ -444,9 +445,19 @@ export class AgentExecutor extends BaseChain<ChainValues, AgentExecutorOutput> {
if (typeof toolOutput === 'string' && toolOutput.includes(ARTIFACTS_PREFIX)) {
toolOutput = toolOutput.split(ARTIFACTS_PREFIX)[0]
}
let toolInput
if (typeof toolOutput === 'string' && toolOutput.includes(TOOL_ARGS_PREFIX)) {
const splitArray = toolOutput.split(TOOL_ARGS_PREFIX)
toolOutput = splitArray[0]
try {
toolInput = JSON.parse(splitArray[1])
} catch (e) {
console.error('Error parsing tool input from tool')
}
}
usedTools.push({
tool: tool.name,
toolInput: action.toolInput as any,
toolInput: toolInput ?? (action.toolInput as any),
toolOutput
})
} else {
@@ -502,6 +513,10 @@ export class AgentExecutor extends BaseChain<ChainValues, AgentExecutorOutput> {
console.error('Error parsing source documents from tool')
}
}
if (typeof observation === 'string' && observation.includes(TOOL_ARGS_PREFIX)) {
const observationArray = observation.split(TOOL_ARGS_PREFIX)
observation = observationArray[0]
}
return { action, observation: observation ?? '' }
})
)
@@ -610,6 +625,10 @@ export class AgentExecutor extends BaseChain<ChainValues, AgentExecutorOutput> {
const observationArray = observation.split(ARTIFACTS_PREFIX)
observation = observationArray[0]
}
if (typeof observation === 'string' && observation.includes(TOOL_ARGS_PREFIX)) {
const observationArray = observation.split(TOOL_ARGS_PREFIX)
observation = observationArray[0]
}
} catch (e) {
if (e instanceof ToolInputParsingException) {
if (this.handleParsingErrors === true) {
+65
View File
@@ -1215,3 +1215,68 @@ export const handleDocumentLoaderDocuments = async (loader: DocumentLoader, text
return docs
}
/**
* Check if OAuth2 token is expired and refresh if needed
* @param {string} credentialId
* @param {ICommonObject} credentialData
* @param {ICommonObject} options
* @param {number} bufferTimeMs - Buffer time in milliseconds before expiry (default: 5 minutes)
* @returns {Promise<ICommonObject>}
*/
export const refreshOAuth2Token = async (
credentialId: string,
credentialData: ICommonObject,
options: ICommonObject,
bufferTimeMs: number = 5 * 60 * 1000
): Promise<ICommonObject> => {
// Check if token is expired and refresh if needed
if (credentialData.expires_at) {
const expiryTime = new Date(credentialData.expires_at)
const currentTime = new Date()
if (currentTime.getTime() > expiryTime.getTime() - bufferTimeMs) {
if (!credentialData.refresh_token) {
throw new Error('Access token is expired and no refresh token is available. Please re-authorize the credential.')
}
try {
// Import fetch dynamically to avoid issues
const fetch = (await import('node-fetch')).default
// Call the refresh API endpoint
const refreshResponse = await fetch(
`${options.baseURL || 'http://localhost:3000'}/api/v1/oauth2-credential/refresh/${credentialId}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
}
)
if (!refreshResponse.ok) {
const errorData = await refreshResponse.text()
throw new Error(`Failed to refresh token: ${refreshResponse.status} ${refreshResponse.statusText} - ${errorData}`)
}
await refreshResponse.json()
// Get the updated credential data
const updatedCredentialData = await getCredentialData(credentialId, options)
return updatedCredentialData
} catch (error) {
console.error('Failed to refresh access token:', error)
throw new Error(
`Failed to refresh access token: ${
error instanceof Error ? error.message : 'Unknown error'
}. Please re-authorize the credential.`
)
}
}
}
// Token is not expired, return original data
return credentialData
}
+2
View File
@@ -31,6 +31,7 @@ import nodeCustomFunctionRouter from './node-custom-functions'
import nodeIconRouter from './node-icons'
import nodeLoadMethodRouter from './node-load-methods'
import nodesRouter from './nodes'
import oauth2Router from './oauth2'
import openaiAssistantsRouter from './openai-assistants'
import openaiAssistantsFileRouter from './openai-assistants-files'
import openaiAssistantsVectorStoreRouter from './openai-assistants-vector-store'
@@ -100,6 +101,7 @@ router.use('/node-custom-function', nodeCustomFunctionRouter)
router.use('/node-icon', nodeIconRouter)
router.use('/node-load-method', nodeLoadMethodRouter)
router.use('/nodes', nodesRouter)
router.use('/oauth2-credential', oauth2Router)
router.use('/openai-assistants', openaiAssistantsRouter)
router.use('/openai-assistants-file', openaiAssistantsFileRouter)
router.use('/openai-assistants-vector-store', openaiAssistantsVectorStoreRouter)
+422
View File
@@ -0,0 +1,422 @@
/**
* OAuth2 Authorization Code Flow Implementation
*
* This module implements a complete OAuth2 authorization code flow for Flowise credentials.
* It supports Microsoft Graph and other OAuth2 providers.
*
* CREDENTIAL DATA STRUCTURE:
* The credential's encryptedData should contain a JSON object with the following fields:
*
* Required fields:
* - client_id: OAuth2 application client ID
* - client_secret: OAuth2 application client secret
*
* Optional fields (provider-specific):
* - tenant_id: Microsoft Graph tenant ID (if using Microsoft Graph)
* - authorization_endpoint: Custom authorization URL (defaults to Microsoft Graph if tenant_id provided)
* - token_endpoint: Custom token URL (defaults to Microsoft Graph if tenant_id provided)
* - redirect_uri: Custom redirect URI (defaults to this callback endpoint)
* - scope: OAuth2 scopes to request (e.g., "user.read mail.read")
* - response_type: OAuth2 response type (defaults to "code")
* - response_mode: OAuth2 response mode (defaults to "query")
*
* ENDPOINTS:
*
* 1. POST /api/v1/oauth2/authorize/:credentialId
* - Generates authorization URL for initiating OAuth2 flow
* - Uses credential ID as state parameter for security
* - Returns authorization URL to redirect user to
*
* 2. GET /api/v1/oauth2/callback
* - Handles OAuth2 callback with authorization code
* - Exchanges code for access token
* - Updates credential with token data
* - Supports Microsoft Graph and custom OAuth2 providers
*
* 3. POST /api/v1/oauth2/refresh/:credentialId
* - Refreshes expired access tokens using refresh token
* - Updates credential with new token data
*
* USAGE FLOW:
* 1. Create a credential with OAuth2 configuration (client_id, client_secret, etc.)
* 2. Call POST /oauth2/authorize/:credentialId to get authorization URL
* 3. Redirect user to authorization URL
* 4. User authorizes and gets redirected to callback endpoint
* 5. Callback endpoint exchanges code for tokens and saves them
* 6. Use POST /oauth2/refresh/:credentialId when tokens expire
*
* TOKEN STORAGE:
* After successful authorization, the credential will contain additional fields:
* - access_token: OAuth2 access token
* - refresh_token: OAuth2 refresh token (if provided)
* - token_type: Token type (usually "Bearer")
* - expires_in: Token lifetime in seconds
* - expires_at: Token expiry timestamp (ISO string)
* - granted_scope: Actual scopes granted by provider
* - token_received_at: When token was received (ISO string)
*/
import express from 'express'
import axios from 'axios'
import { Request, Response, NextFunction } from 'express'
import { getRunningExpressApp } from '../../utils/getRunningExpressApp'
import { Credential } from '../../database/entities/Credential'
import { decryptCredentialData, encryptCredentialData } from '../../utils'
import { InternalFlowiseError } from '../../errors/internalFlowiseError'
import { StatusCodes } from 'http-status-codes'
import { generateSuccessPage, generateErrorPage } from './templates'
const router = express.Router()
// Initiate OAuth2 authorization flow
router.post('/authorize/:credentialId', async (req: Request, res: Response, next: NextFunction) => {
try {
const { credentialId } = req.params
const appServer = getRunningExpressApp()
const credentialRepository = appServer.AppDataSource.getRepository(Credential)
// Find credential by ID
const credential = await credentialRepository.findOneBy({
id: credentialId
})
if (!credential) {
return res.status(404).json({
success: false,
message: 'Credential not found'
})
}
// Decrypt the credential data to get OAuth configuration
const decryptedData = await decryptCredentialData(credential.encryptedData)
const {
clientId,
authorizationUrl,
redirect_uri,
scope,
response_type = 'code',
response_mode = 'query',
additionalParameters = ''
} = decryptedData
if (!clientId) {
return res.status(400).json({
success: false,
message: 'Missing clientId in credential data'
})
}
if (!authorizationUrl) {
return res.status(400).json({
success: false,
message: 'No authorizationUrl specified in credential data'
})
}
const defaultRedirectUri = `${req.protocol}://${req.get('host')}/api/v1/oauth2-credential/callback`
const finalRedirectUri = redirect_uri || defaultRedirectUri
const authParams = new URLSearchParams({
client_id: clientId,
response_type,
response_mode,
state: credentialId, // Use credential ID as state parameter
redirect_uri: finalRedirectUri
})
if (scope) {
authParams.append('scope', scope)
}
let fullAuthorizationUrl = `${authorizationUrl}?${authParams.toString()}`
if (additionalParameters) {
fullAuthorizationUrl += `&${additionalParameters.toString()}`
}
res.json({
success: true,
message: 'Authorization URL generated successfully',
credentialId,
authorizationUrl: fullAuthorizationUrl,
redirectUri: finalRedirectUri
})
} catch (error) {
next(
new InternalFlowiseError(
StatusCodes.INTERNAL_SERVER_ERROR,
`OAuth2 authorization error: ${error instanceof Error ? error.message : 'Unknown error'}`
)
)
}
})
// OAuth2 callback endpoint
router.get('/callback', async (req: Request, res: Response) => {
try {
const { code, state, error, error_description } = req.query
if (error) {
const errorHtml = generateErrorPage(
error as string,
(error_description as string) || 'An error occurred',
error_description ? `Description: ${error_description}` : undefined
)
res.setHeader('Content-Type', 'text/html')
return res.status(400).send(errorHtml)
}
if (!code || !state) {
const errorHtml = generateErrorPage('Missing required parameters', 'Missing code or state', 'Please try again later.')
res.setHeader('Content-Type', 'text/html')
return res.status(400).send(errorHtml)
}
const appServer = getRunningExpressApp()
const credentialRepository = appServer.AppDataSource.getRepository(Credential)
// Find credential by state (assuming state contains the credential ID)
const credential = await credentialRepository.findOneBy({
id: state as string
})
if (!credential) {
const errorHtml = generateErrorPage(
'Credential not found',
`Credential not found for the provided state: ${state}`,
'Please try the authorization process again.'
)
res.setHeader('Content-Type', 'text/html')
return res.status(404).send(errorHtml)
}
const decryptedData = await decryptCredentialData(credential.encryptedData)
const { clientId, clientSecret, accessTokenUrl, redirect_uri, scope } = decryptedData
if (!clientId || !clientSecret) {
const errorHtml = generateErrorPage(
'Missing OAuth configuration',
'Missing clientId or clientSecret',
'Please check your credential setup.'
)
res.setHeader('Content-Type', 'text/html')
return res.status(400).send(errorHtml)
}
let tokenUrl = accessTokenUrl
if (!tokenUrl) {
const errorHtml = generateErrorPage(
'Missing token endpoint URL',
'No Access Token URL specified in credential data',
'Please check your credential configuration.'
)
res.setHeader('Content-Type', 'text/html')
return res.status(400).send(errorHtml)
}
const defaultRedirectUri = `${req.protocol}://${req.get('host')}/api/v1/oauth2-credential/callback`
const finalRedirectUri = redirect_uri || defaultRedirectUri
const tokenRequestData: any = {
client_id: clientId,
client_secret: clientSecret,
code: code as string,
grant_type: 'authorization_code',
redirect_uri: finalRedirectUri
}
if (scope) {
tokenRequestData.scope = scope
}
const tokenResponse = await axios.post(tokenUrl, new URLSearchParams(tokenRequestData).toString(), {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Accept: 'application/json'
}
})
const tokenData = tokenResponse.data
// Update the credential data with token information
const updatedCredentialData: any = {
...decryptedData,
...tokenData,
token_received_at: new Date().toISOString()
}
// Add refresh token if provided
if (tokenData.refresh_token) {
updatedCredentialData.refresh_token = tokenData.refresh_token
}
// Calculate token expiry time
if (tokenData.expires_in) {
const expiryTime = new Date(Date.now() + tokenData.expires_in * 1000)
updatedCredentialData.expires_at = expiryTime.toISOString()
}
// Encrypt the updated credential data
const encryptedData = await encryptCredentialData(updatedCredentialData)
// Update the credential in the database
await credentialRepository.update(credential.id, {
encryptedData,
updatedDate: new Date()
})
// Return HTML that closes the popup window on success
const successHtml = generateSuccessPage(credential.id)
res.setHeader('Content-Type', 'text/html')
res.send(successHtml)
} catch (error) {
if (axios.isAxiosError(error)) {
const axiosError = error
const errorHtml = generateErrorPage(
axiosError.response?.data?.error || 'token_exchange_failed',
axiosError.response?.data?.error_description || 'Token exchange failed',
axiosError.response?.data?.error_description ? `Description: ${axiosError.response?.data?.error_description}` : undefined
)
res.setHeader('Content-Type', 'text/html')
return res.status(400).send(errorHtml)
}
// Generic error HTML page
const errorHtml = generateErrorPage(
'An unexpected error occurred',
'Please try again later.',
error instanceof Error ? error.message : 'Unknown error'
)
res.setHeader('Content-Type', 'text/html')
res.status(500).send(errorHtml)
}
})
// Refresh OAuth2 access token
router.post('/refresh/:credentialId', async (req: Request, res: Response, next: NextFunction) => {
try {
const { credentialId } = req.params
const appServer = getRunningExpressApp()
const credentialRepository = appServer.AppDataSource.getRepository(Credential)
const credential = await credentialRepository.findOneBy({
id: credentialId
})
if (!credential) {
return res.status(404).json({
success: false,
message: 'Credential not found'
})
}
const decryptedData = await decryptCredentialData(credential.encryptedData)
const { clientId, clientSecret, refresh_token, accessTokenUrl, scope } = decryptedData
if (!clientId || !clientSecret || !refresh_token) {
return res.status(400).json({
success: false,
message: 'Missing required OAuth configuration: clientId, clientSecret, or refresh_token'
})
}
let tokenUrl = accessTokenUrl
if (!tokenUrl) {
return res.status(400).json({
success: false,
message: 'No Access Token URL specified in credential data'
})
}
const refreshRequestData: any = {
client_id: clientId,
client_secret: clientSecret,
grant_type: 'refresh_token',
refresh_token
}
if (scope) {
refreshRequestData.scope = scope
}
const tokenResponse = await axios.post(tokenUrl, new URLSearchParams(refreshRequestData).toString(), {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Accept: 'application/json'
}
})
// Extract token data from response
const tokenData = tokenResponse.data
// Update the credential data with new token information
const updatedCredentialData: any = {
...decryptedData,
...tokenData,
token_received_at: new Date().toISOString()
}
// Update refresh token if a new one was provided
if (tokenData.refresh_token) {
updatedCredentialData.refresh_token = tokenData.refresh_token
}
// Calculate token expiry time
if (tokenData.expires_in) {
const expiryTime = new Date(Date.now() + tokenData.expires_in * 1000)
updatedCredentialData.expires_at = expiryTime.toISOString()
}
// Encrypt the updated credential data
const encryptedData = await encryptCredentialData(updatedCredentialData)
// Update the credential in the database
await credentialRepository.update(credential.id, {
encryptedData,
updatedDate: new Date()
})
// Return success response
res.json({
success: true,
message: 'OAuth2 token refreshed successfully',
credentialId: credential.id,
tokenInfo: {
...tokenData,
has_new_refresh_token: !!tokenData.refresh_token,
expires_at: updatedCredentialData.expires_at
}
})
} catch (error) {
if (axios.isAxiosError(error)) {
const axiosError = error
return res.status(400).json({
success: false,
message: `Token refresh failed: ${axiosError.response?.data?.error_description || axiosError.message}`,
details: axiosError.response?.data
})
}
next(
new InternalFlowiseError(
StatusCodes.INTERNAL_SERVER_ERROR,
`OAuth2 token refresh error: ${error instanceof Error ? error.message : 'Unknown error'}`
)
)
}
})
export default router
@@ -0,0 +1,128 @@
/**
* HTML Templates for OAuth2 Callback Pages
*
* This module contains reusable HTML templates for OAuth2 authorization responses.
* The templates provide consistent styling and behavior for success and error pages.
*/
export interface OAuth2PageOptions {
title: string
statusIcon: string
statusText: string
statusColor: string
message: string
details?: string
postMessageType: 'OAUTH2_SUCCESS' | 'OAUTH2_ERROR'
postMessageData: any
autoCloseDelay: number
}
export const generateOAuth2ResponsePage = (options: OAuth2PageOptions): string => {
const { title, statusIcon, statusText, statusColor, message, details, postMessageType, postMessageData, autoCloseDelay } = options
return `
<!DOCTYPE html>
<html>
<head>
<title>${title}</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background-color: #f5f5f5;
}
.container {
text-align: center;
background: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
max-width: 500px;
}
.status {
color: ${statusColor};
font-size: 1.2rem;
margin-bottom: 1rem;
}
.message {
color: #666;
margin-bottom: 1rem;
}
.details {
background: #f9f9f9;
padding: 1rem;
border-radius: 4px;
font-size: 0.9rem;
color: #333;
text-align: left;
margin-top: 1rem;
}
</style>
</head>
<body>
<div class="container">
<div class="status">${statusIcon} ${statusText}</div>
<div class="message">${message}</div>
${details ? `<div class="details">${details}</div>` : ''}
</div>
<script>
// Notify parent window
try {
if (window.opener) {
window.opener.postMessage(${JSON.stringify({
type: postMessageType,
...postMessageData
})}, '*');
}
} catch (error) {
console.log('Could not notify parent window:', error);
}
// Close window after delay
setTimeout(function() {
window.close();
}, ${autoCloseDelay});
</script>
</body>
</html>
`
}
export const generateSuccessPage = (credentialId: string): string => {
return generateOAuth2ResponsePage({
title: 'OAuth2 Authorization Success',
statusIcon: '✓',
statusText: 'Authorization Successful',
statusColor: '#4caf50',
message: 'You can close this window now.',
postMessageType: 'OAUTH2_SUCCESS',
postMessageData: {
credentialId,
success: true,
message: 'OAuth2 authorization completed successfully'
},
autoCloseDelay: 1000
})
}
export const generateErrorPage = (error: string, message: string, details?: string): string => {
return generateOAuth2ResponsePage({
title: 'OAuth2 Authorization Error',
statusIcon: '✗',
statusText: 'Authorization Failed',
statusColor: '#f44336',
message,
details,
postMessageType: 'OAUTH2_ERROR',
postMessageData: {
success: false,
message,
error
},
autoCloseDelay: 3000
})
}
@@ -23,6 +23,7 @@ import { Organization } from '../../enterprise/database/entities/organization.en
const SOURCE_DOCUMENTS_PREFIX = '\n\n----FLOWISE_SOURCE_DOCUMENTS----\n\n'
const ARTIFACTS_PREFIX = '\n\n----FLOWISE_ARTIFACTS----\n\n'
const TOOL_ARGS_PREFIX = '\n\n----FLOWISE_TOOL_ARGS----\n\n'
const buildAndInitTool = async (chatflowid: string, _chatId?: string, _apiMessageId?: string) => {
const appServer = getRunningExpressApp()
@@ -211,6 +212,11 @@ const executeAgentTool = async (
}
}
if (typeof toolOutput === 'string' && toolOutput.includes(TOOL_ARGS_PREFIX)) {
const _splitted = toolOutput.split(TOOL_ARGS_PREFIX)
toolOutput = _splitted[0]
}
return {
output: toolOutput,
sourceDocuments,
+2
View File
@@ -39,6 +39,8 @@ export const WHITELIST_URLS = [
'/api/v1/loginmethod',
'/api/v1/pricing',
'/api/v1/user/test',
'/api/v1/oauth2-credential/callback',
'/api/v1/oauth2-credential/refresh',
AzureSSO.LOGIN_URI,
AzureSSO.LOGOUT_URI,
AzureSSO.CALLBACK_URI,
+13
View File
@@ -0,0 +1,13 @@
import client from './client'
const authorize = (credentialId) => client.post(`/oauth2-credential/authorize/${credentialId}`)
const refresh = (credentialId) => client.post(`/oauth2-credential/refresh/${credentialId}`)
const getCallback = (queryParams) => client.get(`/oauth2-credential/callback?${queryParams}`)
export default {
authorize,
refresh,
getCallback
}
+79 -34
View File
@@ -1118,42 +1118,87 @@ const _showHideOperation = (nodeData, inputParam, displayType, index) => {
if (path.includes('$index')) {
path = path.replace('$index', index)
}
const groundValue = get(nodeData.inputs, path, '')
let groundValue = get(nodeData.inputs, path, '')
if (groundValue && typeof groundValue === 'string' && groundValue.startsWith('[') && groundValue.endsWith(']')) {
groundValue = JSON.parse(groundValue)
}
if (Array.isArray(comparisonValue)) {
if (displayType === 'show' && !comparisonValue.includes(groundValue)) {
inputParam.display = false
// Handle case where groundValue is an array
if (Array.isArray(groundValue)) {
if (Array.isArray(comparisonValue)) {
// Both are arrays - check if there's any intersection
const hasIntersection = comparisonValue.some((val) => groundValue.includes(val))
if (displayType === 'show' && !hasIntersection) {
inputParam.display = false
}
if (displayType === 'hide' && hasIntersection) {
inputParam.display = false
}
} else if (typeof comparisonValue === 'string') {
// comparisonValue is string, groundValue is array - check if array contains the string
const matchFound = groundValue.some((val) => comparisonValue === val || new RegExp(comparisonValue).test(val))
if (displayType === 'show' && !matchFound) {
inputParam.display = false
}
if (displayType === 'hide' && matchFound) {
inputParam.display = false
}
} else if (typeof comparisonValue === 'boolean' || typeof comparisonValue === 'number') {
// For boolean/number comparison with array, check if array contains the value
const matchFound = groundValue.includes(comparisonValue)
if (displayType === 'show' && !matchFound) {
inputParam.display = false
}
if (displayType === 'hide' && matchFound) {
inputParam.display = false
}
} else if (typeof comparisonValue === 'object') {
// For object comparison with array, use deep equality check
const matchFound = groundValue.some((val) => isEqual(comparisonValue, val))
if (displayType === 'show' && !matchFound) {
inputParam.display = false
}
if (displayType === 'hide' && matchFound) {
inputParam.display = false
}
}
if (displayType === 'hide' && comparisonValue.includes(groundValue)) {
inputParam.display = false
}
} else if (typeof comparisonValue === 'string') {
if (displayType === 'show' && !(comparisonValue === groundValue || new RegExp(comparisonValue).test(groundValue))) {
inputParam.display = false
}
if (displayType === 'hide' && (comparisonValue === groundValue || new RegExp(comparisonValue).test(groundValue))) {
inputParam.display = false
}
} else if (typeof comparisonValue === 'boolean') {
if (displayType === 'show' && comparisonValue !== groundValue) {
inputParam.display = false
}
if (displayType === 'hide' && comparisonValue === groundValue) {
inputParam.display = false
}
} else if (typeof comparisonValue === 'object') {
if (displayType === 'show' && !isEqual(comparisonValue, groundValue)) {
inputParam.display = false
}
if (displayType === 'hide' && isEqual(comparisonValue, groundValue)) {
inputParam.display = false
}
} else if (typeof comparisonValue === 'number') {
if (displayType === 'show' && comparisonValue !== groundValue) {
inputParam.display = false
}
if (displayType === 'hide' && comparisonValue === groundValue) {
inputParam.display = false
} else {
// Original logic for non-array groundValue
if (Array.isArray(comparisonValue)) {
if (displayType === 'show' && !comparisonValue.includes(groundValue)) {
inputParam.display = false
}
if (displayType === 'hide' && comparisonValue.includes(groundValue)) {
inputParam.display = false
}
} else if (typeof comparisonValue === 'string') {
if (displayType === 'show' && !(comparisonValue === groundValue || new RegExp(comparisonValue).test(groundValue))) {
inputParam.display = false
}
if (displayType === 'hide' && (comparisonValue === groundValue || new RegExp(comparisonValue).test(groundValue))) {
inputParam.display = false
}
} else if (typeof comparisonValue === 'boolean') {
if (displayType === 'show' && comparisonValue !== groundValue) {
inputParam.display = false
}
if (displayType === 'hide' && comparisonValue === groundValue) {
inputParam.display = false
}
} else if (typeof comparisonValue === 'object') {
if (displayType === 'show' && !isEqual(comparisonValue, groundValue)) {
inputParam.display = false
}
if (displayType === 'hide' && isEqual(comparisonValue, groundValue)) {
inputParam.display = false
}
} else if (typeof comparisonValue === 'number') {
if (displayType === 'show' && comparisonValue !== groundValue) {
inputParam.display = false
}
if (displayType === 'hide' && comparisonValue === groundValue) {
inputParam.display = false
}
}
}
})
@@ -18,6 +18,7 @@ import { IconHandStop, IconX } from '@tabler/icons-react'
// API
import credentialsApi from '@/api/credentials'
import oauth2Api from '@/api/oauth2'
// Hooks
import useApi from '@/hooks/useApi'
@@ -212,6 +213,149 @@ const AddEditCredentialDialog = ({ show, dialogProps, onCancel, onConfirm, setEr
}
}
const setOAuth2 = async () => {
try {
let credentialId = null
// First save or add the credential
if (dialogProps.type === 'ADD') {
// Add new credential first
const obj = {
name,
credentialName: componentCredential.name,
plainDataObj: credentialData
}
const createResp = await credentialsApi.createCredential(obj)
if (createResp.data) {
credentialId = createResp.data.id
}
} else {
// Save existing credential first
const saveObj = {
name,
credentialName: componentCredential.name
}
let plainDataObj = {}
for (const key in credentialData) {
if (credentialData[key] !== REDACTED_CREDENTIAL_VALUE) {
plainDataObj[key] = credentialData[key]
}
}
if (Object.keys(plainDataObj).length) saveObj.plainDataObj = plainDataObj
const saveResp = await credentialsApi.updateCredential(credential.id, saveObj)
if (saveResp.data) {
credentialId = credential.id
}
}
if (!credentialId) {
throw new Error('Failed to save credential')
}
const authResponse = await oauth2Api.authorize(credentialId)
if (authResponse.data && authResponse.data.success && authResponse.data.authorizationUrl) {
// Open the authorization URL in a new window/tab
const authWindow = window.open(
authResponse.data.authorizationUrl,
'_blank',
'width=600,height=700,scrollbars=yes,resizable=yes'
)
if (!authWindow) {
throw new Error('Failed to open authorization window. Please check if popups are blocked.')
}
// Listen for messages from the popup window
const handleMessage = (event) => {
// Verify origin if needed (you may want to add origin checking)
if (event.data && (event.data.type === 'OAUTH2_SUCCESS' || event.data.type === 'OAUTH2_ERROR')) {
window.removeEventListener('message', handleMessage)
if (event.data.type === 'OAUTH2_SUCCESS') {
enqueueSnackbar({
message: 'OAuth2 authorization completed successfully',
options: {
key: new Date().getTime() + Math.random(),
variant: 'success',
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
onConfirm(credentialId)
} else if (event.data.type === 'OAUTH2_ERROR') {
enqueueSnackbar({
message: event.data.message || 'OAuth2 authorization failed',
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
persist: true,
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
}
// Close the auth window if it's still open
if (authWindow && !authWindow.closed) {
authWindow.close()
}
}
}
// Add message listener
window.addEventListener('message', handleMessage)
// Fallback: Monitor the auth window and handle if it closes manually
const checkClosed = setInterval(() => {
if (authWindow.closed) {
clearInterval(checkClosed)
window.removeEventListener('message', handleMessage)
// If no message was received, assume user closed window manually
// Don't show error in this case, just close dialog
onConfirm(credentialId)
}
}, 1000)
// Cleanup after a reasonable timeout (5 minutes)
setTimeout(() => {
clearInterval(checkClosed)
window.removeEventListener('message', handleMessage)
if (authWindow && !authWindow.closed) {
authWindow.close()
}
}, 300000) // 5 minutes
} else {
throw new Error('Invalid response from authorization endpoint')
}
} catch (error) {
console.error('OAuth2 authorization error:', error)
if (setError) setError(error)
enqueueSnackbar({
message: `OAuth2 authorization failed: ${error.response?.data?.message || error.message || 'Unknown error'}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
persist: true,
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
}
}
const component = show ? (
<Dialog
fullWidth
@@ -315,12 +459,34 @@ const AddEditCredentialDialog = ({ show, dialogProps, onCancel, onConfirm, setEr
/>
</Box>
)}
{!shared && componentCredential && componentCredential.name && componentCredential.name.includes('OAuth2') && (
<Box sx={{ p: 2 }}>
<Stack sx={{ position: 'relative' }} direction='row'>
<Typography variant='overline'>OAuth Redirect URL</Typography>
</Stack>
<OutlinedInput
id='oauthRedirectUrl'
type='string'
disabled
fullWidth
value={`${baseURL}/api/v1/oauth2-credential/callback`}
/>
</Box>
)}
{!shared &&
componentCredential &&
componentCredential.inputs &&
componentCredential.inputs.map((inputParam, index) => (
<CredentialInputHandler key={index} inputParam={inputParam} data={credentialData} />
))}
componentCredential.inputs
.filter((inputParam) => inputParam.hidden !== true)
.map((inputParam, index) => <CredentialInputHandler key={index} inputParam={inputParam} data={credentialData} />)}
{!shared && componentCredential && componentCredential.name && componentCredential.name.includes('OAuth2') && (
<Box sx={{ p: 2 }}>
<Button variant='contained' color='secondary' onClick={() => setOAuth2()}>
Authenticate
</Button>
</Box>
)}
</DialogContent>
<DialogActions>
{!shared && (
+113 -5
View File
File diff suppressed because one or more lines are too long