@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.
- package/actions/create-folder/create-folder.mjs +1 -1
- package/actions/create-item/create-item.mjs +1 -1
- package/actions/create-link/create-link.mjs +1 -1
- package/actions/create-list/create-list.mjs +1 -1
- package/actions/download-file/download-file.mjs +93 -7
- package/actions/download-files/download-files.mjs +88 -0
- package/actions/find-file-by-name/find-file-by-name.mjs +1 -1
- package/actions/find-files-with-metadata/find-files-with-metadata.mjs +1 -1
- package/actions/get-excel-table/get-excel-table.mjs +1 -1
- package/actions/get-file-by-id/get-file-by-id.mjs +1 -1
- package/actions/get-site/get-site.mjs +1 -1
- package/actions/list-files-in-folder/list-files-in-folder.mjs +1 -1
- package/actions/list-sites/list-sites.mjs +1 -1
- package/actions/retrieve-file-metadata/retrieve-file-metadata.mjs +55 -0
- package/actions/search-and-filter-files/search-and-filter-files.mjs +1 -1
- package/actions/search-files/search-files.mjs +1 -1
- package/actions/search-sites/search-sites.mjs +1 -1
- package/actions/update-item/update-item.mjs +1 -1
- package/actions/upload-file/upload-file.mjs +1 -1
- package/common/constants.mjs +70 -0
- package/common/file-picker-base.mjs +308 -0
- package/common/utils.mjs +78 -0
- package/package.json +5 -2
- package/sharepoint.app.mjs +400 -3
- package/sources/new-file-created/new-file-created.mjs +1 -1
- package/sources/new-folder-created/new-folder-created.mjs +1 -1
- package/sources/new-list-item/new-list-item.mjs +1 -1
- package/sources/updated-file-instant/updated-file-instant.mjs +361 -0
- package/sources/updated-list-item/updated-list-item.mjs +1 -1
- 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.
|
|
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
|
}
|