n8n-nodes-onedrive-business 1.0.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.
- package/README.md +452 -0
- package/dist/credentials/OneDriveBusinessOAuth2.credentials.d.ts +25 -0
- package/dist/credentials/OneDriveBusinessOAuth2.credentials.js +114 -0
- package/dist/nodes/OneDriveBusiness.node.d.ts +35 -0
- package/dist/nodes/OneDriveBusiness.node.js +661 -0
- package/dist/nodes/OneDriveBusinessTrigger.node.d.ts +63 -0
- package/dist/nodes/OneDriveBusinessTrigger.node.js +332 -0
- package/dist/onedrive.svg +1 -0
- package/dist/types.d.ts +218 -0
- package/dist/types.js +8 -0
- package/dist/utils/DeltaProcessor.d.ts +91 -0
- package/dist/utils/DeltaProcessor.js +267 -0
- package/dist/utils/GraphClient.d.ts +67 -0
- package/dist/utils/GraphClient.js +182 -0
- package/dist/utils/StateStore.d.ts +96 -0
- package/dist/utils/StateStore.js +280 -0
- package/dist/utils/WebhookHandler.d.ts +78 -0
- package/dist/utils/WebhookHandler.js +220 -0
- package/dist/utils/helpers.d.ts +113 -0
- package/dist/utils/helpers.js +250 -0
- package/package.json +56 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { FileStabilityInfo, TriggerEvent } from '../types';
|
|
2
|
+
import { GraphClient } from './GraphClient';
|
|
3
|
+
import { StateStore } from './StateStore';
|
|
4
|
+
/**
|
|
5
|
+
* Delta Query Processor for OneDrive Business
|
|
6
|
+
*
|
|
7
|
+
* This is the CORE deduplication engine that implements:
|
|
8
|
+
*
|
|
9
|
+
* 1. File Stability Checking
|
|
10
|
+
* - Ensures files have completed uploading before processing
|
|
11
|
+
* - Validates quickXorHash presence
|
|
12
|
+
* - Checks timestamp alignment
|
|
13
|
+
*
|
|
14
|
+
* 2. Version-based Deduplication
|
|
15
|
+
* - Tracks ${itemId}_${eTag} to identify unique versions
|
|
16
|
+
* - Prevents multiple executions for the same file version
|
|
17
|
+
*
|
|
18
|
+
* 3. Stability Window
|
|
19
|
+
* - Waits 15 seconds for files still being modified
|
|
20
|
+
* - Prevents processing incomplete uploads
|
|
21
|
+
*
|
|
22
|
+
* 4. Event Classification
|
|
23
|
+
* - Distinguishes 'created' vs 'updated' using eTag comparison
|
|
24
|
+
* - Provides accurate event types to workflows
|
|
25
|
+
*
|
|
26
|
+
* WHY THIS IS NECESSARY:
|
|
27
|
+
* SharePoint-backed OneDrive generates multiple updates per upload:
|
|
28
|
+
* - File created (size=0)
|
|
29
|
+
* - Metadata updated
|
|
30
|
+
* - Content uploaded
|
|
31
|
+
* - Final metadata sync
|
|
32
|
+
*
|
|
33
|
+
* Without this processor, ONE file upload would trigger FOUR workflow executions.
|
|
34
|
+
*/
|
|
35
|
+
export declare class DeltaProcessor {
|
|
36
|
+
private graphClient;
|
|
37
|
+
private stateStore;
|
|
38
|
+
private drivePath;
|
|
39
|
+
private stabilityTracker;
|
|
40
|
+
private readonly STABILITY_WINDOW_MS;
|
|
41
|
+
constructor(graphClient: GraphClient, stateStore: StateStore, drivePath: string);
|
|
42
|
+
/**
|
|
43
|
+
* Process delta query and return triggerable events
|
|
44
|
+
*
|
|
45
|
+
* This is the main entry point called by the trigger node
|
|
46
|
+
*/
|
|
47
|
+
processDeltaQuery(eventFilter: Set<string>): Promise<TriggerEvent[]>;
|
|
48
|
+
/**
|
|
49
|
+
* Fetch all pages from delta query
|
|
50
|
+
* Handles @odata.nextLink pagination and stores @odata.deltaLink
|
|
51
|
+
*/
|
|
52
|
+
private fetchAllDeltaPages;
|
|
53
|
+
/**
|
|
54
|
+
* Process a single delta item
|
|
55
|
+
* Returns a TriggerEvent if the item should trigger a workflow
|
|
56
|
+
*/
|
|
57
|
+
private processItem;
|
|
58
|
+
/**
|
|
59
|
+
* Classify event as created vs updated
|
|
60
|
+
*
|
|
61
|
+
* Logic:
|
|
62
|
+
* - If item NOT in lastKnownItems → created
|
|
63
|
+
* - If item eTag differs from lastKnownItems → updated
|
|
64
|
+
*/
|
|
65
|
+
private classifyEvent;
|
|
66
|
+
/**
|
|
67
|
+
* Check if a file is stable and ready to be processed
|
|
68
|
+
*
|
|
69
|
+
* A file is considered stable when:
|
|
70
|
+
* 1. Size > 0 (not empty placeholder)
|
|
71
|
+
* 2. quickXorHash is present (upload complete)
|
|
72
|
+
* 3. lastModifiedDateTime matches fileSystemInfo.lastModifiedDateTime
|
|
73
|
+
* 4. At least 15 seconds have passed since last modification
|
|
74
|
+
*
|
|
75
|
+
* Returns true if stable, false if still being uploaded
|
|
76
|
+
*/
|
|
77
|
+
private checkFileStability;
|
|
78
|
+
/**
|
|
79
|
+
* Track file stability over time
|
|
80
|
+
* Helps debug upload issues
|
|
81
|
+
*/
|
|
82
|
+
private trackFileStability;
|
|
83
|
+
/**
|
|
84
|
+
* Get stability information for debugging
|
|
85
|
+
*/
|
|
86
|
+
getStabilityInfo(itemId: string): FileStabilityInfo | undefined;
|
|
87
|
+
/**
|
|
88
|
+
* Clean up resources
|
|
89
|
+
*/
|
|
90
|
+
dispose(): void;
|
|
91
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DeltaProcessor = void 0;
|
|
4
|
+
const helpers_1 = require("./helpers");
|
|
5
|
+
/**
|
|
6
|
+
* Delta Query Processor for OneDrive Business
|
|
7
|
+
*
|
|
8
|
+
* This is the CORE deduplication engine that implements:
|
|
9
|
+
*
|
|
10
|
+
* 1. File Stability Checking
|
|
11
|
+
* - Ensures files have completed uploading before processing
|
|
12
|
+
* - Validates quickXorHash presence
|
|
13
|
+
* - Checks timestamp alignment
|
|
14
|
+
*
|
|
15
|
+
* 2. Version-based Deduplication
|
|
16
|
+
* - Tracks ${itemId}_${eTag} to identify unique versions
|
|
17
|
+
* - Prevents multiple executions for the same file version
|
|
18
|
+
*
|
|
19
|
+
* 3. Stability Window
|
|
20
|
+
* - Waits 15 seconds for files still being modified
|
|
21
|
+
* - Prevents processing incomplete uploads
|
|
22
|
+
*
|
|
23
|
+
* 4. Event Classification
|
|
24
|
+
* - Distinguishes 'created' vs 'updated' using eTag comparison
|
|
25
|
+
* - Provides accurate event types to workflows
|
|
26
|
+
*
|
|
27
|
+
* WHY THIS IS NECESSARY:
|
|
28
|
+
* SharePoint-backed OneDrive generates multiple updates per upload:
|
|
29
|
+
* - File created (size=0)
|
|
30
|
+
* - Metadata updated
|
|
31
|
+
* - Content uploaded
|
|
32
|
+
* - Final metadata sync
|
|
33
|
+
*
|
|
34
|
+
* Without this processor, ONE file upload would trigger FOUR workflow executions.
|
|
35
|
+
*/
|
|
36
|
+
class DeltaProcessor {
|
|
37
|
+
constructor(graphClient, stateStore, drivePath) {
|
|
38
|
+
// Stability tracking for files currently being uploaded
|
|
39
|
+
this.stabilityTracker = new Map();
|
|
40
|
+
// Stability window: wait 15 seconds after last modification
|
|
41
|
+
this.STABILITY_WINDOW_MS = 15000;
|
|
42
|
+
this.graphClient = graphClient;
|
|
43
|
+
this.stateStore = stateStore;
|
|
44
|
+
this.drivePath = drivePath;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Process delta query and return triggerable events
|
|
48
|
+
*
|
|
49
|
+
* This is the main entry point called by the trigger node
|
|
50
|
+
*/
|
|
51
|
+
async processDeltaQuery(eventFilter // e.g., ['file.created', 'file.updated']
|
|
52
|
+
) {
|
|
53
|
+
const events = [];
|
|
54
|
+
try {
|
|
55
|
+
// Get current state
|
|
56
|
+
const state = this.stateStore.getState();
|
|
57
|
+
// Determine the delta endpoint
|
|
58
|
+
let deltaEndpoint;
|
|
59
|
+
if (state.deltaLink) {
|
|
60
|
+
// Use existing delta link (contains the full URL)
|
|
61
|
+
// Extract the path after /v1.0
|
|
62
|
+
const deltaUrl = new URL(state.deltaLink);
|
|
63
|
+
deltaEndpoint = deltaUrl.pathname.replace('/v1.0', '') + deltaUrl.search;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
// Initial delta query
|
|
67
|
+
deltaEndpoint = `${this.drivePath}/root/delta`;
|
|
68
|
+
}
|
|
69
|
+
// Fetch delta changes (handle pagination)
|
|
70
|
+
const items = await this.fetchAllDeltaPages(deltaEndpoint);
|
|
71
|
+
// Process each item
|
|
72
|
+
for (const item of items) {
|
|
73
|
+
const event = await this.processItem(item, eventFilter);
|
|
74
|
+
if (event) {
|
|
75
|
+
events.push(event);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return events;
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
console.error('Error processing delta query:', error);
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Fetch all pages from delta query
|
|
87
|
+
* Handles @odata.nextLink pagination and stores @odata.deltaLink
|
|
88
|
+
*/
|
|
89
|
+
async fetchAllDeltaPages(initialEndpoint) {
|
|
90
|
+
const allItems = [];
|
|
91
|
+
let currentEndpoint = initialEndpoint;
|
|
92
|
+
while (currentEndpoint) {
|
|
93
|
+
const response = await this.graphClient.get(currentEndpoint);
|
|
94
|
+
allItems.push(...response.value);
|
|
95
|
+
// Check for next page
|
|
96
|
+
if (response['@odata.nextLink']) {
|
|
97
|
+
// Extract path from full URL
|
|
98
|
+
const url = new URL(response['@odata.nextLink']);
|
|
99
|
+
currentEndpoint = url.pathname.replace('/v1.0', '') + url.search;
|
|
100
|
+
}
|
|
101
|
+
else if (response['@odata.deltaLink']) {
|
|
102
|
+
// Save delta link for next run
|
|
103
|
+
await this.stateStore.updateDeltaLink(response['@odata.deltaLink']);
|
|
104
|
+
currentEndpoint = '';
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
// No more pages
|
|
108
|
+
currentEndpoint = '';
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return allItems;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Process a single delta item
|
|
115
|
+
* Returns a TriggerEvent if the item should trigger a workflow
|
|
116
|
+
*/
|
|
117
|
+
async processItem(item, eventFilter) {
|
|
118
|
+
// Skip deleted items (unless specifically requested)
|
|
119
|
+
if (item.deleted) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
// Skip root folder
|
|
123
|
+
if (!item.parentReference) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
// Determine event type
|
|
127
|
+
const eventType = this.classifyEvent(item);
|
|
128
|
+
// Check if this event type is enabled
|
|
129
|
+
if (!eventFilter.has(eventType)) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
// Check if already processed (deduplication)
|
|
133
|
+
const versionKey = (0, helpers_1.buildVersionKey)(item.id, item.eTag);
|
|
134
|
+
if (this.stateStore.isVersionProcessed(item.id, item.eTag)) {
|
|
135
|
+
// Already processed this version
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
// For files, check stability
|
|
139
|
+
if ((0, helpers_1.isFile)(item)) {
|
|
140
|
+
const isStable = await this.checkFileStability(item);
|
|
141
|
+
if (!isStable) {
|
|
142
|
+
// File is still being uploaded/modified, skip for now
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// Mark as processed
|
|
147
|
+
await this.stateStore.markVersionProcessed(item.id, item.eTag);
|
|
148
|
+
// Update last known state
|
|
149
|
+
await this.stateStore.updateLastKnownItem(item.id, item.eTag, item.lastModifiedDateTime);
|
|
150
|
+
// Return the event
|
|
151
|
+
return {
|
|
152
|
+
eventType,
|
|
153
|
+
item,
|
|
154
|
+
timestamp: new Date().toISOString(),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Classify event as created vs updated
|
|
159
|
+
*
|
|
160
|
+
* Logic:
|
|
161
|
+
* - If item NOT in lastKnownItems → created
|
|
162
|
+
* - If item eTag differs from lastKnownItems → updated
|
|
163
|
+
*/
|
|
164
|
+
classifyEvent(item) {
|
|
165
|
+
const lastKnown = this.stateStore.getLastKnownItem(item.id);
|
|
166
|
+
const isFileItem = (0, helpers_1.isFile)(item);
|
|
167
|
+
const isFolderItem = (0, helpers_1.isFolder)(item);
|
|
168
|
+
if (!lastKnown) {
|
|
169
|
+
// New item
|
|
170
|
+
if (isFileItem) {
|
|
171
|
+
return 'file.created';
|
|
172
|
+
}
|
|
173
|
+
else if (isFolderItem) {
|
|
174
|
+
return 'folder.created';
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
// Existing item
|
|
179
|
+
if (lastKnown.eTag !== item.eTag) {
|
|
180
|
+
if (isFileItem) {
|
|
181
|
+
return 'file.updated';
|
|
182
|
+
}
|
|
183
|
+
else if (isFolderItem) {
|
|
184
|
+
return 'folder.updated';
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// Default to updated
|
|
189
|
+
return isFileItem ? 'file.updated' : 'folder.updated';
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Check if a file is stable and ready to be processed
|
|
193
|
+
*
|
|
194
|
+
* A file is considered stable when:
|
|
195
|
+
* 1. Size > 0 (not empty placeholder)
|
|
196
|
+
* 2. quickXorHash is present (upload complete)
|
|
197
|
+
* 3. lastModifiedDateTime matches fileSystemInfo.lastModifiedDateTime
|
|
198
|
+
* 4. At least 15 seconds have passed since last modification
|
|
199
|
+
*
|
|
200
|
+
* Returns true if stable, false if still being uploaded
|
|
201
|
+
*/
|
|
202
|
+
async checkFileStability(item) {
|
|
203
|
+
var _a, _b, _c;
|
|
204
|
+
// Basic stability checks
|
|
205
|
+
const hasSize = item.size > 0;
|
|
206
|
+
const hasHash = !!((_b = (_a = item.file) === null || _a === void 0 ? void 0 : _a.hashes) === null || _b === void 0 ? void 0 : _b.quickXorHash);
|
|
207
|
+
const timestampsAlign = item.lastModifiedDateTime === ((_c = item.fileSystemInfo) === null || _c === void 0 ? void 0 : _c.lastModifiedDateTime);
|
|
208
|
+
const basicStability = hasSize && hasHash && timestampsAlign;
|
|
209
|
+
if (!basicStability) {
|
|
210
|
+
// Track this file for stability checking
|
|
211
|
+
this.trackFileStability(item, false);
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
// Check stability window (15 seconds)
|
|
215
|
+
const lastModified = new Date(item.lastModifiedDateTime).getTime();
|
|
216
|
+
const now = Date.now();
|
|
217
|
+
const timeSinceModification = now - lastModified;
|
|
218
|
+
if (timeSinceModification < this.STABILITY_WINDOW_MS) {
|
|
219
|
+
// File was modified recently, wait longer
|
|
220
|
+
this.trackFileStability(item, false);
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
// File is stable
|
|
224
|
+
this.trackFileStability(item, true);
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Track file stability over time
|
|
229
|
+
* Helps debug upload issues
|
|
230
|
+
*/
|
|
231
|
+
trackFileStability(item, isStable) {
|
|
232
|
+
var _a, _b;
|
|
233
|
+
const now = Date.now();
|
|
234
|
+
const existing = this.stabilityTracker.get(item.id);
|
|
235
|
+
const stabilityInfo = {
|
|
236
|
+
itemId: item.id,
|
|
237
|
+
eTag: item.eTag,
|
|
238
|
+
size: item.size,
|
|
239
|
+
lastModifiedDateTime: item.lastModifiedDateTime,
|
|
240
|
+
hasHash: !!((_b = (_a = item.file) === null || _a === void 0 ? void 0 : _a.hashes) === null || _b === void 0 ? void 0 : _b.quickXorHash),
|
|
241
|
+
isStable,
|
|
242
|
+
firstSeen: (existing === null || existing === void 0 ? void 0 : existing.firstSeen) || now,
|
|
243
|
+
lastChecked: now,
|
|
244
|
+
};
|
|
245
|
+
this.stabilityTracker.set(item.id, stabilityInfo);
|
|
246
|
+
// Clean up old entries (keep only last hour)
|
|
247
|
+
const oneHourAgo = now - 3600000;
|
|
248
|
+
for (const [itemId, info] of this.stabilityTracker.entries()) {
|
|
249
|
+
if (info.lastChecked < oneHourAgo) {
|
|
250
|
+
this.stabilityTracker.delete(itemId);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Get stability information for debugging
|
|
256
|
+
*/
|
|
257
|
+
getStabilityInfo(itemId) {
|
|
258
|
+
return this.stabilityTracker.get(itemId);
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Clean up resources
|
|
262
|
+
*/
|
|
263
|
+
dispose() {
|
|
264
|
+
this.stabilityTracker.clear();
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
exports.DeltaProcessor = DeltaProcessor;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
/// <reference types="node" />
|
|
3
|
+
import { IExecuteFunctions, IHookFunctions, IPollFunctions } from 'n8n-workflow';
|
|
4
|
+
import type { DriveItem, RetryConfig } from '../types';
|
|
5
|
+
/**
|
|
6
|
+
* Microsoft Graph API Client
|
|
7
|
+
*
|
|
8
|
+
* Centralized client for all Microsoft Graph API interactions.
|
|
9
|
+
* Handles:
|
|
10
|
+
* - Authentication (OAuth2 token management)
|
|
11
|
+
* - Retry logic with exponential backoff
|
|
12
|
+
* - 429 Throttling handling
|
|
13
|
+
* - Error normalization
|
|
14
|
+
* - Request/response type safety
|
|
15
|
+
*/
|
|
16
|
+
export declare class GraphClient {
|
|
17
|
+
private baseUrl;
|
|
18
|
+
private context;
|
|
19
|
+
private retryConfig;
|
|
20
|
+
constructor(context: IExecuteFunctions | IHookFunctions | IPollFunctions, retryConfig?: Partial<RetryConfig>);
|
|
21
|
+
/**
|
|
22
|
+
* Make a GET request to Microsoft Graph API
|
|
23
|
+
*/
|
|
24
|
+
get<T = any>(endpoint: string, queryParameters?: Record<string, any>): Promise<T>;
|
|
25
|
+
/**
|
|
26
|
+
* Make a POST request to Microsoft Graph API
|
|
27
|
+
*/
|
|
28
|
+
post<T = any>(endpoint: string, body?: any, queryParameters?: Record<string, any>): Promise<T>;
|
|
29
|
+
/**
|
|
30
|
+
* Make a PATCH request to Microsoft Graph API
|
|
31
|
+
*/
|
|
32
|
+
patch<T = any>(endpoint: string, body?: any, queryParameters?: Record<string, any>): Promise<T>;
|
|
33
|
+
/**
|
|
34
|
+
* Make a PUT request to Microsoft Graph API
|
|
35
|
+
*/
|
|
36
|
+
put<T = any>(endpoint: string, body?: any, queryParameters?: Record<string, any>): Promise<T>;
|
|
37
|
+
/**
|
|
38
|
+
* Make a DELETE request to Microsoft Graph API
|
|
39
|
+
*/
|
|
40
|
+
delete<T = any>(endpoint: string, queryParameters?: Record<string, any>): Promise<T>;
|
|
41
|
+
/**
|
|
42
|
+
* Download binary content from Microsoft Graph
|
|
43
|
+
*/
|
|
44
|
+
downloadBinary(endpoint: string): Promise<Buffer>;
|
|
45
|
+
/**
|
|
46
|
+
* Upload binary content to Microsoft Graph
|
|
47
|
+
*/
|
|
48
|
+
uploadBinary(endpoint: string, binaryData: Buffer, contentType?: string): Promise<DriveItem>;
|
|
49
|
+
/**
|
|
50
|
+
* Fetch all pages from a paginated endpoint
|
|
51
|
+
* Automatically follows @odata.nextLink
|
|
52
|
+
*/
|
|
53
|
+
getAllPages<T>(endpoint: string, queryParameters?: Record<string, any>): Promise<T[]>;
|
|
54
|
+
/**
|
|
55
|
+
* Core request method with retry logic
|
|
56
|
+
*/
|
|
57
|
+
private request;
|
|
58
|
+
/**
|
|
59
|
+
* Execute request with exponential backoff retry logic
|
|
60
|
+
* Handles 429 throttling and transient errors
|
|
61
|
+
*/
|
|
62
|
+
private executeWithRetry;
|
|
63
|
+
/**
|
|
64
|
+
* Normalize errors to NodeApiError
|
|
65
|
+
*/
|
|
66
|
+
private handleError;
|
|
67
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GraphClient = void 0;
|
|
4
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
5
|
+
const helpers_1 = require("./helpers");
|
|
6
|
+
/**
|
|
7
|
+
* Microsoft Graph API Client
|
|
8
|
+
*
|
|
9
|
+
* Centralized client for all Microsoft Graph API interactions.
|
|
10
|
+
* Handles:
|
|
11
|
+
* - Authentication (OAuth2 token management)
|
|
12
|
+
* - Retry logic with exponential backoff
|
|
13
|
+
* - 429 Throttling handling
|
|
14
|
+
* - Error normalization
|
|
15
|
+
* - Request/response type safety
|
|
16
|
+
*/
|
|
17
|
+
class GraphClient {
|
|
18
|
+
constructor(context, retryConfig) {
|
|
19
|
+
var _a, _b, _c, _d;
|
|
20
|
+
this.baseUrl = 'https://graph.microsoft.com/v1.0';
|
|
21
|
+
this.context = context;
|
|
22
|
+
this.retryConfig = {
|
|
23
|
+
maxRetries: (_a = retryConfig === null || retryConfig === void 0 ? void 0 : retryConfig.maxRetries) !== null && _a !== void 0 ? _a : 3,
|
|
24
|
+
initialDelayMs: (_b = retryConfig === null || retryConfig === void 0 ? void 0 : retryConfig.initialDelayMs) !== null && _b !== void 0 ? _b : 1000,
|
|
25
|
+
maxDelayMs: (_c = retryConfig === null || retryConfig === void 0 ? void 0 : retryConfig.maxDelayMs) !== null && _c !== void 0 ? _c : 60000,
|
|
26
|
+
backoffMultiplier: (_d = retryConfig === null || retryConfig === void 0 ? void 0 : retryConfig.backoffMultiplier) !== null && _d !== void 0 ? _d : 2,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Make a GET request to Microsoft Graph API
|
|
31
|
+
*/
|
|
32
|
+
async get(endpoint, queryParameters) {
|
|
33
|
+
return this.request('GET', endpoint, undefined, queryParameters);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Make a POST request to Microsoft Graph API
|
|
37
|
+
*/
|
|
38
|
+
async post(endpoint, body, queryParameters) {
|
|
39
|
+
return this.request('POST', endpoint, body, queryParameters);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Make a PATCH request to Microsoft Graph API
|
|
43
|
+
*/
|
|
44
|
+
async patch(endpoint, body, queryParameters) {
|
|
45
|
+
return this.request('PATCH', endpoint, body, queryParameters);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Make a PUT request to Microsoft Graph API
|
|
49
|
+
*/
|
|
50
|
+
async put(endpoint, body, queryParameters) {
|
|
51
|
+
return this.request('PUT', endpoint, body, queryParameters);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Make a DELETE request to Microsoft Graph API
|
|
55
|
+
*/
|
|
56
|
+
async delete(endpoint, queryParameters) {
|
|
57
|
+
return this.request('DELETE', endpoint, undefined, queryParameters);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Download binary content from Microsoft Graph
|
|
61
|
+
*/
|
|
62
|
+
async downloadBinary(endpoint) {
|
|
63
|
+
const options = {
|
|
64
|
+
method: 'GET',
|
|
65
|
+
url: `${this.baseUrl}${endpoint}`,
|
|
66
|
+
encoding: null, // Return as Buffer
|
|
67
|
+
json: false,
|
|
68
|
+
};
|
|
69
|
+
try {
|
|
70
|
+
const response = await this.executeWithRetry(options);
|
|
71
|
+
return Buffer.from(response);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
throw this.handleError(error, 'GET', endpoint);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Upload binary content to Microsoft Graph
|
|
79
|
+
*/
|
|
80
|
+
async uploadBinary(endpoint, binaryData, contentType = 'application/octet-stream') {
|
|
81
|
+
const options = {
|
|
82
|
+
method: 'PUT',
|
|
83
|
+
url: `${this.baseUrl}${endpoint}`,
|
|
84
|
+
body: binaryData,
|
|
85
|
+
headers: {
|
|
86
|
+
'Content-Type': contentType,
|
|
87
|
+
},
|
|
88
|
+
json: false,
|
|
89
|
+
};
|
|
90
|
+
try {
|
|
91
|
+
const response = await this.executeWithRetry(options);
|
|
92
|
+
// Parse the JSON response
|
|
93
|
+
return typeof response === 'string' ? JSON.parse(response) : response;
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
throw this.handleError(error, 'PUT', endpoint);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Fetch all pages from a paginated endpoint
|
|
101
|
+
* Automatically follows @odata.nextLink
|
|
102
|
+
*/
|
|
103
|
+
async getAllPages(endpoint, queryParameters) {
|
|
104
|
+
const allItems = [];
|
|
105
|
+
let nextLink = endpoint;
|
|
106
|
+
while (nextLink) {
|
|
107
|
+
const response = await this.get(nextLink,
|
|
108
|
+
// Only send query parameters on the first request
|
|
109
|
+
nextLink === endpoint ? queryParameters : undefined);
|
|
110
|
+
allItems.push(...response.value);
|
|
111
|
+
nextLink = response['@odata.nextLink'];
|
|
112
|
+
}
|
|
113
|
+
return allItems;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Core request method with retry logic
|
|
117
|
+
*/
|
|
118
|
+
async request(method, endpoint, body, queryParameters) {
|
|
119
|
+
const options = {
|
|
120
|
+
method,
|
|
121
|
+
url: `${this.baseUrl}${endpoint}`,
|
|
122
|
+
json: true,
|
|
123
|
+
};
|
|
124
|
+
if (body !== undefined) {
|
|
125
|
+
options.body = body;
|
|
126
|
+
}
|
|
127
|
+
if (queryParameters) {
|
|
128
|
+
options.qs = queryParameters;
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
return await this.executeWithRetry(options);
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
throw this.handleError(error, method, endpoint);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Execute request with exponential backoff retry logic
|
|
139
|
+
* Handles 429 throttling and transient errors
|
|
140
|
+
*/
|
|
141
|
+
async executeWithRetry(options, retryCount = 0) {
|
|
142
|
+
var _a, _b;
|
|
143
|
+
try {
|
|
144
|
+
return await this.context.helpers.requestOAuth2.call(this.context, 'oneDriveBusinessOAuth2Api', options);
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
const statusCode = error.statusCode || ((_a = error.response) === null || _a === void 0 ? void 0 : _a.statusCode);
|
|
148
|
+
// Handle 429 Too Many Requests (Throttling)
|
|
149
|
+
if (statusCode === 429) {
|
|
150
|
+
if (retryCount < this.retryConfig.maxRetries) {
|
|
151
|
+
const retryAfter = (0, helpers_1.parseRetryAfter)((_b = error.response) === null || _b === void 0 ? void 0 : _b.headers['retry-after']);
|
|
152
|
+
await (0, helpers_1.sleep)(retryAfter);
|
|
153
|
+
return this.executeWithRetry(options, retryCount + 1);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Handle transient errors (5xx)
|
|
157
|
+
if (statusCode >= 500 && statusCode < 600) {
|
|
158
|
+
if (retryCount < this.retryConfig.maxRetries) {
|
|
159
|
+
const delay = (0, helpers_1.calculateBackoffDelay)(retryCount, this.retryConfig.initialDelayMs, this.retryConfig.maxDelayMs, this.retryConfig.backoffMultiplier);
|
|
160
|
+
await (0, helpers_1.sleep)(delay);
|
|
161
|
+
return this.executeWithRetry(options, retryCount + 1);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// No retry for other errors
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Normalize errors to NodeApiError
|
|
170
|
+
*/
|
|
171
|
+
handleError(error, method, endpoint) {
|
|
172
|
+
var _a;
|
|
173
|
+
const parsedError = (0, helpers_1.parseGraphError)(error.error || error);
|
|
174
|
+
const errorMessage = `Microsoft Graph API Error [${method} ${endpoint}]: ${parsedError.code} - ${parsedError.message}`;
|
|
175
|
+
return new n8n_workflow_1.NodeApiError(this.context.getNode(), error, {
|
|
176
|
+
message: errorMessage,
|
|
177
|
+
description: parsedError.message,
|
|
178
|
+
httpCode: (_a = error.statusCode) === null || _a === void 0 ? void 0 : _a.toString(),
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
exports.GraphClient = GraphClient;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { NodeState } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* State Store for OneDrive Business Trigger Node
|
|
4
|
+
*
|
|
5
|
+
* Provides persistent storage for:
|
|
6
|
+
* - Delta query links
|
|
7
|
+
* - Processed file versions (deduplication)
|
|
8
|
+
* - Last known item states (for event classification)
|
|
9
|
+
* - Webhook subscription information
|
|
10
|
+
*
|
|
11
|
+
* State MUST survive n8n restarts and redeployments to prevent:
|
|
12
|
+
* - Duplicate workflow executions
|
|
13
|
+
* - Re-processing old changes
|
|
14
|
+
* - Losing track of processed items
|
|
15
|
+
*/
|
|
16
|
+
export declare class StateStore {
|
|
17
|
+
private stateFilePath;
|
|
18
|
+
private nodeId;
|
|
19
|
+
private state;
|
|
20
|
+
private saveDebounceTimer;
|
|
21
|
+
constructor(nodeId: string, storageDir?: string);
|
|
22
|
+
/**
|
|
23
|
+
* Initialize and load state from disk
|
|
24
|
+
* Creates default state if file doesn't exist
|
|
25
|
+
*/
|
|
26
|
+
initialize(tenantId: string, driveId: string, userId?: string, siteId?: string): Promise<NodeState>;
|
|
27
|
+
/**
|
|
28
|
+
* Get current state
|
|
29
|
+
* Throws if not initialized
|
|
30
|
+
*/
|
|
31
|
+
getState(): NodeState;
|
|
32
|
+
/**
|
|
33
|
+
* Update delta link
|
|
34
|
+
*/
|
|
35
|
+
updateDeltaLink(deltaLink: string): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* Mark a file version as processed
|
|
38
|
+
* Returns true if this version was NOT already processed
|
|
39
|
+
*/
|
|
40
|
+
markVersionProcessed(itemId: string, eTag: string): Promise<boolean>;
|
|
41
|
+
/**
|
|
42
|
+
* Check if a version has been processed
|
|
43
|
+
*/
|
|
44
|
+
isVersionProcessed(itemId: string, eTag: string): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Update last known state of an item
|
|
47
|
+
* Used for event classification (created vs updated)
|
|
48
|
+
*/
|
|
49
|
+
updateLastKnownItem(itemId: string, eTag: string, lastModifiedDateTime: string): Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* Get last known state of an item
|
|
52
|
+
*/
|
|
53
|
+
getLastKnownItem(itemId: string): {
|
|
54
|
+
eTag: string;
|
|
55
|
+
lastModifiedDateTime: string;
|
|
56
|
+
} | null;
|
|
57
|
+
/**
|
|
58
|
+
* Update webhook subscription information
|
|
59
|
+
*/
|
|
60
|
+
updateSubscription(subscriptionId: string, expirationTimestamp: number): Promise<void>;
|
|
61
|
+
/**
|
|
62
|
+
* Clear webhook subscription information
|
|
63
|
+
*/
|
|
64
|
+
clearSubscription(): Promise<void>;
|
|
65
|
+
/**
|
|
66
|
+
* Clean up old processed versions to prevent unbounded growth
|
|
67
|
+
* Keeps only the most recent versions per item
|
|
68
|
+
*/
|
|
69
|
+
cleanupOldVersions(maxVersionsPerItem?: number): Promise<void>;
|
|
70
|
+
/**
|
|
71
|
+
* Create default state
|
|
72
|
+
*/
|
|
73
|
+
private createDefaultState;
|
|
74
|
+
/**
|
|
75
|
+
* Ensure state directory exists
|
|
76
|
+
*/
|
|
77
|
+
private ensureStateDirectory;
|
|
78
|
+
/**
|
|
79
|
+
* Load state from disk
|
|
80
|
+
*/
|
|
81
|
+
private loadFromDisk;
|
|
82
|
+
/**
|
|
83
|
+
* Save state to disk immediately
|
|
84
|
+
*/
|
|
85
|
+
private saveToDisk;
|
|
86
|
+
/**
|
|
87
|
+
* Save state to disk with debouncing
|
|
88
|
+
* Prevents excessive writes during rapid updates
|
|
89
|
+
*/
|
|
90
|
+
private saveToDiskDebounced;
|
|
91
|
+
/**
|
|
92
|
+
* Force save any pending changes
|
|
93
|
+
* Call this before deactivating the node
|
|
94
|
+
*/
|
|
95
|
+
flush(): Promise<void>;
|
|
96
|
+
}
|