@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,308 @@
1
+ import sharepoint from "../sharepoint.app.mjs";
2
+
3
+ /**
4
+ * Shared prop definitions for file picker actions.
5
+ * These props provide a consistent file/folder browsing experience across actions.
6
+ */
7
+ export const filePickerProps = {
8
+ sharepoint,
9
+ siteId: {
10
+ propDefinition: [
11
+ sharepoint,
12
+ "siteId",
13
+ ],
14
+ withLabel: true,
15
+ },
16
+ driveId: {
17
+ propDefinition: [
18
+ sharepoint,
19
+ "driveId",
20
+ (c) => ({
21
+ siteId: c.siteId,
22
+ }),
23
+ ],
24
+ withLabel: true,
25
+ },
26
+ folderId: {
27
+ propDefinition: [
28
+ sharepoint,
29
+ "folderId",
30
+ (c) => ({
31
+ siteId: c.siteId,
32
+ driveId: c.driveId,
33
+ }),
34
+ ],
35
+ label: "Folder",
36
+ description: "The folder to browse. Leave empty to browse the root of the drive.",
37
+ optional: true,
38
+ withLabel: true,
39
+ },
40
+ fileOrFolderIds: {
41
+ propDefinition: [
42
+ sharepoint,
43
+ "fileOrFolderId",
44
+ (c) => ({
45
+ siteId: c.siteId,
46
+ driveId: c.driveId,
47
+ folderId: c.folderId,
48
+ }),
49
+ ],
50
+ type: "string[]",
51
+ label: "Files or Folders",
52
+ description: "Select one or more files, or select a folder and click 'Refresh Fields' to browse into it",
53
+ withLabel: true,
54
+ },
55
+ };
56
+
57
+ /**
58
+ * Shared methods for file picker actions.
59
+ * Provides common functionality for parsing selections, fetching metadata,
60
+ * and handling folder-only selections.
61
+ */
62
+ export const filePickerMethods = {
63
+ /**
64
+ * Categorizes selections into files and folders based on isFolder property.
65
+ *
66
+ * @param {Array<{id: string, name: string, isFolder: boolean}>} selections
67
+ * Array of parsed file/folder objects
68
+ * @returns {{files: Array, folders: Array}}
69
+ * Object with separate files and folders arrays
70
+ * @example
71
+ * const { files, folders } = this.categorizeSelections([
72
+ * { id: "1", name: "report.pdf", isFolder: false },
73
+ * { id: "2", name: "Documents", isFolder: true }
74
+ * ]);
75
+ * // files: [{ id: "1", name: "report.pdf", isFolder: false }]
76
+ * // folders: [{ id: "2", name: "Documents", isFolder: true }]
77
+ */
78
+ categorizeSelections(selections) {
79
+ return {
80
+ folders: selections.filter((s) => s.isFolder),
81
+ files: selections.filter((s) => !s.isFolder),
82
+ };
83
+ },
84
+
85
+ /**
86
+ * Handles the case where only folders are selected
87
+ * @param {Object} $ - Pipedream step context
88
+ * @param {Array} folders - Array of folder objects
89
+ * @returns {Object} Response object with folder information
90
+ */
91
+ handleFolderOnlySelection($, folders) {
92
+ const folderNames = folders.map((f) => f.name).join(", ");
93
+ $.export("$summary", `Selected ${folders.length} folder(s): ${folderNames}. Set one as the Folder ID and refresh to browse its contents.`);
94
+ return {
95
+ type: "folders",
96
+ folders: folders.map((f) => ({
97
+ id: f.id,
98
+ name: f.name,
99
+ })),
100
+ message: "To browse a folder, set it as the folderId and reload props",
101
+ };
102
+ },
103
+
104
+ /**
105
+ * Constructs a SharePoint library view URL that opens the file in the
106
+ * document library context. This is useful for providing users with a
107
+ * link to view the file in SharePoint's web interface.
108
+ *
109
+ * @param {string} fileWebUrl - The file's webUrl from Microsoft Graph API
110
+ * @returns {string|null} SharePoint AllItems.aspx URL with file location,
111
+ * or null if construction fails
112
+ * @example
113
+ * // Input: "https://contoso.sharepoint.com/sites/Marketing/..."
114
+ * // Output: "https://contoso.sharepoint.com/sites/Marketing/...aspx?..."
115
+ */
116
+ constructSharePointViewUrl(fileWebUrl) {
117
+ if (!fileWebUrl) return null;
118
+
119
+ try {
120
+ // Parse webUrl to extract components
121
+ // Example: https://tenant.sharepoint.com/sites/sitename/LibraryName/folder/file.ext
122
+ const url = new URL(fileWebUrl);
123
+
124
+ // Extract library path from webUrl (e.g., "Shared%20Documents")
125
+ // Match pattern: /sites/{sitename}/{libraryname}/...
126
+ const libraryMatch = fileWebUrl.match(/\/sites\/[^/]+\/([^/]+)/);
127
+ if (!libraryMatch) return null;
128
+
129
+ const libraryUrlPart = libraryMatch[1]; // This keeps the original encoding from webUrl
130
+
131
+ // Construct site URL
132
+ const siteUrlMatch = fileWebUrl.match(/(https:\/\/[^/]+\/sites\/[^/]+)/);
133
+ if (!siteUrlMatch) return null;
134
+
135
+ const siteUrl = siteUrlMatch[1];
136
+
137
+ // Construct the full file path (decode the pathname to get raw path)
138
+ const filePath = decodeURIComponent(url.pathname);
139
+
140
+ // Construct parent path by removing the filename
141
+ const parentPath = filePath.substring(0, filePath.lastIndexOf("/"));
142
+
143
+ // Build the AllItems.aspx URL - don't re-encode the library name in path
144
+ return `${siteUrl}/${libraryUrlPart}/Forms/AllItems.aspx?id=${encodeURIComponent(filePath)}&parent=${encodeURIComponent(parentPath)}`;
145
+ } catch (error) {
146
+ console.error("Error constructing SharePoint view URL:", error);
147
+ return null;
148
+ }
149
+ },
150
+
151
+ /**
152
+ * Fetches metadata for multiple files in parallel with individual error
153
+ * handling. Uses Promise.allSettled to ensure partial failures don't
154
+ * block successful fetches.
155
+ *
156
+ * @param {Object} $ - Pipedream step context for API calls
157
+ * @param {Array<{id: string, name: string}>} files
158
+ * Array of file objects with id property
159
+ * @param {string} siteId - SharePoint site ID
160
+ * @param {string} driveId - Drive ID within the site
161
+ * @param {Object} [options={}] - Configuration options
162
+ * @param {boolean} [options.includeDownloadUrl=true]
163
+ * Whether to include temporary downloadUrl in response
164
+ * @returns {Promise<{fileResults: Array, errors: Array}>}
165
+ * Object with successful results and errors
166
+ * @example
167
+ * const { fileResults, errors } = await this.fetchFileMetadata(
168
+ * $, files, siteId, driveId, { includeDownloadUrl: true }
169
+ * );
170
+ */
171
+ async fetchFileMetadata($, files, siteId, driveId, options = {}) {
172
+ const { includeDownloadUrl = true } = options;
173
+
174
+ // Fetch metadata for all selected files in parallel, handling individual failures
175
+ const settledResults = await Promise.allSettled(
176
+ files.map(async (selected) => {
177
+ // When includeDownloadUrl is true, omit $select to get @microsoft.graph.downloadUrl
178
+ // (Graph API excludes downloadUrl when using $select)
179
+ const params = includeDownloadUrl
180
+ ? {}
181
+ : {
182
+ $select: "id,name,size,webUrl,createdDateTime,lastModifiedDateTime,createdBy,lastModifiedBy,parentReference,file,folder,image,video,audio,photo,shared,fileSystemInfo,cTag,eTag,sharepointIds",
183
+ };
184
+
185
+ const file = await this.sharepoint.getDriveItem({
186
+ $,
187
+ siteId,
188
+ driveId,
189
+ fileId: selected.id,
190
+ params,
191
+ });
192
+
193
+ // Construct SharePoint library view URL
194
+ const sharepointViewUrl = this.constructSharePointViewUrl(file.webUrl);
195
+
196
+ const result = {
197
+ ...file,
198
+ ...(sharepointViewUrl && {
199
+ sharepointViewUrl,
200
+ }),
201
+ _meta: {
202
+ siteId,
203
+ driveId,
204
+ fileId: selected.id,
205
+ },
206
+ };
207
+
208
+ // Remove the Graph API property name from spread
209
+ delete result["@microsoft.graph.downloadUrl"];
210
+
211
+ // Conditionally include downloadUrl based on options
212
+ if (includeDownloadUrl) {
213
+ result.downloadUrl = file["@microsoft.graph.downloadUrl"];
214
+ }
215
+
216
+ return result;
217
+ }),
218
+ );
219
+
220
+ // Separate successful and failed results
221
+ const fileResults = [];
222
+ const errors = [];
223
+
224
+ settledResults.forEach((result, index) => {
225
+ if (result.status === "fulfilled") {
226
+ fileResults.push(result.value);
227
+ } else {
228
+ const selected = files[index];
229
+ const errorMessage = result.reason?.message || String(result.reason);
230
+ console.error(`Failed to fetch file ${selected.id} (${selected.name}): ${errorMessage}`);
231
+ errors.push({
232
+ fileId: selected.id,
233
+ fileName: selected.name,
234
+ error: errorMessage,
235
+ });
236
+ }
237
+ });
238
+
239
+ return {
240
+ fileResults,
241
+ errors,
242
+ };
243
+ },
244
+
245
+ /**
246
+ * Processes and formats the final results with user-friendly summary
247
+ * export. Handles single file vs. multiple files formatting for UX.
248
+ *
249
+ * @param {Object} $ - Pipedream step context for exports
250
+ * @param {Array} fileResults - Successfully fetched file metadata
251
+ * @param {Array} errors - Failed file fetches with error details
252
+ * @param {Array} folders
253
+ * Selected folders (for informational purposes)
254
+ * @param {Object} [options={}] - Configuration options
255
+ * @param {string} [options.successVerb="Retrieved"]
256
+ * Verb for summary (e.g., "Retrieved", "Downloaded")
257
+ * @param {string} [options.successNoun="file(s)"]
258
+ * Noun for summary (e.g., "download URL(s)", "metadata")
259
+ * @returns {Object|Array} Single file object if one file, otherwise
260
+ * object with files/errors/folders arrays
261
+ * @throws {Error} If all file fetches failed
262
+ * @example
263
+ * return this.processResults($, fileResults, errors, folders, {
264
+ * successVerb: "Retrieved",
265
+ * successNoun: "download URL(s)"
266
+ * });
267
+ */
268
+ processResults($, fileResults, errors, folders, options = {}) {
269
+ const {
270
+ successVerb = "Retrieved",
271
+ successNoun = "file(s)",
272
+ } = options;
273
+
274
+ // If all files failed, throw an error
275
+ if (fileResults.length === 0 && errors.length > 0) {
276
+ throw new Error(`Failed to fetch all selected files: ${errors.map((e) => e.fileName).join(", ")}`);
277
+ }
278
+
279
+ // If single file, return it directly for backwards compatibility
280
+ if (fileResults.length === 1 && folders.length === 0 && errors.length === 0) {
281
+ $.export("$summary", `${successVerb} ${successNoun} for: ${fileResults[0].name}`);
282
+ return fileResults[0];
283
+ }
284
+
285
+ // Multiple files: return as object with metadata
286
+ const fileNames = fileResults.map((f) => f.name).join(", ");
287
+ const summaryParts = [
288
+ `${successVerb} ${successNoun} for ${fileResults.length} file(s): ${fileNames}`,
289
+ ];
290
+ if (errors.length > 0) {
291
+ summaryParts.push(`Failed to fetch ${errors.length} file(s): ${errors.map((e) => e.fileName).join(", ")}`);
292
+ }
293
+ $.export("$summary", summaryParts.join(". "));
294
+
295
+ return {
296
+ files: fileResults,
297
+ ...(errors.length > 0 && {
298
+ errors,
299
+ }),
300
+ ...(folders.length > 0 && {
301
+ folders: folders.map((f) => ({
302
+ id: f.id,
303
+ name: f.name,
304
+ })),
305
+ }),
306
+ };
307
+ },
308
+ };
package/common/utils.mjs CHANGED
@@ -7,6 +7,84 @@ export default {
7
7
  }
8
8
  return o;
9
9
  },
10
+
11
+ /**
12
+ * Internal helper to unwrap a potentially labeled value.
13
+ * Note: For most use cases, prefer using sharepoint.resolveWrappedValue() from the app.
14
+ * This is only for utility functions that don't have access to the app instance.
15
+ * @private
16
+ * @param {*} value - The value to unwrap
17
+ * @returns {*} The unwrapped value
18
+ */
19
+ _unwrapValue(value) {
20
+ return value?.value || value;
21
+ },
22
+
23
+ /**
24
+ * Parses a file or folder value from JSON string or returns wrapped object
25
+ * @param {*} value - The value to parse (JSON string or raw ID)
26
+ * @returns {{ id: string, name?: string, isFolder: boolean } | null}
27
+ */
28
+ parseFileOrFolder(value) {
29
+ if (!value) return null;
30
+ const resolved = this._unwrapValue(value);
31
+ try {
32
+ return JSON.parse(resolved);
33
+ } catch {
34
+ return {
35
+ id: resolved,
36
+ isFolder: false,
37
+ };
38
+ }
39
+ },
40
+
41
+ /**
42
+ * Parses a list of file or folder values
43
+ * @param {*} values - Single value or array of values
44
+ * @returns {Array<{ id: string, name?: string, isFolder: boolean }>}
45
+ */
46
+ parseFileOrFolderList(values) {
47
+ if (!values) return [];
48
+ const list = Array.isArray(values)
49
+ ? values
50
+ : [
51
+ values,
52
+ ];
53
+ return list.map((v) => this.parseFileOrFolder(v)).filter(Boolean);
54
+ },
55
+
56
+ /**
57
+ * Attempts to decode a base64 string (SharePoint encodes group names in base64)
58
+ * @param {string} str - String that may be base64 encoded
59
+ * @returns {string | null} Decoded string or null if not valid base64
60
+ */
61
+ tryDecodeBase64(str) {
62
+ try {
63
+ if (/^[A-Za-z0-9+/]+=*$/.test(str) && str.length > 10) {
64
+ const decoded = Buffer.from(str, "base64").toString("utf-8");
65
+ if (/^[\x20-\x7E\s]+$/.test(decoded)) {
66
+ return decoded;
67
+ }
68
+ }
69
+ } catch {
70
+ // Not valid base64
71
+ }
72
+ return null;
73
+ },
74
+
75
+ /**
76
+ * Determines access level from roles array
77
+ * @param {string[]} roles - Array of role strings
78
+ * @returns {"owner" | "write" | "read"}
79
+ */
80
+ getAccessLevel(roles) {
81
+ if (!Array.isArray(roles)) return "read";
82
+ if (roles.includes("owner")) return "owner";
83
+ if (roles.includes("write")) return "write";
84
+ if (roles.includes("read")) return "read";
85
+ return "read";
86
+ },
87
+
10
88
  parseObject(obj) {
11
89
  if (!obj) return undefined;
12
90
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pipedream/sharepoint",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "Pipedream Microsoft Sharepoint Online Components",
5
5
  "main": "sharepoint.app.mjs",
6
6
  "keywords": [
@@ -13,6 +13,9 @@
13
13
  "access": "public"
14
14
  },
15
15
  "dependencies": {
16
- "@pipedream/platform": "^3.1.1"
16
+ "@pipedream/platform": "^3.1.1",
17
+ "@microsoft/microsoft-graph-client": "^3.0.7",
18
+ "async-retry": "^1.3.3",
19
+ "isomorphic-fetch": "^3.0.0"
17
20
  }
18
21
  }