@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
@@ -1,4 +1,8 @@
1
1
  import { axios } from "@pipedream/platform";
2
+ import { Client } from "@microsoft/microsoft-graph-client";
3
+ import retry from "async-retry";
4
+ import "isomorphic-fetch";
5
+ import { WEBHOOK_SUBSCRIPTION_EXPIRATION_TIME_MILLISECONDS } from "./common/constants.mjs";
2
6
 
3
7
  export default {
4
8
  type: "app",
@@ -136,6 +140,7 @@ export default {
136
140
  if (!siteId) {
137
141
  return [];
138
142
  }
143
+ siteId = this.resolveWrappedValue(siteId);
139
144
  const args = {
140
145
  siteId,
141
146
  };
@@ -235,6 +240,41 @@ export default {
235
240
  }));
236
241
  },
237
242
  },
243
+ fileIds: {
244
+ type: "string[]",
245
+ label: "Files",
246
+ description: "Select files or enter custom file IDs.",
247
+ async options({
248
+ siteId, driveId, folderId,
249
+ }) {
250
+ const resolvedSiteId = this.resolveWrappedValue(siteId);
251
+ const resolvedDriveId = this.resolveWrappedValue(driveId);
252
+ const resolvedFolderId = this.resolveWrappedValue(folderId);
253
+
254
+ if (!resolvedSiteId || !resolvedDriveId) {
255
+ return [];
256
+ }
257
+
258
+ const response = resolvedFolderId
259
+ ? await this.listDriveItemsInFolder({
260
+ driveId: resolvedDriveId,
261
+ folderId: resolvedFolderId,
262
+ })
263
+ : await this.listDriveItems({
264
+ siteId: resolvedSiteId,
265
+ driveId: resolvedDriveId,
266
+ });
267
+
268
+ return response.value
269
+ ?.filter(({ folder }) => !folder)
270
+ .map(({
271
+ id, name,
272
+ }) => ({
273
+ label: name,
274
+ value: id,
275
+ })) || [];
276
+ },
277
+ },
238
278
  excelFileId: {
239
279
  type: "string",
240
280
  label: "Spreadsheet",
@@ -320,13 +360,15 @@ export default {
320
360
  driveId: resolvedDriveId,
321
361
  });
322
362
  return response.value?.map(({
323
- id, name, folder, size,
363
+ id, name, folder, size, lastModifiedDateTime,
324
364
  }) => ({
325
365
  value: JSON.stringify({
326
366
  id,
327
367
  name,
328
368
  isFolder: !!folder,
329
369
  size,
370
+ childCount: folder?.childCount,
371
+ lastModifiedDateTime,
330
372
  }),
331
373
  label: folder
332
374
  ? `📁 ${name}`
@@ -336,12 +378,125 @@ export default {
336
378
  },
337
379
  },
338
380
  methods: {
381
+ _graphClient: null,
382
+ /**
383
+ * Resolves a potentially wrapped labeled value to its actual value.
384
+ * Pipedream props with withLabel: true wrap values in a special format.
385
+ *
386
+ * @param {*} value - The value to resolve (may be wrapped or plain)
387
+ * @returns {*} The unwrapped value, or the original value if not wrapped
388
+ * @example
389
+ * // Wrapped labeled value
390
+ * resolveWrappedValue({ __lv: { label: "My Site", value: "abc123" } }) // "abc123"
391
+ *
392
+ * // Plain value (not wrapped)
393
+ * resolveWrappedValue("abc123") // "abc123"
394
+ */
339
395
  resolveWrappedValue(value) {
340
- return value?.__lv?.value || value;
396
+ return value?.value || value;
397
+ },
398
+ /**
399
+ * Resolves an array of potentially wrapped labeled values.
400
+ * Handles both Pipedream's labeled value array format and plain arrays.
401
+ *
402
+ * @param {Array|Object} arr
403
+ * Array to resolve (may be wrapped or plain)
404
+ * @returns {Array} Array of unwrapped values
405
+ * @example
406
+ * // Wrapped array of labeled values
407
+ * resolveWrappedArrayValues({
408
+ * __lv: [
409
+ * { label: "File 1", value: "id1" },
410
+ * { label: "File 2", value: "id2" }
411
+ * ]
412
+ * }) // ["id1", "id2"]
413
+ *
414
+ * // Plain array
415
+ * resolveWrappedArrayValues(["id1", "id2"]) // ["id1", "id2"]
416
+ */
417
+ resolveWrappedArrayValues(arr) {
418
+ if (!arr) return [];
419
+ // Handle __lv wrapped array
420
+ const unwrapped = arr?.__lv || arr;
421
+ if (!Array.isArray(unwrapped)) return [];
422
+ // Extract value from each item if it's a labeled value object
423
+ return unwrapped.map((item) => item?.value ?? item);
341
424
  },
342
425
  _getAccessToken() {
343
426
  return this.$auth.oauth_access_token;
344
427
  },
428
+ /**
429
+ * Creates a Microsoft Graph SDK client with caching.
430
+ * This provides better consistency with other Microsoft components
431
+ * and includes built-in pagination and type safety.
432
+ */
433
+ client() {
434
+ if (!this._graphClient) {
435
+ this._graphClient = Client.initWithMiddleware({
436
+ authProvider: {
437
+ getAccessToken: () => Promise.resolve(this._getAccessToken()),
438
+ },
439
+ });
440
+ }
441
+ return this._graphClient;
442
+ },
443
+ /**
444
+ * Makes a request to Microsoft Graph API with automatic retry logic.
445
+ * Retries on transient errors (5xx, 429) but not on auth/client
446
+ * errors (4xx).
447
+ *
448
+ * @param {Object} options - Request options
449
+ * @param {string} options.path - API path (e.g., "/sites/{siteId}")
450
+ * @param {string} [options.method="GET"] - HTTP method
451
+ * @param {*} [options.content] - Request body for POST/PATCH
452
+ * @param {number} [options.retries=3] - Number of retry attempts
453
+ * @returns {Promise<*>} API response
454
+ */
455
+ async graphRequest({
456
+ path, method = "GET", content, retries = 3,
457
+ }) {
458
+ return retry(
459
+ async (bail) => {
460
+ try {
461
+ const api = this.client().api(path);
462
+
463
+ switch (method.toUpperCase()) {
464
+ case "GET":
465
+ return await api.get();
466
+ case "POST":
467
+ return await api.post(content);
468
+ case "PATCH":
469
+ return await api.patch(content);
470
+ case "DELETE":
471
+ return await api.delete();
472
+ default:
473
+ throw new Error(`Unsupported HTTP method: ${method}`);
474
+ }
475
+ } catch (error) {
476
+ // Don't retry on auth errors or client errors (4xx except 429)
477
+ const status = error.statusCode || error.response?.status;
478
+ if ([
479
+ 400,
480
+ 401,
481
+ 403,
482
+ 404,
483
+ ].includes(status)) {
484
+ bail(error); // Throw immediately, don't retry
485
+ return;
486
+ }
487
+ throw error; // Retry on other errors
488
+ }
489
+ },
490
+ {
491
+ retries,
492
+ minTimeout: 1000,
493
+ maxTimeout: 10000,
494
+ onRetry: (error, attempt) => {
495
+ console.log(`Retry attempt ${attempt} after error: ${error.message}`);
496
+ },
497
+ },
498
+ );
499
+ },
345
500
  _baseUrl() {
346
501
  return "https://graph.microsoft.com/v1.0";
347
502
  },
@@ -354,15 +509,52 @@ export default {
354
509
  _makeRequest({
355
510
  $ = this,
356
511
  path,
512
+ url,
357
513
  headers,
358
514
  ...args
359
515
  }) {
360
516
  return axios($, {
361
- url: `${this._baseUrl()}${path}`,
517
+ url: url || `${this._baseUrl()}${path}`,
362
518
  headers: this._headers(headers),
363
519
  ...args,
364
520
  });
365
521
  },
522
+ /**
523
+ * Makes a request to SharePoint REST API (not Microsoft Graph).
524
+ * Use this for SharePoint-specific features not available in Graph API,
525
+ * such as native SharePoint site groups.
526
+ *
527
+ * @param {Object} options - Request options
528
+ * @param {string} options.siteWebUrl - Full site URL (e.g., "https://tenant.sharepoint.com/sites/MySite")
529
+ * @param {string} options.path - REST API path (e.g., "/web/sitegroups")
530
+ * @param {Object} [options.headers] - Additional headers
531
+ * @returns {Promise<Object>} API response
532
+ * @example
533
+ * // Get SharePoint site groups
534
+ * await this._makeSharePointRestRequest({
535
+ * siteWebUrl: "https://contoso.sharepoint.com/sites/TeamSite",
536
+ * path: "/web/sitegroups"
537
+ * })
538
+ */
539
+ _makeSharePointRestRequest({
540
+ $ = this,
541
+ siteWebUrl,
542
+ path,
543
+ headers,
544
+ ...args
545
+ }) {
546
+ // SharePoint REST API uses the site's webUrl as base
547
+ // e.g., https://tenant.sharepoint.com/sites/MySite/_api/web/sitegroups
548
+ const baseUrl = siteWebUrl.replace(/\/$/, ""); // Remove trailing slash if present
549
+ return axios($, {
550
+ url: `${baseUrl}/_api${path}`,
551
+ headers: this._headers({
552
+ Accept: "application/json;odata=verbose",
553
+ ...headers,
554
+ }),
555
+ ...args,
556
+ });
557
+ },
366
558
  getSite({
367
559
  siteId, ...args
368
560
  }) {
@@ -371,6 +563,52 @@ export default {
371
563
  ...args,
372
564
  });
373
565
  },
566
+ /**
567
+ * Lists SharePoint-native site groups using the SharePoint REST API.
568
+ * These are different from Microsoft 365 Groups and can only be accessed via REST API.
569
+ *
570
+ * @param {Object} options - Request options
571
+ * @param {string} options.siteWebUrl - Full site URL
572
+ * @returns {Promise<Object>} Response with groups in d.results array
573
+ * @example
574
+ * const groups = await this.listSharePointSiteGroups({
575
+ * siteWebUrl: "https://contoso.sharepoint.com/sites/TeamSite"
576
+ * });
577
+ * // Returns: { d: { results: [{ Id: 5, Title: "Team Site Members" }, ...] } }
578
+ */
579
+ listSharePointSiteGroups({
580
+ siteWebUrl, ...args
581
+ }) {
582
+ return this._makeSharePointRestRequest({
583
+ siteWebUrl,
584
+ path: "/web/sitegroups",
585
+ ...args,
586
+ });
587
+ },
588
+ /**
589
+ * Gets members of a SharePoint-native site group using the SharePoint REST API.
590
+ * Returns user details including email, display name, and login name.
591
+ *
592
+ * @param {Object} options - Request options
593
+ * @param {string} options.siteWebUrl - Full site URL
594
+ * @param {number} options.groupId - SharePoint group ID
595
+ * @returns {Promise<Object>} Response with users in d.results array
596
+ * @example
597
+ * const members = await this.getSharePointSiteGroupMembers({
598
+ * siteWebUrl: "https://contoso.sharepoint.com/sites/TeamSite",
599
+ * groupId: 5
600
+ * });
601
+ * // Returns: { d: { results: [{ Email: "user@contoso.com", Title: "John Doe" }, ...] } }
602
+ */
603
+ getSharePointSiteGroupMembers({
604
+ siteWebUrl, groupId, ...args
605
+ }) {
606
+ return this._makeSharePointRestRequest({
607
+ siteWebUrl,
608
+ path: `/web/sitegroups/getbyid(${groupId})/users`,
609
+ ...args,
610
+ });
611
+ },
374
612
  listSites(args = {}) {
375
613
  return this._makeRequest({
376
614
  path: "/me/followedSites",
@@ -514,6 +752,14 @@ export default {
514
752
  ...args,
515
753
  });
516
754
  },
755
+ listDriveItemPermissions({
756
+ driveId, itemId, ...args
757
+ }) {
758
+ return this._makeRequest({
759
+ path: `/drives/${driveId}/items/${itemId}/permissions`,
760
+ ...args,
761
+ });
762
+ },
517
763
  searchDriveItems({
518
764
  siteId, query, ...args
519
765
  }) {
@@ -559,6 +805,62 @@ export default {
559
805
  ...args,
560
806
  });
561
807
  },
808
+ listGroups(args = {}) {
809
+ return this._makeRequest({
810
+ path: "/groups",
811
+ ...args,
812
+ });
813
+ },
814
+ listUsers(args = {}) {
815
+ return this._makeRequest({
816
+ path: "/users",
817
+ ...args,
818
+ });
819
+ },
820
+ listGroupMembers({
821
+ groupId, ...args
822
+ }) {
823
+ return this._makeRequest({
824
+ path: `/groups/${groupId}/members`,
825
+ ...args,
826
+ });
827
+ },
828
+ /**
829
+ * Get delta changes for a drive using Microsoft Graph delta query.
830
+ * Enables tracking changes to files and folders over time.
831
+ *
832
+ * @param {Object} options - Request options
833
+ * @param {string} options.driveId - Drive ID to track
834
+ * @param {string} [options.deltaLink]
835
+ * Delta link from previous response (for pagination/continuation)
836
+ * @returns {Promise<Object>} Response with changed items and
837
+ * @odata.deltaLink or @odata.nextLink
838
+ * @see https://learn.microsoft.com/en-us/graph/api/driveitem-delta
839
+ * @example
840
+ * // First call - get initial state
841
+ * const initial = await this.getDriveDelta({ driveId: "b!..." });
842
+ * const deltaLink = initial["@odata.deltaLink"];
843
+ *
844
+ * // Later - get changes since last call
845
+ * const changes = await this.getDriveDelta({
846
+ * driveId: "b!...", deltaLink
847
+ * });
848
+ */
849
+ getDriveDelta({
850
+ driveId, deltaLink, ...args
851
+ }) {
852
+ // If we have a deltaLink/nextLink, use it as full URL; otherwise build path
853
+ if (deltaLink) {
854
+ return this._makeRequest({
855
+ url: deltaLink,
856
+ ...args,
857
+ });
858
+ }
859
+ return this._makeRequest({
860
+ path: `/drives/${driveId}/root/delta`,
861
+ ...args,
862
+ });
863
+ },
562
864
  searchQuery(args = {}) {
563
865
  return this._makeRequest({
564
866
  method: "POST",
@@ -586,5 +888,100 @@ export default {
586
888
  nextLink = response["@odata.nextLink"];
587
889
  } while (nextLink);
588
890
  },
891
+ /**
892
+ * Creates a Microsoft Graph subscription for change notifications
893
+ * (webhooks). Subscriptions expire after a set time and must be
894
+ * renewed periodically.
895
+ *
896
+ * @param {Object} options - Subscription options
897
+ * @param {string} options.resource
898
+ * Resource to monitor (e.g., "drives/{driveId}/root")
899
+ * @param {string} options.notificationUrl - Webhook endpoint URL
900
+ * @param {string} [options.changeType="updated"]
901
+ * Type of change to monitor (updated, created, deleted)
902
+ * @param {string} options.clientState
903
+ * Secret string for webhook validation
904
+ * @returns {Promise<Object>}
905
+ * Created subscription with id and expirationDateTime
906
+ * @see https://learn.microsoft.com/en-us/graph/api/subscription-post-subscriptions
907
+ * @example
908
+ * const subscription = await this.createSubscription({
909
+ * resource: "drives/b!abc123.../root",
910
+ * notificationUrl: "https://webhook.site/unique-id",
911
+ * changeType: "updated",
912
+ * clientState: "secret-validation-token"
913
+ * });
914
+ */
915
+ createSubscription({
916
+ resource, notificationUrl, changeType = "updated", clientState, ...args
917
+ }) {
918
+ const expirationDateTime = new Date(
919
+ Date.now() + WEBHOOK_SUBSCRIPTION_EXPIRATION_TIME_MILLISECONDS,
920
+ ).toISOString();
921
+
922
+ return this._makeRequest({
923
+ method: "POST",
924
+ path: "/subscriptions",
925
+ data: {
926
+ changeType,
927
+ notificationUrl,
928
+ resource,
929
+ expirationDateTime,
930
+ clientState,
931
+ },
932
+ ...args,
933
+ });
934
+ },
935
+ /**
936
+ * Updates a subscription's expiration time (renewal).
937
+ * Subscriptions must be renewed before they expire to maintain continuous monitoring.
938
+ *
939
+ * @param {Object} options - Update options
940
+ * @param {string} options.subscriptionId - ID of subscription to renew
941
+ * @returns {Promise<Object>} Updated subscription with new expirationDateTime
942
+ * @see https://learn.microsoft.com/en-us/graph/api/subscription-update
943
+ * @example
944
+ * await this.updateSubscription({
945
+ * subscriptionId: "abc123-def456"
946
+ * });
947
+ */
948
+ updateSubscription({
949
+ subscriptionId, ...args
950
+ }) {
951
+ const expirationDateTime = new Date(
952
+ Date.now() + WEBHOOK_SUBSCRIPTION_EXPIRATION_TIME_MILLISECONDS,
953
+ ).toISOString();
954
+
955
+ return this._makeRequest({
956
+ method: "PATCH",
957
+ path: `/subscriptions/${subscriptionId}`,
958
+ data: {
959
+ expirationDateTime,
960
+ },
961
+ ...args,
962
+ });
963
+ },
964
+ /**
965
+ * Deletes a subscription. Call this when deactivating a webhook source
966
+ * to stop receiving notifications and clean up resources.
967
+ *
968
+ * @param {Object} options - Delete options
969
+ * @param {string} options.subscriptionId - ID of subscription to delete
970
+ * @returns {Promise<void>}
971
+ * @see https://learn.microsoft.com/en-us/graph/api/subscription-delete
972
+ * @example
973
+ * await this.deleteSubscription({
974
+ * subscriptionId: "abc123-def456"
975
+ * });
976
+ */
977
+ deleteSubscription({
978
+ subscriptionId, ...args
979
+ }) {
980
+ return this._makeRequest({
981
+ method: "DELETE",
982
+ path: `/subscriptions/${subscriptionId}`,
983
+ ...args,
984
+ });
985
+ },
589
986
  },
590
987
  };
@@ -5,7 +5,7 @@ export default {
5
5
  key: "sharepoint-new-file-created",
6
6
  name: "New File Created",
7
7
  description: "Emit new event when a new file is created in Microsoft Sharepoint.",
8
- version: "0.0.4",
8
+ version: "0.0.5",
9
9
  type: "source",
10
10
  dedupe: "unique",
11
11
  props: {
@@ -5,7 +5,7 @@ export default {
5
5
  key: "sharepoint-new-folder-created",
6
6
  name: "New Folder Created",
7
7
  description: "Emit new event when a new folder is created in Microsoft Sharepoint.",
8
- version: "0.0.4",
8
+ version: "0.0.5",
9
9
  type: "source",
10
10
  dedupe: "unique",
11
11
  props: {
@@ -5,7 +5,7 @@ export default {
5
5
  key: "sharepoint-new-list-item",
6
6
  name: "New List Item",
7
7
  description: "Emit new event when a new list item is created in Microsoft Sharepoint.",
8
- version: "0.0.9",
8
+ version: "0.0.10",
9
9
  type: "source",
10
10
  dedupe: "unique",
11
11
  props: {