@pipedream/sharepoint 0.7.2 → 0.8.1

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.
Files changed (30) hide show
  1. package/actions/create-folder/create-folder.mjs +1 -1
  2. package/actions/create-item/create-item.mjs +1 -1
  3. package/actions/create-link/create-link.mjs +1 -1
  4. package/actions/create-list/create-list.mjs +1 -1
  5. package/actions/download-file/download-file.mjs +1 -1
  6. package/actions/download-files/download-files.mjs +88 -0
  7. package/actions/find-file-by-name/find-file-by-name.mjs +1 -1
  8. package/actions/find-files-with-metadata/find-files-with-metadata.mjs +1 -1
  9. package/actions/get-excel-table/get-excel-table.mjs +1 -1
  10. package/actions/get-file-by-id/get-file-by-id.mjs +1 -1
  11. package/actions/get-site/get-site.mjs +1 -1
  12. package/actions/list-files-in-folder/list-files-in-folder.mjs +1 -1
  13. package/actions/list-sites/list-sites.mjs +1 -1
  14. package/actions/retrieve-file-metadata/retrieve-file-metadata.mjs +55 -0
  15. package/actions/search-and-filter-files/search-and-filter-files.mjs +1 -1
  16. package/actions/search-files/search-files.mjs +1 -1
  17. package/actions/search-sites/search-sites.mjs +1 -1
  18. package/actions/update-item/update-item.mjs +1 -1
  19. package/actions/upload-file/upload-file.mjs +1 -1
  20. package/common/constants.mjs +23 -0
  21. package/common/file-picker-base.mjs +308 -0
  22. package/common/utils.mjs +78 -0
  23. package/package.json +5 -2
  24. package/sharepoint.app.mjs +400 -3
  25. package/sources/new-file-created/new-file-created.mjs +1 -1
  26. package/sources/new-folder-created/new-folder-created.mjs +1 -1
  27. package/sources/new-list-item/new-list-item.mjs +1 -1
  28. package/sources/updated-file-instant/updated-file-instant.mjs +407 -0
  29. package/sources/updated-list-item/updated-list-item.mjs +1 -1
  30. package/actions/select-files/select-files.mjs +0 -198
@@ -0,0 +1,407 @@
1
+ import { randomUUID } from "crypto";
2
+ import sharepoint from "../../sharepoint.app.mjs";
3
+ import { WEBHOOK_SUBSCRIPTION_RENEWAL_SECONDS } from "../../common/constants.mjs";
4
+
5
+ export default {
6
+ key: "sharepoint-updated-file-instant",
7
+ name: "File Updated or Deleted (Instant)",
8
+ description: "Emit a new event when specific files are updated or deleted in a SharePoint document library",
9
+ version: "0.0.2",
10
+ type: "source",
11
+ dedupe: "unique",
12
+ props: {
13
+ sharepoint,
14
+ db: "$.service.db",
15
+ http: {
16
+ type: "$.interface.http",
17
+ customResponse: true,
18
+ },
19
+ timer: {
20
+ label: "Subscription renewal schedule",
21
+ description:
22
+ "Microsoft Graph subscriptions expire after 30 days. " +
23
+ "This timer automatically renews the subscription. " +
24
+ "**You should not need to modify this schedule.**",
25
+ type: "$.interface.timer",
26
+ static: {
27
+ intervalSeconds: WEBHOOK_SUBSCRIPTION_RENEWAL_SECONDS,
28
+ },
29
+ hidden: true,
30
+ },
31
+ siteId: {
32
+ propDefinition: [
33
+ sharepoint,
34
+ "siteId",
35
+ ],
36
+ },
37
+ driveId: {
38
+ propDefinition: [
39
+ sharepoint,
40
+ "driveId",
41
+ (c) => ({
42
+ siteId: c.siteId,
43
+ }),
44
+ ],
45
+ },
46
+ folderId: {
47
+ propDefinition: [
48
+ sharepoint,
49
+ "folderId",
50
+ (c) => ({
51
+ siteId: c.siteId,
52
+ driveId: c.driveId,
53
+ }),
54
+ ],
55
+ description: "Optional: Select a folder to browse files from. Leave empty to browse from the drive root. " +
56
+ "This helps you navigate to the files you want to monitor.",
57
+ },
58
+ fileIds: {
59
+ propDefinition: [
60
+ sharepoint,
61
+ "fileIds",
62
+ (c) => ({
63
+ siteId: c.siteId,
64
+ driveId: c.driveId,
65
+ folderId: c.folderId,
66
+ }),
67
+ ],
68
+ label: "Files to Monitor",
69
+ description:
70
+ "Select one or more files to monitor for changes. " +
71
+ "You'll receive a real-time event whenever any of these files are modified or deleted.\n\n" +
72
+ "**Important:** Only the selected files will trigger events. Changes to other files in the drive will be ignored. " +
73
+ "This ensures you only receive notifications for the documents you care about.",
74
+ },
75
+ },
76
+ hooks: {
77
+ async activate() {
78
+ // Generate a unique client state for validating incoming webhooks
79
+ const clientState = randomUUID();
80
+
81
+ // Resolve wrapped prop values
82
+ const driveId = this.sharepoint.resolveWrappedValue(this.driveId);
83
+
84
+ // Create subscription on the drive root
85
+ // We'll filter to specific files when notifications arrive
86
+ const subscription = await this.sharepoint.createSubscription({
87
+ resource: `drives/${driveId}/root`,
88
+ notificationUrl: this.http.endpoint,
89
+ changeType: "updated",
90
+ clientState,
91
+ });
92
+
93
+ console.log(
94
+ `Created subscription ${subscription.id}, expires: ${subscription.expirationDateTime}`,
95
+ );
96
+
97
+ // Store subscription metadata
98
+ this._setSubscription({
99
+ id: subscription.id,
100
+ expirationDateTime: subscription.expirationDateTime,
101
+ clientState,
102
+ });
103
+
104
+ // Store the file IDs we're monitoring (unwrap labeled values and parse JSON)
105
+ const wrappedFileIds = this.sharepoint.resolveWrappedArrayValues(this.fileIds);
106
+ // Parse JSON strings to extract just the IDs, handle objects, and trim strings
107
+ const fileIds = wrappedFileIds.map((fileId) => {
108
+ // Handle object values directly
109
+ if (typeof fileId === "object" && fileId !== null) {
110
+ if (!fileId.id) {
111
+ console.log(`Warning: Object fileId missing 'id' field: ${JSON.stringify(fileId)}`);
112
+ return String(fileId);
113
+ }
114
+ return fileId.id;
115
+ }
116
+
117
+ // Handle string values - trim whitespace first
118
+ if (typeof fileId === "string") {
119
+ const trimmedFileId = fileId.trim();
120
+ if (trimmedFileId.startsWith("{")) {
121
+ try {
122
+ const parsed = JSON.parse(trimmedFileId);
123
+ if (!parsed || !parsed.id) {
124
+ throw new Error("Parsed object missing 'id' field");
125
+ }
126
+ return parsed.id;
127
+ } catch (e) {
128
+ console.log(`Warning: Failed to parse fileId: ${trimmedFileId}, error: ${e.message}`);
129
+ return trimmedFileId;
130
+ }
131
+ }
132
+ return trimmedFileId;
133
+ }
134
+
135
+ return fileId;
136
+ });
137
+ this._setMonitoredFileIds(fileIds);
138
+
139
+ // Initialize delta tracking - get current state so we only see future changes
140
+ const deltaResponse = await this.sharepoint.getDriveDelta({
141
+ driveId,
142
+ });
143
+ // Follow pagination to get the final deltaLink
144
+ let nextLink = deltaResponse["@odata.nextLink"];
145
+ let deltaLink = deltaResponse["@odata.deltaLink"];
146
+ while (nextLink && !deltaLink) {
147
+ const nextResponse = await this.sharepoint.getDriveDelta({
148
+ driveId,
149
+ deltaLink: nextLink,
150
+ });
151
+ nextLink = nextResponse["@odata.nextLink"];
152
+ deltaLink = nextResponse["@odata.deltaLink"];
153
+ }
154
+ this._setDeltaLink(deltaLink);
155
+ console.log("Initialized delta tracking");
156
+ },
157
+ async deactivate() {
158
+ const subscription = this._getSubscription();
159
+ if (subscription?.id) {
160
+ try {
161
+ await this.sharepoint.deleteSubscription({
162
+ subscriptionId: subscription.id,
163
+ });
164
+ console.log(`Deleted subscription ${subscription.id}`);
165
+ } catch (err) {
166
+ console.log(`Error deleting subscription: ${err.message}`);
167
+ }
168
+ }
169
+
170
+ // Clear stored state
171
+ this._setSubscription(null);
172
+ this._setMonitoredFileIds(null);
173
+ this._setDeltaLink(null);
174
+ },
175
+ },
176
+ methods: {
177
+ _getSubscription() {
178
+ return this.db.get("subscription");
179
+ },
180
+ _setSubscription(subscription) {
181
+ this.db.set("subscription", subscription);
182
+ },
183
+ _getMonitoredFileIds() {
184
+ return this.db.get("monitoredFileIds") || [];
185
+ },
186
+ _setMonitoredFileIds(fileIds) {
187
+ this.db.set("monitoredFileIds", fileIds);
188
+ },
189
+ _getDeltaLink() {
190
+ return this.db.get("deltaLink");
191
+ },
192
+ _setDeltaLink(deltaLink) {
193
+ this.db.set("deltaLink", deltaLink);
194
+ },
195
+ async renewSubscription() {
196
+ const subscription = this._getSubscription();
197
+ if (!subscription?.id) {
198
+ console.log("No subscription to renew");
199
+ return {
200
+ success: true,
201
+ skipped: true,
202
+ };
203
+ }
204
+
205
+ try {
206
+ const updated = await this.sharepoint.updateSubscription({
207
+ subscriptionId: subscription.id,
208
+ });
209
+
210
+ this._setSubscription({
211
+ ...subscription,
212
+ expirationDateTime: updated.expirationDateTime,
213
+ });
214
+
215
+ console.log(
216
+ `Renewed subscription ${subscription.id}, expires: ${updated.expirationDateTime}`,
217
+ );
218
+ return {
219
+ success: true,
220
+ };
221
+ } catch (error) {
222
+ const status = error.response?.status;
223
+
224
+ // Subscription not found - needs recreation
225
+ if (status === 404) {
226
+ console.log("Subscription not found, will recreate...");
227
+ return {
228
+ success: false,
229
+ shouldRecreate: true,
230
+ };
231
+ }
232
+
233
+ // Auth errors - don't retry
234
+ if ([
235
+ 401,
236
+ 403,
237
+ ].includes(status)) {
238
+ console.error(`Auth error renewing subscription: ${error.message}`);
239
+ return {
240
+ success: false,
241
+ shouldRecreate: false,
242
+ };
243
+ }
244
+
245
+ // Other errors - log but don't recreate (will retry on next timer tick)
246
+ console.error(`Error renewing subscription: ${error.message}`);
247
+ return {
248
+ success: false,
249
+ shouldRecreate: false,
250
+ };
251
+ }
252
+ },
253
+ generateMeta(file) {
254
+ // Use lastModifiedDateTime for updated files, deletedDateTime for deleted files
255
+ // Fall back to current time only if neither is available
256
+ let ts;
257
+ if (file.lastModifiedDateTime) {
258
+ ts = Date.parse(file.lastModifiedDateTime);
259
+ } else if (file.deletedDateTime) {
260
+ ts = Date.parse(file.deletedDateTime);
261
+ } else {
262
+ ts = Date.now();
263
+ }
264
+
265
+ const action = file.deleted
266
+ ? "deleted"
267
+ : "updated";
268
+ return {
269
+ id: `${file.id}-${ts}`,
270
+ summary: `File ${action}: ${file.name}`,
271
+ ts,
272
+ };
273
+ },
274
+ },
275
+ async run(event) {
276
+ // Handle subscription validation request from Microsoft
277
+ // https://learn.microsoft.com/en-us/graph/change-notifications-delivery-webhooks#notificationurl-validation
278
+ if (event.query?.validationToken) {
279
+ console.log("Responding to validation request");
280
+ this.http.respond({
281
+ status: 200,
282
+ headers: {
283
+ "Content-Type": "text/plain",
284
+ },
285
+ body: event.query.validationToken,
286
+ });
287
+ return;
288
+ }
289
+
290
+ // Handle timer event - renew subscription
291
+ if (event.timestamp) {
292
+ const result = await this.renewSubscription();
293
+
294
+ if (!result.success && result.shouldRecreate) {
295
+ console.log("Recreating subscription...");
296
+ await this.hooks.activate.call(this);
297
+ }
298
+ return;
299
+ }
300
+
301
+ // Handle webhook notification
302
+ const { body } = event;
303
+
304
+ if (!body?.value?.length) {
305
+ console.log("No notifications in webhook payload");
306
+ this.http.respond({
307
+ status: 202,
308
+ body: "",
309
+ });
310
+ return;
311
+ }
312
+
313
+ // Filter to only valid notifications for this subscription
314
+ const subscription = this._getSubscription();
315
+ const clientState = subscription?.clientState;
316
+
317
+ const validNotifications = body.value.filter((notification) => {
318
+ if (notification.clientState !== clientState) {
319
+ console.log(
320
+ `Warning: Ignoring notification with unexpected clientState: ${notification.clientState}`,
321
+ );
322
+ return false;
323
+ }
324
+ return true;
325
+ });
326
+
327
+ if (validNotifications.length === 0) {
328
+ console.log("No valid notifications after clientState filtering");
329
+ this.http.respond({
330
+ status: 202,
331
+ body: "",
332
+ });
333
+ return;
334
+ }
335
+
336
+ // Acknowledge receipt after validation
337
+ this.http.respond({
338
+ status: 202,
339
+ body: "",
340
+ });
341
+
342
+ // Use delta API to find what actually changed
343
+ const driveId = this.sharepoint.resolveWrappedValue(this.driveId);
344
+ const monitoredFileIds = this._getMonitoredFileIds();
345
+ let deltaLink = this._getDeltaLink();
346
+
347
+ console.log("Monitored file IDs:", JSON.stringify(monitoredFileIds));
348
+ console.log("Fetching delta changes...");
349
+
350
+ const changedFiles = [];
351
+ let hasMore = true;
352
+
353
+ while (hasMore) {
354
+ const deltaResponse = await this.sharepoint.getDriveDelta({
355
+ driveId,
356
+ deltaLink,
357
+ });
358
+
359
+ // Find files that changed and are in our monitored list
360
+ // Include both updated files (item.file) and deleted files (item.deleted)
361
+ for (const item of deltaResponse.value || []) {
362
+ if ((item.file || item.deleted) && monitoredFileIds.includes(item.id)) {
363
+ changedFiles.push(item);
364
+ }
365
+ }
366
+
367
+ // Update for next iteration or final storage
368
+ if (deltaResponse["@odata.nextLink"]) {
369
+ deltaLink = deltaResponse["@odata.nextLink"];
370
+ } else {
371
+ deltaLink = deltaResponse["@odata.deltaLink"];
372
+ hasMore = false;
373
+ }
374
+ }
375
+
376
+ // Store the new deltaLink for next time
377
+ this._setDeltaLink(deltaLink);
378
+
379
+ console.log(`Found ${changedFiles.length} changed monitored files`);
380
+
381
+ // Emit events for each changed file
382
+ for (const file of changedFiles) {
383
+ // Delta response may not include downloadUrl - fetch fresh if needed
384
+ // Skip fetching download URL for deleted files (they no longer exist)
385
+ let downloadUrl = file["@microsoft.graph.downloadUrl"];
386
+ if (!downloadUrl && !file.deleted) {
387
+ try {
388
+ const freshFile = await this.sharepoint.getDriveItem({
389
+ driveId,
390
+ fileId: file.id,
391
+ });
392
+ downloadUrl = freshFile["@microsoft.graph.downloadUrl"];
393
+ } catch (err) {
394
+ console.log(`Could not fetch download URL for ${file.name}: ${err.message}`);
395
+ }
396
+ }
397
+
398
+ this.$emit(
399
+ {
400
+ file,
401
+ downloadUrl,
402
+ },
403
+ this.generateMeta(file),
404
+ );
405
+ }
406
+ },
407
+ };
@@ -5,7 +5,7 @@ export default {
5
5
  key: "sharepoint-updated-list-item",
6
6
  name: "Updated List Item",
7
7
  description: "Emit new event when a list item is updated in Microsoft Sharepoint.",
8
- version: "0.0.9",
8
+ version: "0.0.10",
9
9
  type: "source",
10
10
  dedupe: "unique",
11
11
  props: {
@@ -1,198 +0,0 @@
1
- import sharepoint from "../../sharepoint.app.mjs";
2
-
3
- export default {
4
- key: "sharepoint-select-files",
5
- name: "Select Files",
6
- description: "A file picker action that allows browsing and selecting one or more files from SharePoint. Returns the selected files' metadata including pre-authenticated download URLs. [See the documentation](https://learn.microsoft.com/en-us/graph/api/driveitem-get)",
7
- version: "0.0.3",
8
- type: "action",
9
- annotations: {
10
- destructiveHint: false,
11
- openWorldHint: true,
12
- readOnlyHint: true,
13
- },
14
- props: {
15
- sharepoint,
16
- siteId: {
17
- propDefinition: [
18
- sharepoint,
19
- "siteId",
20
- ],
21
- withLabel: true,
22
- },
23
- driveId: {
24
- propDefinition: [
25
- sharepoint,
26
- "driveId",
27
- (c) => ({
28
- siteId: c.siteId?.value || c.siteId,
29
- }),
30
- ],
31
- withLabel: true,
32
- },
33
- folderId: {
34
- propDefinition: [
35
- sharepoint,
36
- "folderId",
37
- (c) => ({
38
- siteId: c.siteId?.value || c.siteId,
39
- driveId: c.driveId?.value || c.driveId,
40
- }),
41
- ],
42
- label: "Folder",
43
- description: "The folder to browse. Leave empty to browse the root of the drive.",
44
- optional: true,
45
- withLabel: true,
46
- },
47
- fileOrFolderIds: {
48
- propDefinition: [
49
- sharepoint,
50
- "fileOrFolderId",
51
- (c) => ({
52
- siteId: c.siteId?.value || c.siteId,
53
- driveId: c.driveId?.value || c.driveId,
54
- folderId: c.folderId?.value || c.folderId,
55
- }),
56
- ],
57
- type: "string[]",
58
- label: "Files or Folders",
59
- description: "Select one or more files, or select a folder and click 'Refresh Fields' to browse into it",
60
- withLabel: true,
61
- },
62
- },
63
- methods: {
64
- resolveValue(prop) {
65
- if (!prop) return null;
66
- if (typeof prop === "object" && prop?.value) {
67
- return prop.value;
68
- }
69
- return prop;
70
- },
71
- parseFileOrFolder(value) {
72
- if (!value) return null;
73
- const resolved = this.resolveValue(value);
74
- try {
75
- return JSON.parse(resolved);
76
- } catch {
77
- return {
78
- id: resolved,
79
- isFolder: false,
80
- };
81
- }
82
- },
83
- parseFileOrFolderList(values) {
84
- if (!values) return [];
85
- const list = Array.isArray(values)
86
- ? values
87
- : [
88
- values,
89
- ];
90
- return list.map((v) => this.parseFileOrFolder(v)).filter(Boolean);
91
- },
92
- },
93
- async run({ $ }) {
94
- const selections = this.parseFileOrFolderList(this.fileOrFolderIds);
95
-
96
- if (selections.length === 0) {
97
- throw new Error("Please select at least one file or folder");
98
- }
99
-
100
- const siteId = this.resolveValue(this.siteId);
101
- const driveId = this.resolveValue(this.driveId);
102
-
103
- // Separate files and folders
104
- const folders = selections.filter((s) => s.isFolder);
105
- const files = selections.filter((s) => !s.isFolder);
106
-
107
- // If only folders selected, return folder info
108
- if (files.length === 0 && folders.length > 0) {
109
- const folderNames = folders.map((f) => f.name).join(", ");
110
- $.export("$summary", `Selected ${folders.length} folder(s): ${folderNames}. Set one as the Folder ID and refresh to browse its contents.`);
111
- return {
112
- type: "folders",
113
- folders: folders.map((f) => ({
114
- id: f.id,
115
- name: f.name,
116
- })),
117
- message: "To browse a folder, set it as the folderId and reload props",
118
- };
119
- }
120
-
121
- // Fetch metadata for all selected files in parallel, handling individual failures
122
- const settledResults = await Promise.allSettled(
123
- files.map(async (selected) => {
124
- const file = await this.sharepoint.getDriveItem({
125
- $,
126
- siteId,
127
- driveId,
128
- fileId: selected.id,
129
- });
130
-
131
- const downloadUrl = file["@microsoft.graph.downloadUrl"];
132
-
133
- return {
134
- ...file,
135
- downloadUrl,
136
- _meta: {
137
- siteId,
138
- driveId,
139
- fileId: selected.id,
140
- },
141
- };
142
- }),
143
- );
144
-
145
- // Separate successful and failed results
146
- const fileResults = [];
147
- const errors = [];
148
-
149
- settledResults.forEach((result, index) => {
150
- if (result.status === "fulfilled") {
151
- fileResults.push(result.value);
152
- } else {
153
- const selected = files[index];
154
- const errorMessage = result.reason?.message || String(result.reason);
155
- console.error(`Failed to fetch file ${selected.id} (${selected.name}): ${errorMessage}`);
156
- errors.push({
157
- fileId: selected.id,
158
- fileName: selected.name,
159
- error: errorMessage,
160
- });
161
- }
162
- });
163
-
164
- // If all files failed, throw an error
165
- if (fileResults.length === 0 && errors.length > 0) {
166
- throw new Error(`Failed to fetch all selected files: ${errors.map((e) => e.fileName).join(", ")}`);
167
- }
168
-
169
- // If single file, return it directly for backwards compatibility
170
- if (fileResults.length === 1 && folders.length === 0 && errors.length === 0) {
171
- $.export("$summary", `Selected file: ${fileResults[0].name}`);
172
- return fileResults[0];
173
- }
174
-
175
- // Multiple files: return as array
176
- const fileNames = fileResults.map((f) => f.name).join(", ");
177
- const summaryParts = [
178
- `Selected ${fileResults.length} file(s): ${fileNames}`,
179
- ];
180
- if (errors.length > 0) {
181
- summaryParts.push(`Failed to fetch ${errors.length} file(s): ${errors.map((e) => e.fileName).join(", ")}`);
182
- }
183
- $.export("$summary", summaryParts.join(". "));
184
-
185
- return {
186
- files: fileResults,
187
- ...(errors.length > 0 && {
188
- errors,
189
- }),
190
- ...(folders.length > 0 && {
191
- folders: folders.map((f) => ({
192
- id: f.id,
193
- name: f.name,
194
- })),
195
- }),
196
- };
197
- },
198
- };