@mcp-consultant-tools/sharepoint 32.0.0 → 35.0.0-beta.1
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/build/cli.js +0 -0
- package/build/index.js +0 -0
- package/build/tools/read-tools.d.ts.map +1 -1
- package/build/tools/read-tools.js +20 -16
- package/build/tools/read-tools.js.map +1 -1
- package/build/tools/write-tools.d.ts.map +1 -1
- package/build/tools/write-tools.js +8 -6
- package/build/tools/write-tools.js.map +1 -1
- package/package.json +2 -2
- package/build/SharePointService.d.ts +0 -180
- package/build/SharePointService.d.ts.map +0 -1
- package/build/SharePointService.js +0 -1277
- package/build/SharePointService.js.map +0 -1
- package/build/services/FileOperationsService.d.ts +0 -52
- package/build/services/FileOperationsService.d.ts.map +0 -1
- package/build/services/FileOperationsService.js +0 -451
- package/build/services/FileOperationsService.js.map +0 -1
- package/build/tools/prompts.d.ts +0 -11
- package/build/tools/prompts.d.ts.map +0 -1
- package/build/tools/prompts.js +0 -470
- package/build/tools/prompts.js.map +0 -1
|
@@ -1,1277 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SharePoint Online Service
|
|
3
|
-
*
|
|
4
|
-
* Provides read-only access to SharePoint Online sites, document libraries, and files
|
|
5
|
-
* through Microsoft Graph API with comprehensive PowerPlatform validation capabilities.
|
|
6
|
-
*
|
|
7
|
-
* Authentication: Microsoft Entra ID (OAuth 2.0) via MSAL
|
|
8
|
-
* API: Microsoft Graph API v1.0
|
|
9
|
-
* Primary Use Case: PowerPlatform-SharePoint integration validation
|
|
10
|
-
*/
|
|
11
|
-
import { ConfidentialClientApplication } from '@azure/msal-node';
|
|
12
|
-
import { Client } from '@microsoft/microsoft-graph-client';
|
|
13
|
-
import { auditLogger } from '@mcp-consultant-tools/core';
|
|
14
|
-
/**
|
|
15
|
-
* SharePoint Online Service
|
|
16
|
-
*
|
|
17
|
-
* Manages authentication, caching, and API requests to SharePoint Online via Graph API.
|
|
18
|
-
* Follows established patterns from ApplicationInsightsService and LogAnalyticsService.
|
|
19
|
-
*/
|
|
20
|
-
export class SharePointService {
|
|
21
|
-
config;
|
|
22
|
-
msalClient = null;
|
|
23
|
-
accessToken = null;
|
|
24
|
-
tokenExpirationTime = 0;
|
|
25
|
-
graphClient = null;
|
|
26
|
-
// Cache management (site info, drives, resolved site IDs)
|
|
27
|
-
cache = new Map();
|
|
28
|
-
siteIdCache = new Map(); // siteUrl → siteId
|
|
29
|
-
graphBaseUrl = 'https://graph.microsoft.com/v1.0';
|
|
30
|
-
constructor(config) {
|
|
31
|
-
this.config = config;
|
|
32
|
-
// Validate configuration
|
|
33
|
-
if (!config.tenantId || !config.clientId || !config.clientSecret) {
|
|
34
|
-
throw new Error('SharePoint Entra ID authentication requires tenantId, clientId, and clientSecret');
|
|
35
|
-
}
|
|
36
|
-
// Initialize MSAL client for Entra ID authentication
|
|
37
|
-
this.msalClient = new ConfidentialClientApplication({
|
|
38
|
-
auth: {
|
|
39
|
-
clientId: config.clientId,
|
|
40
|
-
clientSecret: config.clientSecret,
|
|
41
|
-
authority: `https://login.microsoftonline.com/${config.tenantId}`,
|
|
42
|
-
},
|
|
43
|
-
});
|
|
44
|
-
console.error('SharePoint service created (authentication not initialized until first use)');
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Get an access token for Microsoft Graph API (Entra ID auth)
|
|
48
|
-
* Implements 5-minute token buffer before expiry (follows ApplicationInsightsService pattern)
|
|
49
|
-
*/
|
|
50
|
-
async getAccessToken() {
|
|
51
|
-
if (!this.msalClient) {
|
|
52
|
-
throw new Error('MSAL client not initialized');
|
|
53
|
-
}
|
|
54
|
-
const currentTime = Date.now();
|
|
55
|
-
// Return cached token if still valid (with 5 minute buffer)
|
|
56
|
-
if (this.accessToken && this.tokenExpirationTime > currentTime) {
|
|
57
|
-
return this.accessToken;
|
|
58
|
-
}
|
|
59
|
-
try {
|
|
60
|
-
console.error(`[SharePoint] Acquiring token for tenant: ${this.config.tenantId}, client: ${this.config.clientId}`);
|
|
61
|
-
const result = await this.msalClient.acquireTokenByClientCredential({
|
|
62
|
-
scopes: ['https://graph.microsoft.com/.default'],
|
|
63
|
-
});
|
|
64
|
-
if (!result || !result.accessToken) {
|
|
65
|
-
throw new Error('Failed to acquire access token');
|
|
66
|
-
}
|
|
67
|
-
this.accessToken = result.accessToken;
|
|
68
|
-
console.error(`[SharePoint] Token acquired successfully, expires: ${result.expiresOn}`);
|
|
69
|
-
// Set expiration time (subtract 5 minutes to refresh early)
|
|
70
|
-
if (result.expiresOn) {
|
|
71
|
-
this.tokenExpirationTime = result.expiresOn.getTime() - (5 * 60 * 1000);
|
|
72
|
-
}
|
|
73
|
-
else {
|
|
74
|
-
// Default to 1 hour expiry if not provided
|
|
75
|
-
this.tokenExpirationTime = Date.now() + (55 * 60 * 1000);
|
|
76
|
-
}
|
|
77
|
-
return this.accessToken;
|
|
78
|
-
}
|
|
79
|
-
catch (error) {
|
|
80
|
-
console.error('[SharePoint] Token acquisition failed:', error.message);
|
|
81
|
-
console.error('[SharePoint] Error details:', JSON.stringify({
|
|
82
|
-
errorCode: error.errorCode,
|
|
83
|
-
errorMessage: error.errorMessage,
|
|
84
|
-
subError: error.subError,
|
|
85
|
-
}));
|
|
86
|
-
throw new Error('SharePoint authentication failed: ' + error.message);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* Get an authenticated Graph client for use by external services (e.g., FileOperationsService)
|
|
91
|
-
*/
|
|
92
|
-
async getAuthenticatedGraphClient() {
|
|
93
|
-
return this.getGraphClient();
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* Resolve a user-friendly site ID to a Graph API site ID for use by external services
|
|
97
|
-
*/
|
|
98
|
-
async getGraphSiteId(siteId) {
|
|
99
|
-
const site = this.getSiteById(siteId);
|
|
100
|
-
return this.resolveSiteId(site.siteUrl);
|
|
101
|
-
}
|
|
102
|
-
/**
|
|
103
|
-
* Initialize Graph Client with MSAL token provider
|
|
104
|
-
*/
|
|
105
|
-
async getGraphClient() {
|
|
106
|
-
const token = await this.getAccessToken();
|
|
107
|
-
// Client.init() expects authProvider as a callback function
|
|
108
|
-
this.graphClient = Client.init({
|
|
109
|
-
authProvider: (done) => {
|
|
110
|
-
done(null, token);
|
|
111
|
-
},
|
|
112
|
-
});
|
|
113
|
-
return this.graphClient;
|
|
114
|
-
}
|
|
115
|
-
/**
|
|
116
|
-
* Get active sites
|
|
117
|
-
*/
|
|
118
|
-
getActiveSites() {
|
|
119
|
-
return this.config.sites.filter(s => s.active);
|
|
120
|
-
}
|
|
121
|
-
/**
|
|
122
|
-
* Get all sites (including inactive)
|
|
123
|
-
*/
|
|
124
|
-
getAllSites() {
|
|
125
|
-
return this.config.sites;
|
|
126
|
-
}
|
|
127
|
-
/**
|
|
128
|
-
* Get site configuration by ID
|
|
129
|
-
*/
|
|
130
|
-
getSiteById(siteId) {
|
|
131
|
-
const site = this.config.sites.find(s => s.id === siteId);
|
|
132
|
-
if (!site) {
|
|
133
|
-
const availableSites = this.getActiveSites().map(s => s.id).join(', ');
|
|
134
|
-
throw new Error(`SharePoint site '${siteId}' not found. Available sites: ${availableSites}`);
|
|
135
|
-
}
|
|
136
|
-
if (!site.active) {
|
|
137
|
-
throw new Error(`SharePoint site '${siteId}' is inactive. Set active=true in configuration to enable.`);
|
|
138
|
-
}
|
|
139
|
-
return site;
|
|
140
|
-
}
|
|
141
|
-
/**
|
|
142
|
-
* Generate cache key for caching API responses
|
|
143
|
-
* Format: {method}:{siteId}:{resource}:{params}
|
|
144
|
-
*/
|
|
145
|
-
getCacheKey(method, siteId, resource, params) {
|
|
146
|
-
const paramStr = params ? JSON.stringify(params) : '';
|
|
147
|
-
return `${method}:${siteId}:${resource}:${paramStr}`;
|
|
148
|
-
}
|
|
149
|
-
/**
|
|
150
|
-
* Get cached value if not expired
|
|
151
|
-
*/
|
|
152
|
-
getCached(key) {
|
|
153
|
-
const entry = this.cache.get(key);
|
|
154
|
-
if (!entry) {
|
|
155
|
-
return null;
|
|
156
|
-
}
|
|
157
|
-
const now = Date.now();
|
|
158
|
-
if (now > entry.expires) {
|
|
159
|
-
this.cache.delete(key);
|
|
160
|
-
return null;
|
|
161
|
-
}
|
|
162
|
-
return entry.data;
|
|
163
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* Set cached value with TTL
|
|
166
|
-
*/
|
|
167
|
-
setCached(key, data) {
|
|
168
|
-
const ttl = (this.config.cacheTTL || 300) * 1000; // Default: 5 minutes
|
|
169
|
-
const expires = Date.now() + ttl;
|
|
170
|
-
this.cache.set(key, { data, expires });
|
|
171
|
-
}
|
|
172
|
-
/**
|
|
173
|
-
* Clear cache (all entries or by pattern/siteId)
|
|
174
|
-
*/
|
|
175
|
-
clearCache(pattern, siteId) {
|
|
176
|
-
let clearedCount = 0;
|
|
177
|
-
if (!pattern && !siteId) {
|
|
178
|
-
// Clear all cache
|
|
179
|
-
clearedCount = this.cache.size + this.siteIdCache.size;
|
|
180
|
-
this.cache.clear();
|
|
181
|
-
this.siteIdCache.clear();
|
|
182
|
-
}
|
|
183
|
-
else {
|
|
184
|
-
// Clear matching entries
|
|
185
|
-
const keysToDelete = [];
|
|
186
|
-
for (const key of this.cache.keys()) {
|
|
187
|
-
let shouldDelete = false;
|
|
188
|
-
if (siteId && key.includes(`:${siteId}:`)) {
|
|
189
|
-
shouldDelete = true;
|
|
190
|
-
}
|
|
191
|
-
if (pattern && key.includes(pattern)) {
|
|
192
|
-
shouldDelete = true;
|
|
193
|
-
}
|
|
194
|
-
if (shouldDelete) {
|
|
195
|
-
keysToDelete.push(key);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
keysToDelete.forEach(key => this.cache.delete(key));
|
|
199
|
-
clearedCount = keysToDelete.length;
|
|
200
|
-
// Clear site ID cache if siteId specified
|
|
201
|
-
if (siteId) {
|
|
202
|
-
for (const [url, id] of this.siteIdCache.entries()) {
|
|
203
|
-
if (id === siteId) {
|
|
204
|
-
this.siteIdCache.delete(url);
|
|
205
|
-
clearedCount++;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
console.error(`Cleared ${clearedCount} cache entries`);
|
|
211
|
-
return clearedCount;
|
|
212
|
-
}
|
|
213
|
-
/**
|
|
214
|
-
* Resolve SharePoint site URL to Graph API site ID
|
|
215
|
-
* Format: {hostname},{site-collection-id},{web-id}
|
|
216
|
-
* Example: contoso.sharepoint.com,00000000-0000-0000-0000-000000000000,11111111-1111-1111-1111-111111111111
|
|
217
|
-
*/
|
|
218
|
-
async resolveSiteId(siteUrl) {
|
|
219
|
-
const timer = auditLogger.startTimer();
|
|
220
|
-
// Check cache first
|
|
221
|
-
const cached = this.siteIdCache.get(siteUrl);
|
|
222
|
-
if (cached) {
|
|
223
|
-
return cached;
|
|
224
|
-
}
|
|
225
|
-
try {
|
|
226
|
-
// Parse site URL: https://contoso.sharepoint.com/sites/sitename
|
|
227
|
-
const url = new URL(siteUrl);
|
|
228
|
-
const hostname = url.hostname;
|
|
229
|
-
// Remove trailing slash from pathname
|
|
230
|
-
const pathname = url.pathname.replace(/\/+$/, '');
|
|
231
|
-
// Construct Graph API path: /sites/{hostname}:{path}
|
|
232
|
-
const graphPath = `/sites/${hostname}:${pathname}`;
|
|
233
|
-
console.error(`[SharePoint] Resolving site: ${graphPath}`);
|
|
234
|
-
const client = await this.getGraphClient();
|
|
235
|
-
const response = await client.api(graphPath).get();
|
|
236
|
-
if (!response || !response.id) {
|
|
237
|
-
throw new Error('Site ID not found in response');
|
|
238
|
-
}
|
|
239
|
-
const siteId = response.id;
|
|
240
|
-
// Cache for future use
|
|
241
|
-
this.siteIdCache.set(siteUrl, siteId);
|
|
242
|
-
auditLogger.log({
|
|
243
|
-
operation: 'resolve-site-id',
|
|
244
|
-
operationType: 'READ',
|
|
245
|
-
componentType: 'Site',
|
|
246
|
-
success: true,
|
|
247
|
-
parameters: { siteUrl },
|
|
248
|
-
executionTimeMs: timer(),
|
|
249
|
-
});
|
|
250
|
-
return siteId;
|
|
251
|
-
}
|
|
252
|
-
catch (error) {
|
|
253
|
-
// Extract detailed error info from Graph API - check multiple error formats
|
|
254
|
-
let errorDetail = error.message || 'Unknown error';
|
|
255
|
-
// Graph SDK error format
|
|
256
|
-
if (error.code) {
|
|
257
|
-
errorDetail = `${error.code}: ${errorDetail}`;
|
|
258
|
-
}
|
|
259
|
-
// Check for body (Graph API response)
|
|
260
|
-
if (error.body) {
|
|
261
|
-
try {
|
|
262
|
-
const body = typeof error.body === 'string' ? JSON.parse(error.body) : error.body;
|
|
263
|
-
if (body.error) {
|
|
264
|
-
errorDetail = `${body.error.code}: ${body.error.message}`;
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
catch {
|
|
268
|
-
// ignore parse errors
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
// Check for statusCode
|
|
272
|
-
if (error.statusCode) {
|
|
273
|
-
errorDetail = `HTTP ${error.statusCode}: ${errorDetail}`;
|
|
274
|
-
}
|
|
275
|
-
// Log full error object for debugging
|
|
276
|
-
console.error(`[SharePoint] resolveSiteId error details:`, JSON.stringify({
|
|
277
|
-
message: error.message,
|
|
278
|
-
code: error.code,
|
|
279
|
-
statusCode: error.statusCode,
|
|
280
|
-
body: error.body,
|
|
281
|
-
stack: error.stack?.split('\n').slice(0, 3).join('\n')
|
|
282
|
-
}, null, 2));
|
|
283
|
-
auditLogger.log({
|
|
284
|
-
operation: 'resolve-site-id',
|
|
285
|
-
operationType: 'READ',
|
|
286
|
-
componentType: 'Site',
|
|
287
|
-
success: false,
|
|
288
|
-
error: errorDetail,
|
|
289
|
-
parameters: { siteUrl },
|
|
290
|
-
executionTimeMs: timer(),
|
|
291
|
-
});
|
|
292
|
-
throw new Error(`Failed to resolve site ID for ${siteUrl}: ${errorDetail}`);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
/**
|
|
296
|
-
* Sanitize error messages (remove sensitive information like tokens)
|
|
297
|
-
*/
|
|
298
|
-
sanitizeErrorMessage(error) {
|
|
299
|
-
let message = error.message || error.toString();
|
|
300
|
-
// Remove tokens
|
|
301
|
-
message = message.replace(/Bearer\s+[A-Za-z0-9\-_.]+/gi, 'Bearer ***');
|
|
302
|
-
message = message.replace(/\b[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12}\b/g, '***');
|
|
303
|
-
// Handle Graph API errors
|
|
304
|
-
if (error.response?.data?.error) {
|
|
305
|
-
const graphError = error.response.data.error;
|
|
306
|
-
message = `${graphError.code}: ${graphError.message}`;
|
|
307
|
-
}
|
|
308
|
-
return message;
|
|
309
|
-
}
|
|
310
|
-
/**
|
|
311
|
-
* Handle errors with user-friendly messages
|
|
312
|
-
*/
|
|
313
|
-
handleError(error, context) {
|
|
314
|
-
let errorMessage = `SharePoint ${context} failed`;
|
|
315
|
-
if (error.response) {
|
|
316
|
-
const status = error.response.status;
|
|
317
|
-
const data = error.response.data;
|
|
318
|
-
switch (status) {
|
|
319
|
-
case 401:
|
|
320
|
-
errorMessage = 'Authentication failed. Check credentials and permissions.';
|
|
321
|
-
break;
|
|
322
|
-
case 403:
|
|
323
|
-
errorMessage = 'Access denied. Ensure service principal has Sites.Read.All and Files.Read.All permissions.';
|
|
324
|
-
break;
|
|
325
|
-
case 404:
|
|
326
|
-
errorMessage = 'Resource not found. Check site URL or item path.';
|
|
327
|
-
break;
|
|
328
|
-
case 429:
|
|
329
|
-
errorMessage = 'Rate limit exceeded. Reduce request frequency.';
|
|
330
|
-
if (error.response.headers['retry-after']) {
|
|
331
|
-
errorMessage += ` Retry after ${error.response.headers['retry-after']} seconds.`;
|
|
332
|
-
}
|
|
333
|
-
break;
|
|
334
|
-
default:
|
|
335
|
-
if (data?.error) {
|
|
336
|
-
errorMessage = `${data.error.code}: ${data.error.message}`;
|
|
337
|
-
}
|
|
338
|
-
else {
|
|
339
|
-
errorMessage = `HTTP ${status}: ${error.message}`;
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
else if (error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED') {
|
|
344
|
-
errorMessage = 'Network error: Unable to reach SharePoint/Graph API.';
|
|
345
|
-
}
|
|
346
|
-
else if (error.code === 'ETIMEDOUT') {
|
|
347
|
-
errorMessage = 'Request timeout. Try again later.';
|
|
348
|
-
}
|
|
349
|
-
else {
|
|
350
|
-
errorMessage = this.sanitizeErrorMessage(error);
|
|
351
|
-
}
|
|
352
|
-
return new Error(errorMessage);
|
|
353
|
-
}
|
|
354
|
-
/**
|
|
355
|
-
* Test connection to a SharePoint site
|
|
356
|
-
*/
|
|
357
|
-
async testConnection(siteId) {
|
|
358
|
-
const timer = auditLogger.startTimer();
|
|
359
|
-
try {
|
|
360
|
-
const site = this.getSiteById(siteId);
|
|
361
|
-
const siteInfo = await this.getSiteInfo(siteId);
|
|
362
|
-
auditLogger.log({
|
|
363
|
-
operation: 'test-connection',
|
|
364
|
-
operationType: 'READ',
|
|
365
|
-
componentType: 'Site',
|
|
366
|
-
componentName: site.name,
|
|
367
|
-
success: true,
|
|
368
|
-
parameters: { siteId },
|
|
369
|
-
executionTimeMs: timer(),
|
|
370
|
-
});
|
|
371
|
-
return {
|
|
372
|
-
success: true,
|
|
373
|
-
siteInfo,
|
|
374
|
-
timestamp: new Date().toISOString(),
|
|
375
|
-
};
|
|
376
|
-
}
|
|
377
|
-
catch (error) {
|
|
378
|
-
auditLogger.log({
|
|
379
|
-
operation: 'test-connection',
|
|
380
|
-
operationType: 'READ',
|
|
381
|
-
componentType: 'Site',
|
|
382
|
-
success: false,
|
|
383
|
-
error: this.sanitizeErrorMessage(error),
|
|
384
|
-
parameters: { siteId },
|
|
385
|
-
executionTimeMs: timer(),
|
|
386
|
-
});
|
|
387
|
-
return {
|
|
388
|
-
success: false,
|
|
389
|
-
error: this.sanitizeErrorMessage(error),
|
|
390
|
-
timestamp: new Date().toISOString(),
|
|
391
|
-
};
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
/**
|
|
395
|
-
* Get site information
|
|
396
|
-
*/
|
|
397
|
-
async getSiteInfo(siteId) {
|
|
398
|
-
const timer = auditLogger.startTimer();
|
|
399
|
-
const site = this.getSiteById(siteId);
|
|
400
|
-
// Check cache
|
|
401
|
-
const cacheKey = this.getCacheKey('GET', siteId, 'site-info');
|
|
402
|
-
const cached = this.getCached(cacheKey);
|
|
403
|
-
if (cached) {
|
|
404
|
-
return cached;
|
|
405
|
-
}
|
|
406
|
-
try {
|
|
407
|
-
const graphSiteId = await this.resolveSiteId(site.siteUrl);
|
|
408
|
-
const client = await this.getGraphClient();
|
|
409
|
-
const response = await client
|
|
410
|
-
.api(`/sites/${graphSiteId}`)
|
|
411
|
-
.select('id,webUrl,displayName,name,description,createdDateTime,lastModifiedDateTime,siteCollection')
|
|
412
|
-
.get();
|
|
413
|
-
const siteInfo = {
|
|
414
|
-
id: response.id,
|
|
415
|
-
webUrl: response.webUrl,
|
|
416
|
-
displayName: response.displayName,
|
|
417
|
-
name: response.name,
|
|
418
|
-
description: response.description,
|
|
419
|
-
createdDateTime: response.createdDateTime,
|
|
420
|
-
lastModifiedDateTime: response.lastModifiedDateTime,
|
|
421
|
-
siteCollection: response.siteCollection,
|
|
422
|
-
};
|
|
423
|
-
// Cache result
|
|
424
|
-
this.setCached(cacheKey, siteInfo);
|
|
425
|
-
auditLogger.log({
|
|
426
|
-
operation: 'get-site-info',
|
|
427
|
-
operationType: 'READ',
|
|
428
|
-
componentType: 'Site',
|
|
429
|
-
componentName: site.name,
|
|
430
|
-
success: true,
|
|
431
|
-
parameters: { siteId },
|
|
432
|
-
executionTimeMs: timer(),
|
|
433
|
-
});
|
|
434
|
-
return siteInfo;
|
|
435
|
-
}
|
|
436
|
-
catch (error) {
|
|
437
|
-
auditLogger.log({
|
|
438
|
-
operation: 'get-site-info',
|
|
439
|
-
operationType: 'READ',
|
|
440
|
-
componentType: 'Site',
|
|
441
|
-
componentName: site.name,
|
|
442
|
-
success: false,
|
|
443
|
-
error: this.sanitizeErrorMessage(error),
|
|
444
|
-
parameters: { siteId },
|
|
445
|
-
executionTimeMs: timer(),
|
|
446
|
-
});
|
|
447
|
-
throw this.handleError(error, 'get site info');
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
/**
|
|
451
|
-
* List drives (document libraries) in a site
|
|
452
|
-
*/
|
|
453
|
-
async listDrives(siteId) {
|
|
454
|
-
const timer = auditLogger.startTimer();
|
|
455
|
-
const site = this.getSiteById(siteId);
|
|
456
|
-
// Check cache
|
|
457
|
-
const cacheKey = this.getCacheKey('GET', siteId, 'drives');
|
|
458
|
-
const cached = this.getCached(cacheKey);
|
|
459
|
-
if (cached) {
|
|
460
|
-
return cached;
|
|
461
|
-
}
|
|
462
|
-
try {
|
|
463
|
-
const graphSiteId = await this.resolveSiteId(site.siteUrl);
|
|
464
|
-
const client = await this.getGraphClient();
|
|
465
|
-
const response = await client
|
|
466
|
-
.api(`/sites/${graphSiteId}/drives`)
|
|
467
|
-
.select('id,name,description,webUrl,driveType,createdDateTime,lastModifiedDateTime,quota,owner')
|
|
468
|
-
.get();
|
|
469
|
-
const drives = response.value || [];
|
|
470
|
-
// Cache result
|
|
471
|
-
this.setCached(cacheKey, drives);
|
|
472
|
-
auditLogger.log({
|
|
473
|
-
operation: 'list-drives',
|
|
474
|
-
operationType: 'READ',
|
|
475
|
-
componentType: 'Site',
|
|
476
|
-
componentName: site.name,
|
|
477
|
-
success: true,
|
|
478
|
-
parameters: { siteId, driveCount: drives.length },
|
|
479
|
-
executionTimeMs: timer(),
|
|
480
|
-
});
|
|
481
|
-
return drives;
|
|
482
|
-
}
|
|
483
|
-
catch (error) {
|
|
484
|
-
auditLogger.log({
|
|
485
|
-
operation: 'list-drives',
|
|
486
|
-
operationType: 'READ',
|
|
487
|
-
componentType: 'Site',
|
|
488
|
-
componentName: site.name,
|
|
489
|
-
success: false,
|
|
490
|
-
error: this.sanitizeErrorMessage(error),
|
|
491
|
-
parameters: { siteId },
|
|
492
|
-
executionTimeMs: timer(),
|
|
493
|
-
});
|
|
494
|
-
throw this.handleError(error, 'list drives');
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
/**
|
|
498
|
-
* Get drive (document library) information
|
|
499
|
-
*/
|
|
500
|
-
async getDriveInfo(siteId, driveId) {
|
|
501
|
-
const timer = auditLogger.startTimer();
|
|
502
|
-
const site = this.getSiteById(siteId);
|
|
503
|
-
// Check cache
|
|
504
|
-
const cacheKey = this.getCacheKey('GET', siteId, `drive:${driveId}`);
|
|
505
|
-
const cached = this.getCached(cacheKey);
|
|
506
|
-
if (cached) {
|
|
507
|
-
return cached;
|
|
508
|
-
}
|
|
509
|
-
try {
|
|
510
|
-
const client = await this.getGraphClient();
|
|
511
|
-
const response = await client
|
|
512
|
-
.api(`/drives/${driveId}`)
|
|
513
|
-
.select('id,name,description,webUrl,driveType,createdDateTime,lastModifiedDateTime,quota,owner')
|
|
514
|
-
.get();
|
|
515
|
-
const driveInfo = response;
|
|
516
|
-
// Cache result
|
|
517
|
-
this.setCached(cacheKey, driveInfo);
|
|
518
|
-
auditLogger.log({
|
|
519
|
-
operation: 'get-drive-info',
|
|
520
|
-
operationType: 'READ',
|
|
521
|
-
componentType: 'Drive',
|
|
522
|
-
componentName: site.name,
|
|
523
|
-
success: true,
|
|
524
|
-
parameters: { siteId, driveId },
|
|
525
|
-
executionTimeMs: timer(),
|
|
526
|
-
});
|
|
527
|
-
return driveInfo;
|
|
528
|
-
}
|
|
529
|
-
catch (error) {
|
|
530
|
-
auditLogger.log({
|
|
531
|
-
operation: 'get-drive-info',
|
|
532
|
-
operationType: 'READ',
|
|
533
|
-
componentType: 'Drive',
|
|
534
|
-
componentName: site.name,
|
|
535
|
-
success: false,
|
|
536
|
-
error: this.sanitizeErrorMessage(error),
|
|
537
|
-
parameters: { siteId, driveId },
|
|
538
|
-
executionTimeMs: timer(),
|
|
539
|
-
});
|
|
540
|
-
throw this.handleError(error, 'get drive info');
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
/**
|
|
544
|
-
* List items (files/folders) in a drive or folder
|
|
545
|
-
*/
|
|
546
|
-
async listItems(siteId, driveId, folderId) {
|
|
547
|
-
const timer = auditLogger.startTimer();
|
|
548
|
-
const site = this.getSiteById(siteId);
|
|
549
|
-
try {
|
|
550
|
-
const client = await this.getGraphClient();
|
|
551
|
-
const path = folderId
|
|
552
|
-
? `/drives/${driveId}/items/${folderId}/children`
|
|
553
|
-
: `/drives/${driveId}/root/children`;
|
|
554
|
-
const response = await client
|
|
555
|
-
.api(path)
|
|
556
|
-
.select('id,name,webUrl,size,createdDateTime,lastModifiedDateTime,createdBy,lastModifiedBy,file,folder,parentReference')
|
|
557
|
-
.get();
|
|
558
|
-
const items = response.value || [];
|
|
559
|
-
auditLogger.log({
|
|
560
|
-
operation: 'list-items',
|
|
561
|
-
operationType: 'READ',
|
|
562
|
-
componentType: 'Drive',
|
|
563
|
-
componentName: site.name,
|
|
564
|
-
success: true,
|
|
565
|
-
parameters: { siteId, driveId, folderId, itemCount: items.length },
|
|
566
|
-
executionTimeMs: timer(),
|
|
567
|
-
});
|
|
568
|
-
return items;
|
|
569
|
-
}
|
|
570
|
-
catch (error) {
|
|
571
|
-
auditLogger.log({
|
|
572
|
-
operation: 'list-items',
|
|
573
|
-
operationType: 'READ',
|
|
574
|
-
componentType: 'Drive',
|
|
575
|
-
componentName: site.name,
|
|
576
|
-
success: false,
|
|
577
|
-
error: this.sanitizeErrorMessage(error),
|
|
578
|
-
parameters: { siteId, driveId, folderId },
|
|
579
|
-
executionTimeMs: timer(),
|
|
580
|
-
});
|
|
581
|
-
throw this.handleError(error, 'list items');
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
/**
|
|
585
|
-
* Get item (file/folder) by ID
|
|
586
|
-
*/
|
|
587
|
-
async getItem(siteId, driveId, itemId) {
|
|
588
|
-
const timer = auditLogger.startTimer();
|
|
589
|
-
const site = this.getSiteById(siteId);
|
|
590
|
-
try {
|
|
591
|
-
const client = await this.getGraphClient();
|
|
592
|
-
const response = await client
|
|
593
|
-
.api(`/drives/${driveId}/items/${itemId}`)
|
|
594
|
-
.select('id,name,webUrl,size,createdDateTime,lastModifiedDateTime,createdBy,lastModifiedBy,file,folder,parentReference')
|
|
595
|
-
.get();
|
|
596
|
-
const item = response;
|
|
597
|
-
auditLogger.log({
|
|
598
|
-
operation: 'get-item',
|
|
599
|
-
operationType: 'READ',
|
|
600
|
-
componentType: 'Item',
|
|
601
|
-
componentName: site.name,
|
|
602
|
-
success: true,
|
|
603
|
-
parameters: { siteId, driveId, itemId },
|
|
604
|
-
executionTimeMs: timer(),
|
|
605
|
-
});
|
|
606
|
-
return item;
|
|
607
|
-
}
|
|
608
|
-
catch (error) {
|
|
609
|
-
auditLogger.log({
|
|
610
|
-
operation: 'get-item',
|
|
611
|
-
operationType: 'READ',
|
|
612
|
-
componentType: 'Item',
|
|
613
|
-
componentName: site.name,
|
|
614
|
-
success: false,
|
|
615
|
-
error: this.sanitizeErrorMessage(error),
|
|
616
|
-
parameters: { siteId, driveId, itemId },
|
|
617
|
-
executionTimeMs: timer(),
|
|
618
|
-
});
|
|
619
|
-
throw this.handleError(error, 'get item');
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
/**
|
|
623
|
-
* Get item by path (relative to drive root)
|
|
624
|
-
*/
|
|
625
|
-
async getItemByPath(siteId, driveId, path) {
|
|
626
|
-
const timer = auditLogger.startTimer();
|
|
627
|
-
const site = this.getSiteById(siteId);
|
|
628
|
-
try {
|
|
629
|
-
const client = await this.getGraphClient();
|
|
630
|
-
// Ensure path starts with /
|
|
631
|
-
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
|
632
|
-
const response = await client
|
|
633
|
-
.api(`/drives/${driveId}/root:${normalizedPath}`)
|
|
634
|
-
.select('id,name,webUrl,size,createdDateTime,lastModifiedDateTime,createdBy,lastModifiedBy,file,folder,parentReference')
|
|
635
|
-
.get();
|
|
636
|
-
const item = response;
|
|
637
|
-
auditLogger.log({
|
|
638
|
-
operation: 'get-item-by-path',
|
|
639
|
-
operationType: 'READ',
|
|
640
|
-
componentType: 'Item',
|
|
641
|
-
componentName: site.name,
|
|
642
|
-
success: true,
|
|
643
|
-
parameters: { siteId, driveId, path },
|
|
644
|
-
executionTimeMs: timer(),
|
|
645
|
-
});
|
|
646
|
-
return item;
|
|
647
|
-
}
|
|
648
|
-
catch (error) {
|
|
649
|
-
auditLogger.log({
|
|
650
|
-
operation: 'get-item-by-path',
|
|
651
|
-
operationType: 'READ',
|
|
652
|
-
componentType: 'Item',
|
|
653
|
-
componentName: site.name,
|
|
654
|
-
success: false,
|
|
655
|
-
error: this.sanitizeErrorMessage(error),
|
|
656
|
-
parameters: { siteId, driveId, path },
|
|
657
|
-
executionTimeMs: timer(),
|
|
658
|
-
});
|
|
659
|
-
throw this.handleError(error, 'get item by path');
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
/**
|
|
663
|
-
* Search items in a drive or site
|
|
664
|
-
*/
|
|
665
|
-
async searchItems(siteId, query, driveId, limit) {
|
|
666
|
-
const timer = auditLogger.startTimer();
|
|
667
|
-
const site = this.getSiteById(siteId);
|
|
668
|
-
const maxResults = Math.min(limit || 100, this.config.maxSearchResults || 100);
|
|
669
|
-
try {
|
|
670
|
-
const graphSiteId = await this.resolveSiteId(site.siteUrl);
|
|
671
|
-
const client = await this.getGraphClient();
|
|
672
|
-
const path = driveId
|
|
673
|
-
? `/drives/${driveId}/root/search(q='${encodeURIComponent(query)}')`
|
|
674
|
-
: `/sites/${graphSiteId}/drive/root/search(q='${encodeURIComponent(query)}')`;
|
|
675
|
-
const response = await client
|
|
676
|
-
.api(path)
|
|
677
|
-
.top(maxResults)
|
|
678
|
-
.select('id,name,webUrl,size,createdDateTime,lastModifiedDateTime,createdBy,lastModifiedBy,file,folder,parentReference')
|
|
679
|
-
.get();
|
|
680
|
-
const items = response.value || [];
|
|
681
|
-
auditLogger.log({
|
|
682
|
-
operation: 'search-items',
|
|
683
|
-
operationType: 'READ',
|
|
684
|
-
componentType: 'Search',
|
|
685
|
-
componentName: site.name,
|
|
686
|
-
success: true,
|
|
687
|
-
parameters: { siteId, query, driveId, resultCount: items.length },
|
|
688
|
-
executionTimeMs: timer(),
|
|
689
|
-
});
|
|
690
|
-
return {
|
|
691
|
-
items,
|
|
692
|
-
totalCount: items.length,
|
|
693
|
-
};
|
|
694
|
-
}
|
|
695
|
-
catch (error) {
|
|
696
|
-
auditLogger.log({
|
|
697
|
-
operation: 'search-items',
|
|
698
|
-
operationType: 'READ',
|
|
699
|
-
componentType: 'Search',
|
|
700
|
-
componentName: site.name,
|
|
701
|
-
success: false,
|
|
702
|
-
error: this.sanitizeErrorMessage(error),
|
|
703
|
-
parameters: { siteId, query, driveId },
|
|
704
|
-
executionTimeMs: timer(),
|
|
705
|
-
});
|
|
706
|
-
throw this.handleError(error, 'search items');
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
/**
|
|
710
|
-
* Get recent items in a drive
|
|
711
|
-
*/
|
|
712
|
-
async getRecentItems(siteId, driveId, limit, days) {
|
|
713
|
-
const timer = auditLogger.startTimer();
|
|
714
|
-
const site = this.getSiteById(siteId);
|
|
715
|
-
const maxResults = Math.min(limit || 20, 100);
|
|
716
|
-
const daysBack = days || 30;
|
|
717
|
-
try {
|
|
718
|
-
const client = await this.getGraphClient();
|
|
719
|
-
// Calculate date threshold
|
|
720
|
-
const dateThreshold = new Date();
|
|
721
|
-
dateThreshold.setDate(dateThreshold.getDate() - daysBack);
|
|
722
|
-
const dateFilter = dateThreshold.toISOString();
|
|
723
|
-
const response = await client
|
|
724
|
-
.api(`/drives/${driveId}/root/children`)
|
|
725
|
-
.select('id,name,webUrl,size,createdDateTime,lastModifiedDateTime,createdBy,lastModifiedBy,file,folder,parentReference')
|
|
726
|
-
.filter(`lastModifiedDateTime gt ${dateFilter}`)
|
|
727
|
-
.orderby('lastModifiedDateTime desc')
|
|
728
|
-
.top(maxResults)
|
|
729
|
-
.get();
|
|
730
|
-
const items = response.value || [];
|
|
731
|
-
auditLogger.log({
|
|
732
|
-
operation: 'get-recent-items',
|
|
733
|
-
operationType: 'READ',
|
|
734
|
-
componentType: 'Drive',
|
|
735
|
-
componentName: site.name,
|
|
736
|
-
success: true,
|
|
737
|
-
parameters: { siteId, driveId, limit: maxResults, days: daysBack, resultCount: items.length },
|
|
738
|
-
executionTimeMs: timer(),
|
|
739
|
-
});
|
|
740
|
-
return items;
|
|
741
|
-
}
|
|
742
|
-
catch (error) {
|
|
743
|
-
auditLogger.log({
|
|
744
|
-
operation: 'get-recent-items',
|
|
745
|
-
operationType: 'READ',
|
|
746
|
-
componentType: 'Drive',
|
|
747
|
-
componentName: site.name,
|
|
748
|
-
success: false,
|
|
749
|
-
error: this.sanitizeErrorMessage(error),
|
|
750
|
-
parameters: { siteId, driveId, limit: maxResults, days: daysBack },
|
|
751
|
-
executionTimeMs: timer(),
|
|
752
|
-
});
|
|
753
|
-
throw this.handleError(error, 'get recent items');
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
/**
|
|
757
|
-
* Get folder structure (recursive)
|
|
758
|
-
*/
|
|
759
|
-
async getFolderStructure(siteId, driveId, folderId, depth) {
|
|
760
|
-
const timer = auditLogger.startTimer();
|
|
761
|
-
const site = this.getSiteById(siteId);
|
|
762
|
-
const maxDepth = Math.min(depth || 3, 10); // Limit to 10 levels max
|
|
763
|
-
try {
|
|
764
|
-
// Get root folder or specified folder
|
|
765
|
-
const rootItem = folderId
|
|
766
|
-
? await this.getItem(siteId, driveId, folderId)
|
|
767
|
-
: await this.getItemByPath(siteId, driveId, '/');
|
|
768
|
-
// Build tree recursively
|
|
769
|
-
const tree = await this.buildFolderTree(siteId, driveId, rootItem, 0, maxDepth);
|
|
770
|
-
auditLogger.log({
|
|
771
|
-
operation: 'get-folder-structure',
|
|
772
|
-
operationType: 'READ',
|
|
773
|
-
componentType: 'Drive',
|
|
774
|
-
componentName: site.name,
|
|
775
|
-
success: true,
|
|
776
|
-
parameters: { siteId, driveId, folderId, depth: maxDepth },
|
|
777
|
-
executionTimeMs: timer(),
|
|
778
|
-
});
|
|
779
|
-
return tree;
|
|
780
|
-
}
|
|
781
|
-
catch (error) {
|
|
782
|
-
auditLogger.log({
|
|
783
|
-
operation: 'get-folder-structure',
|
|
784
|
-
operationType: 'READ',
|
|
785
|
-
componentType: 'Drive',
|
|
786
|
-
componentName: site.name,
|
|
787
|
-
success: false,
|
|
788
|
-
error: this.sanitizeErrorMessage(error),
|
|
789
|
-
parameters: { siteId, driveId, folderId, depth: maxDepth },
|
|
790
|
-
executionTimeMs: timer(),
|
|
791
|
-
});
|
|
792
|
-
throw this.handleError(error, 'get folder structure');
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
/**
|
|
796
|
-
* Build folder tree recursively
|
|
797
|
-
*/
|
|
798
|
-
async buildFolderTree(siteId, driveId, item, currentDepth, maxDepth) {
|
|
799
|
-
const tree = { item };
|
|
800
|
-
// Stop if max depth reached or item is a file
|
|
801
|
-
if (currentDepth >= maxDepth || !item.folder) {
|
|
802
|
-
return tree;
|
|
803
|
-
}
|
|
804
|
-
// Get children
|
|
805
|
-
const children = await this.listItems(siteId, driveId, item.id);
|
|
806
|
-
// Recursively build tree for folders
|
|
807
|
-
tree.children = await Promise.all(children
|
|
808
|
-
.filter(child => child.folder)
|
|
809
|
-
.map(child => this.buildFolderTree(siteId, driveId, child, currentDepth + 1, maxDepth)));
|
|
810
|
-
return tree;
|
|
811
|
-
}
|
|
812
|
-
// ============================================================================
|
|
813
|
-
// POWERPLATFORM VALIDATION METHODS (HIGH PRIORITY)
|
|
814
|
-
// ============================================================================
|
|
815
|
-
// These methods integrate SharePoint with PowerPlatform Dataverse to validate
|
|
816
|
-
// document location configurations and migrations.
|
|
817
|
-
// ============================================================================
|
|
818
|
-
/**
|
|
819
|
-
* Get CRM document locations from PowerPlatform
|
|
820
|
-
*
|
|
821
|
-
* Queries the sharepointdocumentlocation entity in Dataverse to retrieve
|
|
822
|
-
* document location configurations for validation.
|
|
823
|
-
*
|
|
824
|
-
* @param powerPlatformService PowerPlatformService instance for querying Dataverse
|
|
825
|
-
* @param entityName Optional entity logical name to filter by (e.g., 'account', 'contact')
|
|
826
|
-
* @param recordId Optional record ID to filter by specific record
|
|
827
|
-
* @returns Array of SharePoint document location records
|
|
828
|
-
*/
|
|
829
|
-
async getCrmDocumentLocations(powerPlatformService, entityName, recordId) {
|
|
830
|
-
const timer = auditLogger.startTimer();
|
|
831
|
-
try {
|
|
832
|
-
// Build OData filter
|
|
833
|
-
let filter = 'statecode eq 0'; // Active only
|
|
834
|
-
if (entityName && recordId) {
|
|
835
|
-
// Filter by specific record
|
|
836
|
-
filter += ` and _regardingobjectid_value eq ${recordId}`;
|
|
837
|
-
}
|
|
838
|
-
else if (entityName) {
|
|
839
|
-
// Filter by entity type (need to query by logical name)
|
|
840
|
-
// This requires a join which is complex in OData, so we'll fetch all and filter client-side
|
|
841
|
-
}
|
|
842
|
-
// Query sharepointdocumentlocation entity
|
|
843
|
-
const response = await powerPlatformService.queryRecords('sharepointdocumentlocations', filter, 1000 // Max records
|
|
844
|
-
);
|
|
845
|
-
// Parse response
|
|
846
|
-
const locations = [];
|
|
847
|
-
for (const record of response.value || []) {
|
|
848
|
-
const location = {
|
|
849
|
-
sharepointdocumentlocationid: record.sharepointdocumentlocationid,
|
|
850
|
-
name: record.name || '',
|
|
851
|
-
absoluteurl: record.absoluteurl || '',
|
|
852
|
-
relativeurl: record.relativeurl || '',
|
|
853
|
-
statecode: record.statecode || 0,
|
|
854
|
-
statuscode: record.statuscode || 1,
|
|
855
|
-
};
|
|
856
|
-
// Parse regarding object (related entity)
|
|
857
|
-
if (record._regardingobjectid_value) {
|
|
858
|
-
location.regardingobjectid = {
|
|
859
|
-
id: record._regardingobjectid_value,
|
|
860
|
-
logicalName: record['_regardingobjectid_value@Microsoft.Dynamics.CRM.lookuplogicalname'] || '',
|
|
861
|
-
};
|
|
862
|
-
}
|
|
863
|
-
// Parse parent site or location
|
|
864
|
-
if (record._parentsiteorlocation_value) {
|
|
865
|
-
location.parentsiteorlocation = {
|
|
866
|
-
id: record._parentsiteorlocation_value,
|
|
867
|
-
logicalName: record['_parentsiteorlocation_value@Microsoft.Dynamics.CRM.lookuplogicalname'] || '',
|
|
868
|
-
};
|
|
869
|
-
}
|
|
870
|
-
// Site collection ID
|
|
871
|
-
if (record.sitecollectionid) {
|
|
872
|
-
location.sitecollectionid = record.sitecollectionid;
|
|
873
|
-
}
|
|
874
|
-
locations.push(location);
|
|
875
|
-
}
|
|
876
|
-
// Client-side filtering by entity name if needed
|
|
877
|
-
let filtered = locations;
|
|
878
|
-
if (entityName && !recordId) {
|
|
879
|
-
filtered = locations.filter(loc => loc.regardingobjectid?.logicalName === entityName);
|
|
880
|
-
}
|
|
881
|
-
auditLogger.log({
|
|
882
|
-
operation: 'get-crm-document-locations',
|
|
883
|
-
operationType: 'READ',
|
|
884
|
-
componentType: 'DocumentLocation',
|
|
885
|
-
success: true,
|
|
886
|
-
parameters: { entityName, recordId, resultCount: filtered.length },
|
|
887
|
-
executionTimeMs: timer(),
|
|
888
|
-
});
|
|
889
|
-
return filtered;
|
|
890
|
-
}
|
|
891
|
-
catch (error) {
|
|
892
|
-
auditLogger.log({
|
|
893
|
-
operation: 'get-crm-document-locations',
|
|
894
|
-
operationType: 'READ',
|
|
895
|
-
componentType: 'DocumentLocation',
|
|
896
|
-
success: false,
|
|
897
|
-
error: this.sanitizeErrorMessage(error),
|
|
898
|
-
parameters: { entityName, recordId },
|
|
899
|
-
executionTimeMs: timer(),
|
|
900
|
-
});
|
|
901
|
-
throw this.handleError(error, 'get CRM document locations');
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
|
-
/**
|
|
905
|
-
* Validate a document location configuration
|
|
906
|
-
*
|
|
907
|
-
* Validates that a PowerPlatform document location configuration matches
|
|
908
|
-
* the actual SharePoint site structure. Checks:
|
|
909
|
-
* - Site exists and is accessible
|
|
910
|
-
* - Folder exists at the specified path
|
|
911
|
-
* - Folder is accessible
|
|
912
|
-
* - File count and empty folder detection
|
|
913
|
-
*
|
|
914
|
-
* @param powerPlatformService PowerPlatformService instance
|
|
915
|
-
* @param documentLocationId GUID of the sharepointdocumentlocation record
|
|
916
|
-
* @returns Validation result with status, issues, and recommendations
|
|
917
|
-
*/
|
|
918
|
-
async validateDocumentLocation(powerPlatformService, documentLocationId) {
|
|
919
|
-
const timer = auditLogger.startTimer();
|
|
920
|
-
try {
|
|
921
|
-
// Get document location from CRM
|
|
922
|
-
const record = await powerPlatformService.getRecord('sharepointdocumentlocations', documentLocationId);
|
|
923
|
-
if (!record) {
|
|
924
|
-
throw new Error(`Document location ${documentLocationId} not found`);
|
|
925
|
-
}
|
|
926
|
-
// Parse CRM configuration
|
|
927
|
-
const absoluteUrl = record.absoluteurl || '';
|
|
928
|
-
const relativeUrl = record.relativeurl || '';
|
|
929
|
-
const regardingEntityName = record['_regardingobjectid_value@Microsoft.Dynamics.CRM.lookuplogicalname'] || '';
|
|
930
|
-
const regardingRecordId = record._regardingobjectid_value || '';
|
|
931
|
-
const isActive = record.statecode === 0;
|
|
932
|
-
// Initialize validation result
|
|
933
|
-
const result = {
|
|
934
|
-
documentLocationId,
|
|
935
|
-
documentLocationName: record.name || '',
|
|
936
|
-
crmConfig: {
|
|
937
|
-
absoluteUrl,
|
|
938
|
-
relativeUrl,
|
|
939
|
-
regardingEntity: regardingEntityName,
|
|
940
|
-
regardingRecordId,
|
|
941
|
-
isActive,
|
|
942
|
-
},
|
|
943
|
-
spoValidation: {
|
|
944
|
-
siteExists: false,
|
|
945
|
-
folderExists: false,
|
|
946
|
-
folderAccessible: false,
|
|
947
|
-
fileCount: 0,
|
|
948
|
-
isEmpty: true,
|
|
949
|
-
},
|
|
950
|
-
status: 'error',
|
|
951
|
-
issues: [],
|
|
952
|
-
recommendations: [],
|
|
953
|
-
};
|
|
954
|
-
// Check if absolute URL is configured
|
|
955
|
-
if (!absoluteUrl) {
|
|
956
|
-
result.issues.push('Absolute URL is not configured in CRM');
|
|
957
|
-
result.recommendations.push('Configure the absoluteurl field in the document location record');
|
|
958
|
-
auditLogger.log({
|
|
959
|
-
operation: 'validate-document-location',
|
|
960
|
-
operationType: 'READ',
|
|
961
|
-
componentType: 'DocumentLocation',
|
|
962
|
-
componentName: result.documentLocationName,
|
|
963
|
-
success: true,
|
|
964
|
-
parameters: { documentLocationId, status: result.status },
|
|
965
|
-
executionTimeMs: timer(),
|
|
966
|
-
});
|
|
967
|
-
return result;
|
|
968
|
-
}
|
|
969
|
-
// Parse site URL and folder path from absolute URL
|
|
970
|
-
// Format: https://contoso.sharepoint.com/sites/sitename/DocumentLibrary/folder1/folder2
|
|
971
|
-
let siteUrl;
|
|
972
|
-
let folderPath;
|
|
973
|
-
try {
|
|
974
|
-
const url = new URL(absoluteUrl);
|
|
975
|
-
const pathParts = url.pathname.split('/').filter(p => p);
|
|
976
|
-
// Find /sites/ index
|
|
977
|
-
const sitesIndex = pathParts.indexOf('sites');
|
|
978
|
-
if (sitesIndex === -1) {
|
|
979
|
-
throw new Error('URL does not contain /sites/ path');
|
|
980
|
-
}
|
|
981
|
-
// Site URL: https://contoso.sharepoint.com/sites/sitename
|
|
982
|
-
const siteName = pathParts[sitesIndex + 1];
|
|
983
|
-
siteUrl = `${url.protocol}//${url.hostname}/sites/${siteName}`;
|
|
984
|
-
// Folder path: /DocumentLibrary/folder1/folder2
|
|
985
|
-
const libraryAndFolder = pathParts.slice(sitesIndex + 2);
|
|
986
|
-
folderPath = '/' + libraryAndFolder.join('/');
|
|
987
|
-
}
|
|
988
|
-
catch (parseError) {
|
|
989
|
-
result.issues.push(`Failed to parse absolute URL: ${parseError.message}`);
|
|
990
|
-
result.recommendations.push('Verify the absolute URL format in CRM');
|
|
991
|
-
auditLogger.log({
|
|
992
|
-
operation: 'validate-document-location',
|
|
993
|
-
operationType: 'READ',
|
|
994
|
-
componentType: 'DocumentLocation',
|
|
995
|
-
componentName: result.documentLocationName,
|
|
996
|
-
success: true,
|
|
997
|
-
parameters: { documentLocationId, status: result.status },
|
|
998
|
-
executionTimeMs: timer(),
|
|
999
|
-
});
|
|
1000
|
-
return result;
|
|
1001
|
-
}
|
|
1002
|
-
// Validate site exists
|
|
1003
|
-
try {
|
|
1004
|
-
const siteId = await this.resolveSiteId(siteUrl);
|
|
1005
|
-
const siteInfo = await this.getSiteInfo(siteId);
|
|
1006
|
-
result.spoValidation.siteExists = true;
|
|
1007
|
-
}
|
|
1008
|
-
catch (siteError) {
|
|
1009
|
-
result.issues.push(`SharePoint site not found: ${siteUrl}`);
|
|
1010
|
-
result.recommendations.push('Verify the site URL is correct and accessible');
|
|
1011
|
-
result.recommendations.push('Check that the service principal has access to the site');
|
|
1012
|
-
auditLogger.log({
|
|
1013
|
-
operation: 'validate-document-location',
|
|
1014
|
-
operationType: 'READ',
|
|
1015
|
-
componentType: 'DocumentLocation',
|
|
1016
|
-
componentName: result.documentLocationName,
|
|
1017
|
-
success: true,
|
|
1018
|
-
parameters: { documentLocationId, status: result.status },
|
|
1019
|
-
executionTimeMs: timer(),
|
|
1020
|
-
});
|
|
1021
|
-
return result;
|
|
1022
|
-
}
|
|
1023
|
-
// Find the site in configured sites
|
|
1024
|
-
const configuredSite = this.config.sites.find(s => s.siteUrl === siteUrl);
|
|
1025
|
-
if (!configuredSite) {
|
|
1026
|
-
result.issues.push(`Site ${siteUrl} is not configured in SHAREPOINT_SITES`);
|
|
1027
|
-
result.recommendations.push('Add the site to SHAREPOINT_SITES configuration');
|
|
1028
|
-
result.status = 'warning';
|
|
1029
|
-
auditLogger.log({
|
|
1030
|
-
operation: 'validate-document-location',
|
|
1031
|
-
operationType: 'READ',
|
|
1032
|
-
componentType: 'DocumentLocation',
|
|
1033
|
-
componentName: result.documentLocationName,
|
|
1034
|
-
success: true,
|
|
1035
|
-
parameters: { documentLocationId, status: result.status },
|
|
1036
|
-
executionTimeMs: timer(),
|
|
1037
|
-
});
|
|
1038
|
-
return result;
|
|
1039
|
-
}
|
|
1040
|
-
const siteId = configuredSite.id;
|
|
1041
|
-
// Validate folder exists and is accessible
|
|
1042
|
-
try {
|
|
1043
|
-
// Get drives (document libraries)
|
|
1044
|
-
const drives = await this.listDrives(siteId);
|
|
1045
|
-
// Parse library name from folder path
|
|
1046
|
-
const libraryName = folderPath.split('/').filter(p => p)[0];
|
|
1047
|
-
const drive = drives.find(d => d.name === libraryName);
|
|
1048
|
-
if (!drive) {
|
|
1049
|
-
result.issues.push(`Document library '${libraryName}' not found in site`);
|
|
1050
|
-
result.recommendations.push('Verify the library name in the absolute URL');
|
|
1051
|
-
result.status = 'warning';
|
|
1052
|
-
auditLogger.log({
|
|
1053
|
-
operation: 'validate-document-location',
|
|
1054
|
-
operationType: 'READ',
|
|
1055
|
-
componentType: 'DocumentLocation',
|
|
1056
|
-
componentName: result.documentLocationName,
|
|
1057
|
-
success: true,
|
|
1058
|
-
parameters: { documentLocationId, status: result.status },
|
|
1059
|
-
executionTimeMs: timer(),
|
|
1060
|
-
});
|
|
1061
|
-
return result;
|
|
1062
|
-
}
|
|
1063
|
-
result.spoValidation.folderExists = true;
|
|
1064
|
-
// Get folder contents
|
|
1065
|
-
try {
|
|
1066
|
-
const items = await this.getItemByPath(siteId, drive.id, folderPath);
|
|
1067
|
-
result.spoValidation.folderAccessible = true;
|
|
1068
|
-
// Count files in folder
|
|
1069
|
-
if (items.folder) {
|
|
1070
|
-
const folderContents = await this.listItems(siteId, drive.id, items.id);
|
|
1071
|
-
result.spoValidation.fileCount = folderContents.length;
|
|
1072
|
-
result.spoValidation.isEmpty = folderContents.length === 0;
|
|
1073
|
-
}
|
|
1074
|
-
// Determine overall status
|
|
1075
|
-
if (result.spoValidation.isEmpty) {
|
|
1076
|
-
result.status = 'warning';
|
|
1077
|
-
result.issues.push('Folder is empty (no files found)');
|
|
1078
|
-
result.recommendations.push('Upload documents to the folder or verify the folder path');
|
|
1079
|
-
}
|
|
1080
|
-
else {
|
|
1081
|
-
result.status = 'valid';
|
|
1082
|
-
}
|
|
1083
|
-
}
|
|
1084
|
-
catch (folderError) {
|
|
1085
|
-
result.spoValidation.folderExists = false;
|
|
1086
|
-
result.issues.push(`Folder not accessible at path: ${folderPath}`);
|
|
1087
|
-
result.recommendations.push('Verify the folder path is correct');
|
|
1088
|
-
result.recommendations.push('Check that the folder exists in SharePoint');
|
|
1089
|
-
result.status = 'error';
|
|
1090
|
-
}
|
|
1091
|
-
}
|
|
1092
|
-
catch (driveError) {
|
|
1093
|
-
result.issues.push(`Failed to access document libraries: ${driveError.message}`);
|
|
1094
|
-
result.recommendations.push('Verify service principal has Read permissions on the site');
|
|
1095
|
-
result.status = 'error';
|
|
1096
|
-
}
|
|
1097
|
-
auditLogger.log({
|
|
1098
|
-
operation: 'validate-document-location',
|
|
1099
|
-
operationType: 'READ',
|
|
1100
|
-
componentType: 'DocumentLocation',
|
|
1101
|
-
componentName: result.documentLocationName,
|
|
1102
|
-
success: true,
|
|
1103
|
-
parameters: { documentLocationId, status: result.status },
|
|
1104
|
-
executionTimeMs: timer(),
|
|
1105
|
-
});
|
|
1106
|
-
return result;
|
|
1107
|
-
}
|
|
1108
|
-
catch (error) {
|
|
1109
|
-
auditLogger.log({
|
|
1110
|
-
operation: 'validate-document-location',
|
|
1111
|
-
operationType: 'READ',
|
|
1112
|
-
componentType: 'DocumentLocation',
|
|
1113
|
-
success: false,
|
|
1114
|
-
error: this.sanitizeErrorMessage(error),
|
|
1115
|
-
parameters: { documentLocationId },
|
|
1116
|
-
executionTimeMs: timer(),
|
|
1117
|
-
});
|
|
1118
|
-
throw this.handleError(error, 'validate document location');
|
|
1119
|
-
}
|
|
1120
|
-
}
|
|
1121
|
-
/**
|
|
1122
|
-
* Verify document migration between two SharePoint locations
|
|
1123
|
-
*
|
|
1124
|
-
* Compares file counts, sizes, and names between source and target folders
|
|
1125
|
-
* to verify successful migration.
|
|
1126
|
-
*
|
|
1127
|
-
* @param powerPlatformService PowerPlatformService instance (unused but kept for consistency)
|
|
1128
|
-
* @param sourceSiteId Source SharePoint site ID
|
|
1129
|
-
* @param sourcePath Source folder path
|
|
1130
|
-
* @param targetSiteId Target SharePoint site ID
|
|
1131
|
-
* @param targetPath Target folder path
|
|
1132
|
-
* @returns Migration verification result with comparison details
|
|
1133
|
-
*/
|
|
1134
|
-
async verifyDocumentMigration(powerPlatformService, sourceSiteId, sourcePath, targetSiteId, targetPath) {
|
|
1135
|
-
const timer = auditLogger.startTimer();
|
|
1136
|
-
try {
|
|
1137
|
-
const sourceSite = this.getSiteById(sourceSiteId);
|
|
1138
|
-
const targetSite = this.getSiteById(targetSiteId);
|
|
1139
|
-
// Get source folder contents
|
|
1140
|
-
const sourceDrives = await this.listDrives(sourceSiteId);
|
|
1141
|
-
const sourceLibraryName = sourcePath.split('/').filter(p => p)[0];
|
|
1142
|
-
const sourceDrive = sourceDrives.find(d => d.name === sourceLibraryName);
|
|
1143
|
-
if (!sourceDrive) {
|
|
1144
|
-
throw new Error(`Source library '${sourceLibraryName}' not found`);
|
|
1145
|
-
}
|
|
1146
|
-
const sourceFolder = await this.getItemByPath(sourceSiteId, sourceDrive.id, sourcePath);
|
|
1147
|
-
const sourceItems = await this.listItems(sourceSiteId, sourceDrive.id, sourceFolder.id);
|
|
1148
|
-
// Get target folder contents
|
|
1149
|
-
const targetDrives = await this.listDrives(targetSiteId);
|
|
1150
|
-
const targetLibraryName = targetPath.split('/').filter(p => p)[0];
|
|
1151
|
-
const targetDrive = targetDrives.find(d => d.name === targetLibraryName);
|
|
1152
|
-
if (!targetDrive) {
|
|
1153
|
-
throw new Error(`Target library '${targetLibraryName}' not found`);
|
|
1154
|
-
}
|
|
1155
|
-
const targetFolder = await this.getItemByPath(targetSiteId, targetDrive.id, targetPath);
|
|
1156
|
-
const targetItems = await this.listItems(targetSiteId, targetDrive.id, targetFolder.id);
|
|
1157
|
-
// Calculate totals
|
|
1158
|
-
const sourceTotalSize = sourceItems.reduce((sum, item) => sum + (item.size || 0), 0);
|
|
1159
|
-
const targetTotalSize = targetItems.reduce((sum, item) => sum + (item.size || 0), 0);
|
|
1160
|
-
// Compare files
|
|
1161
|
-
const sourceFileNames = new Set(sourceItems.map(i => i.name));
|
|
1162
|
-
const targetFileNames = new Set(targetItems.map(i => i.name));
|
|
1163
|
-
const missingFiles = sourceItems
|
|
1164
|
-
.filter(i => !targetFileNames.has(i.name))
|
|
1165
|
-
.map(i => i.name);
|
|
1166
|
-
const extraFiles = targetItems
|
|
1167
|
-
.filter(i => !sourceFileNames.has(i.name))
|
|
1168
|
-
.map(i => i.name);
|
|
1169
|
-
// Compare sizes
|
|
1170
|
-
const sizeMismatches = [];
|
|
1171
|
-
const modifiedDateMismatches = [];
|
|
1172
|
-
for (const sourceItem of sourceItems) {
|
|
1173
|
-
const targetItem = targetItems.find(t => t.name === sourceItem.name);
|
|
1174
|
-
if (targetItem) {
|
|
1175
|
-
if (sourceItem.size !== targetItem.size) {
|
|
1176
|
-
sizeMismatches.push({
|
|
1177
|
-
name: sourceItem.name,
|
|
1178
|
-
sourceSize: sourceItem.size || 0,
|
|
1179
|
-
targetSize: targetItem.size || 0,
|
|
1180
|
-
});
|
|
1181
|
-
}
|
|
1182
|
-
if (sourceItem.lastModifiedDateTime !== targetItem.lastModifiedDateTime) {
|
|
1183
|
-
modifiedDateMismatches.push({
|
|
1184
|
-
name: sourceItem.name,
|
|
1185
|
-
sourceDate: sourceItem.lastModifiedDateTime,
|
|
1186
|
-
targetDate: targetItem.lastModifiedDateTime,
|
|
1187
|
-
});
|
|
1188
|
-
}
|
|
1189
|
-
}
|
|
1190
|
-
}
|
|
1191
|
-
// Calculate success rate
|
|
1192
|
-
const expectedFileCount = sourceItems.length;
|
|
1193
|
-
const actualFileCount = targetItems.length - extraFiles.length;
|
|
1194
|
-
const successRate = expectedFileCount > 0
|
|
1195
|
-
? Math.round((actualFileCount / expectedFileCount) * 100)
|
|
1196
|
-
: 100;
|
|
1197
|
-
// Determine status
|
|
1198
|
-
let status;
|
|
1199
|
-
if (missingFiles.length === 0 && sizeMismatches.length === 0) {
|
|
1200
|
-
status = 'complete';
|
|
1201
|
-
}
|
|
1202
|
-
else if (missingFiles.length > 0 || sizeMismatches.length > 0) {
|
|
1203
|
-
if (successRate < 50) {
|
|
1204
|
-
status = 'failed';
|
|
1205
|
-
}
|
|
1206
|
-
else {
|
|
1207
|
-
status = 'incomplete';
|
|
1208
|
-
}
|
|
1209
|
-
}
|
|
1210
|
-
else {
|
|
1211
|
-
status = 'complete';
|
|
1212
|
-
}
|
|
1213
|
-
const result = {
|
|
1214
|
-
source: {
|
|
1215
|
-
path: sourcePath,
|
|
1216
|
-
fileCount: sourceItems.length,
|
|
1217
|
-
totalSize: sourceTotalSize,
|
|
1218
|
-
files: sourceItems,
|
|
1219
|
-
},
|
|
1220
|
-
target: {
|
|
1221
|
-
path: targetPath,
|
|
1222
|
-
fileCount: targetItems.length,
|
|
1223
|
-
totalSize: targetTotalSize,
|
|
1224
|
-
files: targetItems,
|
|
1225
|
-
},
|
|
1226
|
-
comparison: {
|
|
1227
|
-
missingFiles,
|
|
1228
|
-
extraFiles,
|
|
1229
|
-
sizeMismatches,
|
|
1230
|
-
modifiedDateMismatches,
|
|
1231
|
-
},
|
|
1232
|
-
successRate,
|
|
1233
|
-
status,
|
|
1234
|
-
};
|
|
1235
|
-
auditLogger.log({
|
|
1236
|
-
operation: 'verify-document-migration',
|
|
1237
|
-
operationType: 'READ',
|
|
1238
|
-
componentType: 'Migration',
|
|
1239
|
-
success: true,
|
|
1240
|
-
parameters: {
|
|
1241
|
-
sourceSiteId,
|
|
1242
|
-
sourcePath,
|
|
1243
|
-
targetSiteId,
|
|
1244
|
-
targetPath,
|
|
1245
|
-
status,
|
|
1246
|
-
successRate,
|
|
1247
|
-
},
|
|
1248
|
-
executionTimeMs: timer(),
|
|
1249
|
-
});
|
|
1250
|
-
return result;
|
|
1251
|
-
}
|
|
1252
|
-
catch (error) {
|
|
1253
|
-
auditLogger.log({
|
|
1254
|
-
operation: 'verify-document-migration',
|
|
1255
|
-
operationType: 'READ',
|
|
1256
|
-
componentType: 'Migration',
|
|
1257
|
-
success: false,
|
|
1258
|
-
error: this.sanitizeErrorMessage(error),
|
|
1259
|
-
parameters: { sourceSiteId, sourcePath, targetSiteId, targetPath },
|
|
1260
|
-
executionTimeMs: timer(),
|
|
1261
|
-
});
|
|
1262
|
-
throw this.handleError(error, 'verify document migration');
|
|
1263
|
-
}
|
|
1264
|
-
}
|
|
1265
|
-
/**
|
|
1266
|
-
* Close service and clear resources
|
|
1267
|
-
*/
|
|
1268
|
-
async close() {
|
|
1269
|
-
this.accessToken = null;
|
|
1270
|
-
this.tokenExpirationTime = 0;
|
|
1271
|
-
this.graphClient = null;
|
|
1272
|
-
this.cache.clear();
|
|
1273
|
-
this.siteIdCache.clear();
|
|
1274
|
-
console.error('SharePoint service closed');
|
|
1275
|
-
}
|
|
1276
|
-
}
|
|
1277
|
-
//# sourceMappingURL=SharePointService.js.map
|