@pipedream/sharepoint 0.7.1 → 0.8.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.
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 +93 -7
  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 +70 -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 +361 -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,361 @@
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: "New File Updated (Instant)",
8
+ description: "Emit new event when specific files are updated in a SharePoint document library",
9
+ version: "0.0.1",
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 updates. " +
71
+ "You'll receive a real-time event whenever any of these files are modified.\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)
105
+ const fileIds = this.sharepoint.resolveWrappedArrayValues(this.fileIds);
106
+ this._setMonitoredFileIds(fileIds);
107
+
108
+ // Initialize delta tracking - get current state so we only see future changes
109
+ const deltaResponse = await this.sharepoint.getDriveDelta({
110
+ driveId,
111
+ });
112
+ // Follow pagination to get the final deltaLink
113
+ let nextLink = deltaResponse["@odata.nextLink"];
114
+ let deltaLink = deltaResponse["@odata.deltaLink"];
115
+ while (nextLink && !deltaLink) {
116
+ const nextResponse = await this.sharepoint.getDriveDelta({
117
+ driveId,
118
+ deltaLink: nextLink,
119
+ });
120
+ nextLink = nextResponse["@odata.nextLink"];
121
+ deltaLink = nextResponse["@odata.deltaLink"];
122
+ }
123
+ this._setDeltaLink(deltaLink);
124
+ console.log("Initialized delta tracking");
125
+ },
126
+ async deactivate() {
127
+ const subscription = this._getSubscription();
128
+ if (subscription?.id) {
129
+ try {
130
+ await this.sharepoint.deleteSubscription({
131
+ subscriptionId: subscription.id,
132
+ });
133
+ console.log(`Deleted subscription ${subscription.id}`);
134
+ } catch (err) {
135
+ console.log(`Error deleting subscription: ${err.message}`);
136
+ }
137
+ }
138
+
139
+ // Clear stored state
140
+ this._setSubscription(null);
141
+ this._setMonitoredFileIds(null);
142
+ this._setDeltaLink(null);
143
+ },
144
+ },
145
+ methods: {
146
+ _getSubscription() {
147
+ return this.db.get("subscription");
148
+ },
149
+ _setSubscription(subscription) {
150
+ this.db.set("subscription", subscription);
151
+ },
152
+ _getMonitoredFileIds() {
153
+ return this.db.get("monitoredFileIds") || [];
154
+ },
155
+ _setMonitoredFileIds(fileIds) {
156
+ this.db.set("monitoredFileIds", fileIds);
157
+ },
158
+ _getDeltaLink() {
159
+ return this.db.get("deltaLink");
160
+ },
161
+ _setDeltaLink(deltaLink) {
162
+ this.db.set("deltaLink", deltaLink);
163
+ },
164
+ async renewSubscription() {
165
+ const subscription = this._getSubscription();
166
+ if (!subscription?.id) {
167
+ console.log("No subscription to renew");
168
+ return {
169
+ success: true,
170
+ skipped: true,
171
+ };
172
+ }
173
+
174
+ try {
175
+ const updated = await this.sharepoint.updateSubscription({
176
+ subscriptionId: subscription.id,
177
+ });
178
+
179
+ this._setSubscription({
180
+ ...subscription,
181
+ expirationDateTime: updated.expirationDateTime,
182
+ });
183
+
184
+ console.log(
185
+ `Renewed subscription ${subscription.id}, expires: ${updated.expirationDateTime}`,
186
+ );
187
+ return {
188
+ success: true,
189
+ };
190
+ } catch (error) {
191
+ const status = error.response?.status;
192
+
193
+ // Subscription not found - needs recreation
194
+ if (status === 404) {
195
+ console.log("Subscription not found, will recreate...");
196
+ return {
197
+ success: false,
198
+ shouldRecreate: true,
199
+ };
200
+ }
201
+
202
+ // Auth errors - don't retry
203
+ if ([
204
+ 401,
205
+ 403,
206
+ ].includes(status)) {
207
+ console.error(`Auth error renewing subscription: ${error.message}`);
208
+ return {
209
+ success: false,
210
+ shouldRecreate: false,
211
+ };
212
+ }
213
+
214
+ // Other errors - log but don't recreate (will retry on next timer tick)
215
+ console.error(`Error renewing subscription: ${error.message}`);
216
+ return {
217
+ success: false,
218
+ shouldRecreate: false,
219
+ };
220
+ }
221
+ },
222
+ generateMeta(file) {
223
+ const ts = Date.parse(file.lastModifiedDateTime);
224
+ return {
225
+ id: `${file.id}-${ts}`,
226
+ summary: `File updated: ${file.name}`,
227
+ ts,
228
+ };
229
+ },
230
+ },
231
+ async run(event) {
232
+ // Handle subscription validation request from Microsoft
233
+ // https://learn.microsoft.com/en-us/graph/change-notifications-delivery-webhooks#notificationurl-validation
234
+ if (event.query?.validationToken) {
235
+ console.log("Responding to validation request");
236
+ this.http.respond({
237
+ status: 200,
238
+ headers: {
239
+ "Content-Type": "text/plain",
240
+ },
241
+ body: event.query.validationToken,
242
+ });
243
+ return;
244
+ }
245
+
246
+ // Handle timer event - renew subscription
247
+ if (event.timestamp) {
248
+ const result = await this.renewSubscription();
249
+
250
+ if (!result.success && result.shouldRecreate) {
251
+ console.log("Recreating subscription...");
252
+ await this.hooks.activate.call(this);
253
+ }
254
+ return;
255
+ }
256
+
257
+ // Handle webhook notification
258
+ const { body } = event;
259
+
260
+ if (!body?.value?.length) {
261
+ console.log("No notifications in webhook payload");
262
+ this.http.respond({
263
+ status: 202,
264
+ body: "",
265
+ });
266
+ return;
267
+ }
268
+
269
+ // Filter to only valid notifications for this subscription
270
+ const subscription = this._getSubscription();
271
+ const clientState = subscription?.clientState;
272
+
273
+ const validNotifications = body.value.filter((notification) => {
274
+ if (notification.clientState !== clientState) {
275
+ console.warn(
276
+ `Ignoring notification with unexpected clientState: ${notification.clientState}`,
277
+ );
278
+ return false;
279
+ }
280
+ return true;
281
+ });
282
+
283
+ if (validNotifications.length === 0) {
284
+ console.log("No valid notifications after clientState filtering");
285
+ this.http.respond({
286
+ status: 202,
287
+ body: "",
288
+ });
289
+ return;
290
+ }
291
+
292
+ // Acknowledge receipt after validation
293
+ this.http.respond({
294
+ status: 202,
295
+ body: "",
296
+ });
297
+
298
+ // Use delta API to find what actually changed
299
+ const driveId = this.sharepoint.resolveWrappedValue(this.driveId);
300
+ const monitoredFileIds = this._getMonitoredFileIds();
301
+ let deltaLink = this._getDeltaLink();
302
+
303
+ console.log("Monitored file IDs:", JSON.stringify(monitoredFileIds));
304
+ console.log("Fetching delta changes...");
305
+
306
+ const changedFiles = [];
307
+ let hasMore = true;
308
+
309
+ while (hasMore) {
310
+ const deltaResponse = await this.sharepoint.getDriveDelta({
311
+ driveId,
312
+ deltaLink,
313
+ });
314
+
315
+ // Find files that changed and are in our monitored list
316
+ for (const item of deltaResponse.value || []) {
317
+ if (item.file && monitoredFileIds.includes(item.id)) {
318
+ changedFiles.push(item);
319
+ }
320
+ }
321
+
322
+ // Update for next iteration or final storage
323
+ if (deltaResponse["@odata.nextLink"]) {
324
+ deltaLink = deltaResponse["@odata.nextLink"];
325
+ } else {
326
+ deltaLink = deltaResponse["@odata.deltaLink"];
327
+ hasMore = false;
328
+ }
329
+ }
330
+
331
+ // Store the new deltaLink for next time
332
+ this._setDeltaLink(deltaLink);
333
+
334
+ console.log(`Found ${changedFiles.length} changed monitored files`);
335
+
336
+ // Emit events for each changed file
337
+ for (const file of changedFiles) {
338
+ // Delta response may not include downloadUrl - fetch fresh if needed
339
+ let downloadUrl = file["@microsoft.graph.downloadUrl"];
340
+ if (!downloadUrl) {
341
+ try {
342
+ const freshFile = await this.sharepoint.getDriveItem({
343
+ driveId,
344
+ fileId: file.id,
345
+ });
346
+ downloadUrl = freshFile["@microsoft.graph.downloadUrl"];
347
+ } catch (err) {
348
+ console.log(`Could not fetch download URL for ${file.name}: ${err.message}`);
349
+ }
350
+ }
351
+
352
+ this.$emit(
353
+ {
354
+ file,
355
+ downloadUrl,
356
+ },
357
+ this.generateMeta(file),
358
+ );
359
+ }
360
+ },
361
+ };
@@ -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
- };