@slates-integrations/sharepoint 0.2.0-rc.6

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.
@@ -0,0 +1,562 @@
1
+ import { createAxios } from 'slates';
2
+
3
+ let trimSlashes = (value: string) => value.replace(/^\/+|\/+$/g, '');
4
+
5
+ let buildRootUploadPath = (driveId: string, parentPath: string, fileName: string) => {
6
+ let normalizedParentPath = trimSlashes(parentPath);
7
+ let relativePath = normalizedParentPath
8
+ ? `${normalizedParentPath}/${fileName}`
9
+ : fileName;
10
+ return `/drives/${driveId}/root:/${relativePath}:/content`;
11
+ };
12
+
13
+ let getLocationHeader = (headers: any) =>
14
+ headers?.location ??
15
+ headers?.Location ??
16
+ headers?.get?.('location') ??
17
+ headers?.get?.('Location');
18
+
19
+ export class SharePointClient {
20
+ private http: ReturnType<typeof createAxios>;
21
+
22
+ constructor(token: string) {
23
+ this.http = createAxios({
24
+ baseURL: 'https://graph.microsoft.com/v1.0',
25
+ headers: {
26
+ Authorization: `Bearer ${token}`,
27
+ Accept: 'application/json',
28
+ 'Content-Type': 'application/json'
29
+ }
30
+ });
31
+ }
32
+
33
+ // ─── Sites ──────────────────────────────────────────────────────
34
+
35
+ async getSite(siteId: string) {
36
+ let response = await this.http.get(`/sites/${siteId}`);
37
+ return response.data as any;
38
+ }
39
+
40
+ async getSiteByHostnameAndPath(hostname: string, path?: string) {
41
+ let url = path ? `/sites/${hostname}:/${path}` : `/sites/${hostname}`;
42
+ let response = await this.http.get(url);
43
+ return response.data as any;
44
+ }
45
+
46
+ async searchSites(query: string) {
47
+ let response = await this.http.get('/sites', {
48
+ params: { search: query }
49
+ });
50
+ return response.data as any;
51
+ }
52
+
53
+ async getRootSite() {
54
+ let response = await this.http.get('/sites/root');
55
+ return response.data as any;
56
+ }
57
+
58
+ async listSubsites(siteId: string) {
59
+ let response = await this.http.get(`/sites/${siteId}/sites`);
60
+ return response.data as any;
61
+ }
62
+
63
+ // ─── Drives (Document Libraries) ───────────────────────────────
64
+
65
+ async listDrives(siteId: string) {
66
+ let response = await this.http.get(`/sites/${siteId}/drives`);
67
+ return response.data as any;
68
+ }
69
+
70
+ async getDrive(driveId: string) {
71
+ let response = await this.http.get(`/drives/${driveId}`);
72
+ return response.data as any;
73
+ }
74
+
75
+ async getDefaultDrive(siteId: string) {
76
+ let response = await this.http.get(`/sites/${siteId}/drive`);
77
+ return response.data as any;
78
+ }
79
+
80
+ // ─── Drive Items (Files & Folders) ─────────────────────────────
81
+
82
+ async listDriveItems(driveId: string, folderId?: string) {
83
+ let path = folderId
84
+ ? `/drives/${driveId}/items/${folderId}/children`
85
+ : `/drives/${driveId}/root/children`;
86
+ let response = await this.http.get(path);
87
+ return response.data as any;
88
+ }
89
+
90
+ async getDriveItem(driveId: string, itemId: string) {
91
+ let response = await this.http.get(`/drives/${driveId}/items/${itemId}`);
92
+ return response.data as any;
93
+ }
94
+
95
+ async getDriveItemByPath(driveId: string, itemPath: string) {
96
+ let normalizedPath = trimSlashes(itemPath);
97
+ let response = await this.http.get(
98
+ normalizedPath ? `/drives/${driveId}/root:/${normalizedPath}` : `/drives/${driveId}/root`
99
+ );
100
+ return response.data as any;
101
+ }
102
+
103
+ async createFolder(driveId: string, parentId: string, name: string) {
104
+ let path =
105
+ parentId === 'root'
106
+ ? `/drives/${driveId}/root/children`
107
+ : `/drives/${driveId}/items/${parentId}/children`;
108
+ let response = await this.http.post(path, {
109
+ name,
110
+ folder: {},
111
+ '@microsoft.graph.conflictBehavior': 'rename'
112
+ });
113
+ return response.data as any;
114
+ }
115
+
116
+ async uploadSmallFile(
117
+ driveId: string,
118
+ parentPath: string,
119
+ fileName: string,
120
+ content: string
121
+ ) {
122
+ let response = await this.http.put(buildRootUploadPath(driveId, parentPath, fileName), content, {
123
+ headers: { 'Content-Type': 'application/octet-stream' }
124
+ });
125
+ return response.data as any;
126
+ }
127
+
128
+ async uploadSmallFileToFolder(
129
+ driveId: string,
130
+ folderId: string,
131
+ fileName: string,
132
+ content: string
133
+ ) {
134
+ let response = await this.http.put(
135
+ `/drives/${driveId}/items/${folderId}:/${fileName}:/content`,
136
+ content,
137
+ {
138
+ headers: { 'Content-Type': 'application/octet-stream' }
139
+ }
140
+ );
141
+ return response.data as any;
142
+ }
143
+
144
+ async getFileContent(driveId: string, itemId: string) {
145
+ let response = await this.http.get(`/drives/${driveId}/items/${itemId}/content`, {
146
+ responseType: 'text'
147
+ });
148
+ return response.data as string;
149
+ }
150
+
151
+ async getFileDownloadUrl(driveId: string, itemId: string) {
152
+ let response = await this.http.get(`/drives/${driveId}/items/${itemId}`);
153
+ let data = response.data as any;
154
+ return data['@microsoft.graph.downloadUrl'] as string;
155
+ }
156
+
157
+ async deleteDriveItem(driveId: string, itemId: string) {
158
+ await this.http.delete(`/drives/${driveId}/items/${itemId}`);
159
+ }
160
+
161
+ async moveDriveItem(driveId: string, itemId: string, newParentId: string, newName?: string) {
162
+ let body: any = {
163
+ parentReference: { id: newParentId }
164
+ };
165
+ if (newName) {
166
+ body.name = newName;
167
+ }
168
+ let response = await this.http.patch(`/drives/${driveId}/items/${itemId}`, body);
169
+ return response.data as any;
170
+ }
171
+
172
+ async copyDriveItem(
173
+ driveId: string,
174
+ itemId: string,
175
+ newParentDriveId: string,
176
+ newParentId: string,
177
+ newName?: string
178
+ ) {
179
+ let body: any = {
180
+ parentReference: {
181
+ driveId: newParentDriveId,
182
+ id: newParentId
183
+ }
184
+ };
185
+ if (newName) {
186
+ body.name = newName;
187
+ }
188
+ let response = await this.http.post(`/drives/${driveId}/items/${itemId}/copy`, body);
189
+ return {
190
+ copyMonitorUrl: getLocationHeader(response.headers)
191
+ };
192
+ }
193
+
194
+ async renameDriveItem(driveId: string, itemId: string, newName: string) {
195
+ let response = await this.http.patch(`/drives/${driveId}/items/${itemId}`, {
196
+ name: newName
197
+ });
198
+ return response.data as any;
199
+ }
200
+
201
+ async listDriveItemVersions(driveId: string, itemId: string) {
202
+ let response = await this.http.get(`/drives/${driveId}/items/${itemId}/versions`);
203
+ return response.data as any;
204
+ }
205
+
206
+ async searchDriveItems(driveId: string, query: string) {
207
+ let response = await this.http.get(
208
+ `/drives/${driveId}/root/search(q='${encodeURIComponent(query)}')`
209
+ );
210
+ return response.data as any;
211
+ }
212
+
213
+ // ─── Lists ──────────────────────────────────────────────────────
214
+
215
+ async listLists(siteId: string) {
216
+ let response = await this.http.get(`/sites/${siteId}/lists`);
217
+ return response.data as any;
218
+ }
219
+
220
+ async getList(siteId: string, listId: string) {
221
+ let response = await this.http.get(`/sites/${siteId}/lists/${listId}`, {
222
+ params: { expand: 'columns' }
223
+ });
224
+ return response.data as any;
225
+ }
226
+
227
+ async createList(
228
+ siteId: string,
229
+ displayName: string,
230
+ template: string,
231
+ columns?: Array<{ name: string; type: string; description?: string }>
232
+ ) {
233
+ let body: any = {
234
+ displayName,
235
+ list: { template }
236
+ };
237
+ if (columns && columns.length > 0) {
238
+ body.columns = columns.map(col => {
239
+ let colDef: any = {
240
+ name: col.name,
241
+ description: col.description
242
+ };
243
+ switch (col.type) {
244
+ case 'text':
245
+ colDef.text = {};
246
+ break;
247
+ case 'number':
248
+ colDef.number = {};
249
+ break;
250
+ case 'boolean':
251
+ colDef.boolean = {};
252
+ break;
253
+ case 'dateTime':
254
+ colDef.dateTime = {};
255
+ break;
256
+ case 'choice':
257
+ colDef.choice = {};
258
+ break;
259
+ case 'currency':
260
+ colDef.currency = {};
261
+ break;
262
+ default:
263
+ colDef.text = {};
264
+ }
265
+ return colDef;
266
+ });
267
+ }
268
+ let response = await this.http.post(`/sites/${siteId}/lists`, body);
269
+ return response.data as any;
270
+ }
271
+
272
+ async updateList(
273
+ siteId: string,
274
+ listId: string,
275
+ updates: { displayName?: string; description?: string }
276
+ ) {
277
+ let response = await this.http.patch(`/sites/${siteId}/lists/${listId}`, updates);
278
+ return response.data as any;
279
+ }
280
+
281
+ async deleteList(siteId: string, listId: string) {
282
+ await this.http.delete(`/sites/${siteId}/lists/${listId}`);
283
+ }
284
+
285
+ // ─── List Items ─────────────────────────────────────────────────
286
+
287
+ async listListItems(
288
+ siteId: string,
289
+ listId: string,
290
+ params?: {
291
+ expand?: string;
292
+ top?: number;
293
+ filter?: string;
294
+ orderby?: string;
295
+ skipToken?: string;
296
+ allowUnindexedQuery?: boolean;
297
+ }
298
+ ) {
299
+ let headers: Record<string, string> = {};
300
+ if (params?.allowUnindexedQuery) {
301
+ headers['Prefer'] = 'HonorNonIndexedQueriesWarningMayFailRandomly';
302
+ }
303
+
304
+ if (params?.skipToken) {
305
+ let response = await this.http.get(params.skipToken, { headers });
306
+ return response.data as any;
307
+ }
308
+
309
+ let queryParams: any = {};
310
+ if (params?.expand) queryParams.$expand = params.expand;
311
+ if (params?.top) queryParams.$top = params.top;
312
+ if (params?.filter) queryParams.$filter = params.filter;
313
+ if (params?.orderby) queryParams.$orderby = params.orderby;
314
+
315
+ let response = await this.http.get(`/sites/${siteId}/lists/${listId}/items`, {
316
+ params: queryParams,
317
+ headers
318
+ });
319
+ return response.data as any;
320
+ }
321
+
322
+ async getListItem(siteId: string, listId: string, itemId: string) {
323
+ let response = await this.http.get(`/sites/${siteId}/lists/${listId}/items/${itemId}`, {
324
+ params: { expand: 'fields' }
325
+ });
326
+ return response.data as any;
327
+ }
328
+
329
+ async createListItem(siteId: string, listId: string, fields: Record<string, any>) {
330
+ let response = await this.http.post(`/sites/${siteId}/lists/${listId}/items`, {
331
+ fields
332
+ });
333
+ return response.data as any;
334
+ }
335
+
336
+ async updateListItem(
337
+ siteId: string,
338
+ listId: string,
339
+ itemId: string,
340
+ fields: Record<string, any>
341
+ ) {
342
+ let response = await this.http.patch(
343
+ `/sites/${siteId}/lists/${listId}/items/${itemId}/fields`,
344
+ fields
345
+ );
346
+ return response.data as any;
347
+ }
348
+
349
+ async deleteListItem(siteId: string, listId: string, itemId: string) {
350
+ await this.http.delete(`/sites/${siteId}/lists/${listId}/items/${itemId}`);
351
+ }
352
+
353
+ // ─── List Columns ───────────────────────────────────────────────
354
+
355
+ async listColumns(siteId: string, listId: string) {
356
+ let response = await this.http.get(`/sites/${siteId}/lists/${listId}/columns`);
357
+ return response.data as any;
358
+ }
359
+
360
+ async createColumn(
361
+ siteId: string,
362
+ listId: string,
363
+ column: {
364
+ name: string;
365
+ description?: string;
366
+ type: string;
367
+ required?: boolean;
368
+ choices?: string[];
369
+ }
370
+ ) {
371
+ let body: any = {
372
+ name: column.name,
373
+ description: column.description,
374
+ required: column.required
375
+ };
376
+
377
+ switch (column.type) {
378
+ case 'text':
379
+ body.text = {};
380
+ break;
381
+ case 'number':
382
+ body.number = {};
383
+ break;
384
+ case 'boolean':
385
+ body.boolean = {};
386
+ break;
387
+ case 'dateTime':
388
+ body.dateTime = { format: 'dateOnly' };
389
+ break;
390
+ case 'choice':
391
+ body.choice = { choices: column.choices || [] };
392
+ break;
393
+ case 'currency':
394
+ body.currency = {};
395
+ break;
396
+ case 'personOrGroup':
397
+ body.personOrGroup = {};
398
+ break;
399
+ default:
400
+ body.text = {};
401
+ }
402
+
403
+ let response = await this.http.post(`/sites/${siteId}/lists/${listId}/columns`, body);
404
+ return response.data as any;
405
+ }
406
+
407
+ async deleteColumn(siteId: string, listId: string, columnId: string) {
408
+ await this.http.delete(`/sites/${siteId}/lists/${listId}/columns/${columnId}`);
409
+ }
410
+
411
+ async updateColumn(
412
+ siteId: string,
413
+ listId: string,
414
+ columnId: string,
415
+ updates: { description?: string; required?: boolean }
416
+ ) {
417
+ let response = await this.http.patch(
418
+ `/sites/${siteId}/lists/${listId}/columns/${columnId}`,
419
+ updates
420
+ );
421
+ return response.data as any;
422
+ }
423
+
424
+ // ─── Permissions ────────────────────────────────────────────────
425
+
426
+ async listSitePermissions(siteId: string) {
427
+ let response = await this.http.get(`/sites/${siteId}/permissions`);
428
+ return response.data as any;
429
+ }
430
+
431
+ async getDriveItemPermissions(driveId: string, itemId: string) {
432
+ let response = await this.http.get(`/drives/${driveId}/items/${itemId}/permissions`);
433
+ return response.data as any;
434
+ }
435
+
436
+ async createSharingLink(
437
+ driveId: string,
438
+ itemId: string,
439
+ type: string,
440
+ scope: string,
441
+ expirationDateTime?: string,
442
+ password?: string
443
+ ) {
444
+ let body: any = { type, scope };
445
+ if (expirationDateTime) body.expirationDateTime = expirationDateTime;
446
+ if (password) body.password = password;
447
+
448
+ let response = await this.http.post(`/drives/${driveId}/items/${itemId}/createLink`, body);
449
+ return response.data as any;
450
+ }
451
+
452
+ async inviteToItem(
453
+ driveId: string,
454
+ itemId: string,
455
+ recipients: Array<{ email: string }>,
456
+ roles: string[],
457
+ message?: string,
458
+ requireSignIn?: boolean,
459
+ sendInvitation?: boolean
460
+ ) {
461
+ let body: any = {
462
+ recipients: recipients.map(r => ({ email: r.email })),
463
+ roles,
464
+ requireSignIn: requireSignIn ?? true,
465
+ sendInvitation: sendInvitation ?? true
466
+ };
467
+ if (message) body.message = message;
468
+
469
+ let response = await this.http.post(`/drives/${driveId}/items/${itemId}/invite`, body);
470
+ return response.data as any;
471
+ }
472
+
473
+ async deletePermission(driveId: string, itemId: string, permissionId: string) {
474
+ await this.http.delete(`/drives/${driveId}/items/${itemId}/permissions/${permissionId}`);
475
+ }
476
+
477
+ // ─── Content Types ──────────────────────────────────────────────
478
+
479
+ async listContentTypes(siteId: string) {
480
+ let response = await this.http.get(`/sites/${siteId}/contentTypes`);
481
+ return response.data as any;
482
+ }
483
+
484
+ async getContentType(siteId: string, contentTypeId: string) {
485
+ let response = await this.http.get(`/sites/${siteId}/contentTypes/${contentTypeId}`);
486
+ return response.data as any;
487
+ }
488
+
489
+ async listSiteColumns(siteId: string) {
490
+ let response = await this.http.get(`/sites/${siteId}/columns`);
491
+ return response.data as any;
492
+ }
493
+
494
+ // ─── Search ─────────────────────────────────────────────────────
495
+
496
+ async search(query: string, entityTypes: string[], from?: number, size?: number) {
497
+ let body: any = {
498
+ requests: [
499
+ {
500
+ entityTypes,
501
+ query: { queryString: query },
502
+ from: from || 0,
503
+ size: size || 25
504
+ }
505
+ ]
506
+ };
507
+
508
+ let response = await this.http.post('/search/query', body);
509
+ return response.data as any;
510
+ }
511
+
512
+ // ─── Subscriptions (Graph Change Notifications) ─────────────────
513
+
514
+ async createSubscription(
515
+ resource: string,
516
+ changeType: string,
517
+ notificationUrl: string,
518
+ expirationDateTime: string,
519
+ clientState?: string
520
+ ) {
521
+ let body: any = {
522
+ changeType,
523
+ notificationUrl,
524
+ resource,
525
+ expirationDateTime
526
+ };
527
+ if (clientState) body.clientState = clientState;
528
+
529
+ let response = await this.http.post('/subscriptions', body);
530
+ return response.data as any;
531
+ }
532
+
533
+ async updateSubscription(subscriptionId: string, expirationDateTime: string) {
534
+ let response = await this.http.patch(`/subscriptions/${subscriptionId}`, {
535
+ expirationDateTime
536
+ });
537
+ return response.data as any;
538
+ }
539
+
540
+ async deleteSubscription(subscriptionId: string) {
541
+ await this.http.delete(`/subscriptions/${subscriptionId}`);
542
+ }
543
+
544
+ async getSubscription(subscriptionId: string) {
545
+ let response = await this.http.get(`/subscriptions/${subscriptionId}`);
546
+ return response.data as any;
547
+ }
548
+
549
+ // ─── Delta Queries ──────────────────────────────────────────────
550
+
551
+ async getDelta(driveId: string, deltaToken?: string) {
552
+ let url = deltaToken ? deltaToken : `/drives/${driveId}/root/delta`;
553
+ let response = await this.http.get(url);
554
+ return response.data as any;
555
+ }
556
+
557
+ async getListItemsDelta(siteId: string, listId: string, deltaToken?: string) {
558
+ let url = deltaToken ? deltaToken : `/sites/${siteId}/lists/${listId}/items/delta`;
559
+ let response = await this.http.get(url);
560
+ return response.data as any;
561
+ }
562
+ }
package/src/spec.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { SlateSpecification } from 'slates';
2
+ import { auth } from './auth';
3
+ import { config } from './config';
4
+
5
+ export let spec = SlateSpecification.create({
6
+ key: 'sharepoint',
7
+ name: 'SharePoint',
8
+ description:
9
+ 'Microsoft SharePoint cloud-based platform for document management, content collaboration, and intranet sites. Manage sites, document libraries, lists, files, and permissions via the Microsoft Graph API.',
10
+ metadata: {},
11
+ config,
12
+ auth
13
+ });
@@ -0,0 +1,16 @@
1
+ import { ServiceError, badRequestError } from '@lowerdeck/error';
2
+
3
+ export let oneOfRequiredError = (
4
+ message: string,
5
+ fields: [string, string, ...string[]]
6
+ ) =>
7
+ new ServiceError(
8
+ badRequestError({
9
+ message,
10
+ errors: fields.map(field => ({
11
+ path: [field],
12
+ code: 'missing_required_alternative',
13
+ message
14
+ }))
15
+ })
16
+ );
@@ -0,0 +1,109 @@
1
+ import { SlateTool } from 'slates';
2
+ import { SharePointClient } from '../lib/client';
3
+ import { spec } from '../spec';
4
+ import { z } from 'zod';
5
+
6
+ let contentTypeSchema = z.object({
7
+ contentTypeId: z.string().describe('Content type ID'),
8
+ contentTypeName: z.string().describe('Name of the content type'),
9
+ contentTypeDescription: z.string().optional().describe('Description'),
10
+ group: z.string().optional().describe('Content type group'),
11
+ hidden: z.boolean().optional().describe('Whether the content type is hidden'),
12
+ readOnly: z.boolean().optional().describe('Whether the content type is read-only'),
13
+ parentId: z.string().optional().describe('Parent content type ID')
14
+ });
15
+
16
+ let siteColumnSchema = z.object({
17
+ columnId: z.string().describe('Column ID'),
18
+ columnName: z.string().describe('Internal name'),
19
+ displayName: z.string().describe('Display name'),
20
+ columnDescription: z.string().optional().describe('Column description'),
21
+ columnGroup: z.string().optional().describe('Column group'),
22
+ readOnly: z.boolean().optional().describe('Whether the column is read-only'),
23
+ hidden: z.boolean().optional().describe('Whether the column is hidden')
24
+ });
25
+
26
+ export let getContentTypes = SlateTool.create(spec, {
27
+ name: 'Get Content Types',
28
+ key: 'get_content_types',
29
+ description: `Retrieve content types and site columns for a SharePoint site. Content types define reusable schemas for lists and libraries. Site columns are reusable field definitions that can be added to content types and lists.`,
30
+ instructions: [
31
+ 'Set **resource** to "contentTypes" to list content types, or "siteColumns" to list site columns.',
32
+ 'Provide **contentTypeId** to get details of a specific content type.'
33
+ ],
34
+ tags: {
35
+ readOnly: true,
36
+ destructive: false
37
+ }
38
+ })
39
+ .input(
40
+ z.object({
41
+ siteId: z.string().describe('SharePoint site ID'),
42
+ resource: z.enum(['contentTypes', 'siteColumns']).describe('Which resource to retrieve'),
43
+ contentTypeId: z
44
+ .string()
45
+ .optional()
46
+ .describe('Specific content type ID (for getting a single content type)')
47
+ })
48
+ )
49
+ .output(
50
+ z.object({
51
+ contentTypes: z.array(contentTypeSchema).optional().describe('List of content types'),
52
+ contentType: contentTypeSchema.optional().describe('Single content type details'),
53
+ siteColumns: z.array(siteColumnSchema).optional().describe('List of site columns')
54
+ })
55
+ )
56
+ .handleInvocation(async ctx => {
57
+ let client = new SharePointClient(ctx.auth.token);
58
+ let { siteId, resource, contentTypeId } = ctx.input;
59
+
60
+ if (resource === 'contentTypes') {
61
+ if (contentTypeId) {
62
+ let ct = await client.getContentType(siteId, contentTypeId);
63
+ let mapped = {
64
+ contentTypeId: ct.id,
65
+ contentTypeName: ct.name,
66
+ contentTypeDescription: ct.description,
67
+ group: ct.group,
68
+ hidden: ct.hidden,
69
+ readOnly: ct.readOnly,
70
+ parentId: ct.parentId
71
+ };
72
+ return {
73
+ output: { contentType: mapped },
74
+ message: `Retrieved content type **${ct.name}**.`
75
+ };
76
+ }
77
+
78
+ let data = await client.listContentTypes(siteId);
79
+ let contentTypes = (data.value || []).map((ct: any) => ({
80
+ contentTypeId: ct.id,
81
+ contentTypeName: ct.name,
82
+ contentTypeDescription: ct.description,
83
+ group: ct.group,
84
+ hidden: ct.hidden,
85
+ readOnly: ct.readOnly,
86
+ parentId: ct.parentId
87
+ }));
88
+ return {
89
+ output: { contentTypes },
90
+ message: `Found **${contentTypes.length}** content type(s).`
91
+ };
92
+ }
93
+
94
+ let data = await client.listSiteColumns(siteId);
95
+ let siteColumns = (data.value || []).map((col: any) => ({
96
+ columnId: col.id,
97
+ columnName: col.name,
98
+ displayName: col.displayName || col.name,
99
+ columnDescription: col.description,
100
+ columnGroup: col.columnGroup,
101
+ readOnly: col.readOnly,
102
+ hidden: col.hidden
103
+ }));
104
+ return {
105
+ output: { siteColumns },
106
+ message: `Found **${siteColumns.length}** site column(s).`
107
+ };
108
+ })
109
+ .build();