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
JavaScript
;
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