n8n-nodes-onedrive-business-sp 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -109,11 +109,51 @@ class MicrosoftOneDriveBusinessTrigger {
|
|
|
109
109
|
default: {},
|
|
110
110
|
options: [
|
|
111
111
|
{
|
|
112
|
-
displayName: '
|
|
113
|
-
name: '
|
|
112
|
+
displayName: 'Watch Subfolders',
|
|
113
|
+
name: 'recursive',
|
|
114
|
+
type: 'boolean',
|
|
115
|
+
default: false,
|
|
116
|
+
description: 'Whether to watch subfolders recursively',
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
displayName: 'Ignore Duplicates',
|
|
120
|
+
name: 'ignoreDuplicates',
|
|
121
|
+
type: 'boolean',
|
|
122
|
+
default: true,
|
|
123
|
+
description: 'Whether to ignore duplicate notifications for the same file',
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
displayName: 'Wait for File Completion (seconds)',
|
|
127
|
+
name: 'waitForCompletion',
|
|
114
128
|
type: 'number',
|
|
115
|
-
default:
|
|
116
|
-
description: '
|
|
129
|
+
default: 0,
|
|
130
|
+
description: 'Wait time after detecting change to ensure file is fully uploaded. Useful for large files.',
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
displayName: 'Include File Metadata',
|
|
134
|
+
name: 'includeMetadata',
|
|
135
|
+
type: 'boolean',
|
|
136
|
+
default: true,
|
|
137
|
+
description: 'Whether to include extended metadata like version, lock status, etc.',
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
displayName: 'Periodic Full Scan',
|
|
141
|
+
name: 'fullScanEnabled',
|
|
142
|
+
type: 'boolean',
|
|
143
|
+
default: false,
|
|
144
|
+
description: 'Whether to periodically perform a full scan as backup (in case delta misses changes)',
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
displayName: 'Full Scan Interval (hours)',
|
|
148
|
+
name: 'fullScanInterval',
|
|
149
|
+
type: 'number',
|
|
150
|
+
default: 24,
|
|
151
|
+
displayOptions: {
|
|
152
|
+
show: {
|
|
153
|
+
fullScanEnabled: [true],
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
description: 'How often to perform a full scan (in hours)',
|
|
117
157
|
},
|
|
118
158
|
],
|
|
119
159
|
},
|
|
@@ -232,6 +272,14 @@ class MicrosoftOneDriveBusinessTrigger {
|
|
|
232
272
|
else {
|
|
233
273
|
watchFolderId = watchFolder.value || 'root';
|
|
234
274
|
}
|
|
275
|
+
// Validate webhook URL accessibility (basic check)
|
|
276
|
+
if (!webhookUrl.startsWith('https://')) {
|
|
277
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Webhook URL must use HTTPS. Microsoft Graph requires secure endpoints.');
|
|
278
|
+
}
|
|
279
|
+
// Check if webhook URL contains localhost/127.0.0.1
|
|
280
|
+
if (webhookUrl.includes('localhost') || webhookUrl.includes('127.0.0.1')) {
|
|
281
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Webhook URL cannot be localhost. Microsoft Graph cannot reach local addresses. Use a public URL or tunneling service like ngrok.');
|
|
282
|
+
}
|
|
235
283
|
// Get drive ID if not provided
|
|
236
284
|
let actualDriveId = driveId;
|
|
237
285
|
if (!actualDriveId) {
|
|
@@ -242,8 +290,27 @@ class MicrosoftOneDriveBusinessTrigger {
|
|
|
242
290
|
});
|
|
243
291
|
actualDriveId = driveResponse.id;
|
|
244
292
|
}
|
|
245
|
-
|
|
246
|
-
|
|
293
|
+
// Validate drive and folder accessibility
|
|
294
|
+
try {
|
|
295
|
+
await this.helpers.requestOAuth2.call(this, 'microsoftOneDriveBusinessOAuth2Api', {
|
|
296
|
+
method: 'GET',
|
|
297
|
+
url: `https://graph.microsoft.com/v1.0/drives/${actualDriveId}`,
|
|
298
|
+
json: true,
|
|
299
|
+
});
|
|
300
|
+
if (watchFolderId !== 'root') {
|
|
301
|
+
await this.helpers.requestOAuth2.call(this, 'microsoftOneDriveBusinessOAuth2Api', {
|
|
302
|
+
method: 'GET',
|
|
303
|
+
url: `https://graph.microsoft.com/v1.0/drives/${actualDriveId}/items/${watchFolderId}`,
|
|
304
|
+
json: true,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
catch (validationError) {
|
|
309
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Cannot access drive/folder. It may have been deleted, moved, or you may not have permission. Error: ${validationError.message}`);
|
|
310
|
+
}
|
|
311
|
+
// Microsoft Graph only supports subscriptions at drive root level
|
|
312
|
+
// We'll filter by folder in the webhook handler
|
|
313
|
+
const resource = `/drives/${actualDriveId}/root`;
|
|
247
314
|
// Create subscription
|
|
248
315
|
const expirationDateTime = new Date();
|
|
249
316
|
expirationDateTime.setHours(expirationDateTime.getHours() + 72); // 72 hours max
|
|
@@ -263,7 +330,23 @@ class MicrosoftOneDriveBusinessTrigger {
|
|
|
263
330
|
});
|
|
264
331
|
webhookData.subscriptionId = responseData.id;
|
|
265
332
|
webhookData.driveId = actualDriveId;
|
|
266
|
-
webhookData.
|
|
333
|
+
webhookData.watchFolderId = watchFolderId;
|
|
334
|
+
webhookData.subscriptionExpiry = responseData.expirationDateTime;
|
|
335
|
+
webhookData.processedItems = {};
|
|
336
|
+
// Initialize delta link to avoid getting all existing items
|
|
337
|
+
try {
|
|
338
|
+
const deltaResponse = await this.helpers.requestOAuth2.call(this, 'microsoftOneDriveBusinessOAuth2Api', {
|
|
339
|
+
method: 'GET',
|
|
340
|
+
url: `https://graph.microsoft.com/v1.0/drives/${actualDriveId}/root/delta`,
|
|
341
|
+
json: true,
|
|
342
|
+
});
|
|
343
|
+
// Store the delta link to track only future changes
|
|
344
|
+
webhookData.deltaLink = deltaResponse['@odata.deltaLink'];
|
|
345
|
+
}
|
|
346
|
+
catch (deltaError) {
|
|
347
|
+
// If delta initialization fails, set to null
|
|
348
|
+
webhookData.deltaLink = null;
|
|
349
|
+
}
|
|
267
350
|
}
|
|
268
351
|
catch (error) {
|
|
269
352
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
@@ -303,6 +386,39 @@ class MicrosoftOneDriveBusinessTrigger {
|
|
|
303
386
|
webhookResponse: query.validationToken,
|
|
304
387
|
};
|
|
305
388
|
}
|
|
389
|
+
// Check if subscription needs renewal (renew if less than 24h remaining)
|
|
390
|
+
const subscriptionExpiry = webhookData.subscriptionExpiry;
|
|
391
|
+
const isRenewing = webhookData.isRenewing;
|
|
392
|
+
if (subscriptionExpiry && !isRenewing) {
|
|
393
|
+
const expiryTime = new Date(subscriptionExpiry).getTime();
|
|
394
|
+
const now = Date.now();
|
|
395
|
+
const hoursRemaining = (expiryTime - now) / (1000 * 60 * 60);
|
|
396
|
+
if (hoursRemaining < 24 && hoursRemaining > 0) {
|
|
397
|
+
try {
|
|
398
|
+
// Set renewal flag to prevent concurrent renewals
|
|
399
|
+
webhookData.isRenewing = true;
|
|
400
|
+
const newExpirationDateTime = new Date();
|
|
401
|
+
newExpirationDateTime.setHours(newExpirationDateTime.getHours() + 72);
|
|
402
|
+
const renewResponse = await this.helpers.requestOAuth2.call(this, 'microsoftOneDriveBusinessOAuth2Api', {
|
|
403
|
+
method: 'PATCH',
|
|
404
|
+
url: `https://graph.microsoft.com/v1.0/subscriptions/${webhookData.subscriptionId}`,
|
|
405
|
+
body: {
|
|
406
|
+
expirationDateTime: newExpirationDateTime.toISOString(),
|
|
407
|
+
},
|
|
408
|
+
json: true,
|
|
409
|
+
});
|
|
410
|
+
webhookData.subscriptionExpiry = renewResponse.expirationDateTime;
|
|
411
|
+
}
|
|
412
|
+
catch (renewError) {
|
|
413
|
+
// If renewal fails, log but continue processing
|
|
414
|
+
console.log('Failed to renew subscription:', renewError);
|
|
415
|
+
}
|
|
416
|
+
finally {
|
|
417
|
+
// Clear renewal flag
|
|
418
|
+
webhookData.isRenewing = false;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
306
422
|
// Verify client state
|
|
307
423
|
const notification = bodyData.value?.[0];
|
|
308
424
|
if (notification?.clientState !== 'n8n-secret-token') {
|
|
@@ -312,6 +428,13 @@ class MicrosoftOneDriveBusinessTrigger {
|
|
|
312
428
|
}
|
|
313
429
|
const event = this.getNodeParameter('event');
|
|
314
430
|
const watchFolder = this.getNodeParameter('watchFolderId', {});
|
|
431
|
+
const options = this.getNodeParameter('options', {});
|
|
432
|
+
const recursive = options.recursive || false;
|
|
433
|
+
const ignoreDuplicates = options.ignoreDuplicates !== false;
|
|
434
|
+
const waitForCompletion = options.waitForCompletion || 0;
|
|
435
|
+
const includeMetadata = options.includeMetadata !== false;
|
|
436
|
+
const fullScanEnabled = options.fullScanEnabled || false;
|
|
437
|
+
const fullScanInterval = options.fullScanInterval || 24;
|
|
315
438
|
// Extract folder ID from resource locator
|
|
316
439
|
let watchFolderId;
|
|
317
440
|
if (typeof watchFolder === 'string') {
|
|
@@ -321,52 +444,265 @@ class MicrosoftOneDriveBusinessTrigger {
|
|
|
321
444
|
watchFolderId = watchFolder.value || 'root';
|
|
322
445
|
}
|
|
323
446
|
const driveId = webhookData.driveId;
|
|
324
|
-
|
|
447
|
+
const storedWatchFolderId = webhookData.watchFolderId || 'root';
|
|
448
|
+
// Check if full scan is needed
|
|
449
|
+
if (fullScanEnabled) {
|
|
450
|
+
const lastFullScan = webhookData.lastFullScan || 0;
|
|
451
|
+
const now = Date.now();
|
|
452
|
+
const hoursSinceLastScan = (now - lastFullScan) / (1000 * 60 * 60);
|
|
453
|
+
if (hoursSinceLastScan >= fullScanInterval) {
|
|
454
|
+
// Reset delta link to force full scan
|
|
455
|
+
webhookData.deltaLink = null;
|
|
456
|
+
webhookData.lastFullScan = now;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
// Always use root delta query (Microsoft Graph requirement)
|
|
325
460
|
let deltaUrl = webhookData.deltaLink;
|
|
326
461
|
if (!deltaUrl) {
|
|
327
|
-
|
|
328
|
-
deltaUrl = `https://graph.microsoft.com/v1.0/drives/${driveId}/${watchPath}/delta`;
|
|
462
|
+
deltaUrl = `https://graph.microsoft.com/v1.0/drives/${driveId}/root/delta`;
|
|
329
463
|
}
|
|
330
464
|
try {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
465
|
+
// Wait for file completion if configured
|
|
466
|
+
if (waitForCompletion > 0) {
|
|
467
|
+
await new Promise(resolve => setTimeout(resolve, waitForCompletion * 1000));
|
|
468
|
+
}
|
|
469
|
+
// Fetch all pages of delta changes
|
|
470
|
+
let allChanges = [];
|
|
471
|
+
let currentDeltaUrl = deltaUrl;
|
|
472
|
+
let finalDeltaLink;
|
|
473
|
+
let retryCount = 0;
|
|
474
|
+
const maxRetries = 3;
|
|
475
|
+
while (currentDeltaUrl) {
|
|
476
|
+
try {
|
|
477
|
+
const deltaResponse = await this.helpers.requestOAuth2.call(this, 'microsoftOneDriveBusinessOAuth2Api', {
|
|
478
|
+
method: 'GET',
|
|
479
|
+
url: currentDeltaUrl,
|
|
480
|
+
json: true,
|
|
481
|
+
});
|
|
482
|
+
const pageChanges = deltaResponse.value;
|
|
483
|
+
allChanges = allChanges.concat(pageChanges);
|
|
484
|
+
// Check for next page or delta link
|
|
485
|
+
if (deltaResponse['@odata.nextLink']) {
|
|
486
|
+
currentDeltaUrl = deltaResponse['@odata.nextLink'];
|
|
487
|
+
}
|
|
488
|
+
else {
|
|
489
|
+
currentDeltaUrl = undefined;
|
|
490
|
+
finalDeltaLink = deltaResponse['@odata.deltaLink'];
|
|
491
|
+
}
|
|
492
|
+
retryCount = 0; // Reset retry count on success
|
|
493
|
+
}
|
|
494
|
+
catch (pageError) {
|
|
495
|
+
// Retry logic for transient failures
|
|
496
|
+
const statusCode = pageError?.statusCode || pageError?.response?.status;
|
|
497
|
+
if ((statusCode === 429 || statusCode >= 500) && retryCount < maxRetries) {
|
|
498
|
+
retryCount++;
|
|
499
|
+
const delay = Math.min(1000 * Math.pow(2, retryCount), 10000); // Exponential backoff
|
|
500
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
501
|
+
continue; // Retry same URL
|
|
502
|
+
}
|
|
503
|
+
throw pageError; // Re-throw if not retryable or max retries reached
|
|
504
|
+
}
|
|
505
|
+
}
|
|
336
506
|
// Store the new delta link
|
|
337
|
-
|
|
338
|
-
|
|
507
|
+
if (finalDeltaLink) {
|
|
508
|
+
webhookData.deltaLink = finalDeltaLink;
|
|
509
|
+
}
|
|
510
|
+
const changes = allChanges;
|
|
511
|
+
// Get folder path if watching specific folder
|
|
512
|
+
let watchFolderPath = null;
|
|
513
|
+
if (storedWatchFolderId !== 'root') {
|
|
514
|
+
try {
|
|
515
|
+
const folderInfo = await this.helpers.requestOAuth2.call(this, 'microsoftOneDriveBusinessOAuth2Api', {
|
|
516
|
+
method: 'GET',
|
|
517
|
+
url: `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${storedWatchFolderId}`,
|
|
518
|
+
qs: { $select: 'id,name,parentReference' },
|
|
519
|
+
json: true,
|
|
520
|
+
});
|
|
521
|
+
const parentRef = folderInfo.parentReference;
|
|
522
|
+
watchFolderPath = (parentRef?.path || '') + '/' + folderInfo.name;
|
|
523
|
+
}
|
|
524
|
+
catch (error) {
|
|
525
|
+
// If can't get folder path, don't filter
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
// Deduplication: track processed items
|
|
529
|
+
const processedItems = webhookData.processedItems || {};
|
|
530
|
+
const now = Date.now();
|
|
531
|
+
// Clean up old processed items (older than 1 hour)
|
|
532
|
+
for (const key in processedItems) {
|
|
533
|
+
if (now - processedItems[key] > 3600000) {
|
|
534
|
+
delete processedItems[key];
|
|
535
|
+
}
|
|
536
|
+
}
|
|
339
537
|
const filteredChanges = changes.filter((change) => {
|
|
538
|
+
// Validate required fields
|
|
539
|
+
if (!change.id || !change.name) {
|
|
540
|
+
return false; // Skip incomplete items
|
|
541
|
+
}
|
|
340
542
|
const isFile = change.file !== undefined;
|
|
341
543
|
const isFolder = change.folder !== undefined;
|
|
342
544
|
const isDeleted = change.deleted !== undefined;
|
|
343
545
|
if (isDeleted)
|
|
344
546
|
return false;
|
|
547
|
+
// Skip the root folder itself
|
|
548
|
+
if (change.root !== undefined)
|
|
549
|
+
return false;
|
|
550
|
+
// Skip if missing critical metadata
|
|
551
|
+
if (!change.createdDateTime || !change.lastModifiedDateTime) {
|
|
552
|
+
return false;
|
|
553
|
+
}
|
|
554
|
+
// Deduplication check
|
|
555
|
+
if (ignoreDuplicates) {
|
|
556
|
+
const itemKey = `${change.id}_${change.lastModifiedDateTime}`;
|
|
557
|
+
if (processedItems[itemKey]) {
|
|
558
|
+
return false; // Already processed this exact change
|
|
559
|
+
}
|
|
560
|
+
processedItems[itemKey] = now;
|
|
561
|
+
}
|
|
562
|
+
// Filter by folder path if watching specific folder
|
|
563
|
+
if (storedWatchFolderId !== 'root' && change.parentReference) {
|
|
564
|
+
const parentRef = change.parentReference;
|
|
565
|
+
const parentId = parentRef.id;
|
|
566
|
+
if (recursive) {
|
|
567
|
+
// For recursive, check if item is in watched folder or any subfolder
|
|
568
|
+
const itemPath = parentRef.path || '';
|
|
569
|
+
if (watchFolderPath) {
|
|
570
|
+
if (!itemPath.includes(watchFolderPath)) {
|
|
571
|
+
return false;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
// Fallback: check parent ID chain (would need additional API calls)
|
|
576
|
+
// For now, just check direct parent
|
|
577
|
+
if (parentId !== storedWatchFolderId) {
|
|
578
|
+
// Could be in subfolder, but we can't verify without more API calls
|
|
579
|
+
// Let it through for now
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
// Non-recursive: must be direct child
|
|
585
|
+
if (parentId !== storedWatchFolderId) {
|
|
586
|
+
return false;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
345
590
|
if (event === 'fileCreated' && isFile) {
|
|
346
|
-
|
|
591
|
+
// Compare timestamps instead of strings for better accuracy
|
|
592
|
+
const created = new Date(change.createdDateTime).getTime();
|
|
593
|
+
const modified = new Date(change.lastModifiedDateTime).getTime();
|
|
594
|
+
// Allow small time difference (within 5 seconds) for creation detection
|
|
595
|
+
return Math.abs(created - modified) < 5000;
|
|
347
596
|
}
|
|
348
597
|
else if (event === 'fileUpdated' && isFile) {
|
|
349
|
-
|
|
598
|
+
const created = new Date(change.createdDateTime).getTime();
|
|
599
|
+
const modified = new Date(change.lastModifiedDateTime).getTime();
|
|
600
|
+
return modified > created + 5000;
|
|
350
601
|
}
|
|
351
602
|
else if (event === 'folderCreated' && isFolder) {
|
|
352
|
-
|
|
603
|
+
const created = new Date(change.createdDateTime).getTime();
|
|
604
|
+
const modified = new Date(change.lastModifiedDateTime).getTime();
|
|
605
|
+
return Math.abs(created - modified) < 5000;
|
|
353
606
|
}
|
|
354
607
|
else if (event === 'folderUpdated' && isFolder) {
|
|
355
|
-
|
|
608
|
+
const created = new Date(change.createdDateTime).getTime();
|
|
609
|
+
const modified = new Date(change.lastModifiedDateTime).getTime();
|
|
610
|
+
return modified > created + 5000;
|
|
356
611
|
}
|
|
357
612
|
return false;
|
|
358
613
|
});
|
|
614
|
+
// Save processed items
|
|
615
|
+
webhookData.processedItems = processedItems;
|
|
359
616
|
if (filteredChanges.length === 0) {
|
|
360
617
|
return {
|
|
361
618
|
webhookResponse: 'No matching changes',
|
|
362
619
|
workflowData: [],
|
|
363
620
|
};
|
|
364
621
|
}
|
|
622
|
+
// Enrich with extended metadata if requested
|
|
623
|
+
if (includeMetadata && filteredChanges.length > 0) {
|
|
624
|
+
for (const change of filteredChanges) {
|
|
625
|
+
try {
|
|
626
|
+
const itemDetails = await this.helpers.requestOAuth2.call(this, 'microsoftOneDriveBusinessOAuth2Api', {
|
|
627
|
+
method: 'GET',
|
|
628
|
+
url: `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${change.id}`,
|
|
629
|
+
qs: {
|
|
630
|
+
$select: 'id,name,size,webUrl,file,folder,publication,createdBy,lastModifiedBy,fileSystemInfo,versions'
|
|
631
|
+
},
|
|
632
|
+
json: true,
|
|
633
|
+
});
|
|
634
|
+
// Add version information
|
|
635
|
+
if (itemDetails.file) {
|
|
636
|
+
change.versionNumber = itemDetails.file.hashes ?
|
|
637
|
+
itemDetails['@microsoft.graph.version'] : undefined;
|
|
638
|
+
}
|
|
639
|
+
// Add lock/checkout status (SharePoint feature)
|
|
640
|
+
if (itemDetails.publication) {
|
|
641
|
+
const pub = itemDetails.publication;
|
|
642
|
+
change.checkoutUser = pub.checkedOutBy;
|
|
643
|
+
change.isCheckedOut = !!pub.checkedOutBy;
|
|
644
|
+
}
|
|
645
|
+
// Merge additional details
|
|
646
|
+
Object.assign(change, {
|
|
647
|
+
webUrl: itemDetails.webUrl,
|
|
648
|
+
createdBy: itemDetails.createdBy,
|
|
649
|
+
lastModifiedBy: itemDetails.lastModifiedBy,
|
|
650
|
+
fileSystemInfo: itemDetails.fileSystemInfo,
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
catch (enrichError) {
|
|
654
|
+
// If enrichment fails, continue with basic data
|
|
655
|
+
console.log('Failed to enrich metadata:', enrichError);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
365
659
|
return {
|
|
366
660
|
workflowData: [filteredChanges.map((change) => ({ json: change }))],
|
|
367
661
|
};
|
|
368
662
|
}
|
|
369
663
|
catch (error) {
|
|
664
|
+
// Handle specific error cases
|
|
665
|
+
const statusCode = error?.statusCode || error?.response?.status;
|
|
666
|
+
const errorCode = error?.error?.code || error?.response?.data?.error?.code;
|
|
667
|
+
// Delta link expired (410 Gone) or invalid (404)
|
|
668
|
+
if (statusCode === 410 || statusCode === 404 || errorCode === 'resyncRequired') {
|
|
669
|
+
// Reset delta link and try again with fresh sync
|
|
670
|
+
webhookData.deltaLink = null;
|
|
671
|
+
return {
|
|
672
|
+
webhookResponse: 'Delta link expired, will resync on next change',
|
|
673
|
+
workflowData: [],
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
// Folder not found or access denied
|
|
677
|
+
if (statusCode === 404 && storedWatchFolderId !== 'root') {
|
|
678
|
+
const errorMessage = 'Watched folder no longer exists or is inaccessible. Please reconfigure the trigger.';
|
|
679
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), errorMessage);
|
|
680
|
+
}
|
|
681
|
+
// Forbidden - permissions changed
|
|
682
|
+
if (statusCode === 403) {
|
|
683
|
+
const errorMessage = 'Access denied. Permissions may have changed or credentials need to be refreshed.';
|
|
684
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), errorMessage);
|
|
685
|
+
}
|
|
686
|
+
// Unauthorized - token expired/revoked
|
|
687
|
+
if (statusCode === 401) {
|
|
688
|
+
const errorMessage = 'Authentication failed. Please re-authenticate your OneDrive connection.';
|
|
689
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), errorMessage);
|
|
690
|
+
}
|
|
691
|
+
// Service unavailable or gateway timeout
|
|
692
|
+
if (statusCode === 503 || statusCode === 504) {
|
|
693
|
+
return {
|
|
694
|
+
webhookResponse: 'Microsoft Graph service temporarily unavailable, will retry on next notification',
|
|
695
|
+
workflowData: [],
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
// Rate limiting (429)
|
|
699
|
+
if (statusCode === 429) {
|
|
700
|
+
const retryAfter = error?.response?.headers?.['retry-after'] || '60';
|
|
701
|
+
return {
|
|
702
|
+
webhookResponse: `Rate limited, retry after ${retryAfter} seconds`,
|
|
703
|
+
workflowData: [],
|
|
704
|
+
};
|
|
705
|
+
}
|
|
370
706
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
371
707
|
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Failed to fetch delta changes: ${errorMessage}`);
|
|
372
708
|
}
|