UNPKG

n8n-nodes-smartgent

Version:

SmartGent custom nodes for n8n - AI-powered automation and intelligent workflow integrations including LiteLLM chat completions, SharePoint file monitoring, and enterprise search

568 lines 27.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SmartgentSharePointTrigger = void 0; const n8n_workflow_1 = require("n8n-workflow"); class SmartgentSharePointTrigger { constructor() { this.description = { displayName: 'Smartgent SharePoint Trigger', name: 'smartgentSharePointTrigger', icon: 'file:sharepoint.svg', group: ['trigger'], version: 1, subtitle: '={{$parameter["event"]}}', description: 'Monitor SharePoint folders for new files and changes', defaults: { name: 'Smartgent SharePoint Trigger', }, inputs: [], outputs: ['main'], credentials: [ { name: 'smartgentMicrosoftSharePointApi', required: true, }, ], polling: true, properties: [ { displayName: 'Event', name: 'event', type: 'options', noDataExpression: true, options: [ { name: 'File Added', value: 'fileAdded', description: 'Trigger when a new file is added to the monitored folder', }, { name: 'File Modified', value: 'fileModified', description: 'Trigger when a file is modified in the monitored folder', }, { name: 'File Added or Modified', value: 'fileAddedOrModified', description: 'Trigger when a file is added or modified in the monitored folder', }, ], default: 'fileAdded', }, { displayName: 'Folder Path to Monitor', name: 'folderPath', type: 'string', default: '/', required: true, description: 'Path to the folder to monitor. Use "/" for root Documents library, or specify a subfolder path like "MyFolder" or "ParentFolder/ChildFolder". Paths are relative to the Documents library root.', placeholder: 'MyFolder/SubFolder', }, { displayName: 'Poll Interval', name: 'pollInterval', type: 'number', default: 60, description: 'How often to check for changes (in seconds)', typeOptions: { minValue: 10, maxValue: 3600, }, }, { displayName: 'Additional Options', name: 'additionalOptions', type: 'collection', placeholder: 'Add Option', default: {}, options: [ { displayName: 'File Extensions Filter', name: 'fileExtensions', type: 'string', default: '', description: 'Comma-separated list of file extensions to monitor (e.g., pdf,docx,xlsx). Leave empty to monitor all files.', placeholder: 'pdf,docx,xlsx', }, { displayName: 'Include Subfolders', name: 'includeSubfolders', type: 'boolean', default: false, description: 'Whether to monitor subfolders recursively', }, { displayName: 'Download File Content', name: 'downloadContent', type: 'boolean', default: false, description: 'Whether to download file content as binary data', }, ], }, ], }; } async poll() { const pollData = this.getWorkflowStaticData('node'); const event = this.getNodeParameter('event'); const folderPath = this.getNodeParameter('folderPath'); const additionalOptions = this.getNodeParameter('additionalOptions'); const fileExtensions = (additionalOptions.fileExtensions || '') .split(',') .map((ext) => ext.trim().toLowerCase()) .filter((ext) => ext); const includeSubfolders = additionalOptions.includeSubfolders; const downloadContent = additionalOptions.downloadContent; const isTestMode = this.getMode() === 'manual'; try { const credentials = await this.getCredentials('smartgentMicrosoftSharePointApi'); console.log(`SharePoint Trigger: Starting ${isTestMode ? 'TEST' : 'POLL'} for folder path: "${folderPath}"`); const tokenResponse = await SmartgentSharePointTrigger.getAccessToken(credentials, this); const accessToken = tokenResponse.access_token; console.log('SharePoint Trigger: Successfully obtained access token'); const siteId = await SmartgentSharePointTrigger.getSiteId(credentials.siteUrl, accessToken, this); console.log(`SharePoint Trigger: Site ID: ${siteId}`); const driveId = await SmartgentSharePointTrigger.getDriveId(siteId, accessToken, this); console.log(`SharePoint Trigger: Drive ID: ${driveId}`); console.log(`SharePoint Trigger: Listing documents for path: "${folderPath}", includeSubfolders: ${includeSubfolders}`); const currentFiles = await SmartgentSharePointTrigger.listDocuments(driveId, folderPath, accessToken, { includeSubfolders }, this); console.log(`SharePoint Trigger: Found ${currentFiles.length} total files`); console.log('SharePoint Trigger: Files found:', currentFiles.map((f) => ({ name: f.name, id: f.id }))); let filteredFiles = currentFiles; if (fileExtensions.length > 0) { console.log(`SharePoint Trigger: Filtering by extensions: ${fileExtensions.join(', ')}`); filteredFiles = currentFiles.filter((file) => { var _a; const fileExt = (_a = file.name.split('.').pop()) === null || _a === void 0 ? void 0 : _a.toLowerCase(); return fileExt && fileExtensions.includes(fileExt); }); console.log(`SharePoint Trigger: After extension filtering: ${filteredFiles.length} files`); } const lastPollTime = pollData.lastPollTime; const lastKnownFiles = pollData.lastKnownFiles || []; const currentPollTime = new Date().toISOString(); let newOrModifiedFiles = []; if (isTestMode) { console.log('SharePoint Trigger: Running in test mode - will return existing files for demo'); if (filteredFiles.length > 0) { const maxFiles = Math.min(3, filteredFiles.length); const shuffled = [...filteredFiles].sort(() => 0.5 - Math.random()); newOrModifiedFiles = shuffled.slice(0, maxFiles); console.log(`SharePoint Trigger: Test mode - returning ${newOrModifiedFiles.length} random files for testing`); console.log('SharePoint Trigger: NOTE - These are existing files shown for testing. In normal mode, only NEW/MODIFIED files would trigger.'); } else { console.log('SharePoint Trigger: Test mode - no files found in folder'); } } else if (!lastPollTime) { console.log('SharePoint Trigger: First poll - not triggering for existing files'); newOrModifiedFiles = []; } else { const lastPollDate = new Date(lastPollTime); console.log(`SharePoint Trigger: Checking for changes since ${lastPollTime}`); for (const file of filteredFiles) { const fileModifiedDate = new Date(file.lastModifiedDateTime); const lastKnownFile = lastKnownFiles.find((f) => f.id === file.id); if (event === 'fileAdded') { if (!lastKnownFile) { newOrModifiedFiles.push(file); console.log(`SharePoint Trigger: New file detected: ${file.name}`); } } else if (event === 'fileModified') { if (lastKnownFile && fileModifiedDate > lastPollDate) { newOrModifiedFiles.push(file); console.log(`SharePoint Trigger: Modified file detected: ${file.name}`); } } else if (event === 'fileAddedOrModified') { if (!lastKnownFile || fileModifiedDate > lastPollDate) { newOrModifiedFiles.push(file); console.log(`SharePoint Trigger: New or modified file detected: ${file.name}`); } } } } if (!isTestMode) { pollData.lastPollTime = currentPollTime; pollData.lastKnownFiles = filteredFiles; } console.log(`SharePoint Trigger: Found ${newOrModifiedFiles.length} new/modified files to trigger on`); if (newOrModifiedFiles.length === 0) { if (isTestMode) { console.log('SharePoint Trigger: Test mode - no files found in folder, providing helpful message'); return [ [ { json: { event: 'test', trigger: 'smartgentSharePointTrigger', testMode: true, testNote: 'This is a TEST execution. No files were found in the specified folder.', message: `TEST MODE: No files found in folder path: "${folderPath}". Please check: 1) The folder path is correct, 2) There are files in the folder, 3) Your credentials have access to the folder.`, folderPath, totalFilesFound: filteredFiles.length, debug: { originalFilesFound: currentFiles.length, afterExtensionFilter: filteredFiles.length, fileExtensionsFilter: fileExtensions.length > 0 ? fileExtensions : 'none', }, timestamp: new Date().toISOString(), nextSteps: [ '1. Verify the folder path exists in SharePoint', '2. Check if there are actually files in that folder', '3. Ensure your app registration has proper permissions', '4. Try using "/" to test the root folder first', ], }, }, ], ]; } else { console.log('SharePoint Trigger: No new/modified files found, returning null (no trigger)'); return null; } } const returnData = []; for (const file of newOrModifiedFiles) { let executionData = { json: { event: isTestMode ? 'test' : event, trigger: 'smartgentSharePointTrigger', ...(isTestMode && { testMode: true, testNote: 'This is a TEST execution showing existing files. In normal operation, only NEW or MODIFIED files will trigger this workflow.', testExecutionTime: currentPollTime, }), file: { id: file.id, name: file.name, lastModifiedDateTime: file.lastModifiedDateTime, size: file.size, webUrl: file.webUrl, downloadUrl: file.downloadUrl, mimeType: file.mimeType, }, folder: { path: folderPath, driveId, siteId, }, timestamp: currentPollTime, }, }; if (downloadContent) { try { const fileContent = await SmartgentSharePointTrigger.downloadDocument(driveId, file.id, accessToken, this); executionData.binary = { data: { data: fileContent.toString('base64'), mimeType: file.mimeType || 'application/octet-stream', fileName: file.name, fileSize: file.size, }, }; } catch (error) { executionData.json.downloadError = error.message; } } returnData.push(executionData); } return [returnData]; } catch (error) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), `SharePoint Trigger failed: ${error.message}`); } } static async getAccessToken(credentials, context) { var _a, _b, _c, _d; const tokenUrl = credentials.tokenEndpoint || `https://login.microsoftonline.com/${credentials.tenantId}/oauth2/v2.0/token`; const data = { grant_type: 'client_credentials', client_id: credentials.clientId, client_secret: credentials.clientSecret, scope: 'https://graph.microsoft.com/.default', }; const formDataString = Object.keys(data) .map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(data[key])}`) .join('&'); const options = { method: 'POST', url: tokenUrl, headers: { 'content-type': 'application/x-www-form-urlencoded', }, body: formDataString, json: true, }; try { const response = await context.helpers.httpRequest(options); if (!response.access_token) { throw new n8n_workflow_1.ApplicationError('Failed to obtain access token from Microsoft. Check your tenant ID, client ID, and client secret.'); } return response; } catch (error) { if (((_a = error.response) === null || _a === void 0 ? void 0 : _a.status) === 400) { throw new n8n_workflow_1.ApplicationError(`Authentication failed: ${((_c = (_b = error.response) === null || _b === void 0 ? void 0 : _b.data) === null || _c === void 0 ? void 0 : _c.error_description) || 'Invalid client credentials or tenant ID'}`); } else if (((_d = error.response) === null || _d === void 0 ? void 0 : _d.status) === 401) { throw new n8n_workflow_1.ApplicationError('Authentication failed: Invalid client credentials. Please check your Client ID and Client Secret.'); } throw new n8n_workflow_1.ApplicationError(`Authentication error: ${error.message}`); } } static async getSiteId(siteUrl, token, context) { try { const urlObj = new URL(siteUrl); const hostname = urlObj.hostname; const relativePath = urlObj.pathname; const directUrl = `https://graph.microsoft.com/v1.0/sites/${hostname}:${relativePath}`; const directOptions = { method: 'GET', url: directUrl, headers: { Authorization: `Bearer ${token}`, }, json: true, }; const directResponse = await context.helpers.httpRequest(directOptions); if (directResponse.id) { return directResponse.id; } } catch (directError) { } const urlParts = siteUrl.split('sites/'); if (urlParts.length < 2) { throw new n8n_workflow_1.ApplicationError('Invalid SharePoint site URL format'); } const siteName = urlParts[1].split('/')[0]; const options = { method: 'GET', url: `https://graph.microsoft.com/v1.0/sites?search=${siteName}`, headers: { Authorization: `Bearer ${token}`, }, json: true, }; const response = await context.helpers.httpRequest(options); if (!response.value || response.value.length === 0) { throw new n8n_workflow_1.ApplicationError(`No sites found matching '${siteName}'`); } const site = response.value.find((x) => x.name && x.name.toLowerCase() === siteName.toLowerCase()); if (!site) { const partialMatch = response.value.find((x) => x.name && x.name.toLowerCase().includes(siteName.toLowerCase())); if (!partialMatch) { throw new n8n_workflow_1.ApplicationError(`Site '${siteName}' not found. Available sites: ${response.value.map((x) => x.name).join(', ')}`); } return partialMatch.id; } return site.id; } static async getDriveId(siteId, token, context) { const options = { method: 'GET', url: `https://graph.microsoft.com/v1.0/sites/${siteId}/drives`, headers: { Authorization: `Bearer ${token}`, }, json: true, }; const response = await context.helpers.httpRequest(options); if (!response.value || response.value.length === 0) { throw new n8n_workflow_1.ApplicationError('No drives found for the site'); } const documentsDrive = response.value.find((drive) => drive.name === 'Documents' || drive.webUrl.includes('Shared%20Documents') || drive.webUrl.includes('Shared Documents')); if (documentsDrive) { return documentsDrive.id; } return response.value[0].id; } static async listDocuments(driveId, folderPath, token, options, context) { const includeSubfolders = options.includeSubfolders || false; let allDocuments = []; const mainFolderDocs = await SmartgentSharePointTrigger.getDocumentsFromFolder(driveId, folderPath, token, context); allDocuments = allDocuments.concat(mainFolderDocs); if (includeSubfolders) { const subfolderDocs = await SmartgentSharePointTrigger.getDocumentsFromSubfolders(driveId, folderPath, token, context); allDocuments = allDocuments.concat(subfolderDocs); } return allDocuments; } static async getDocumentsFromFolder(driveId, folderPath, token, context) { var _a, _b, _c; let url; const decodedPath = decodeURIComponent(folderPath).trim(); console.log(`SharePoint Trigger: getDocumentsFromFolder - Original path: "${folderPath}", Decoded: "${decodedPath}"`); if (this.isRootPath(decodedPath)) { url = `https://graph.microsoft.com/v1.0/drives/${driveId}/root/children`; console.log('SharePoint Trigger: Using root path URL'); } else { const cleanPath = this.cleanFolderPath(decodedPath); console.log(`SharePoint Trigger: Cleaned path: "${cleanPath}"`); if (!cleanPath) { throw new n8n_workflow_1.ApplicationError(`Invalid folder path: ${folderPath}`); } url = `https://graph.microsoft.com/v1.0/drives/${driveId}/root:/${cleanPath}:/children`; console.log('SharePoint Trigger: Using subfolder path URL'); } url += '?$select=id,name,lastModifiedDateTime,size,file,folder,webUrl,@microsoft.graph.downloadUrl'; console.log(`SharePoint Trigger: Final API URL: ${url}`); const options = { method: 'GET', url, headers: { Authorization: `Bearer ${token}`, }, json: true, }; try { const response = await context.helpers.httpRequest(options); console.log(`SharePoint Trigger: API Response status: ${response ? 'Success' : 'No response'}`); if (!response || !response.value || !Array.isArray(response.value)) { console.log('SharePoint Trigger: Invalid response structure:', response); throw new n8n_workflow_1.ApplicationError(`Invalid response from SharePoint API for path: ${folderPath}`); } console.log(`SharePoint Trigger: Response contains ${response.value.length} items`); response.value.forEach((item, index) => { console.log(`SharePoint Trigger: Item ${index}: ${item.name} (${item.file ? 'file' : 'folder'})`); }); const documents = response.value.filter((item) => item.file !== undefined); console.log(`SharePoint Trigger: Filtered to ${documents.length} files`); return documents.map((doc) => { var _a; return ({ id: doc.id, name: doc.name, lastModifiedDateTime: doc.lastModifiedDateTime, size: doc.size, webUrl: doc.webUrl, downloadUrl: doc['@microsoft.graph.downloadUrl'], mimeType: (_a = doc.file) === null || _a === void 0 ? void 0 : _a.mimeType, }); }); } catch (error) { if (((_a = error.response) === null || _a === void 0 ? void 0 : _a.status) === 404) { throw new n8n_workflow_1.ApplicationError(`Folder not found: ${folderPath}. Please verify the path exists in SharePoint.`); } else if (((_b = error.response) === null || _b === void 0 ? void 0 : _b.status) === 403) { throw new n8n_workflow_1.ApplicationError(`Access denied to folder: ${folderPath}. Please check your permissions.`); } else if (((_c = error.response) === null || _c === void 0 ? void 0 : _c.status) === 401) { throw new n8n_workflow_1.ApplicationError(`Authentication failed. Please check your SharePoint credentials.`); } throw new n8n_workflow_1.ApplicationError(`Failed to access folder ${folderPath}: ${error.message}`); } } static async getDocumentsFromSubfolders(driveId, folderPath, token, context) { try { const allFiles = []; const folders = await this.getFoldersFromPath(driveId, folderPath, token, context); for (const folder of folders) { const folderFiles = await this.getDocumentsFromFolder(driveId, folder.path, token, context); allFiles.push(...folderFiles); const nestedFiles = await this.getDocumentsFromSubfolders(driveId, folder.path, token, context); allFiles.push(...nestedFiles); } return allFiles; } catch (error) { console.warn(`Failed to get documents from subfolders of ${folderPath}:`, error); return []; } } static isRootPath(path) { const normalizedPath = path.toLowerCase().trim(); return (normalizedPath === '/' || normalizedPath === '' || normalizedPath === '/root' || normalizedPath === '/shared documents' || normalizedPath === 'shared documents' || normalizedPath === '/shared%20documents' || normalizedPath === 'shared%20documents'); } static cleanFolderPath(path) { if (!path || path.trim() === '') { return ''; } let cleanPath = path.trim(); if (cleanPath.startsWith('/')) { cleanPath = cleanPath.substring(1); } if (cleanPath.endsWith('/')) { cleanPath = cleanPath.substring(0, cleanPath.length - 1); } const invalidChars = /[<>:"|?*]/; if (invalidChars.test(cleanPath)) { throw new n8n_workflow_1.ApplicationError(`Invalid characters in folder path: ${path}`); } return cleanPath; } static async getFoldersFromPath(driveId, folderPath, token, context) { let url; const decodedPath = decodeURIComponent(folderPath).trim(); if (this.isRootPath(decodedPath)) { url = `https://graph.microsoft.com/v1.0/drives/${driveId}/root/children`; } else { const cleanPath = this.cleanFolderPath(decodedPath); if (!cleanPath) { return []; } url = `https://graph.microsoft.com/v1.0/drives/${driveId}/root:/${cleanPath}:/children`; } url += '?$select=id,name,folder&$filter=folder ne null'; const options = { method: 'GET', url, headers: { Authorization: `Bearer ${token}`, }, json: true, }; try { const response = await context.helpers.httpRequest(options); if (!response || !response.value || !Array.isArray(response.value)) { return []; } return response.value .filter((item) => item.folder !== undefined) .map((folder) => ({ name: folder.name, path: this.isRootPath(decodedPath) ? folder.name : `${this.cleanFolderPath(decodedPath)}/${folder.name}`, })); } catch (error) { console.warn(`Failed to get folders from path ${folderPath}:`, error); return []; } } static async downloadDocument(driveId, documentId, token, context) { const options = { method: 'GET', url: `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${documentId}/content`, headers: { Authorization: `Bearer ${token}`, }, encoding: 'arraybuffer', }; const response = await context.helpers.httpRequest(options); return Buffer.from(response); } } exports.SmartgentSharePointTrigger = SmartgentSharePointTrigger; //# sourceMappingURL=SmartgentSharePointTrigger.node.js.map