mcp-consultant-tools 0.4.5

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,655 @@
1
+ import { ConfidentialClientApplication } from '@azure/msal-node';
2
+ import axios from 'axios';
3
+ export class PowerPlatformService {
4
+ config;
5
+ msalClient;
6
+ accessToken = null;
7
+ tokenExpirationTime = 0;
8
+ constructor(config) {
9
+ this.config = config;
10
+ // Initialize MSAL client
11
+ this.msalClient = new ConfidentialClientApplication({
12
+ auth: {
13
+ clientId: this.config.clientId,
14
+ clientSecret: this.config.clientSecret,
15
+ authority: `https://login.microsoftonline.com/${this.config.tenantId}`,
16
+ }
17
+ });
18
+ }
19
+ /**
20
+ * Get an access token for the PowerPlatform API
21
+ */
22
+ async getAccessToken() {
23
+ const currentTime = Date.now();
24
+ // If we have a token that isn't expired, return it
25
+ if (this.accessToken && this.tokenExpirationTime > currentTime) {
26
+ return this.accessToken;
27
+ }
28
+ try {
29
+ // Get a new token
30
+ const result = await this.msalClient.acquireTokenByClientCredential({
31
+ scopes: [`${this.config.organizationUrl}/.default`],
32
+ });
33
+ if (!result || !result.accessToken) {
34
+ throw new Error('Failed to acquire access token');
35
+ }
36
+ this.accessToken = result.accessToken;
37
+ // Set expiration time (subtract 5 minutes to refresh early)
38
+ if (result.expiresOn) {
39
+ this.tokenExpirationTime = result.expiresOn.getTime() - (5 * 60 * 1000);
40
+ }
41
+ return this.accessToken;
42
+ }
43
+ catch (error) {
44
+ console.error('Error acquiring access token:', error);
45
+ throw new Error('Authentication failed');
46
+ }
47
+ }
48
+ /**
49
+ * Make an authenticated request to the PowerPlatform API
50
+ */
51
+ async makeRequest(endpoint) {
52
+ try {
53
+ const token = await this.getAccessToken();
54
+ const response = await axios({
55
+ method: 'GET',
56
+ url: `${this.config.organizationUrl}/${endpoint}`,
57
+ headers: {
58
+ 'Authorization': `Bearer ${token}`,
59
+ 'Accept': 'application/json',
60
+ 'OData-MaxVersion': '4.0',
61
+ 'OData-Version': '4.0'
62
+ }
63
+ });
64
+ return response.data;
65
+ }
66
+ catch (error) {
67
+ const errorDetails = error.response?.data?.error || error.response?.data || error.message;
68
+ console.error('PowerPlatform API request failed:', {
69
+ endpoint,
70
+ status: error.response?.status,
71
+ statusText: error.response?.statusText,
72
+ error: errorDetails
73
+ });
74
+ throw new Error(`PowerPlatform API request failed: ${error.message} - ${JSON.stringify(errorDetails)}`);
75
+ }
76
+ }
77
+ /**
78
+ * Get metadata about an entity
79
+ * @param entityName The logical name of the entity
80
+ */
81
+ async getEntityMetadata(entityName) {
82
+ const response = await this.makeRequest(`api/data/v9.2/EntityDefinitions(LogicalName='${entityName}')`);
83
+ // Remove Privileges property if it exists
84
+ if (response && typeof response === 'object' && 'Privileges' in response) {
85
+ delete response.Privileges;
86
+ }
87
+ return response;
88
+ }
89
+ /**
90
+ * Get metadata about entity attributes/fields
91
+ * @param entityName The logical name of the entity
92
+ */
93
+ async getEntityAttributes(entityName) {
94
+ const selectProperties = [
95
+ 'LogicalName',
96
+ ].join(',');
97
+ // Make the request to get attributes
98
+ const response = await this.makeRequest(`api/data/v9.2/EntityDefinitions(LogicalName='${entityName}')/Attributes?$select=${selectProperties}&$filter=AttributeType ne 'Virtual'`);
99
+ if (response && response.value) {
100
+ // First pass: Filter out attributes that end with 'yominame'
101
+ response.value = response.value.filter((attribute) => {
102
+ const logicalName = attribute.LogicalName || '';
103
+ return !logicalName.endsWith('yominame');
104
+ });
105
+ // Filter out attributes that end with 'name' if there is another attribute with the same name without the 'name' suffix
106
+ const baseNames = new Set();
107
+ const namesAttributes = new Map();
108
+ for (const attribute of response.value) {
109
+ const logicalName = attribute.LogicalName || '';
110
+ if (logicalName.endsWith('name') && logicalName.length > 4) {
111
+ const baseName = logicalName.slice(0, -4); // Remove 'name' suffix
112
+ namesAttributes.set(baseName, attribute);
113
+ }
114
+ else {
115
+ // This is a potential base attribute
116
+ baseNames.add(logicalName);
117
+ }
118
+ }
119
+ // Find attributes to remove that match the pattern
120
+ const attributesToRemove = new Set();
121
+ for (const [baseName, nameAttribute] of namesAttributes.entries()) {
122
+ if (baseNames.has(baseName)) {
123
+ attributesToRemove.add(nameAttribute);
124
+ }
125
+ }
126
+ response.value = response.value.filter(attribute => !attributesToRemove.has(attribute));
127
+ }
128
+ return response;
129
+ }
130
+ /**
131
+ * Get metadata about a specific entity attribute/field
132
+ * @param entityName The logical name of the entity
133
+ * @param attributeName The logical name of the attribute
134
+ */
135
+ async getEntityAttribute(entityName, attributeName) {
136
+ return this.makeRequest(`api/data/v9.2/EntityDefinitions(LogicalName='${entityName}')/Attributes(LogicalName='${attributeName}')`);
137
+ }
138
+ /**
139
+ * Get one-to-many relationships for an entity
140
+ * @param entityName The logical name of the entity
141
+ */
142
+ async getEntityOneToManyRelationships(entityName) {
143
+ const selectProperties = [
144
+ 'SchemaName',
145
+ 'RelationshipType',
146
+ 'ReferencedAttribute',
147
+ 'ReferencedEntity',
148
+ 'ReferencingAttribute',
149
+ 'ReferencingEntity',
150
+ 'ReferencedEntityNavigationPropertyName',
151
+ 'ReferencingEntityNavigationPropertyName'
152
+ ].join(',');
153
+ // Only filter by ReferencingAttribute in the OData query since startswith isn't supported
154
+ const response = await this.makeRequest(`api/data/v9.2/EntityDefinitions(LogicalName='${entityName}')/OneToManyRelationships?$select=${selectProperties}&$filter=ReferencingAttribute ne 'regardingobjectid'`);
155
+ // Filter the response to exclude relationships with ReferencingEntity starting with 'msdyn_' or 'adx_'
156
+ if (response && response.value) {
157
+ response.value = response.value.filter((relationship) => {
158
+ const referencingEntity = relationship.ReferencingEntity || '';
159
+ return !(referencingEntity.startsWith('msdyn_') || referencingEntity.startsWith('adx_'));
160
+ });
161
+ }
162
+ return response;
163
+ }
164
+ /**
165
+ * Get many-to-many relationships for an entity
166
+ * @param entityName The logical name of the entity
167
+ */
168
+ async getEntityManyToManyRelationships(entityName) {
169
+ const selectProperties = [
170
+ 'SchemaName',
171
+ 'RelationshipType',
172
+ 'Entity1LogicalName',
173
+ 'Entity2LogicalName',
174
+ 'Entity1IntersectAttribute',
175
+ 'Entity2IntersectAttribute',
176
+ 'Entity1NavigationPropertyName',
177
+ 'Entity2NavigationPropertyName'
178
+ ].join(',');
179
+ return this.makeRequest(`api/data/v9.2/EntityDefinitions(LogicalName='${entityName}')/ManyToManyRelationships?$select=${selectProperties}`);
180
+ }
181
+ /**
182
+ * Get all relationships (one-to-many and many-to-many) for an entity
183
+ * @param entityName The logical name of the entity
184
+ */
185
+ async getEntityRelationships(entityName) {
186
+ const [oneToMany, manyToMany] = await Promise.all([
187
+ this.getEntityOneToManyRelationships(entityName),
188
+ this.getEntityManyToManyRelationships(entityName)
189
+ ]);
190
+ return {
191
+ oneToMany,
192
+ manyToMany
193
+ };
194
+ }
195
+ /**
196
+ * Get a global option set definition by name
197
+ * @param optionSetName The name of the global option set
198
+ * @returns The global option set definition
199
+ */
200
+ async getGlobalOptionSet(optionSetName) {
201
+ return this.makeRequest(`api/data/v9.2/GlobalOptionSetDefinitions(Name='${optionSetName}')`);
202
+ }
203
+ /**
204
+ * Get a specific record by entity name (plural) and ID
205
+ * @param entityNamePlural The plural name of the entity (e.g., 'accounts', 'contacts')
206
+ * @param recordId The GUID of the record
207
+ * @returns The record data
208
+ */
209
+ async getRecord(entityNamePlural, recordId) {
210
+ return this.makeRequest(`api/data/v9.2/${entityNamePlural}(${recordId})`);
211
+ }
212
+ /**
213
+ * Query records using entity name (plural) and a filter expression
214
+ * @param entityNamePlural The plural name of the entity (e.g., 'accounts', 'contacts')
215
+ * @param filter OData filter expression (e.g., "name eq 'test'")
216
+ * @param maxRecords Maximum number of records to retrieve (default: 50)
217
+ * @returns Filtered list of records
218
+ */
219
+ async queryRecords(entityNamePlural, filter, maxRecords = 50) {
220
+ return this.makeRequest(`api/data/v9.2/${entityNamePlural}?$filter=${encodeURIComponent(filter)}&$top=${maxRecords}`);
221
+ }
222
+ /**
223
+ * Get all plugin assemblies in the environment
224
+ * @param includeManaged Include managed assemblies (default: false)
225
+ * @param maxRecords Maximum number of assemblies to return (default: 100)
226
+ * @returns List of plugin assemblies with basic information
227
+ */
228
+ async getPluginAssemblies(includeManaged = false, maxRecords = 100) {
229
+ const managedFilter = includeManaged ? '' : '$filter=ismanaged eq false&';
230
+ const assemblies = await this.makeRequest(`api/data/v9.2/pluginassemblies?${managedFilter}$select=pluginassemblyid,name,version,culture,publickeytoken,isolationmode,sourcetype,major,minor,createdon,modifiedon,ismanaged,ishidden&$expand=modifiedby($select=fullname)&$orderby=name&$top=${maxRecords}`);
231
+ // Filter out hidden assemblies and format the results with more readable properties
232
+ // Note: ishidden is a ManagedProperty object with a Value property
233
+ const formattedAssemblies = assemblies.value
234
+ .filter((assembly) => {
235
+ const isHidden = assembly.ishidden?.Value !== undefined ? assembly.ishidden.Value : assembly.ishidden;
236
+ return !isHidden;
237
+ })
238
+ .map((assembly) => ({
239
+ pluginassemblyid: assembly.pluginassemblyid,
240
+ name: assembly.name,
241
+ version: assembly.version,
242
+ isolationMode: assembly.isolationmode === 1 ? 'None' : assembly.isolationmode === 2 ? 'Sandbox' : 'External',
243
+ isManaged: assembly.ismanaged,
244
+ modifiedOn: assembly.modifiedon,
245
+ modifiedBy: assembly.modifiedby?.fullname,
246
+ major: assembly.major,
247
+ minor: assembly.minor
248
+ }));
249
+ return {
250
+ totalCount: formattedAssemblies.length,
251
+ assemblies: formattedAssemblies
252
+ };
253
+ }
254
+ /**
255
+ * Get a plugin assembly by name with all related plugin types, steps, and images
256
+ * @param assemblyName The name of the plugin assembly
257
+ * @param includeDisabled Include disabled steps (default: false)
258
+ * @returns Complete plugin assembly information with validation
259
+ */
260
+ async getPluginAssemblyComplete(assemblyName, includeDisabled = false) {
261
+ // Get the plugin assembly (excluding content_binary which is large and not useful for review)
262
+ const assemblies = await this.makeRequest(`api/data/v9.2/pluginassemblies?$filter=name eq '${assemblyName}'&$select=pluginassemblyid,name,version,culture,publickeytoken,isolationmode,sourcetype,major,minor,createdon,modifiedon,ismanaged,ishidden,description&$expand=modifiedby($select=fullname)`);
263
+ if (!assemblies.value || assemblies.value.length === 0) {
264
+ throw new Error(`Plugin assembly '${assemblyName}' not found`);
265
+ }
266
+ const assembly = assemblies.value[0];
267
+ const assemblyId = assembly.pluginassemblyid;
268
+ // Get plugin types for this assembly
269
+ const pluginTypes = await this.makeRequest(`api/data/v9.2/plugintypes?$filter=_pluginassemblyid_value eq ${assemblyId}&$select=plugintypeid,typename,friendlyname,name,assemblyname,description,workflowactivitygroupname`);
270
+ // Get all steps for each plugin type
271
+ const pluginTypeIds = pluginTypes.value.map((pt) => pt.plugintypeid);
272
+ let allSteps = [];
273
+ if (pluginTypeIds.length > 0) {
274
+ const statusFilter = includeDisabled ? '' : ' and statuscode eq 1';
275
+ // Build filter for all plugin type IDs
276
+ const typeFilter = pluginTypeIds.map((id) => `_plugintypeid_value eq ${id}`).join(' or ');
277
+ const steps = await this.makeRequest(`api/data/v9.2/sdkmessageprocessingsteps?$filter=(${typeFilter})${statusFilter}&$select=sdkmessageprocessingstepid,name,stage,mode,rank,statuscode,asyncautodelete,filteringattributes,supporteddeployment,configuration,description,invocationsource,_plugintypeid_value,_sdkmessagefilterid_value,_impersonatinguserid_value,_eventhandler_value&$expand=sdkmessageid($select=name),plugintypeid($select=typename),impersonatinguserid($select=fullname),modifiedby($select=fullname),sdkmessagefilterid($select=primaryobjecttypecode)&$orderby=stage,rank`);
278
+ allSteps = steps.value;
279
+ }
280
+ // Get all images for these steps
281
+ const stepIds = allSteps.map((s) => s.sdkmessageprocessingstepid);
282
+ let allImages = [];
283
+ if (stepIds.length > 0) {
284
+ // Build filter for all step IDs
285
+ const imageFilter = stepIds.map((id) => `_sdkmessageprocessingstepid_value eq ${id}`).join(' or ');
286
+ const images = await this.makeRequest(`api/data/v9.2/sdkmessageprocessingstepimages?$filter=${imageFilter}&$select=sdkmessageprocessingstepimageid,name,imagetype,messagepropertyname,entityalias,attributes,_sdkmessageprocessingstepid_value`);
287
+ allImages = images.value;
288
+ }
289
+ // Attach images to their respective steps
290
+ const stepsWithImages = allSteps.map((step) => ({
291
+ ...step,
292
+ images: allImages.filter((img) => img._sdkmessageprocessingstepid_value === step.sdkmessageprocessingstepid)
293
+ }));
294
+ // Validation checks
295
+ const validation = {
296
+ hasDisabledSteps: allSteps.some((s) => s.statuscode !== 1),
297
+ hasAsyncSteps: allSteps.some((s) => s.mode === 1),
298
+ hasSyncSteps: allSteps.some((s) => s.mode === 0),
299
+ stepsWithoutFilteringAttributes: stepsWithImages
300
+ .filter((s) => (s.sdkmessageid?.name === 'Update' || s.sdkmessageid?.name === 'Delete') && !s.filteringattributes)
301
+ .map((s) => s.name),
302
+ stepsWithoutImages: stepsWithImages
303
+ .filter((s) => s.images.length === 0 && (s.sdkmessageid?.name === 'Update' || s.sdkmessageid?.name === 'Delete'))
304
+ .map((s) => s.name),
305
+ potentialIssues: []
306
+ };
307
+ // Add potential issues
308
+ if (validation.stepsWithoutFilteringAttributes.length > 0) {
309
+ validation.potentialIssues.push(`${validation.stepsWithoutFilteringAttributes.length} Update/Delete steps without filtering attributes (performance concern)`);
310
+ }
311
+ if (validation.stepsWithoutImages.length > 0) {
312
+ validation.potentialIssues.push(`${validation.stepsWithoutImages.length} Update/Delete steps without images (may need entity data)`);
313
+ }
314
+ return {
315
+ assembly,
316
+ pluginTypes: pluginTypes.value,
317
+ steps: stepsWithImages,
318
+ validation
319
+ };
320
+ }
321
+ /**
322
+ * Get all plugins that execute on a specific entity, organized by message and execution order
323
+ * @param entityName The logical name of the entity
324
+ * @param messageFilter Optional filter by message name (e.g., "Create", "Update")
325
+ * @param includeDisabled Include disabled steps (default: false)
326
+ * @returns Complete plugin pipeline for the entity
327
+ */
328
+ async getEntityPluginPipeline(entityName, messageFilter, includeDisabled = false) {
329
+ const statusFilter = includeDisabled ? '' : ' and statuscode eq 1';
330
+ const msgFilter = messageFilter ? ` and sdkmessageid/name eq '${messageFilter}'` : '';
331
+ // Get all steps for this entity
332
+ const steps = await this.makeRequest(`api/data/v9.2/sdkmessageprocessingsteps?$filter=sdkmessagefilterid/primaryobjecttypecode eq '${entityName}'${statusFilter}${msgFilter}&$select=sdkmessageprocessingstepid,name,stage,mode,rank,statuscode,asyncautodelete,filteringattributes,supporteddeployment,configuration,description,_plugintypeid_value,_sdkmessagefilterid_value,_impersonatinguserid_value&$expand=sdkmessageid($select=name),plugintypeid($select=typename),impersonatinguserid($select=fullname),sdkmessagefilterid($select=primaryobjecttypecode)&$orderby=stage,rank`);
333
+ // Get assembly information for each plugin type (filter out nulls)
334
+ const pluginTypeIds = [...new Set(steps.value.map((s) => s._plugintypeid_value).filter((id) => id != null))];
335
+ const assemblyMap = new Map();
336
+ for (const typeId of pluginTypeIds) {
337
+ const pluginType = await this.makeRequest(`api/data/v9.2/plugintypes(${typeId})?$expand=pluginassemblyid($select=name,version)`);
338
+ assemblyMap.set(typeId, pluginType.pluginassemblyid);
339
+ }
340
+ // Get images for all steps
341
+ const stepIds = steps.value.map((s) => s.sdkmessageprocessingstepid);
342
+ let allImages = [];
343
+ if (stepIds.length > 0) {
344
+ const imageFilter = stepIds.map((id) => `_sdkmessageprocessingstepid_value eq ${id}`).join(' or ');
345
+ const images = await this.makeRequest(`api/data/v9.2/sdkmessageprocessingstepimages?$filter=${imageFilter}&$select=sdkmessageprocessingstepimageid,name,imagetype,messagepropertyname,entityalias,attributes,_sdkmessageprocessingstepid_value`);
346
+ allImages = images.value;
347
+ }
348
+ // Format steps with all information
349
+ const formattedSteps = steps.value.map((step) => {
350
+ const assembly = assemblyMap.get(step._plugintypeid_value);
351
+ const images = allImages.filter((img) => img._sdkmessageprocessingstepid_value === step.sdkmessageprocessingstepid);
352
+ return {
353
+ sdkmessageprocessingstepid: step.sdkmessageprocessingstepid,
354
+ name: step.name,
355
+ stage: step.stage,
356
+ stageName: step.stage === 10 ? 'PreValidation' : step.stage === 20 ? 'PreOperation' : 'PostOperation',
357
+ mode: step.mode,
358
+ modeName: step.mode === 0 ? 'Synchronous' : 'Asynchronous',
359
+ rank: step.rank,
360
+ message: step.sdkmessageid?.name,
361
+ pluginType: step.plugintypeid?.typename,
362
+ assemblyName: assembly?.name,
363
+ assemblyVersion: assembly?.version,
364
+ filteringAttributes: step.filteringattributes ? step.filteringattributes.split(',') : [],
365
+ statuscode: step.statuscode,
366
+ enabled: step.statuscode === 1,
367
+ deployment: step.supporteddeployment === 0 ? 'Server' : step.supporteddeployment === 1 ? 'Offline' : 'Both',
368
+ impersonatingUser: step.impersonatinguserid?.fullname,
369
+ hasPreImage: images.some((img) => img.imagetype === 0 || img.imagetype === 2),
370
+ hasPostImage: images.some((img) => img.imagetype === 1 || img.imagetype === 2),
371
+ images: images
372
+ };
373
+ });
374
+ // Organize by message
375
+ const messageGroups = new Map();
376
+ formattedSteps.forEach((step) => {
377
+ if (!messageGroups.has(step.message)) {
378
+ messageGroups.set(step.message, {
379
+ messageName: step.message,
380
+ stages: {
381
+ preValidation: [],
382
+ preOperation: [],
383
+ postOperation: []
384
+ }
385
+ });
386
+ }
387
+ const msg = messageGroups.get(step.message);
388
+ if (step.stage === 10)
389
+ msg.stages.preValidation.push(step);
390
+ else if (step.stage === 20)
391
+ msg.stages.preOperation.push(step);
392
+ else if (step.stage === 40)
393
+ msg.stages.postOperation.push(step);
394
+ });
395
+ return {
396
+ entity: entityName,
397
+ messages: Array.from(messageGroups.values()),
398
+ steps: formattedSteps,
399
+ executionOrder: formattedSteps.map((s) => s.name)
400
+ };
401
+ }
402
+ /**
403
+ * Get plugin trace logs with filtering
404
+ * @param options Filtering options for trace logs
405
+ * @returns Filtered trace logs with parsed exception details
406
+ */
407
+ async getPluginTraceLogs(options) {
408
+ const { entityName, messageName, correlationId, pluginStepId, exceptionOnly = false, hoursBack = 24, maxRecords = 50 } = options;
409
+ // Build filter
410
+ const filters = [];
411
+ // Date filter
412
+ const dateThreshold = new Date();
413
+ dateThreshold.setHours(dateThreshold.getHours() - hoursBack);
414
+ filters.push(`createdon gt ${dateThreshold.toISOString()}`);
415
+ if (entityName)
416
+ filters.push(`primaryentity eq '${entityName}'`);
417
+ if (messageName)
418
+ filters.push(`messagename eq '${messageName}'`);
419
+ if (correlationId)
420
+ filters.push(`correlationid eq '${correlationId}'`);
421
+ if (pluginStepId)
422
+ filters.push(`_sdkmessageprocessingstepid_value eq ${pluginStepId}`);
423
+ if (exceptionOnly)
424
+ filters.push(`exceptiondetails ne null`);
425
+ const filterString = filters.join(' and ');
426
+ const logs = await this.makeRequest(`api/data/v9.2/plugintracelogs?$filter=${filterString}&$orderby=createdon desc&$top=${maxRecords}`);
427
+ // Parse logs for better readability
428
+ const parsedLogs = logs.value.map((log) => ({
429
+ ...log,
430
+ modeName: log.mode === 0 ? 'Synchronous' : 'Asynchronous',
431
+ operationTypeName: this.getOperationTypeName(log.operationtype),
432
+ parsed: {
433
+ hasException: !!log.exceptiondetails,
434
+ exceptionType: log.exceptiondetails ? this.extractExceptionType(log.exceptiondetails) : null,
435
+ exceptionMessage: log.exceptiondetails ? this.extractExceptionMessage(log.exceptiondetails) : null,
436
+ stackTrace: log.exceptiondetails
437
+ }
438
+ }));
439
+ return {
440
+ totalCount: parsedLogs.length,
441
+ logs: parsedLogs
442
+ };
443
+ }
444
+ // Helper methods for trace log parsing
445
+ getOperationTypeName(operationType) {
446
+ const types = {
447
+ 0: 'None',
448
+ 1: 'Create',
449
+ 2: 'Update',
450
+ 3: 'Delete',
451
+ 4: 'Retrieve',
452
+ 5: 'RetrieveMultiple',
453
+ 6: 'Associate',
454
+ 7: 'Disassociate'
455
+ };
456
+ return types[operationType] || 'Unknown';
457
+ }
458
+ extractExceptionType(exceptionDetails) {
459
+ const match = exceptionDetails.match(/^([^:]+):/);
460
+ return match ? match[1].trim() : null;
461
+ }
462
+ extractExceptionMessage(exceptionDetails) {
463
+ const lines = exceptionDetails.split('\n');
464
+ if (lines.length > 0) {
465
+ const firstLine = lines[0];
466
+ const colonIndex = firstLine.indexOf(':');
467
+ if (colonIndex > 0) {
468
+ return firstLine.substring(colonIndex + 1).trim();
469
+ }
470
+ }
471
+ return null;
472
+ }
473
+ /**
474
+ * Get all Power Automate flows (cloud flows) in the environment
475
+ * @param activeOnly Only return activated flows (default: false)
476
+ * @param maxRecords Maximum number of flows to return (default: 100)
477
+ * @returns List of Power Automate flows with basic information
478
+ */
479
+ async getFlows(activeOnly = false, maxRecords = 100) {
480
+ // Category 5 = Modern Flow (Power Automate cloud flows)
481
+ // StateCode: 0=Draft, 1=Activated, 2=Suspended
482
+ // Type: 1=Definition, 2=Activation
483
+ const stateFilter = activeOnly ? ' and statecode eq 1' : '';
484
+ const flows = await this.makeRequest(`api/data/v9.2/workflows?$filter=category eq 5${stateFilter}&$select=workflowid,name,statecode,statuscode,description,createdon,modifiedon,type,ismanaged,iscrmuiworkflow,primaryentity,clientdata&$expand=modifiedby($select=fullname)&$orderby=modifiedon desc&$top=${maxRecords}`);
485
+ // Format the results for better readability
486
+ const formattedFlows = flows.value.map((flow) => ({
487
+ workflowid: flow.workflowid,
488
+ name: flow.name,
489
+ description: flow.description,
490
+ state: flow.statecode === 0 ? 'Draft' : flow.statecode === 1 ? 'Activated' : 'Suspended',
491
+ statecode: flow.statecode,
492
+ statuscode: flow.statuscode,
493
+ type: flow.type === 1 ? 'Definition' : flow.type === 2 ? 'Activation' : 'Template',
494
+ primaryEntity: flow.primaryentity,
495
+ isManaged: flow.ismanaged,
496
+ ownerId: flow._ownerid_value,
497
+ modifiedOn: flow.modifiedon,
498
+ modifiedBy: flow.modifiedby?.fullname,
499
+ createdOn: flow.createdon,
500
+ hasDefinition: !!flow.clientdata
501
+ }));
502
+ return {
503
+ totalCount: formattedFlows.length,
504
+ flows: formattedFlows
505
+ };
506
+ }
507
+ /**
508
+ * Get a specific Power Automate flow with its complete definition
509
+ * @param flowId The GUID of the flow (workflowid)
510
+ * @returns Complete flow information including the flow definition JSON
511
+ */
512
+ async getFlowDefinition(flowId) {
513
+ const flow = await this.makeRequest(`api/data/v9.2/workflows(${flowId})?$select=workflowid,name,statecode,statuscode,description,createdon,modifiedon,type,category,ismanaged,iscrmuiworkflow,primaryentity,clientdata,xaml&$expand=modifiedby($select=fullname),createdby($select=fullname)`);
514
+ // Parse the clientdata (flow definition) if it exists
515
+ let flowDefinition = null;
516
+ if (flow.clientdata) {
517
+ try {
518
+ flowDefinition = JSON.parse(flow.clientdata);
519
+ }
520
+ catch (error) {
521
+ console.error('Failed to parse flow definition JSON:', error);
522
+ flowDefinition = { parseError: 'Failed to parse flow definition', raw: flow.clientdata };
523
+ }
524
+ }
525
+ return {
526
+ workflowid: flow.workflowid,
527
+ name: flow.name,
528
+ description: flow.description,
529
+ state: flow.statecode === 0 ? 'Draft' : flow.statecode === 1 ? 'Activated' : 'Suspended',
530
+ statecode: flow.statecode,
531
+ statuscode: flow.statuscode,
532
+ type: flow.type === 1 ? 'Definition' : flow.type === 2 ? 'Activation' : 'Template',
533
+ category: flow.category,
534
+ primaryEntity: flow.primaryentity,
535
+ isManaged: flow.ismanaged,
536
+ ownerId: flow._ownerid_value,
537
+ createdOn: flow.createdon,
538
+ createdBy: flow.createdby?.fullname,
539
+ modifiedOn: flow.modifiedon,
540
+ modifiedBy: flow.modifiedby?.fullname,
541
+ flowDefinition: flowDefinition
542
+ };
543
+ }
544
+ /**
545
+ * Get flow run history for a specific Power Automate flow
546
+ * @param flowId The GUID of the flow (workflowid)
547
+ * @param maxRecords Maximum number of runs to return (default: 100)
548
+ * @returns List of flow runs with status, start time, duration, and error details
549
+ */
550
+ async getFlowRuns(flowId, maxRecords = 100) {
551
+ // Flow runs are stored in the flowruns entity (not flowsession)
552
+ // Status: "Succeeded", "Failed", "Faulted", "TimedOut", "Cancelled", "Running", etc.
553
+ const flowRuns = await this.makeRequest(`api/data/v9.2/flowruns?$filter=_workflow_value eq ${flowId}&$select=flowrunid,name,status,starttime,endtime,duration,errormessage,errorcode,triggertype&$orderby=starttime desc&$top=${maxRecords}`);
554
+ // Format the results for better readability
555
+ const formattedRuns = flowRuns.value.map((run) => {
556
+ // Parse error message if it's JSON
557
+ let parsedError = run.errormessage;
558
+ if (run.errormessage) {
559
+ try {
560
+ parsedError = JSON.parse(run.errormessage);
561
+ }
562
+ catch (e) {
563
+ // Keep as string if not valid JSON
564
+ }
565
+ }
566
+ return {
567
+ flowrunid: run.flowrunid,
568
+ name: run.name,
569
+ status: run.status,
570
+ startedOn: run.starttime,
571
+ completedOn: run.endtime,
572
+ duration: run.duration,
573
+ errorMessage: parsedError || null,
574
+ errorCode: run.errorcode || null,
575
+ triggerType: run.triggertype || null
576
+ };
577
+ });
578
+ return {
579
+ flowId: flowId,
580
+ totalCount: formattedRuns.length,
581
+ runs: formattedRuns
582
+ };
583
+ }
584
+ /**
585
+ * Get all classic Dynamics workflows in the environment
586
+ * @param activeOnly Only return activated workflows (default: false)
587
+ * @param maxRecords Maximum number of workflows to return (default: 100)
588
+ * @returns List of classic workflows with basic information
589
+ */
590
+ async getWorkflows(activeOnly = false, maxRecords = 100) {
591
+ // Category 0 = Classic Workflow
592
+ // StateCode: 0=Draft, 1=Activated, 2=Suspended
593
+ // Type: 1=Definition, 2=Activation
594
+ const stateFilter = activeOnly ? ' and statecode eq 1' : '';
595
+ const workflows = await this.makeRequest(`api/data/v9.2/workflows?$filter=category eq 0${stateFilter}&$select=workflowid,name,statecode,statuscode,description,createdon,modifiedon,type,ismanaged,iscrmuiworkflow,primaryentity,mode,subprocess,ondemand,triggeroncreate,triggerondelete,syncworkflowlogonfailure&$expand=ownerid($select=fullname),modifiedby($select=fullname)&$orderby=modifiedon desc&$top=${maxRecords}`);
596
+ // Format the results for better readability
597
+ const formattedWorkflows = workflows.value.map((workflow) => ({
598
+ workflowid: workflow.workflowid,
599
+ name: workflow.name,
600
+ description: workflow.description,
601
+ state: workflow.statecode === 0 ? 'Draft' : workflow.statecode === 1 ? 'Activated' : 'Suspended',
602
+ statecode: workflow.statecode,
603
+ statuscode: workflow.statuscode,
604
+ type: workflow.type === 1 ? 'Definition' : workflow.type === 2 ? 'Activation' : 'Template',
605
+ mode: workflow.mode === 0 ? 'Background' : 'Real-time',
606
+ primaryEntity: workflow.primaryentity,
607
+ isManaged: workflow.ismanaged,
608
+ isOnDemand: workflow.ondemand,
609
+ triggerOnCreate: workflow.triggeroncreate,
610
+ triggerOnDelete: workflow.triggerondelete,
611
+ isSubprocess: workflow.subprocess,
612
+ owner: workflow.ownerid?.fullname,
613
+ modifiedOn: workflow.modifiedon,
614
+ modifiedBy: workflow.modifiedby?.fullname,
615
+ createdOn: workflow.createdon
616
+ }));
617
+ return {
618
+ totalCount: formattedWorkflows.length,
619
+ workflows: formattedWorkflows
620
+ };
621
+ }
622
+ /**
623
+ * Get a specific classic workflow with its complete XAML definition
624
+ * @param workflowId The GUID of the workflow (workflowid)
625
+ * @returns Complete workflow information including the XAML definition
626
+ */
627
+ async getWorkflowDefinition(workflowId) {
628
+ const workflow = await this.makeRequest(`api/data/v9.2/workflows(${workflowId})?$select=workflowid,name,statecode,statuscode,description,createdon,modifiedon,type,category,ismanaged,iscrmuiworkflow,primaryentity,mode,subprocess,ondemand,triggeroncreate,triggerondelete,triggeronupdateattributelist,syncworkflowlogonfailure,xaml&$expand=ownerid($select=fullname),modifiedby($select=fullname),createdby($select=fullname)`);
629
+ return {
630
+ workflowid: workflow.workflowid,
631
+ name: workflow.name,
632
+ description: workflow.description,
633
+ state: workflow.statecode === 0 ? 'Draft' : workflow.statecode === 1 ? 'Activated' : 'Suspended',
634
+ statecode: workflow.statecode,
635
+ statuscode: workflow.statuscode,
636
+ type: workflow.type === 1 ? 'Definition' : workflow.type === 2 ? 'Activation' : 'Template',
637
+ category: workflow.category,
638
+ mode: workflow.mode === 0 ? 'Background' : 'Real-time',
639
+ primaryEntity: workflow.primaryentity,
640
+ isManaged: workflow.ismanaged,
641
+ isOnDemand: workflow.ondemand,
642
+ triggerOnCreate: workflow.triggeroncreate,
643
+ triggerOnDelete: workflow.triggerondelete,
644
+ triggerOnUpdateAttributes: workflow.triggeronupdateattributelist ? workflow.triggeronupdateattributelist.split(',') : [],
645
+ isSubprocess: workflow.subprocess,
646
+ syncWorkflowLogOnFailure: workflow.syncworkflowlogonfailure,
647
+ owner: workflow.ownerid?.fullname,
648
+ createdOn: workflow.createdon,
649
+ createdBy: workflow.createdby?.fullname,
650
+ modifiedOn: workflow.modifiedon,
651
+ modifiedBy: workflow.modifiedby?.fullname,
652
+ xaml: workflow.xaml
653
+ };
654
+ }
655
+ }