@mcp-consultant-tools/powerplatform-customization 25.0.0-beta.5 → 26.0.0-beta.2

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.
@@ -1,2977 +1,585 @@
1
- import { ConfidentialClientApplication } from '@azure/msal-node';
2
- import axios from 'axios';
3
- import { bestPracticesValidator } from './utils/bestPractices.js';
4
- import { iconManager } from './utils/iconManager.js';
5
- import { auditLogger } from '@mcp-consultant-tools/core';
6
- import { rateLimiter } from './utils/rate-limiter.js';
7
- import { PowerPlatformService as PowerPlatformReadOnlyService } from '@mcp-consultant-tools/powerplatform';
1
+ /**
2
+ * PowerPlatformService - Slim Customization Facade
3
+ *
4
+ * This is a facade that delegates to services in @mcp-consultant-tools/powerplatform-core.
5
+ * It provides CUSTOMIZATION operations for PowerPlatform/Dataverse entities.
6
+ *
7
+ * For read-only operations, use @mcp-consultant-tools/powerplatform.
8
+ * For data CRUD operations, use @mcp-consultant-tools/powerplatform-data.
9
+ */
10
+ import {
11
+ // Client and types
12
+ PowerPlatformClient,
13
+ // Read-only services (needed for inspecting before customizing)
14
+ MetadataService, PluginService, FlowService, WorkflowService, BusinessRuleService, AppService, ValidationService,
15
+ // Customization services
16
+ EntityService, AttributeService, RelationshipService, OptionSetService, FormService, ViewService, WebResourceService, SolutionService, PublishingService, DependencyService, PluginDeploymentService, AppManagementService, WorkflowManagementService, createAuthProvider, } from '@mcp-consultant-tools/powerplatform-core';
8
17
  export class PowerPlatformService {
9
- config;
10
- msalClient;
11
- accessToken = null;
12
- tokenExpirationTime = 0;
13
- constructor(config) {
14
- this.config = config;
15
- // Initialize MSAL client
16
- this.msalClient = new ConfidentialClientApplication({
17
- auth: {
18
- clientId: this.config.clientId,
19
- clientSecret: this.config.clientSecret,
20
- authority: `https://login.microsoftonline.com/${this.config.tenantId}`,
21
- }
22
- });
23
- }
24
- /**
25
- * Get an access token for the PowerPlatform API
26
- */
27
- async getAccessToken() {
28
- const currentTime = Date.now();
29
- // If we have a token that isn't expired, return it
30
- if (this.accessToken && this.tokenExpirationTime > currentTime) {
31
- return this.accessToken;
32
- }
33
- try {
34
- // Get a new token
35
- const result = await this.msalClient.acquireTokenByClientCredential({
36
- scopes: [`${this.config.organizationUrl}/.default`],
37
- });
38
- if (!result || !result.accessToken) {
39
- throw new Error('Failed to acquire access token');
40
- }
41
- this.accessToken = result.accessToken;
42
- // Set expiration time (subtract 5 minutes to refresh early)
43
- if (result.expiresOn) {
44
- this.tokenExpirationTime = result.expiresOn.getTime() - (5 * 60 * 1000);
45
- }
46
- return this.accessToken;
47
- }
48
- catch (error) {
49
- console.error('Error acquiring access token:', error);
50
- throw new Error('Authentication failed');
51
- }
52
- }
53
- /**
54
- * Make an authenticated request to the PowerPlatform API
55
- * Extended to support all HTTP methods for write operations
56
- */
57
- async makeRequest(endpoint, method = 'GET', data, additionalHeaders) {
58
- try {
59
- const token = await this.getAccessToken();
60
- const headers = {
61
- 'Authorization': `Bearer ${token}`,
62
- 'Accept': 'application/json',
63
- 'OData-MaxVersion': '4.0',
64
- 'OData-Version': '4.0',
65
- ...additionalHeaders
66
- };
67
- // Add Content-Type for POST/PUT/PATCH requests
68
- if (method !== 'GET' && method !== 'DELETE' && data) {
69
- headers['Content-Type'] = 'application/json';
70
- }
71
- const response = await axios({
72
- method,
73
- url: `${this.config.organizationUrl}/${endpoint}`,
74
- headers,
75
- data
76
- });
77
- return response.data;
78
- }
79
- catch (error) {
80
- const errorDetails = error.response?.data?.error || error.response?.data || error.message;
81
- console.error('PowerPlatform API request failed:', {
82
- endpoint,
83
- method,
84
- status: error.response?.status,
85
- statusText: error.response?.statusText,
86
- error: errorDetails
87
- });
88
- throw new Error(`PowerPlatform API request failed: ${error.message} - ${JSON.stringify(errorDetails)}`);
89
- }
90
- }
91
- /**
92
- * Get metadata about an entity
93
- * @param entityName The logical name of the entity
94
- */
18
+ client;
19
+ // Read-only services
20
+ metadata;
21
+ plugin;
22
+ flow;
23
+ workflow;
24
+ businessRule;
25
+ app;
26
+ validation;
27
+ // Customization services
28
+ entity;
29
+ attribute;
30
+ relationship;
31
+ optionSet;
32
+ form;
33
+ view;
34
+ webResource;
35
+ solution;
36
+ publishing;
37
+ dependency;
38
+ pluginDeployment;
39
+ appManagement;
40
+ workflowManagement;
41
+ constructor(config, authProvider) {
42
+ const auth = authProvider ||
43
+ createAuthProvider({
44
+ organizationUrl: config.organizationUrl,
45
+ clientId: config.clientId,
46
+ clientSecret: config.clientSecret,
47
+ tenantId: config.tenantId,
48
+ });
49
+ this.client = new PowerPlatformClient(config, auth);
50
+ // Initialize read-only services
51
+ this.metadata = new MetadataService(this.client);
52
+ this.plugin = new PluginService(this.client);
53
+ this.flow = new FlowService(this.client);
54
+ this.workflow = new WorkflowService(this.client);
55
+ this.businessRule = new BusinessRuleService(this.client);
56
+ this.app = new AppService(this.client);
57
+ this.validation = new ValidationService(this.client);
58
+ // Initialize customization services
59
+ this.entity = new EntityService(this.client);
60
+ this.attribute = new AttributeService(this.client);
61
+ this.relationship = new RelationshipService(this.client);
62
+ this.optionSet = new OptionSetService(this.client);
63
+ this.form = new FormService(this.client);
64
+ this.view = new ViewService(this.client);
65
+ this.webResource = new WebResourceService(this.client);
66
+ this.solution = new SolutionService(this.client);
67
+ this.publishing = new PublishingService(this.client);
68
+ this.dependency = new DependencyService(this.client);
69
+ // These services need cross-service dependencies
70
+ this.pluginDeployment = new PluginDeploymentService(this.client, (solutionUniqueName, componentId, componentType) => this.solution.addComponentToSolution(solutionUniqueName, componentId, componentType));
71
+ this.appManagement = new AppManagementService(this.client, async (appId) => this.app.getAppSitemap(appId));
72
+ this.workflowManagement = new WorkflowManagementService(this.client);
73
+ }
74
+ // =====================================================
75
+ // AUTH METHODS
76
+ // =====================================================
77
+ getAuthMode() {
78
+ return this.client.getAuthMode();
79
+ }
80
+ async getUserInfo() {
81
+ return this.client.getUserInfo();
82
+ }
83
+ async logout() {
84
+ return this.client.logout();
85
+ }
86
+ // =====================================================
87
+ // METADATA METHODS (Read-only)
88
+ // =====================================================
95
89
  async getEntityMetadata(entityName) {
96
- const response = await this.makeRequest(`api/data/v9.2/EntityDefinitions(LogicalName='${entityName}')`);
97
- // Remove Privileges property if it exists
98
- if (response && typeof response === 'object' && 'Privileges' in response) {
99
- delete response.Privileges;
100
- }
101
- return response;
90
+ return this.metadata.getEntityMetadata(entityName);
102
91
  }
103
- /**
104
- * Get metadata about entity attributes/fields
105
- * @param entityName The logical name of the entity
106
- */
107
- async getEntityAttributes(entityName) {
108
- const selectProperties = [
109
- 'LogicalName',
110
- ].join(',');
111
- // Make the request to get attributes
112
- const response = await this.makeRequest(`api/data/v9.2/EntityDefinitions(LogicalName='${entityName}')/Attributes?$select=${selectProperties}&$filter=AttributeType ne 'Virtual'`);
113
- if (response && response.value) {
114
- // First pass: Filter out attributes that end with 'yominame'
115
- response.value = response.value.filter((attribute) => {
116
- const logicalName = attribute.LogicalName || '';
117
- return !logicalName.endsWith('yominame');
118
- });
119
- // Filter out attributes that end with 'name' if there is another attribute with the same name without the 'name' suffix
120
- const baseNames = new Set();
121
- const namesAttributes = new Map();
122
- for (const attribute of response.value) {
123
- const logicalName = attribute.LogicalName || '';
124
- if (logicalName.endsWith('name') && logicalName.length > 4) {
125
- const baseName = logicalName.slice(0, -4); // Remove 'name' suffix
126
- namesAttributes.set(baseName, attribute);
127
- }
128
- else {
129
- // This is a potential base attribute
130
- baseNames.add(logicalName);
131
- }
132
- }
133
- // Find attributes to remove that match the pattern
134
- const attributesToRemove = new Set();
135
- for (const [baseName, nameAttribute] of namesAttributes.entries()) {
136
- if (baseNames.has(baseName)) {
137
- attributesToRemove.add(nameAttribute);
138
- }
139
- }
140
- response.value = response.value.filter(attribute => !attributesToRemove.has(attribute));
141
- }
142
- return response;
92
+ async getEntityAttributes(entityName, options) {
93
+ return this.metadata.getEntityAttributes(entityName, options);
143
94
  }
144
- /**
145
- * Get metadata about a specific entity attribute/field
146
- * @param entityName The logical name of the entity
147
- * @param attributeName The logical name of the attribute
148
- */
149
95
  async getEntityAttribute(entityName, attributeName) {
150
- return this.makeRequest(`api/data/v9.2/EntityDefinitions(LogicalName='${entityName}')/Attributes(LogicalName='${attributeName}')`);
96
+ return this.metadata.getEntityAttribute(entityName, attributeName);
151
97
  }
152
- /**
153
- * Get one-to-many relationships for an entity
154
- * @param entityName The logical name of the entity
155
- */
156
- async getEntityOneToManyRelationships(entityName) {
157
- const selectProperties = [
158
- 'SchemaName',
159
- 'RelationshipType',
160
- 'ReferencedAttribute',
161
- 'ReferencedEntity',
162
- 'ReferencingAttribute',
163
- 'ReferencingEntity',
164
- 'ReferencedEntityNavigationPropertyName',
165
- 'ReferencingEntityNavigationPropertyName'
166
- ].join(',');
167
- // Only filter by ReferencingAttribute in the OData query since startswith isn't supported
168
- const response = await this.makeRequest(`api/data/v9.2/EntityDefinitions(LogicalName='${entityName}')/OneToManyRelationships?$select=${selectProperties}&$filter=ReferencingAttribute ne 'regardingobjectid'`);
169
- // Filter the response to exclude relationships with ReferencingEntity starting with 'msdyn_' or 'adx_'
170
- if (response && response.value) {
171
- response.value = response.value.filter((relationship) => {
172
- const referencingEntity = relationship.ReferencingEntity || '';
173
- return !(referencingEntity.startsWith('msdyn_') || referencingEntity.startsWith('adx_'));
174
- });
175
- }
176
- return response;
177
- }
178
- /**
179
- * Get many-to-many relationships for an entity
180
- * @param entityName The logical name of the entity
181
- */
182
- async getEntityManyToManyRelationships(entityName) {
183
- const selectProperties = [
184
- 'SchemaName',
185
- 'RelationshipType',
186
- 'Entity1LogicalName',
187
- 'Entity2LogicalName',
188
- 'Entity1IntersectAttribute',
189
- 'Entity2IntersectAttribute',
190
- 'Entity1NavigationPropertyName',
191
- 'Entity2NavigationPropertyName'
192
- ].join(',');
193
- return this.makeRequest(`api/data/v9.2/EntityDefinitions(LogicalName='${entityName}')/ManyToManyRelationships?$select=${selectProperties}`);
194
- }
195
- /**
196
- * Get all relationships (one-to-many and many-to-many) for an entity
197
- * @param entityName The logical name of the entity
198
- */
199
98
  async getEntityRelationships(entityName) {
200
- const [oneToMany, manyToMany] = await Promise.all([
201
- this.getEntityOneToManyRelationships(entityName),
202
- this.getEntityManyToManyRelationships(entityName)
203
- ]);
204
- return {
205
- oneToMany,
206
- manyToMany
207
- };
99
+ return this.metadata.getEntityRelationships(entityName);
208
100
  }
209
- /**
210
- * Get a global option set definition by name
211
- * @param optionSetName The name of the global option set
212
- * @returns The global option set definition
213
- */
214
101
  async getGlobalOptionSet(optionSetName) {
215
- return this.makeRequest(`api/data/v9.2/GlobalOptionSetDefinitions(Name='${optionSetName}')`);
102
+ return this.metadata.getGlobalOptionSet(optionSetName);
216
103
  }
217
- /**
218
- * Get a specific record by entity name (plural) and ID
219
- * @param entityNamePlural The plural name of the entity (e.g., 'accounts', 'contacts')
220
- * @param recordId The GUID of the record
221
- * @returns The record data
222
- */
223
- async getRecord(entityNamePlural, recordId) {
224
- return this.makeRequest(`api/data/v9.2/${entityNamePlural}(${recordId})`);
104
+ async getGlobalOptionSets(options) {
105
+ return this.metadata.getGlobalOptionSets(options);
225
106
  }
226
- /**
227
- * Query records using entity name (plural) and a filter expression
228
- * @param entityNamePlural The plural name of the entity (e.g., 'accounts', 'contacts')
229
- * @param filter OData filter expression (e.g., "name eq 'test'")
230
- * @param maxRecords Maximum number of records to retrieve (default: 50)
231
- * @returns Filtered list of records
232
- */
233
- async queryRecords(entityNamePlural, filter, maxRecords = 50) {
234
- return this.makeRequest(`api/data/v9.2/${entityNamePlural}?$filter=${encodeURIComponent(filter)}&$top=${maxRecords}`);
107
+ // =====================================================
108
+ // PLUGIN METHODS (Read-only)
109
+ // =====================================================
110
+ async getPluginAssemblies(includeManaged, maxRecords) {
111
+ return this.plugin.getPluginAssemblies(includeManaged, maxRecords);
235
112
  }
236
- /**
237
- * Create a new record in Dataverse
238
- * @param entityNamePlural The plural name of the entity (e.g., 'accounts', 'contacts')
239
- * @param data Record data as JSON object (field names must match logical names)
240
- * @returns Created record with ID and OData context
241
- */
242
- async createRecord(entityNamePlural, data) {
243
- const timer = auditLogger.startTimer();
244
- try {
245
- // Validate data is not empty
246
- if (!data || Object.keys(data).length === 0) {
247
- throw new Error('Record data cannot be empty');
248
- }
249
- // Make POST request to create record
250
- const response = await this.makeRequest(`api/data/v9.2/${entityNamePlural}`, 'POST', data, {
251
- 'Prefer': 'return=representation', // Return the created record
252
- });
253
- // Audit logging
254
- auditLogger.log({
255
- operation: 'create-record',
256
- operationType: 'CREATE',
257
- componentType: 'Record',
258
- componentName: entityNamePlural,
259
- success: true,
260
- executionTimeMs: timer(),
261
- });
262
- return response;
263
- }
264
- catch (error) {
265
- // Audit failed operation
266
- auditLogger.log({
267
- operation: 'create-record',
268
- operationType: 'CREATE',
269
- componentType: 'Record',
270
- componentName: entityNamePlural,
271
- success: false,
272
- error: error.message,
273
- executionTimeMs: timer(),
274
- });
275
- throw error;
276
- }
113
+ async getPluginAssemblyComplete(assemblyName, includeDisabled) {
114
+ return this.plugin.getPluginAssemblyComplete(assemblyName, includeDisabled);
277
115
  }
278
- /**
279
- * Update an existing record in Dataverse
280
- * @param entityNamePlural The plural name of the entity (e.g., 'accounts', 'contacts')
281
- * @param recordId The GUID of the record to update
282
- * @param data Partial record data to update (only fields being changed)
283
- * @returns Updated record (if Prefer header used) or void
284
- */
285
- async updateRecord(entityNamePlural, recordId, data) {
286
- const timer = auditLogger.startTimer();
287
- try {
288
- // Validate data is not empty
289
- if (!data || Object.keys(data).length === 0) {
290
- throw new Error('Update data cannot be empty');
291
- }
292
- // Validate recordId is a valid GUID
293
- const guidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
294
- if (!guidRegex.test(recordId)) {
295
- throw new Error(`Invalid record ID format: ${recordId}. Must be a valid GUID.`);
296
- }
297
- // Make PATCH request to update record
298
- const response = await this.makeRequest(`api/data/v9.2/${entityNamePlural}(${recordId})`, 'PATCH', data, {
299
- 'Prefer': 'return=representation', // Return the updated record
300
- });
301
- // Audit logging
302
- auditLogger.log({
303
- operation: 'update-record',
304
- operationType: 'UPDATE',
305
- componentType: 'Record',
306
- componentName: `${entityNamePlural}(${recordId})`,
307
- success: true,
308
- executionTimeMs: timer(),
309
- });
310
- return response;
311
- }
312
- catch (error) {
313
- // Audit failed operation
314
- auditLogger.log({
315
- operation: 'update-record',
316
- operationType: 'UPDATE',
317
- componentType: 'Record',
318
- componentName: `${entityNamePlural}(${recordId})`,
319
- success: false,
320
- error: error.message,
321
- executionTimeMs: timer(),
322
- });
323
- throw error;
324
- }
116
+ async getEntityPluginPipeline(entityName, messageFilter, includeDisabled) {
117
+ return this.plugin.getEntityPluginPipeline(entityName, messageFilter, includeDisabled);
325
118
  }
326
- /**
327
- * Delete a record from Dataverse
328
- * @param entityNamePlural The plural name of the entity (e.g., 'accounts', 'contacts')
329
- * @param recordId The GUID of the record to delete
330
- * @returns Void (successful deletion returns 204 No Content)
331
- */
332
- async deleteRecord(entityNamePlural, recordId) {
333
- const timer = auditLogger.startTimer();
334
- try {
335
- // Validate recordId is a valid GUID
336
- const guidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
337
- if (!guidRegex.test(recordId)) {
338
- throw new Error(`Invalid record ID format: ${recordId}. Must be a valid GUID.`);
339
- }
340
- // Make DELETE request
341
- await this.makeRequest(`api/data/v9.2/${entityNamePlural}(${recordId})`, 'DELETE');
342
- // Audit logging
343
- auditLogger.log({
344
- operation: 'delete-record',
345
- operationType: 'DELETE',
346
- componentType: 'Record',
347
- componentName: `${entityNamePlural}(${recordId})`,
348
- success: true,
349
- executionTimeMs: timer(),
350
- });
351
- }
352
- catch (error) {
353
- // Audit failed operation
354
- auditLogger.log({
355
- operation: 'delete-record',
356
- operationType: 'DELETE',
357
- componentType: 'Record',
358
- componentName: `${entityNamePlural}(${recordId})`,
359
- success: false,
360
- error: error.message,
361
- executionTimeMs: timer(),
362
- });
363
- throw error;
364
- }
365
- }
366
- /**
367
- * Get all plugin assemblies in the environment
368
- * @param includeManaged Include managed assemblies (default: false)
369
- * @param maxRecords Maximum number of assemblies to return (default: 100)
370
- * @returns List of plugin assemblies with basic information
371
- */
372
- async getPluginAssemblies(includeManaged = false, maxRecords = 100) {
373
- const managedFilter = includeManaged ? '' : '$filter=ismanaged eq false&';
374
- 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}`);
375
- // Filter out hidden assemblies and format the results with more readable properties
376
- // Note: ishidden is a ManagedProperty object with a Value property
377
- const formattedAssemblies = assemblies.value
378
- .filter((assembly) => {
379
- const isHidden = assembly.ishidden?.Value !== undefined ? assembly.ishidden.Value : assembly.ishidden;
380
- return !isHidden;
381
- })
382
- .map((assembly) => ({
383
- pluginassemblyid: assembly.pluginassemblyid,
384
- name: assembly.name,
385
- version: assembly.version,
386
- isolationMode: assembly.isolationmode === 1 ? 'None' : assembly.isolationmode === 2 ? 'Sandbox' : 'External',
387
- isManaged: assembly.ismanaged,
388
- modifiedOn: assembly.modifiedon,
389
- modifiedBy: assembly.modifiedby?.fullname,
390
- major: assembly.major,
391
- minor: assembly.minor
392
- }));
393
- return {
394
- totalCount: formattedAssemblies.length,
395
- assemblies: formattedAssemblies
396
- };
397
- }
398
- /**
399
- * Get a plugin assembly by name with all related plugin types, steps, and images
400
- * @param assemblyName The name of the plugin assembly
401
- * @param includeDisabled Include disabled steps (default: false)
402
- * @returns Complete plugin assembly information with validation
403
- */
404
- async getPluginAssemblyComplete(assemblyName, includeDisabled = false) {
405
- // Get the plugin assembly (excluding content_binary which is large and not useful for review)
406
- 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)`);
407
- if (!assemblies.value || assemblies.value.length === 0) {
408
- throw new Error(`Plugin assembly '${assemblyName}' not found`);
409
- }
410
- const assembly = assemblies.value[0];
411
- const assemblyId = assembly.pluginassemblyid;
412
- // Get plugin types for this assembly
413
- const pluginTypes = await this.makeRequest(`api/data/v9.2/plugintypes?$filter=_pluginassemblyid_value eq ${assemblyId}&$select=plugintypeid,typename,friendlyname,name,assemblyname,description,workflowactivitygroupname`);
414
- // Get all steps for each plugin type
415
- const pluginTypeIds = pluginTypes.value.map((pt) => pt.plugintypeid);
416
- let allSteps = [];
417
- if (pluginTypeIds.length > 0) {
418
- const statusFilter = includeDisabled ? '' : ' and statuscode eq 1';
419
- // Build filter for all plugin type IDs
420
- const typeFilter = pluginTypeIds.map((id) => `_plugintypeid_value eq ${id}`).join(' or ');
421
- 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`);
422
- allSteps = steps.value;
423
- }
424
- // Get all images for these steps
425
- const stepIds = allSteps.map((s) => s.sdkmessageprocessingstepid);
426
- let allImages = [];
427
- if (stepIds.length > 0) {
428
- // Build filter for all step IDs
429
- const imageFilter = stepIds.map((id) => `_sdkmessageprocessingstepid_value eq ${id}`).join(' or ');
430
- const images = await this.makeRequest(`api/data/v9.2/sdkmessageprocessingstepimages?$filter=${imageFilter}&$select=sdkmessageprocessingstepimageid,name,imagetype,messagepropertyname,entityalias,attributes,_sdkmessageprocessingstepid_value`);
431
- allImages = images.value;
432
- }
433
- // Attach images to their respective steps
434
- const stepsWithImages = allSteps.map((step) => ({
435
- ...step,
436
- images: allImages.filter((img) => img._sdkmessageprocessingstepid_value === step.sdkmessageprocessingstepid)
437
- }));
438
- // Validation checks
439
- const validation = {
440
- hasDisabledSteps: allSteps.some((s) => s.statuscode !== 1),
441
- hasAsyncSteps: allSteps.some((s) => s.mode === 1),
442
- hasSyncSteps: allSteps.some((s) => s.mode === 0),
443
- stepsWithoutFilteringAttributes: stepsWithImages
444
- .filter((s) => (s.sdkmessageid?.name === 'Update' || s.sdkmessageid?.name === 'Delete') && !s.filteringattributes)
445
- .map((s) => s.name),
446
- stepsWithoutImages: stepsWithImages
447
- .filter((s) => s.images.length === 0 && (s.sdkmessageid?.name === 'Update' || s.sdkmessageid?.name === 'Delete'))
448
- .map((s) => s.name),
449
- potentialIssues: []
450
- };
451
- // Add potential issues
452
- if (validation.stepsWithoutFilteringAttributes.length > 0) {
453
- validation.potentialIssues.push(`${validation.stepsWithoutFilteringAttributes.length} Update/Delete steps without filtering attributes (performance concern)`);
454
- }
455
- if (validation.stepsWithoutImages.length > 0) {
456
- validation.potentialIssues.push(`${validation.stepsWithoutImages.length} Update/Delete steps without images (may need entity data)`);
457
- }
458
- return {
459
- assembly,
460
- pluginTypes: pluginTypes.value,
461
- steps: stepsWithImages,
462
- validation
463
- };
464
- }
465
- /**
466
- * Get all plugins that execute on a specific entity, organized by message and execution order
467
- * @param entityName The logical name of the entity
468
- * @param messageFilter Optional filter by message name (e.g., "Create", "Update")
469
- * @param includeDisabled Include disabled steps (default: false)
470
- * @returns Complete plugin pipeline for the entity
471
- */
472
- async getEntityPluginPipeline(entityName, messageFilter, includeDisabled = false) {
473
- const statusFilter = includeDisabled ? '' : ' and statuscode eq 1';
474
- const msgFilter = messageFilter ? ` and sdkmessageid/name eq '${messageFilter}'` : '';
475
- // Get all steps for this entity
476
- 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`);
477
- // Get assembly information for each plugin type (filter out nulls)
478
- const pluginTypeIds = [...new Set(steps.value.map((s) => s._plugintypeid_value).filter((id) => id != null))];
479
- const assemblyMap = new Map();
480
- for (const typeId of pluginTypeIds) {
481
- const pluginType = await this.makeRequest(`api/data/v9.2/plugintypes(${typeId})?$expand=pluginassemblyid($select=name,version)`);
482
- assemblyMap.set(typeId, pluginType.pluginassemblyid);
483
- }
484
- // Get images for all steps
485
- const stepIds = steps.value.map((s) => s.sdkmessageprocessingstepid);
486
- let allImages = [];
487
- if (stepIds.length > 0) {
488
- const imageFilter = stepIds.map((id) => `_sdkmessageprocessingstepid_value eq ${id}`).join(' or ');
489
- const images = await this.makeRequest(`api/data/v9.2/sdkmessageprocessingstepimages?$filter=${imageFilter}&$select=sdkmessageprocessingstepimageid,name,imagetype,messagepropertyname,entityalias,attributes,_sdkmessageprocessingstepid_value`);
490
- allImages = images.value;
491
- }
492
- // Format steps with all information
493
- const formattedSteps = steps.value.map((step) => {
494
- const assembly = assemblyMap.get(step._plugintypeid_value);
495
- const images = allImages.filter((img) => img._sdkmessageprocessingstepid_value === step.sdkmessageprocessingstepid);
496
- return {
497
- sdkmessageprocessingstepid: step.sdkmessageprocessingstepid,
498
- name: step.name,
499
- stage: step.stage,
500
- stageName: step.stage === 10 ? 'PreValidation' : step.stage === 20 ? 'PreOperation' : 'PostOperation',
501
- mode: step.mode,
502
- modeName: step.mode === 0 ? 'Synchronous' : 'Asynchronous',
503
- rank: step.rank,
504
- message: step.sdkmessageid?.name,
505
- pluginType: step.plugintypeid?.typename,
506
- assemblyName: assembly?.name,
507
- assemblyVersion: assembly?.version,
508
- filteringAttributes: step.filteringattributes ? step.filteringattributes.split(',') : [],
509
- statuscode: step.statuscode,
510
- enabled: step.statuscode === 1,
511
- deployment: step.supporteddeployment === 0 ? 'Server' : step.supporteddeployment === 1 ? 'Offline' : 'Both',
512
- impersonatingUser: step.impersonatinguserid?.fullname,
513
- hasPreImage: images.some((img) => img.imagetype === 0 || img.imagetype === 2),
514
- hasPostImage: images.some((img) => img.imagetype === 1 || img.imagetype === 2),
515
- images: images
516
- };
517
- });
518
- // Organize by message
519
- const messageGroups = new Map();
520
- formattedSteps.forEach((step) => {
521
- if (!messageGroups.has(step.message)) {
522
- messageGroups.set(step.message, {
523
- messageName: step.message,
524
- stages: {
525
- preValidation: [],
526
- preOperation: [],
527
- postOperation: []
528
- }
529
- });
530
- }
531
- const msg = messageGroups.get(step.message);
532
- if (step.stage === 10)
533
- msg.stages.preValidation.push(step);
534
- else if (step.stage === 20)
535
- msg.stages.preOperation.push(step);
536
- else if (step.stage === 40)
537
- msg.stages.postOperation.push(step);
538
- });
539
- return {
540
- entity: entityName,
541
- messages: Array.from(messageGroups.values()),
542
- steps: formattedSteps,
543
- executionOrder: formattedSteps.map((s) => s.name)
544
- };
545
- }
546
- /**
547
- * Get plugin trace logs with filtering
548
- * @param options Filtering options for trace logs
549
- * @returns Filtered trace logs with parsed exception details
550
- */
551
119
  async getPluginTraceLogs(options) {
552
- const { entityName, messageName, correlationId, pluginStepId, exceptionOnly = false, hoursBack = 24, maxRecords = 50 } = options;
553
- // Build filter
554
- const filters = [];
555
- // Date filter
556
- const dateThreshold = new Date();
557
- dateThreshold.setHours(dateThreshold.getHours() - hoursBack);
558
- filters.push(`createdon gt ${dateThreshold.toISOString()}`);
559
- if (entityName)
560
- filters.push(`primaryentity eq '${entityName}'`);
561
- if (messageName)
562
- filters.push(`messagename eq '${messageName}'`);
563
- if (correlationId)
564
- filters.push(`correlationid eq '${correlationId}'`);
565
- if (pluginStepId)
566
- filters.push(`_sdkmessageprocessingstepid_value eq ${pluginStepId}`);
567
- if (exceptionOnly)
568
- filters.push(`exceptiondetails ne null`);
569
- const filterString = filters.join(' and ');
570
- const logs = await this.makeRequest(`api/data/v9.2/plugintracelogs?$filter=${filterString}&$orderby=createdon desc&$top=${maxRecords}`);
571
- // Parse logs for better readability
572
- const parsedLogs = logs.value.map((log) => ({
573
- ...log,
574
- modeName: log.mode === 0 ? 'Synchronous' : 'Asynchronous',
575
- operationTypeName: this.getOperationTypeName(log.operationtype),
576
- parsed: {
577
- hasException: !!log.exceptiondetails,
578
- exceptionType: log.exceptiondetails ? this.extractExceptionType(log.exceptiondetails) : null,
579
- exceptionMessage: log.exceptiondetails ? this.extractExceptionMessage(log.exceptiondetails) : null,
580
- stackTrace: log.exceptiondetails
581
- }
582
- }));
583
- return {
584
- totalCount: parsedLogs.length,
585
- logs: parsedLogs
586
- };
120
+ return this.plugin.getPluginTraceLogs(options);
587
121
  }
588
- // Helper methods for trace log parsing
589
- getOperationTypeName(operationType) {
590
- const types = {
591
- 0: 'None',
592
- 1: 'Create',
593
- 2: 'Update',
594
- 3: 'Delete',
595
- 4: 'Retrieve',
596
- 5: 'RetrieveMultiple',
597
- 6: 'Associate',
598
- 7: 'Disassociate'
599
- };
600
- return types[operationType] || 'Unknown';
601
- }
602
- extractExceptionType(exceptionDetails) {
603
- const match = exceptionDetails.match(/^([^:]+):/);
604
- return match ? match[1].trim() : null;
605
- }
606
- extractExceptionMessage(exceptionDetails) {
607
- const lines = exceptionDetails.split('\n');
608
- if (lines.length > 0) {
609
- const firstLine = lines[0];
610
- const colonIndex = firstLine.indexOf(':');
611
- if (colonIndex > 0) {
612
- return firstLine.substring(colonIndex + 1).trim();
613
- }
614
- }
615
- return null;
122
+ // =====================================================
123
+ // FLOW METHODS (Read-only)
124
+ // =====================================================
125
+ async getFlows(options) {
126
+ return this.flow.getFlows(options);
616
127
  }
617
- /**
618
- * Get all Power Automate flows (cloud flows) in the environment
619
- * @param activeOnly Only return activated flows (default: false)
620
- * @param maxRecords Maximum number of flows to return (default: 100)
621
- * @returns List of Power Automate flows with basic information
622
- */
623
- async getFlows(activeOnly = false, maxRecords = 100) {
624
- // Category 5 = Modern Flow (Power Automate cloud flows)
625
- // StateCode: 0=Draft, 1=Activated, 2=Suspended
626
- // Type: 1=Definition, 2=Activation
627
- const stateFilter = activeOnly ? ' and statecode eq 1' : '';
628
- 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}`);
629
- // Format the results for better readability
630
- const formattedFlows = flows.value.map((flow) => ({
631
- workflowid: flow.workflowid,
632
- name: flow.name,
633
- description: flow.description,
634
- state: flow.statecode === 0 ? 'Draft' : flow.statecode === 1 ? 'Activated' : 'Suspended',
635
- statecode: flow.statecode,
636
- statuscode: flow.statuscode,
637
- type: flow.type === 1 ? 'Definition' : flow.type === 2 ? 'Activation' : 'Template',
638
- primaryEntity: flow.primaryentity,
639
- isManaged: flow.ismanaged,
640
- ownerId: flow._ownerid_value,
641
- modifiedOn: flow.modifiedon,
642
- modifiedBy: flow.modifiedby?.fullname,
643
- createdOn: flow.createdon,
644
- hasDefinition: !!flow.clientdata
645
- }));
646
- return {
647
- totalCount: formattedFlows.length,
648
- flows: formattedFlows
649
- };
128
+ async getFlowDefinition(flowId, summary) {
129
+ return this.flow.getFlowDefinition(flowId, summary);
650
130
  }
651
- /**
652
- * Get a specific Power Automate flow with its complete definition
653
- * @param flowId The GUID of the flow (workflowid)
654
- * @returns Complete flow information including the flow definition JSON
655
- */
656
- async getFlowDefinition(flowId) {
657
- 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)`);
658
- // Parse the clientdata (flow definition) if it exists
659
- let flowDefinition = null;
660
- if (flow.clientdata) {
661
- try {
662
- flowDefinition = JSON.parse(flow.clientdata);
663
- }
664
- catch (error) {
665
- console.error('Failed to parse flow definition JSON:', error);
666
- flowDefinition = { parseError: 'Failed to parse flow definition', raw: flow.clientdata };
667
- }
668
- }
669
- return {
670
- workflowid: flow.workflowid,
671
- name: flow.name,
672
- description: flow.description,
673
- state: flow.statecode === 0 ? 'Draft' : flow.statecode === 1 ? 'Activated' : 'Suspended',
674
- statecode: flow.statecode,
675
- statuscode: flow.statuscode,
676
- type: flow.type === 1 ? 'Definition' : flow.type === 2 ? 'Activation' : 'Template',
677
- category: flow.category,
678
- primaryEntity: flow.primaryentity,
679
- isManaged: flow.ismanaged,
680
- ownerId: flow._ownerid_value,
681
- createdOn: flow.createdon,
682
- createdBy: flow.createdby?.fullname,
683
- modifiedOn: flow.modifiedon,
684
- modifiedBy: flow.modifiedby?.fullname,
685
- flowDefinition: flowDefinition
686
- };
131
+ async getFlowRuns(flowId, maxRecords) {
132
+ return this.flow.getFlowRuns(flowId, maxRecords);
687
133
  }
688
- /**
689
- * Get flow run history for a specific Power Automate flow
690
- * @param flowId The GUID of the flow (workflowid)
691
- * @param maxRecords Maximum number of runs to return (default: 100)
692
- * @returns List of flow runs with status, start time, duration, and error details
693
- */
694
- async getFlowRuns(flowId, maxRecords = 100) {
695
- // Flow runs are stored in the flowruns entity (not flowsession)
696
- // Status: "Succeeded", "Failed", "Faulted", "TimedOut", "Cancelled", "Running", etc.
697
- 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}`);
698
- // Format the results for better readability
699
- const formattedRuns = flowRuns.value.map((run) => {
700
- // Parse error message if it's JSON
701
- let parsedError = run.errormessage;
702
- if (run.errormessage) {
703
- try {
704
- parsedError = JSON.parse(run.errormessage);
705
- }
706
- catch (e) {
707
- // Keep as string if not valid JSON
708
- }
709
- }
710
- return {
711
- flowrunid: run.flowrunid,
712
- name: run.name,
713
- status: run.status,
714
- startedOn: run.starttime,
715
- completedOn: run.endtime,
716
- duration: run.duration,
717
- errorMessage: parsedError || null,
718
- errorCode: run.errorcode || null,
719
- triggerType: run.triggertype || null
720
- };
721
- });
722
- return {
723
- flowId: flowId,
724
- totalCount: formattedRuns.length,
725
- runs: formattedRuns
726
- };
134
+ // =====================================================
135
+ // WORKFLOW METHODS (Read-only)
136
+ // =====================================================
137
+ async getWorkflows(activeOnly, maxRecords) {
138
+ return this.workflow.getWorkflows(activeOnly, maxRecords);
727
139
  }
728
- /**
729
- * Get all classic Dynamics workflows in the environment
730
- * @param activeOnly Only return activated workflows (default: false)
731
- * @param maxRecords Maximum number of workflows to return (default: 100)
732
- * @returns List of classic workflows with basic information
733
- */
734
- async getWorkflows(activeOnly = false, maxRecords = 100) {
735
- // Category 0 = Classic Workflow
736
- // StateCode: 0=Draft, 1=Activated, 2=Suspended
737
- // Type: 1=Definition, 2=Activation
738
- const stateFilter = activeOnly ? ' and statecode eq 1' : '';
739
- 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}`);
740
- // Format the results for better readability
741
- const formattedWorkflows = workflows.value.map((workflow) => ({
742
- workflowid: workflow.workflowid,
743
- name: workflow.name,
744
- description: workflow.description,
745
- state: workflow.statecode === 0 ? 'Draft' : workflow.statecode === 1 ? 'Activated' : 'Suspended',
746
- statecode: workflow.statecode,
747
- statuscode: workflow.statuscode,
748
- type: workflow.type === 1 ? 'Definition' : workflow.type === 2 ? 'Activation' : 'Template',
749
- mode: workflow.mode === 0 ? 'Background' : 'Real-time',
750
- primaryEntity: workflow.primaryentity,
751
- isManaged: workflow.ismanaged,
752
- isOnDemand: workflow.ondemand,
753
- triggerOnCreate: workflow.triggeroncreate,
754
- triggerOnDelete: workflow.triggerondelete,
755
- isSubprocess: workflow.subprocess,
756
- owner: workflow.ownerid?.fullname,
757
- modifiedOn: workflow.modifiedon,
758
- modifiedBy: workflow.modifiedby?.fullname,
759
- createdOn: workflow.createdon
760
- }));
761
- return {
762
- totalCount: formattedWorkflows.length,
763
- workflows: formattedWorkflows
764
- };
140
+ async getWorkflowDefinition(workflowId, summary) {
141
+ return this.workflow.getWorkflowDefinition(workflowId, summary);
765
142
  }
766
- /**
767
- * Get a specific classic workflow with its complete XAML definition
768
- * @param workflowId The GUID of the workflow (workflowid)
769
- * @returns Complete workflow information including the XAML definition
770
- */
771
- async getWorkflowDefinition(workflowId) {
772
- 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)`);
773
- return {
774
- workflowid: workflow.workflowid,
775
- name: workflow.name,
776
- description: workflow.description,
777
- state: workflow.statecode === 0 ? 'Draft' : workflow.statecode === 1 ? 'Activated' : 'Suspended',
778
- statecode: workflow.statecode,
779
- statuscode: workflow.statuscode,
780
- type: workflow.type === 1 ? 'Definition' : workflow.type === 2 ? 'Activation' : 'Template',
781
- category: workflow.category,
782
- mode: workflow.mode === 0 ? 'Background' : 'Real-time',
783
- primaryEntity: workflow.primaryentity,
784
- isManaged: workflow.ismanaged,
785
- isOnDemand: workflow.ondemand,
786
- triggerOnCreate: workflow.triggeroncreate,
787
- triggerOnDelete: workflow.triggerondelete,
788
- triggerOnUpdateAttributes: workflow.triggeronupdateattributelist ? workflow.triggeronupdateattributelist.split(',') : [],
789
- isSubprocess: workflow.subprocess,
790
- syncWorkflowLogOnFailure: workflow.syncworkflowlogonfailure,
791
- owner: workflow.ownerid?.fullname,
792
- createdOn: workflow.createdon,
793
- createdBy: workflow.createdby?.fullname,
794
- modifiedOn: workflow.modifiedon,
795
- modifiedBy: workflow.modifiedby?.fullname,
796
- xaml: workflow.xaml
797
- };
798
- }
799
- /**
800
- * Update a workflow's description field
801
- * @param workflowId GUID of the workflow
802
- * @param description New description content
803
- * @returns Previous and new description
804
- */
805
- async updateWorkflowDescription(workflowId, description) {
806
- // Client-side length validation (Power Platform limit: 1024 characters)
807
- const MAX_DESCRIPTION_LENGTH = 1024;
808
- if (description.length > MAX_DESCRIPTION_LENGTH) {
809
- const overage = description.length - MAX_DESCRIPTION_LENGTH;
810
- throw new Error(`Description too long (${description.length}/${MAX_DESCRIPTION_LENGTH} chars). ` +
811
- `Please shorten by ${overage} character${overage === 1 ? '' : 's'}.`);
812
- }
813
- // First, get current description for audit trail
814
- const currentWorkflow = await this.makeRequest(`api/data/v9.2/workflows(${workflowId})?$select=description`);
815
- const previousDescription = currentWorkflow.description || '';
816
- // Update description
817
- await this.makeRequest(`api/data/v9.2/workflows(${workflowId})`, 'PATCH', { description });
818
- return {
819
- success: true,
820
- previousDescription,
821
- newDescription: description
822
- };
823
- }
824
- /**
825
- * Generate YAML metadata block for automation documentation
826
- */
827
- generateAutomationYaml(analysis) {
828
- const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
829
- return `[AUTO-DOCS:v1]
830
- tables_modified: ${analysis.tablesModified.join(', ') || 'none'}
831
- trigger: ${analysis.trigger}
832
- trigger_fields: ${analysis.triggerFields.join(', ') || 'none'}
833
- actions: ${analysis.actions.join(', ')}
834
- analyzed: ${today}
835
- ---`;
836
- }
837
- /**
838
- * Merge YAML metadata with existing description, preserving manual notes
839
- */
840
- mergeDescriptionWithYaml(yamlBlock, existingDescription) {
841
- // Case 1: Empty description
842
- if (!existingDescription || existingDescription.trim() === '') {
843
- return `${yamlBlock}\n[Manual notes below this line are preserved on re-analysis]`;
844
- }
845
- // Case 2: Has [AUTO-DOCS: tag - extract and preserve content after ---
846
- const autoDocsMatch = existingDescription.match(/\[AUTO-DOCS:v\d+\]([\s\S]*?)^---$/m);
847
- if (autoDocsMatch) {
848
- const separatorIndex = existingDescription.indexOf('---', autoDocsMatch.index);
849
- const manualNotes = existingDescription.substring(separatorIndex + 3).trim();
850
- if (manualNotes) {
851
- return `${yamlBlock}\n${manualNotes}`;
852
- }
853
- else {
854
- return `${yamlBlock}\n[Manual notes below this line are preserved on re-analysis]`;
855
- }
856
- }
857
- // Case 3: Has content but no [AUTO-DOCS: tag - treat entire content as manual notes
858
- return `${yamlBlock}\n${existingDescription.trim()}`;
859
- }
860
- /**
861
- * Analyze a flow or workflow and document it with YAML metadata
862
- * @param automationId GUID of the flow or workflow
863
- * @param type Type of automation ('flow' or 'workflow'), auto-detected if not provided
864
- * @returns Analysis and description update result
865
- */
866
- async documentAutomation(automationId, type) {
867
- // Fetch automation (flow or workflow)
868
- const accessToken = await this.getAccessToken();
869
- const response = await fetch(`${this.config.organizationUrl}/api/data/v9.2/workflows(${automationId})?$select=category,clientdata,description`, {
870
- headers: {
871
- 'Authorization': `Bearer ${accessToken}`,
872
- 'Accept': 'application/json',
873
- 'OData-MaxVersion': '4.0',
874
- 'OData-Version': '4.0'
875
- }
876
- });
877
- if (!response.ok) {
878
- throw new Error(`Failed to fetch automation: ${response.status} ${response.statusText}`);
879
- }
880
- const automation = await response.json();
881
- const previousDescription = automation.description || '';
882
- // Detect type if not provided (category 5 = flow, 0 = workflow)
883
- const detectedType = type || (automation.category === 5 ? 'flow' : 'workflow');
884
- let analysis;
885
- if (detectedType === 'flow') {
886
- // Parse flow definition from clientdata
887
- if (!automation.clientdata) {
888
- throw new Error('Flow has no clientdata (definition)');
889
- }
890
- const flowDefinition = JSON.parse(automation.clientdata);
891
- // Use enhanced parser from base package
892
- const readOnlyService = new PowerPlatformReadOnlyService({
893
- organizationUrl: this.config.organizationUrl,
894
- clientId: this.config.clientId,
895
- clientSecret: this.config.clientSecret,
896
- tenantId: this.config.tenantId
897
- });
898
- // Call public method from base package
899
- const summary = readOnlyService.parseFlowSummary(flowDefinition);
900
- analysis = {
901
- tablesModified: summary.tablesModified,
902
- trigger: summary.triggerInfo,
903
- triggerFields: summary.triggerFields,
904
- actions: summary.actions
905
- };
906
- }
907
- else {
908
- // Fetch workflow XAML
909
- const xamlResponse = await fetch(`${this.config.organizationUrl}/api/data/v9.2/workflows(${automationId})?$select=xaml`, {
910
- headers: {
911
- 'Authorization': `Bearer ${accessToken}`,
912
- 'Accept': 'application/json',
913
- 'OData-MaxVersion': '4.0',
914
- 'OData-Version': '4.0'
915
- }
916
- });
917
- if (!xamlResponse.ok) {
918
- throw new Error(`Failed to fetch workflow XAML: ${xamlResponse.status}`);
919
- }
920
- const xamlData = await xamlResponse.json();
921
- if (!xamlData.xaml) {
922
- throw new Error('Workflow has no XAML definition');
923
- }
924
- // Use enhanced parser from base package
925
- const readOnlyService = new PowerPlatformReadOnlyService({
926
- organizationUrl: this.config.organizationUrl,
927
- clientId: this.config.clientId,
928
- clientSecret: this.config.clientSecret,
929
- tenantId: this.config.tenantId
930
- });
931
- const summary = readOnlyService.parseWorkflowXamlSummary(xamlData.xaml);
932
- // Get action types from XAML
933
- const actions = [];
934
- if (summary.createEntityCount > 0)
935
- actions.push('create_record');
936
- if (summary.updateEntityCount > 0)
937
- actions.push('update_record');
938
- if (summary.assignEntityCount > 0)
939
- actions.push('assign_record');
940
- if (summary.setStateCount > 0)
941
- actions.push('set_state');
942
- analysis = {
943
- tablesModified: summary.tablesModified,
944
- trigger: summary.triggerInfo,
945
- triggerFields: summary.triggerFields,
946
- actions
947
- };
948
- }
949
- // Generate YAML and merge with existing description
950
- const yamlBlock = this.generateAutomationYaml(analysis);
951
- const newDescription = this.mergeDescriptionWithYaml(yamlBlock, previousDescription);
952
- // Update description
953
- const updateResult = await this.updateWorkflowDescription(automationId, newDescription);
954
- return {
955
- analysis,
956
- descriptionUpdated: updateResult.success,
957
- previousDescription: updateResult.previousDescription,
958
- newDescription: updateResult.newDescription
959
- };
143
+ // =====================================================
144
+ // BUSINESS RULE METHODS (Read-only)
145
+ // =====================================================
146
+ async getBusinessRules(activeOnly, maxRecords) {
147
+ return this.businessRule.getBusinessRules(activeOnly, maxRecords);
960
148
  }
961
- /**
962
- * Update a Power Automate flow's description field
963
- * @param flowId GUID of the flow (workflowid in workflows entity)
964
- * @param description New description content
965
- * @returns Previous and new description
966
- */
967
- async updateFlowDescription(flowId, description) {
968
- // Flows and workflows share the same entity and update mechanism
969
- // The only difference is category (5 = flow, 0 = workflow)
970
- return this.updateWorkflowDescription(flowId, description);
971
- }
972
- /**
973
- * Get all business rules in the environment
974
- * @param activeOnly Only return activated business rules (default: false)
975
- * @param maxRecords Maximum number of business rules to return (default: 100)
976
- * @returns List of business rules with basic information
977
- */
978
- async getBusinessRules(activeOnly = false, maxRecords = 100) {
979
- // Category 2 = Business Rule
980
- // StateCode: 0=Draft, 1=Activated, 2=Suspended
981
- // Type: 1=Definition
982
- const stateFilter = activeOnly ? ' and statecode eq 1' : '';
983
- const businessRules = await this.makeRequest(`api/data/v9.2/workflows?$filter=category eq 2${stateFilter}&$select=workflowid,name,statecode,statuscode,description,createdon,modifiedon,type,ismanaged,primaryentity&$expand=ownerid($select=fullname),modifiedby($select=fullname)&$orderby=modifiedon desc&$top=${maxRecords}`);
984
- // Format the results for better readability
985
- const formattedBusinessRules = businessRules.value.map((rule) => ({
986
- workflowid: rule.workflowid,
987
- name: rule.name,
988
- description: rule.description,
989
- state: rule.statecode === 0 ? 'Draft' : rule.statecode === 1 ? 'Activated' : 'Suspended',
990
- statecode: rule.statecode,
991
- statuscode: rule.statuscode,
992
- type: rule.type === 1 ? 'Definition' : rule.type === 2 ? 'Activation' : 'Template',
993
- primaryEntity: rule.primaryentity,
994
- isManaged: rule.ismanaged,
995
- owner: rule.ownerid?.fullname,
996
- modifiedOn: rule.modifiedon,
997
- modifiedBy: rule.modifiedby?.fullname,
998
- createdOn: rule.createdon
999
- }));
1000
- return {
1001
- totalCount: formattedBusinessRules.length,
1002
- businessRules: formattedBusinessRules
1003
- };
1004
- }
1005
- /**
1006
- * Get a specific business rule with its complete XAML definition
1007
- * @param workflowId The GUID of the business rule (workflowid)
1008
- * @returns Complete business rule information including the XAML definition
1009
- */
1010
149
  async getBusinessRule(workflowId) {
1011
- const businessRule = await this.makeRequest(`api/data/v9.2/workflows(${workflowId})?$select=workflowid,name,statecode,statuscode,description,createdon,modifiedon,type,category,ismanaged,primaryentity,xaml&$expand=ownerid($select=fullname),modifiedby($select=fullname),createdby($select=fullname)`);
1012
- // Verify it's actually a business rule
1013
- if (businessRule.category !== 2) {
1014
- throw new Error(`Workflow ${workflowId} is not a business rule (category: ${businessRule.category})`);
1015
- }
1016
- return {
1017
- workflowid: businessRule.workflowid,
1018
- name: businessRule.name,
1019
- description: businessRule.description,
1020
- state: businessRule.statecode === 0 ? 'Draft' : businessRule.statecode === 1 ? 'Activated' : 'Suspended',
1021
- statecode: businessRule.statecode,
1022
- statuscode: businessRule.statuscode,
1023
- type: businessRule.type === 1 ? 'Definition' : businessRule.type === 2 ? 'Activation' : 'Template',
1024
- category: businessRule.category,
1025
- primaryEntity: businessRule.primaryentity,
1026
- isManaged: businessRule.ismanaged,
1027
- owner: businessRule.ownerid?.fullname,
1028
- createdOn: businessRule.createdon,
1029
- createdBy: businessRule.createdby?.fullname,
1030
- modifiedOn: businessRule.modifiedon,
1031
- modifiedBy: businessRule.modifiedby?.fullname,
1032
- xaml: businessRule.xaml
1033
- };
150
+ return this.businessRule.getBusinessRule(workflowId);
1034
151
  }
1035
- // ==================== MODEL-DRIVEN APP OPERATIONS ====================
1036
- /**
1037
- * Get all model-driven apps in the environment
1038
- * @param activeOnly Only return active apps (default: false)
1039
- * @param maxRecords Maximum number of apps to return (default: 100)
1040
- * @returns List of model-driven apps with basic information
1041
- */
1042
- async getApps(activeOnly = false, maxRecords = 100, includeUnpublished = true, solutionUniqueName) {
1043
- // Build filter conditions
1044
- const filters = [];
1045
- // StateCode: 0=Active, 1=Inactive
1046
- if (activeOnly) {
1047
- filters.push('statecode eq 0');
1048
- }
1049
- // Published status: publishedon null = unpublished
1050
- if (!includeUnpublished) {
1051
- filters.push('publishedon ne null');
1052
- }
1053
- const filterString = filters.length > 0 ? `&$filter=${filters.join(' and ')}` : '';
1054
- const apps = await this.makeRequest(`api/data/v9.2/appmodules?$select=appmoduleid,name,uniquename,description,webresourceid,clienttype,formfactor,navigationtype,url,isfeatured,isdefault,publishedon,statecode,statuscode,_publisherid_value,createdon,modifiedon&$orderby=modifiedon desc&$top=${maxRecords}${filterString}`);
1055
- // If solution filter specified, filter results by solution
1056
- let filteredApps = apps.value;
1057
- if (solutionUniqueName) {
1058
- // Query solution components to find apps in the specified solution
1059
- const solution = await this.makeRequest(`api/data/v9.2/solutions?$filter=uniquename eq '${solutionUniqueName}'&$select=solutionid`);
1060
- if (solution.value.length > 0) {
1061
- const solutionId = solution.value[0].solutionid;
1062
- // Query solution components for app modules
1063
- const solutionComponents = await this.makeRequest(`api/data/v9.2/solutioncomponents?$filter=_solutionid_value eq ${solutionId} and componenttype eq 80&$select=objectid`);
1064
- const appIdsInSolution = new Set(solutionComponents.value.map((c) => c.objectid.toLowerCase()));
1065
- filteredApps = apps.value.filter((app) => appIdsInSolution.has(app.appmoduleid.toLowerCase()));
1066
- }
1067
- }
1068
- // Format the results for better readability
1069
- const formattedApps = filteredApps.map((app) => ({
1070
- appmoduleid: app.appmoduleid,
1071
- name: app.name,
1072
- uniquename: app.uniquename,
1073
- description: app.description,
1074
- webresourceid: app.webresourceid,
1075
- clienttype: app.clienttype,
1076
- formfactor: app.formfactor,
1077
- navigationtype: app.navigationtype,
1078
- url: app.url,
1079
- isfeatured: app.isfeatured,
1080
- isdefault: app.isdefault,
1081
- state: app.statecode === 0 ? 'Active' : 'Inactive',
1082
- statecode: app.statecode,
1083
- statuscode: app.statuscode,
1084
- publishedon: app.publishedon,
1085
- published: app.publishedon ? true : false,
1086
- publisherid: app._publisherid_value || null,
1087
- createdon: app.createdon,
1088
- modifiedon: app.modifiedon
1089
- }));
1090
- return {
1091
- totalCount: formattedApps.length,
1092
- apps: formattedApps,
1093
- filters: {
1094
- activeOnly,
1095
- includeUnpublished,
1096
- solutionUniqueName: solutionUniqueName || 'all'
1097
- }
1098
- };
152
+ // =====================================================
153
+ // APP METHODS (Read-only)
154
+ // =====================================================
155
+ async getApps(activeOnly, maxRecords, includeUnpublished, solutionUniqueName) {
156
+ return this.app.getApps(activeOnly, maxRecords, includeUnpublished, solutionUniqueName);
1099
157
  }
1100
- /**
1101
- * Get a specific model-driven app by ID
1102
- * @param appId The GUID of the app (appmoduleid)
1103
- * @returns Complete app information including publisher details
1104
- */
1105
158
  async getApp(appId) {
1106
- const app = await this.makeRequest(`api/data/v9.2/appmodules(${appId})?$select=appmoduleid,name,uniquename,description,webresourceid,clienttype,formfactor,navigationtype,url,isfeatured,isdefault,publishedon,statecode,statuscode,configxml,createdon,modifiedon,_publisherid_value,_createdby_value,_modifiedby_value`);
1107
- return {
1108
- appmoduleid: app.appmoduleid,
1109
- name: app.name,
1110
- uniquename: app.uniquename,
1111
- description: app.description,
1112
- webresourceid: app.webresourceid,
1113
- clienttype: app.clienttype,
1114
- formfactor: app.formfactor,
1115
- navigationtype: app.navigationtype === 0 ? 'Single Session' : 'Multi Session',
1116
- url: app.url,
1117
- isfeatured: app.isfeatured,
1118
- isdefault: app.isdefault,
1119
- state: app.statecode === 0 ? 'Active' : 'Inactive',
1120
- statecode: app.statecode,
1121
- statuscode: app.statuscode,
1122
- publishedon: app.publishedon,
1123
- createdon: app.createdon,
1124
- modifiedon: app.modifiedon,
1125
- createdBy: app._createdby_value || null,
1126
- modifiedBy: app._modifiedby_value || null,
1127
- publisherid: app._publisherid_value || null
1128
- };
159
+ return this.app.getApp(appId);
1129
160
  }
1130
- /**
1131
- * Get all components (entities, forms, views, sitemaps) associated with an app
1132
- * @param appId The GUID of the app (appmoduleid)
1133
- * @returns List of app components with type information
1134
- */
1135
161
  async getAppComponents(appId) {
1136
- const components = await this.makeRequest(`api/data/v9.2/appmodulecomponents?$filter=_appmoduleidunique_value eq ${appId}&$select=appmodulecomponentid,objectid,componenttype,rootappmodulecomponentid,createdon,modifiedon&$orderby=componenttype asc`);
1137
- // Map component type numbers to friendly names
1138
- const componentTypeMap = {
1139
- 1: 'Entity',
1140
- 24: 'Form',
1141
- 26: 'View',
1142
- 29: 'Business Process Flow',
1143
- 48: 'Ribbon Command',
1144
- 59: 'Chart/Dashboard',
1145
- 60: 'System Form',
1146
- 62: 'SiteMap'
1147
- };
1148
- const formattedComponents = components.value.map((component) => ({
1149
- appmodulecomponentid: component.appmodulecomponentid,
1150
- objectid: component.objectid,
1151
- componenttype: component.componenttype,
1152
- componenttypeName: componentTypeMap[component.componenttype] || `Unknown (${component.componenttype})`,
1153
- rootappmodulecomponentid: component.rootappmodulecomponentid,
1154
- createdon: component.createdon,
1155
- modifiedon: component.modifiedon
1156
- }));
1157
- // Group by component type for easier reading
1158
- const groupedByType = {};
1159
- formattedComponents.forEach((comp) => {
1160
- const typeName = comp.componenttypeName;
1161
- if (!groupedByType[typeName]) {
1162
- groupedByType[typeName] = [];
1163
- }
1164
- groupedByType[typeName].push(comp);
1165
- });
1166
- return {
1167
- totalCount: formattedComponents.length,
1168
- components: formattedComponents,
1169
- groupedByType
1170
- };
162
+ return this.app.getAppComponents(appId);
1171
163
  }
1172
- /**
1173
- * Get the sitemap for a specific app
1174
- * @param appId The GUID of the app (appmoduleid)
1175
- * @returns Sitemap information including XML
1176
- */
1177
164
  async getAppSitemap(appId) {
1178
- // First get the app components to find the sitemap
1179
- const components = await this.makeRequest(`api/data/v9.2/appmodulecomponents?$filter=_appmoduleidunique_value eq ${appId} and componenttype eq 62&$select=objectid`);
1180
- if (components.value.length === 0) {
1181
- return {
1182
- hasSitemap: false,
1183
- message: 'No sitemap found for this app'
1184
- };
1185
- }
1186
- // Get the sitemap details
1187
- const sitemapId = components.value[0].objectid;
1188
- const sitemap = await this.makeRequest(`api/data/v9.2/sitemaps(${sitemapId})?$select=sitemapid,sitemapname,sitemapnameunique,sitemapxml,isappaware,enablecollapsiblegroups,showhome,showpinned,showrecents,ismanaged,createdon,modifiedon`);
1189
- return {
1190
- hasSitemap: true,
1191
- sitemapid: sitemap.sitemapid,
1192
- sitemapname: sitemap.sitemapname,
1193
- sitemapnameunique: sitemap.sitemapnameunique,
1194
- sitemapxml: sitemap.sitemapxml,
1195
- isappaware: sitemap.isappaware,
1196
- enablecollapsiblegroups: sitemap.enablecollapsiblegroups,
1197
- showhome: sitemap.showhome,
1198
- showpinned: sitemap.showpinned,
1199
- showrecents: sitemap.showrecents,
1200
- ismanaged: sitemap.ismanaged,
1201
- createdon: sitemap.createdon,
1202
- modifiedon: sitemap.modifiedon
1203
- };
1204
- }
1205
- /**
1206
- * Create a new model-driven app
1207
- * @param appDefinition The app definition object
1208
- * @param solutionUniqueName Optional solution to add the app to
1209
- * @returns The created app information including ID
1210
- */
1211
- async createApp(appDefinition, solutionUniqueName) {
1212
- const startTime = Date.now();
1213
- try {
1214
- // Validate uniquename format (English chars/numbers only, no spaces)
1215
- const uniquename = appDefinition.uniquename;
1216
- if (!/^[a-zA-Z0-9_]+$/.test(uniquename)) {
1217
- throw new Error('App uniquename must contain only English letters, numbers, and underscores (no spaces)');
1218
- }
1219
- // Set defaults
1220
- const appRequest = {
1221
- name: appDefinition.name,
1222
- uniquename: appDefinition.uniquename,
1223
- description: appDefinition.description || '',
1224
- webresourceid: appDefinition.webresourceid || '953b9fac-1e5e-e611-80d6-00155ded156f', // Default icon
1225
- welcomepageid: '00000000-0000-0000-0000-000000000000', // Required: empty GUID for no welcome page
1226
- clienttype: appDefinition.clienttype || 4, // UCI
1227
- formfactor: appDefinition.formfactor || 1, // Unknown/All
1228
- navigationtype: appDefinition.navigationtype !== undefined ? appDefinition.navigationtype : 0, // Single session
1229
- isfeatured: appDefinition.isfeatured || false,
1230
- isdefault: appDefinition.isdefault || false,
1231
- url: appDefinition.url || ''
1232
- };
1233
- // Headers with solution context and return representation
1234
- const headers = {
1235
- 'Prefer': 'return=representation'
1236
- };
1237
- if (solutionUniqueName) {
1238
- headers['MSCRM.SolutionUniqueName'] = solutionUniqueName;
1239
- }
1240
- // Execute with rate limiting
1241
- const response = await rateLimiter.execute(async () => {
1242
- return await this.makeRequest('api/data/v9.2/appmodules', 'POST', appRequest, headers);
1243
- });
1244
- // Extract app ID from response (now returned due to Prefer header)
1245
- const appId = response.appmoduleid;
1246
- if (!appId) {
1247
- throw new Error('App creation response missing appmoduleid. Full response: ' + JSON.stringify(response));
1248
- }
1249
- // Verify the app is queryable (retry with delay if needed)
1250
- let appVerified = false;
1251
- let retryCount = 0;
1252
- const maxRetries = 3;
1253
- const retryDelayMs = 2000;
1254
- while (!appVerified && retryCount < maxRetries) {
1255
- try {
1256
- await this.makeRequest(`api/data/v9.2/appmodules(${appId})?$select=appmoduleid,name,uniquename`);
1257
- appVerified = true;
1258
- }
1259
- catch (error) {
1260
- retryCount++;
1261
- if (retryCount < maxRetries) {
1262
- await new Promise(resolve => setTimeout(resolve, retryDelayMs));
1263
- }
1264
- }
1265
- }
1266
- // Audit log success
1267
- auditLogger.log({
1268
- operation: 'createApp',
1269
- operationType: 'CREATE',
1270
- componentType: 'AppModule',
1271
- componentName: appDefinition.name,
1272
- componentId: appId,
1273
- success: true,
1274
- executionTimeMs: Date.now() - startTime
1275
- });
1276
- return {
1277
- appId,
1278
- name: appDefinition.name,
1279
- uniquename: appDefinition.uniquename,
1280
- verified: appVerified,
1281
- message: appVerified
1282
- ? 'App created successfully and verified. Remember to add entities, validate, and publish.'
1283
- : `App created successfully (ID: ${appId}) but verification timed out. The app may need time to propagate in the system. Use get-app with the returned appId to check status.`
1284
- };
1285
- }
1286
- catch (error) {
1287
- // Audit log failure
1288
- auditLogger.log({
1289
- operation: 'createApp',
1290
- operationType: 'CREATE',
1291
- componentType: 'AppModule',
1292
- componentName: appDefinition.name,
1293
- success: false,
1294
- error: error.message,
1295
- executionTimeMs: Date.now() - startTime
1296
- });
1297
- throw new Error(`Failed to create app: ${error.message}`);
1298
- }
1299
- }
1300
- /**
1301
- * Create a sitemap from simplified configuration (no XML knowledge required)
1302
- * @param config Simplified sitemap configuration
1303
- * @param solutionUniqueName Optional solution to add the sitemap to
1304
- * @returns The created sitemap information including ID and XML
1305
- */
1306
- async createSimpleSitemap(config, solutionUniqueName) {
1307
- const startTime = Date.now();
1308
- try {
1309
- // Generate sitemap XML from simplified configuration
1310
- let xml = '<SiteMap>';
1311
- config.areas.forEach((area) => {
1312
- xml += `<Area Id="${area.id}"`;
1313
- if (area.icon) {
1314
- xml += ` Icon="${area.icon}"`;
1315
- }
1316
- if (area.showGroups !== undefined) {
1317
- xml += ` ShowGroups="${area.showGroups}"`;
1318
- }
1319
- xml += '>';
1320
- xml += `<Titles><Title LCID="1033" Title="${this.escapeXml(area.title)}" /></Titles>`;
1321
- if (area.description) {
1322
- xml += `<Descriptions><Description LCID="1033" Description="${this.escapeXml(area.description)}" /></Descriptions>`;
1323
- }
1324
- area.groups.forEach((group) => {
1325
- xml += `<Group Id="${group.id}"`;
1326
- if (group.isProfile !== undefined) {
1327
- xml += ` IsProfile="${group.isProfile}"`;
1328
- }
1329
- xml += '>';
1330
- xml += `<Titles><Title LCID="1033" Title="${this.escapeXml(group.title)}" /></Titles>`;
1331
- if (group.description) {
1332
- xml += `<Descriptions><Description LCID="1033" Description="${this.escapeXml(group.description)}" /></Descriptions>`;
1333
- }
1334
- group.subareas.forEach((subarea) => {
1335
- xml += `<SubArea Id="${subarea.id}"`;
1336
- if (subarea.entity) {
1337
- xml += ` Entity="${subarea.entity}"`;
1338
- }
1339
- if (subarea.url) {
1340
- xml += ` Url="${subarea.url}"`;
1341
- }
1342
- if (subarea.icon) {
1343
- xml += ` Icon="${subarea.icon}"`;
1344
- }
1345
- if (subarea.availableOffline !== undefined) {
1346
- xml += ` AvailableOffline="${subarea.availableOffline}"`;
1347
- }
1348
- if (subarea.passParams !== undefined) {
1349
- xml += ` PassParams="${subarea.passParams}"`;
1350
- }
1351
- xml += '>';
1352
- xml += `<Titles><Title LCID="1033" Title="${this.escapeXml(subarea.title)}" /></Titles>`;
1353
- if (subarea.description) {
1354
- xml += `<Descriptions><Description LCID="1033" Description="${this.escapeXml(subarea.description)}" /></Descriptions>`;
1355
- }
1356
- xml += '</SubArea>';
1357
- });
1358
- xml += '</Group>';
1359
- });
1360
- xml += '</Area>';
1361
- });
1362
- xml += '</SiteMap>';
1363
- // Create sitemap entity
1364
- const sitemapRequest = {
1365
- sitemapname: config.name,
1366
- sitemapxml: xml,
1367
- isappaware: true,
1368
- enablecollapsiblegroups: config.enableCollapsibleGroups !== undefined ? config.enableCollapsibleGroups : false,
1369
- showhome: config.showHome !== undefined ? config.showHome : true,
1370
- showpinned: config.showPinned !== undefined ? config.showPinned : true,
1371
- showrecents: config.showRecents !== undefined ? config.showRecents : true
1372
- };
1373
- // Headers with solution context
1374
- const headers = {};
1375
- if (solutionUniqueName) {
1376
- headers['MSCRM.SolutionUniqueName'] = solutionUniqueName;
1377
- }
1378
- // Execute with rate limiting
1379
- const response = await rateLimiter.execute(async () => {
1380
- return await this.makeRequest('api/data/v9.2/sitemaps', 'POST', sitemapRequest, headers);
1381
- });
1382
- // Extract sitemap ID from response
1383
- const sitemapId = response.sitemapid;
1384
- // Audit log success
1385
- auditLogger.log({
1386
- operation: 'createSimpleSitemap',
1387
- operationType: 'CREATE',
1388
- componentType: 'SiteMap',
1389
- componentName: config.name,
1390
- componentId: sitemapId,
1391
- success: true,
1392
- executionTimeMs: Date.now() - startTime
1393
- });
1394
- return {
1395
- sitemapId,
1396
- sitemapName: config.name,
1397
- sitemapXml: xml,
1398
- message: 'Sitemap created successfully. Add it to your app using add-entities-to-app or add specific components.'
1399
- };
1400
- }
1401
- catch (error) {
1402
- // Audit log failure
1403
- auditLogger.log({
1404
- operation: 'createSimpleSitemap',
1405
- operationType: 'CREATE',
1406
- componentType: 'SiteMap',
1407
- componentName: config.name,
1408
- success: false,
1409
- error: error.message,
1410
- executionTimeMs: Date.now() - startTime
1411
- });
1412
- throw new Error(`Failed to create sitemap: ${error.message}`);
1413
- }
1414
- }
1415
- /**
1416
- * Add entities to an app by modifying the sitemap XML
1417
- * @param appId The GUID of the app
1418
- * @param entityNames Array of entity logical names to add
1419
- * @returns Result of the operation
1420
- */
1421
- async addEntitiesToApp(appId, entityNames) {
1422
- const startTime = Date.now();
1423
- try {
1424
- // Get app details
1425
- const app = await this.makeRequest(`api/data/v9.2/appmodules(${appId})?$select=appmoduleid,name,uniquename`);
1426
- // Validate entities exist and get their display names
1427
- const entityPromises = entityNames.map(name => this.makeRequest(`api/data/v9.2/EntityDefinitions(LogicalName='${name}')?$select=LogicalName,DisplayName,MetadataId`));
1428
- const entities = await Promise.all(entityPromises);
1429
- // Try to get the app's sitemap via components first
1430
- let sitemapInfo = await this.getAppSitemap(appId);
1431
- // If not found via components, try to find by matching name
1432
- if (!sitemapInfo.hasSitemap) {
1433
- const sitemapQuery = await this.makeRequest(`api/data/v9.2/sitemaps?$filter=sitemapnameunique eq '${app.uniquename}'&$select=sitemapid,sitemapname,sitemapnameunique,sitemapxml`);
1434
- if (sitemapQuery.value.length > 0) {
1435
- const sitemap = sitemapQuery.value[0];
1436
- sitemapInfo = {
1437
- hasSitemap: true,
1438
- sitemapid: sitemap.sitemapid,
1439
- sitemapname: sitemap.sitemapname,
1440
- sitemapnameunique: sitemap.sitemapnameunique,
1441
- sitemapxml: sitemap.sitemapxml
1442
- };
1443
- }
1444
- else {
1445
- throw new Error(`App '${app.name}' does not have a sitemap. Cannot add entities without a sitemap.`);
1446
- }
1447
- }
1448
- // Parse sitemap XML
1449
- let sitemapXml = sitemapInfo.sitemapxml;
1450
- // Find or create a "Tables" area and group
1451
- // Check if <Area> with Id="Area_Tables" exists
1452
- const areaRegex = /<Area[^>]+Id="Area_Tables"[^>]*>/;
1453
- const hasTablesArea = areaRegex.test(sitemapXml);
1454
- if (!hasTablesArea) {
1455
- // Add a new Area for tables before the closing </SiteMap>
1456
- const newArea = `
1457
- <Area Id="Area_Tables" Title="Tables" ShowGroups="true">
1458
- <Group Id="Group_Tables" Title="Custom Tables">
1459
- </Group>
1460
- </Area>`;
1461
- sitemapXml = sitemapXml.replace('</SiteMap>', newArea + '\n</SiteMap>');
1462
- }
1463
- // Add SubArea elements for each entity within Group_Tables
1464
- for (const entity of entities) {
1465
- const displayName = entity.DisplayName?.UserLocalizedLabel?.Label || entity.LogicalName;
1466
- const subAreaId = `SubArea_${entity.LogicalName}`;
1467
- // Check if SubArea already exists
1468
- const subAreaRegex = new RegExp(`<SubArea[^>]+Id="${subAreaId}"[^>]*>`);
1469
- if (subAreaRegex.test(sitemapXml)) {
1470
- continue; // Skip if already exists
1471
- }
1472
- // Add SubArea within Group_Tables
1473
- const newSubArea = `
1474
- <SubArea Id="${subAreaId}" Entity="${entity.LogicalName}" Title="${displayName}" />`;
1475
- // Find the Group_Tables closing tag and add before it
1476
- sitemapXml = sitemapXml.replace(/<\/Group>/, newSubArea + '\n </Group>');
1477
- }
1478
- // Update the sitemap
1479
- await rateLimiter.execute(async () => {
1480
- return await this.makeRequest(`api/data/v9.2/sitemaps(${sitemapInfo.sitemapid})`, 'PATCH', {
1481
- sitemapxml: sitemapXml
1482
- });
1483
- });
1484
- // CRITICAL: Also add entity components to app for Advanced Find/Search
1485
- // Use deep insert via appmodule_appmodulecomponent collection navigation property
1486
- for (const entity of entities) {
1487
- try {
1488
- await rateLimiter.execute(async () => {
1489
- return await this.makeRequest(`api/data/v9.2/appmodules(${appId})/appmodule_appmodulecomponent`, 'POST', {
1490
- componenttype: 1, // Entity
1491
- objectid: entity.MetadataId
1492
- });
1493
- });
1494
- }
1495
- catch (componentError) {
1496
- // If deep insert fails, try to continue with other entities
1497
- auditLogger.log({
1498
- operation: 'addEntitiesToApp',
1499
- operationType: 'CREATE',
1500
- componentType: 'AppModuleComponent',
1501
- componentName: entity.LogicalName,
1502
- success: false,
1503
- error: `Failed to add ${entity.LogicalName} as app component: ${componentError.message}`,
1504
- executionTimeMs: Date.now() - startTime
1505
- });
1506
- }
1507
- }
1508
- // Audit log success
1509
- auditLogger.log({
1510
- operation: 'addEntitiesToApp',
1511
- operationType: 'UPDATE',
1512
- componentType: 'AppModule',
1513
- componentId: appId,
1514
- success: true,
1515
- executionTimeMs: Date.now() - startTime
1516
- });
1517
- return {
1518
- appId,
1519
- sitemapId: sitemapInfo.sitemapid,
1520
- entitiesAdded: entityNames,
1521
- message: `Successfully added ${entityNames.length} entities to app sitemap. Remember to publish the app.`
1522
- };
1523
- }
1524
- catch (error) {
1525
- // Audit log failure
1526
- auditLogger.log({
1527
- operation: 'addEntitiesToApp',
1528
- operationType: 'UPDATE',
1529
- componentType: 'AppModule',
1530
- componentId: appId,
1531
- success: false,
1532
- error: error.message,
1533
- executionTimeMs: Date.now() - startTime
1534
- });
1535
- throw new Error(`Failed to add entities to app: ${error.message}`);
1536
- }
1537
- }
1538
- /**
1539
- * Validate an app before publishing
1540
- * @param appId The GUID of the app
1541
- * @returns Validation result with any issues found
1542
- */
1543
- async validateApp(appId) {
1544
- try {
1545
- const response = await this.makeRequest(`api/data/v9.2/ValidateApp(AppModuleId=${appId})`);
1546
- const validationResponse = response.AppValidationResponse;
1547
- const isValid = validationResponse.ValidationSuccess;
1548
- const issues = validationResponse.ValidationIssueList || [];
1549
- return {
1550
- appId,
1551
- isValid,
1552
- issueCount: issues.length,
1553
- issues: issues.map((issue) => ({
1554
- errorType: issue.ErrorType,
1555
- message: issue.Message,
1556
- componentId: issue.ComponentId,
1557
- componentType: issue.ComponentType
1558
- })),
1559
- message: isValid
1560
- ? 'App validation passed. Ready to publish.'
1561
- : `App validation found ${issues.length} issue(s). Fix them before publishing.`
1562
- };
1563
- }
1564
- catch (error) {
1565
- throw new Error(`Failed to validate app: ${error.message}`);
1566
- }
1567
- }
1568
- /**
1569
- * Publish an app to make it available to users
1570
- * @param appId The GUID of the app
1571
- * @returns Result of the publish operation
1572
- */
1573
- async publishApp(appId) {
1574
- const startTime = Date.now();
1575
- try {
1576
- // First validate the app
1577
- const validation = await this.validateApp(appId);
1578
- if (!validation.isValid) {
1579
- throw new Error(`Cannot publish app with validation errors: ${JSON.stringify(validation.issues)}`);
1580
- }
1581
- // Publish using PublishXml with app parameter
1582
- const parameterXml = `<importexportxml><appmodules><appmodule>${appId}</appmodule></appmodules></importexportxml>`;
1583
- await rateLimiter.execute(async () => {
1584
- return await this.publishXml(parameterXml);
1585
- });
1586
- // Audit log success
1587
- auditLogger.log({
1588
- operation: 'publishApp',
1589
- operationType: 'PUBLISH',
1590
- componentType: 'AppModule',
1591
- componentId: appId,
1592
- success: true,
1593
- executionTimeMs: Date.now() - startTime
1594
- });
1595
- return {
1596
- appId,
1597
- message: 'App published successfully. It is now available to users with appropriate security roles.'
1598
- };
1599
- }
1600
- catch (error) {
1601
- // Audit log failure
1602
- auditLogger.log({
1603
- operation: 'publishApp',
1604
- operationType: 'PUBLISH',
1605
- componentType: 'AppModule',
1606
- componentId: appId,
1607
- success: false,
1608
- error: error.message,
1609
- executionTimeMs: Date.now() - startTime
1610
- });
1611
- throw new Error(`Failed to publish app: ${error.message}`);
1612
- }
165
+ return this.app.getAppSitemap(appId);
1613
166
  }
1614
- /**
1615
- * Helper to escape XML special characters
1616
- */
1617
- escapeXml(unsafe) {
1618
- return unsafe.replace(/[<>&'"]/g, (c) => {
1619
- switch (c) {
1620
- case '<': return '&lt;';
1621
- case '>': return '&gt;';
1622
- case '&': return '&amp;';
1623
- case '\'': return '&apos;';
1624
- case '"': return '&quot;';
1625
- default: return c;
1626
- }
1627
- });
1628
- }
1629
- // ==================== CUSTOMIZATION WRITE OPERATIONS ====================
1630
- /**
1631
- * Create a new custom entity (table)
1632
- * @param entityDefinition The entity definition object
1633
- * @param solutionUniqueName Optional solution to add the entity to
1634
- * @returns The created entity metadata
1635
- */
167
+ // =====================================================
168
+ // ENTITY CUSTOMIZATION METHODS
169
+ // =====================================================
1636
170
  async createEntity(entityDefinition, solutionUniqueName) {
1637
- const startTime = Date.now();
1638
- try {
1639
- // Validate entity name against best practices
1640
- const schemaName = entityDefinition.SchemaName || entityDefinition.LogicalName;
1641
- const isRefData = schemaName?.toLowerCase().includes('ref_') || false;
1642
- const nameValidation = bestPracticesValidator.validateEntityName(schemaName, isRefData);
1643
- if (!nameValidation.isValid) {
1644
- const error = `Entity name validation failed: ${nameValidation.issues.join(', ')}`;
1645
- auditLogger.log({
1646
- operation: 'createEntity',
1647
- operationType: 'CREATE',
1648
- componentType: 'Entity',
1649
- componentName: schemaName,
1650
- success: false,
1651
- error,
1652
- executionTimeMs: Date.now() - startTime
1653
- });
1654
- throw new Error(error);
1655
- }
1656
- // Log warnings if any
1657
- if (nameValidation.warnings.length > 0) {
1658
- console.error(`[WARNING] Entity name warnings: ${nameValidation.warnings.join(', ')}`);
1659
- }
1660
- // Validate ownership type
1661
- const ownershipType = entityDefinition.OwnershipType;
1662
- if (ownershipType) {
1663
- const ownershipValidation = bestPracticesValidator.validateOwnershipType(ownershipType);
1664
- if (!ownershipValidation.isValid) {
1665
- console.error(`[WARNING] ${ownershipValidation.issues.join(', ')}`);
1666
- }
1667
- }
1668
- // Check for required columns
1669
- const requiredColumnsValidation = bestPracticesValidator.validateRequiredColumns([], isRefData);
1670
- if (requiredColumnsValidation.missingColumns && requiredColumnsValidation.missingColumns.length > 0) {
1671
- console.error('[WARNING] Entity will need required columns added after creation:', requiredColumnsValidation.missingColumns.map(c => c.schemaName).join(', '));
1672
- }
1673
- const headers = {};
1674
- if (solutionUniqueName) {
1675
- headers['MSCRM.SolutionUniqueName'] = solutionUniqueName;
1676
- }
1677
- // Execute with rate limiting
1678
- const response = await rateLimiter.execute(async () => {
1679
- return await this.makeRequest('api/data/v9.2/EntityDefinitions', 'POST', entityDefinition, headers);
1680
- });
1681
- // Log success
1682
- auditLogger.log({
1683
- operation: 'createEntity',
1684
- operationType: 'CREATE',
1685
- componentType: 'Entity',
1686
- componentName: schemaName,
1687
- success: true,
1688
- executionTimeMs: Date.now() - startTime
1689
- });
1690
- return response;
1691
- }
1692
- catch (error) {
1693
- // Log failure
1694
- auditLogger.log({
1695
- operation: 'createEntity',
1696
- operationType: 'CREATE',
1697
- componentType: 'Entity',
1698
- componentName: entityDefinition.SchemaName || entityDefinition.LogicalName,
1699
- success: false,
1700
- error: error.message,
1701
- executionTimeMs: Date.now() - startTime
1702
- });
1703
- throw error;
1704
- }
171
+ return this.entity.createEntity(entityDefinition, solutionUniqueName);
1705
172
  }
1706
- /**
1707
- * Update an existing entity
1708
- * @param metadataId The MetadataId of the entity
1709
- * @param updates The properties to update
1710
- * @param solutionUniqueName Optional solution context
1711
- */
1712
173
  async updateEntity(metadataId, updates, solutionUniqueName) {
1713
- const headers = {
1714
- 'MSCRM.MergeLabels': 'true'
1715
- };
1716
- if (solutionUniqueName) {
1717
- headers['MSCRM.SolutionUniqueName'] = solutionUniqueName;
1718
- }
1719
- await this.makeRequest(`api/data/v9.2/EntityDefinitions(${metadataId})`, 'PUT', updates, headers);
174
+ return this.entity.updateEntity(metadataId, updates, solutionUniqueName);
1720
175
  }
1721
- /**
1722
- * Delete a custom entity
1723
- * @param metadataId The MetadataId of the entity to delete
1724
- */
1725
176
  async deleteEntity(metadataId) {
1726
- await this.makeRequest(`api/data/v9.2/EntityDefinitions(${metadataId})`, 'DELETE');
177
+ return this.entity.deleteEntity(metadataId);
1727
178
  }
1728
- /**
1729
- * Update entity icon using Fluent UI System Icon
1730
- * @param entityLogicalName The logical name of the entity
1731
- * @param iconFileName The Fluent UI icon file name (e.g., 'people_community_24_filled.svg')
1732
- * @param solutionUniqueName Optional solution to add the web resource to
1733
- * @returns Result with web resource ID and icon vector name
1734
- */
1735
179
  async updateEntityIcon(entityLogicalName, iconFileName, solutionUniqueName) {
1736
- const startTime = Date.now();
1737
- try {
1738
- // Step 1: Get entity metadata to retrieve schema name and metadata ID
1739
- const entityMetadata = await this.getEntityMetadata(entityLogicalName);
1740
- const entitySchemaName = entityMetadata.SchemaName;
1741
- const metadataId = entityMetadata.MetadataId;
1742
- if (!metadataId) {
1743
- throw new Error(`Could not find MetadataId for entity '${entityLogicalName}'`);
1744
- }
1745
- // Note: No need to clear existing IconVectorName - PowerPlatform will override it
1746
- // when we set the new icon. This avoids potential API errors from setting null values.
1747
- // Step 2: Fetch the icon SVG from Fluent UI GitHub
1748
- const svgContent = await iconManager.fetchIcon(iconFileName);
1749
- // Step 3: Validate the SVG
1750
- const validation = iconManager.validateIconSvg(svgContent);
1751
- if (!validation.valid) {
1752
- throw new Error(`Invalid SVG: ${validation.error}`);
1753
- }
1754
- // Step 4: Convert SVG to base64
1755
- const base64Content = Buffer.from(svgContent).toString('base64');
1756
- // Step 5: Generate web resource name
1757
- const webResourceName = iconManager.generateWebResourceName(entitySchemaName, iconFileName.replace('.svg', ''));
1758
- // Step 6: Check if web resource already exists (use exact name match)
1759
- const existingResourcesResponse = await this.makeRequest(`api/data/v9.2/webresourceset?$filter=name eq '${webResourceName}'&$select=webresourceid,name`);
1760
- let webResourceId;
1761
- if (existingResourcesResponse.value && existingResourcesResponse.value.length > 0) {
1762
- // Web resource exists, update it
1763
- const existing = existingResourcesResponse.value[0];
1764
- webResourceId = existing.webresourceid;
1765
- const webResourceUpdates = {
1766
- displayname: `Icon for ${entityMetadata.DisplayName?.UserLocalizedLabel?.Label || entityLogicalName}`,
1767
- content: base64Content,
1768
- description: `Fluent UI icon (${iconFileName}) for ${entityLogicalName} entity`
1769
- };
1770
- await this.updateWebResource(webResourceId, webResourceUpdates, solutionUniqueName);
1771
- }
1772
- else {
1773
- // Web resource doesn't exist, create new
1774
- const webResource = {
1775
- name: webResourceName,
1776
- displayname: `Icon for ${entityMetadata.DisplayName?.UserLocalizedLabel?.Label || entityLogicalName}`,
1777
- webresourcetype: 11, // SVG
1778
- content: base64Content,
1779
- description: `Fluent UI icon (${iconFileName}) for ${entityLogicalName} entity`
1780
- };
1781
- const webResourceResult = await this.createWebResource(webResource, solutionUniqueName);
1782
- webResourceId = webResourceResult.webresourceid;
1783
- }
1784
- // Step 7: Generate icon vector name
1785
- const iconVectorName = iconManager.generateIconVectorName(webResourceName);
1786
- // Step 8: Update entity metadata with icon reference
1787
- const entityUpdates = {
1788
- '@odata.type': 'Microsoft.Dynamics.CRM.EntityMetadata',
1789
- IconVectorName: iconVectorName
1790
- };
1791
- await this.updateEntity(metadataId, entityUpdates, solutionUniqueName);
1792
- // Step 9: Publish the web resource (component type 61)
1793
- await this.publishComponent(webResourceId, 61);
1794
- // Step 10: Publish the entity (component type 1)
1795
- await this.publishComponent(metadataId, 1);
1796
- // Log success
1797
- auditLogger.log({
1798
- operation: 'updateEntityIcon',
1799
- operationType: 'UPDATE',
1800
- componentType: 'Entity',
1801
- componentName: entityLogicalName,
1802
- success: true,
1803
- parameters: {
1804
- iconFileName,
1805
- webResourceName,
1806
- webResourceId,
1807
- iconVectorName
1808
- },
1809
- executionTimeMs: Date.now() - startTime
1810
- });
1811
- return {
1812
- success: true,
1813
- entityLogicalName,
1814
- entitySchemaName,
1815
- iconFileName,
1816
- webResourceId,
1817
- webResourceName,
1818
- iconVectorName,
1819
- message: 'Entity icon updated and published successfully. The icon should now be visible in the UI.'
1820
- };
1821
- }
1822
- catch (error) {
1823
- // Log failure
1824
- auditLogger.log({
1825
- operation: 'updateEntityIcon',
1826
- operationType: 'UPDATE',
1827
- componentType: 'Entity',
1828
- componentName: entityLogicalName,
1829
- success: false,
1830
- error: error.message,
1831
- executionTimeMs: Date.now() - startTime
1832
- });
1833
- throw error;
1834
- }
1835
- }
1836
- /**
1837
- * Create a new attribute on an entity
1838
- * @param entityLogicalName The logical name of the entity
1839
- * @param attributeDefinition The attribute definition object
1840
- * @param solutionUniqueName Optional solution to add the attribute to
1841
- * @returns The created attribute metadata
1842
- */
180
+ return this.entity.updateEntityIcon(entityLogicalName, iconFileName, {
181
+ getEntityMetadata: (name) => this.metadata.getEntityMetadata(name),
182
+ createWebResource: (resource, solution) => this.webResource.createWebResource(resource, solution),
183
+ updateWebResource: (id, updates, solution) => this.webResource.updateWebResource(id, updates, solution),
184
+ publishComponent: (id, componentType) => this.publishing.publishComponent(id, componentType),
185
+ }, solutionUniqueName);
186
+ }
187
+ // =====================================================
188
+ // ATTRIBUTE CUSTOMIZATION METHODS
189
+ // =====================================================
1843
190
  async createAttribute(entityLogicalName, attributeDefinition, solutionUniqueName) {
1844
- const startTime = Date.now();
1845
- try {
1846
- // Validate attribute name against best practices
1847
- const schemaName = attributeDefinition.SchemaName || attributeDefinition.LogicalName;
1848
- const isLookup = attributeDefinition['@odata.type'] === 'Microsoft.Dynamics.CRM.LookupAttributeMetadata';
1849
- const nameValidation = bestPracticesValidator.validateAttributeName(schemaName, isLookup);
1850
- if (!nameValidation.isValid) {
1851
- const error = `Attribute name validation failed: ${nameValidation.issues.join(', ')}`;
1852
- auditLogger.log({
1853
- operation: 'createAttribute',
1854
- operationType: 'CREATE',
1855
- componentType: 'Attribute',
1856
- componentName: `${entityLogicalName}.${schemaName}`,
1857
- success: false,
1858
- error,
1859
- executionTimeMs: Date.now() - startTime
1860
- });
1861
- throw new Error(error);
1862
- }
1863
- // Log warnings if any
1864
- if (nameValidation.warnings.length > 0) {
1865
- console.error(`[WARNING] Attribute name warnings: ${nameValidation.warnings.join(', ')}`);
1866
- }
1867
- // Validate boolean usage (best practice is to avoid booleans)
1868
- const isBoolean = attributeDefinition['@odata.type'] === 'Microsoft.Dynamics.CRM.BooleanAttributeMetadata';
1869
- if (isBoolean) {
1870
- const booleanValidation = bestPracticesValidator.validateBooleanUsage('Boolean', schemaName);
1871
- if (!booleanValidation.isValid) {
1872
- console.error(`[WARNING] ${booleanValidation.warnings.join(', ')}`);
1873
- }
1874
- }
1875
- const headers = {};
1876
- if (solutionUniqueName) {
1877
- headers['MSCRM.SolutionUniqueName'] = solutionUniqueName;
1878
- }
1879
- // Execute with rate limiting
1880
- const response = await rateLimiter.execute(async () => {
1881
- return await this.makeRequest(`api/data/v9.2/EntityDefinitions(LogicalName='${entityLogicalName}')/Attributes`, 'POST', attributeDefinition, headers);
1882
- });
1883
- // Log success
1884
- auditLogger.log({
1885
- operation: 'createAttribute',
1886
- operationType: 'CREATE',
1887
- componentType: 'Attribute',
1888
- componentName: `${entityLogicalName}.${schemaName}`,
1889
- success: true,
1890
- executionTimeMs: Date.now() - startTime
1891
- });
1892
- return response;
1893
- }
1894
- catch (error) {
1895
- // Log failure
1896
- const schemaName = attributeDefinition.SchemaName || attributeDefinition.LogicalName;
1897
- auditLogger.log({
1898
- operation: 'createAttribute',
1899
- operationType: 'CREATE',
1900
- componentType: 'Attribute',
1901
- componentName: `${entityLogicalName}.${schemaName}`,
1902
- success: false,
1903
- error: error.message,
1904
- executionTimeMs: Date.now() - startTime
1905
- });
1906
- throw error;
1907
- }
191
+ return this.attribute.createAttribute(entityLogicalName, attributeDefinition, solutionUniqueName);
1908
192
  }
1909
- /**
1910
- * Update an existing attribute
1911
- * @param entityLogicalName The logical name of the entity
1912
- * @param attributeLogicalName The logical name of the attribute
1913
- * @param updates The properties to update
1914
- * @param solutionUniqueName Optional solution context
1915
- */
1916
193
  async updateAttribute(entityLogicalName, attributeLogicalName, updates, solutionUniqueName) {
1917
- const headers = {
1918
- 'MSCRM.MergeLabels': 'true'
1919
- };
1920
- if (solutionUniqueName) {
1921
- headers['MSCRM.SolutionUniqueName'] = solutionUniqueName;
1922
- }
1923
- // First, get the existing attribute to retrieve its @odata.type and merge updates
1924
- const existingAttribute = await this.getEntityAttribute(entityLogicalName, attributeLogicalName);
1925
- // Merge the updates with required fields
1926
- const payload = {
1927
- ...updates,
1928
- '@odata.type': existingAttribute['@odata.type'],
1929
- LogicalName: attributeLogicalName,
1930
- AttributeType: existingAttribute.AttributeType
1931
- };
1932
- await this.makeRequest(`api/data/v9.2/EntityDefinitions(LogicalName='${entityLogicalName}')/Attributes(LogicalName='${attributeLogicalName}')`, 'PUT', payload, headers);
194
+ return this.attribute.updateAttribute(entityLogicalName, attributeLogicalName, updates, (entityName, attrName) => this.metadata.getEntityAttribute(entityName, attrName), solutionUniqueName);
1933
195
  }
1934
- /**
1935
- * Delete an attribute
1936
- * @param entityLogicalName The logical name of the entity
1937
- * @param attributeMetadataId The MetadataId of the attribute to delete
1938
- */
1939
196
  async deleteAttribute(entityLogicalName, attributeMetadataId) {
1940
- await this.makeRequest(`api/data/v9.2/EntityDefinitions(LogicalName='${entityLogicalName}')/Attributes(${attributeMetadataId})`, 'DELETE');
1941
- }
1942
- /**
1943
- * Create a picklist attribute using a global option set
1944
- * @param entityLogicalName The logical name of the entity
1945
- * @param attributeDefinition The attribute definition (must reference a global option set)
1946
- * @param solutionUniqueName Optional solution to add the attribute to
1947
- * @returns The created attribute metadata
1948
- */
1949
- async createGlobalOptionSetAttribute(entityLogicalName, attributeDefinition, solutionUniqueName) {
1950
- const headers = {};
1951
- if (solutionUniqueName) {
1952
- headers['MSCRM.SolutionUniqueName'] = solutionUniqueName;
1953
- }
1954
- // Ensure the attribute is of type PicklistAttributeMetadata with GlobalOptionSet
1955
- if (!attributeDefinition['@odata.type']) {
1956
- attributeDefinition['@odata.type'] = 'Microsoft.Dynamics.CRM.PicklistAttributeMetadata';
1957
- }
1958
- return await this.makeRequest(`api/data/v9.2/EntityDefinitions(LogicalName='${entityLogicalName}')/Attributes`, 'POST', attributeDefinition, headers);
1959
- }
1960
- /**
1961
- * Create a one-to-many relationship
1962
- * @param relationshipDefinition The relationship definition
1963
- * @param solutionUniqueName Optional solution to add the relationship to
1964
- */
1965
- async createOneToManyRelationship(relationshipDefinition, solutionUniqueName) {
1966
- const headers = {};
1967
- if (solutionUniqueName) {
1968
- headers['MSCRM.SolutionUniqueName'] = solutionUniqueName;
1969
- }
1970
- const response = await this.makeRequest('api/data/v9.2/RelationshipDefinitions', 'POST', relationshipDefinition, headers);
1971
- return response;
1972
- }
1973
- /**
1974
- * Create a many-to-many relationship
1975
- * @param relationshipDefinition The relationship definition
1976
- * @param solutionUniqueName Optional solution to add the relationship to
1977
- */
1978
- async createManyToManyRelationship(relationshipDefinition, solutionUniqueName) {
1979
- const headers = {};
1980
- if (solutionUniqueName) {
1981
- headers['MSCRM.SolutionUniqueName'] = solutionUniqueName;
197
+ return this.attribute.deleteAttribute(entityLogicalName, attributeMetadataId);
198
+ }
199
+ async createGlobalOptionSetAttribute(entityLogicalName, schemaName, displayName, globalOptionSetName, options) {
200
+ const attributeDefinition = {
201
+ '@odata.type': 'Microsoft.Dynamics.CRM.PicklistAttributeMetadata',
202
+ SchemaName: schemaName,
203
+ DisplayName: {
204
+ '@odata.type': 'Microsoft.Dynamics.CRM.Label',
205
+ LocalizedLabels: [
206
+ {
207
+ '@odata.type': 'Microsoft.Dynamics.CRM.LocalizedLabel',
208
+ Label: displayName,
209
+ LanguageCode: 1033,
210
+ },
211
+ ],
212
+ },
213
+ RequiredLevel: {
214
+ Value: options?.requiredLevel || 'None',
215
+ },
216
+ GlobalOptionSet: {
217
+ '@odata.type': 'Microsoft.Dynamics.CRM.OptionSetMetadata',
218
+ Name: globalOptionSetName,
219
+ },
220
+ };
221
+ if (options?.description) {
222
+ attributeDefinition.Description = {
223
+ '@odata.type': 'Microsoft.Dynamics.CRM.Label',
224
+ LocalizedLabels: [
225
+ {
226
+ '@odata.type': 'Microsoft.Dynamics.CRM.LocalizedLabel',
227
+ Label: options.description,
228
+ LanguageCode: 1033,
229
+ },
230
+ ],
231
+ };
1982
232
  }
1983
- const response = await this.makeRequest('api/data/v9.2/RelationshipDefinitions', 'POST', relationshipDefinition, headers);
1984
- return response;
233
+ return this.attribute.createGlobalOptionSetAttribute(entityLogicalName, attributeDefinition, options?.solutionUniqueName);
1985
234
  }
1986
- /**
1987
- * Delete a relationship
1988
- * @param metadataId The MetadataId of the relationship to delete
1989
- */
1990
- async deleteRelationship(metadataId) {
1991
- await this.makeRequest(`api/data/v9.2/RelationshipDefinitions(${metadataId})`, 'DELETE');
235
+ // =====================================================
236
+ // RELATIONSHIP CUSTOMIZATION METHODS
237
+ // =====================================================
238
+ async createOneToManyRelationship(definition, solutionUniqueName) {
239
+ return this.relationship.createOneToManyRelationship(definition, solutionUniqueName);
1992
240
  }
1993
- /**
1994
- * Update a relationship
1995
- * Note: Most relationship properties are immutable, only labels can be updated
1996
- * @param metadataId The MetadataId of the relationship
1997
- * @param updates The properties to update (typically labels)
1998
- */
1999
- async updateRelationship(metadataId, updates) {
2000
- await this.makeRequest(`api/data/v9.2/RelationshipDefinitions(${metadataId})`, 'PUT', updates, { 'MSCRM.MergeLabels': 'true' });
241
+ async createManyToManyRelationship(definition, solutionUniqueName) {
242
+ return this.relationship.createManyToManyRelationship(definition, solutionUniqueName);
2001
243
  }
2002
- /**
2003
- * Get detailed information about a relationship
2004
- * @param metadataId The MetadataId of the relationship
2005
- * @returns The relationship metadata
2006
- */
2007
- async getRelationshipDetails(metadataId) {
2008
- return await this.makeRequest(`api/data/v9.2/RelationshipDefinitions(${metadataId})`);
2009
- }
2010
- /**
2011
- * Publish all customizations
2012
- */
2013
- async publishAllCustomizations() {
2014
- await this.makeRequest('api/data/v9.2/PublishAllXml', 'POST', {});
2015
- }
2016
- /**
2017
- * Publish specific customizations
2018
- * @param parameterXml The ParameterXml specifying what to publish
2019
- */
2020
- async publishXml(parameterXml) {
2021
- await this.makeRequest('api/data/v9.2/PublishXml', 'POST', { ParameterXml: parameterXml });
244
+ async deleteRelationship(metadataId) {
245
+ return this.relationship.deleteRelationship(metadataId);
2022
246
  }
2023
- /**
2024
- * Create a global option set
2025
- * @param optionSetDefinition The option set definition
2026
- * @param solutionUniqueName Optional solution to add the option set to
2027
- */
2028
- async createGlobalOptionSet(optionSetDefinition, solutionUniqueName) {
2029
- const headers = {};
2030
- if (solutionUniqueName) {
2031
- headers['MSCRM.SolutionUniqueName'] = solutionUniqueName;
2032
- }
2033
- const response = await this.makeRequest('api/data/v9.2/GlobalOptionSetDefinitions', 'POST', optionSetDefinition, headers);
2034
- return response;
247
+ async updateRelationship(metadataId, updates) {
248
+ return this.relationship.updateRelationship(metadataId, updates);
2035
249
  }
2036
- /**
2037
- * Delete a global option set
2038
- * @param metadataId The MetadataId of the option set to delete
2039
- */
2040
- async deleteGlobalOptionSet(metadataId) {
2041
- await this.makeRequest(`api/data/v9.2/GlobalOptionSetDefinitions(${metadataId})`, 'DELETE');
250
+ // =====================================================
251
+ // OPTION SET CUSTOMIZATION METHODS
252
+ // =====================================================
253
+ async createGlobalOptionSet(definition, solutionUniqueName) {
254
+ return this.optionSet.createGlobalOptionSet(definition, solutionUniqueName);
2042
255
  }
2043
- // ===== Phase 2: UI Components (Forms, Views, Option Sets) =====
2044
- /**
2045
- * Update a global option set
2046
- */
2047
256
  async updateGlobalOptionSet(metadataId, updates, solutionUniqueName) {
2048
- const headers = solutionUniqueName ? { 'MSCRM.SolutionUniqueName': solutionUniqueName } : undefined;
2049
- await this.makeRequest(`api/data/v9.2/GlobalOptionSetDefinitions(${metadataId})`, 'PUT', updates, headers);
257
+ return this.optionSet.updateGlobalOptionSet(metadataId, updates, solutionUniqueName);
2050
258
  }
2051
- /**
2052
- * Add a value to a global option set
2053
- */
2054
259
  async addOptionSetValue(optionSetName, value, label, solutionUniqueName) {
2055
- const headers = solutionUniqueName ? { 'MSCRM.SolutionUniqueName': solutionUniqueName } : undefined;
2056
- return await this.makeRequest(`api/data/v9.2/InsertOptionValue`, 'POST', {
2057
- OptionSetName: optionSetName,
2058
- Value: value,
2059
- Label: {
2060
- LocalizedLabels: [{ Label: label, LanguageCode: 1033 }]
2061
- }
2062
- }, headers);
260
+ return this.optionSet.addOptionSetValue(optionSetName, value, label, solutionUniqueName);
2063
261
  }
2064
- /**
2065
- * Update an option set value
2066
- */
2067
262
  async updateOptionSetValue(optionSetName, value, label, solutionUniqueName) {
2068
- const headers = { 'MSCRM.MergeLabels': 'true' };
2069
- if (solutionUniqueName) {
2070
- headers['MSCRM.SolutionUniqueName'] = solutionUniqueName;
2071
- }
2072
- await this.makeRequest(`api/data/v9.2/UpdateOptionValue`, 'POST', {
2073
- OptionSetName: optionSetName,
2074
- Value: value,
2075
- Label: {
2076
- LocalizedLabels: [{ Label: label, LanguageCode: 1033 }]
2077
- },
2078
- MergeLabels: true
2079
- }, headers);
263
+ return this.optionSet.updateOptionSetValue(optionSetName, value, label, solutionUniqueName);
2080
264
  }
2081
- /**
2082
- * Delete an option set value
2083
- */
2084
265
  async deleteOptionSetValue(optionSetName, value) {
2085
- await this.makeRequest(`api/data/v9.2/DeleteOptionValue`, 'POST', {
2086
- OptionSetName: optionSetName,
2087
- Value: value
2088
- });
266
+ return this.optionSet.deleteOptionSetValue(optionSetName, value);
2089
267
  }
2090
- /**
2091
- * Reorder option set values
2092
- */
2093
268
  async reorderOptionSetValues(optionSetName, values, solutionUniqueName) {
2094
- const headers = solutionUniqueName ? { 'MSCRM.SolutionUniqueName': solutionUniqueName } : undefined;
2095
- await this.makeRequest(`api/data/v9.2/OrderOption`, 'POST', {
2096
- OptionSetName: optionSetName,
2097
- Values: values
2098
- }, headers);
2099
- }
2100
- /**
2101
- * Create a form (systemform)
2102
- */
2103
- async createForm(form, solutionUniqueName) {
2104
- const headers = solutionUniqueName ? { 'MSCRM.SolutionUniqueName': solutionUniqueName } : undefined;
2105
- return await this.makeRequest('api/data/v9.2/systemforms', 'POST', form, headers);
2106
- }
2107
- /**
2108
- * Update a form
2109
- */
2110
- async updateForm(formId, updates, solutionUniqueName) {
2111
- const headers = solutionUniqueName ? { 'MSCRM.SolutionUniqueName': solutionUniqueName } : undefined;
2112
- await this.makeRequest(`api/data/v9.2/systemforms(${formId})`, 'PATCH', updates, headers);
2113
- }
2114
- /**
2115
- * Delete a form
2116
- */
2117
- async deleteForm(formId) {
2118
- await this.makeRequest(`api/data/v9.2/systemforms(${formId})`, 'DELETE');
269
+ return this.optionSet.reorderOptionSetValues(optionSetName, values, solutionUniqueName);
2119
270
  }
2120
- /**
2121
- * Get forms for an entity
2122
- */
271
+ // =====================================================
272
+ // FORM CUSTOMIZATION METHODS
273
+ // =====================================================
2123
274
  async getForms(entityLogicalName) {
2124
- return await this.makeRequest(`api/data/v9.2/systemforms?$filter=objecttypecode eq '${entityLogicalName}'&$orderby=type`);
2125
- }
2126
- /**
2127
- * Create a view (savedquery)
2128
- */
2129
- async createView(view, solutionUniqueName) {
2130
- const headers = solutionUniqueName ? { 'MSCRM.SolutionUniqueName': solutionUniqueName } : undefined;
2131
- return await this.makeRequest('api/data/v9.2/savedqueries', 'POST', view, headers);
275
+ return this.form.getForms(entityLogicalName);
276
+ }
277
+ async createForm(name, entityLogicalName, formType, formXml, options) {
278
+ const formData = {
279
+ name,
280
+ objecttypecode: entityLogicalName,
281
+ type: this.getFormTypeCode(formType),
282
+ formxml: formXml,
283
+ };
284
+ if (options?.description) {
285
+ formData.description = options.description;
286
+ }
287
+ return this.form.createForm(formData, options?.solutionUniqueName);
2132
288
  }
2133
- /**
2134
- * Update a view
2135
- */
2136
- async updateView(viewId, updates, solutionUniqueName) {
2137
- const headers = solutionUniqueName ? { 'MSCRM.SolutionUniqueName': solutionUniqueName } : undefined;
2138
- await this.makeRequest(`api/data/v9.2/savedqueries(${viewId})`, 'PATCH', updates, headers);
289
+ getFormTypeCode(formType) {
290
+ const typeMap = {
291
+ Main: 2,
292
+ QuickCreate: 7,
293
+ QuickView: 6,
294
+ Card: 11,
295
+ };
296
+ return typeMap[formType] || 2;
2139
297
  }
2140
- /**
2141
- * Delete a view
2142
- */
2143
- async deleteView(viewId) {
2144
- await this.makeRequest(`api/data/v9.2/savedqueries(${viewId})`, 'DELETE');
298
+ async updateForm(formId, updates, solutionUniqueName) {
299
+ return this.form.updateForm(formId, updates, solutionUniqueName);
2145
300
  }
2146
- /**
2147
- * Get views for an entity
2148
- */
2149
- async getViews(entityLogicalName) {
2150
- return await this.makeRequest(`api/data/v9.2/savedqueries?$filter=returnedtypecode eq '${entityLogicalName}'&$orderby=querytype`);
301
+ async deleteForm(formId) {
302
+ return this.form.deleteForm(formId);
2151
303
  }
2152
- /**
2153
- * Activate a form (set statecode=1)
2154
- * @param formId The systemformid (GUID)
2155
- */
2156
304
  async activateForm(formId) {
2157
- await this.makeRequest(`api/data/v9.2/systemforms(${formId})`, 'PATCH', { statecode: 1, statuscode: 1 });
305
+ return this.form.activateForm(formId);
2158
306
  }
2159
- /**
2160
- * Deactivate a form (set statecode=0)
2161
- * @param formId The systemformid (GUID)
2162
- */
2163
307
  async deactivateForm(formId) {
2164
- await this.makeRequest(`api/data/v9.2/systemforms(${formId})`, 'PATCH', { statecode: 0, statuscode: 2 });
308
+ return this.form.deactivateForm(formId);
2165
309
  }
2166
- /**
2167
- * Set a view as the default view for its entity
2168
- * @param viewId The savedqueryid (GUID)
2169
- */
2170
- async setDefaultView(viewId) {
2171
- await this.makeRequest(`api/data/v9.2/savedqueries(${viewId})`, 'PATCH', { isdefault: true });
310
+ // =====================================================
311
+ // VIEW CUSTOMIZATION METHODS
312
+ // =====================================================
313
+ async getViews(entityLogicalName) {
314
+ return this.view.getViews(entityLogicalName);
2172
315
  }
2173
- /**
2174
- * Get the FetchXML from a view
2175
- * @param viewId The savedqueryid (GUID)
2176
- * @returns The view with FetchXML
2177
- */
2178
316
  async getViewFetchXml(viewId) {
2179
- return await this.makeRequest(`api/data/v9.2/savedqueries(${viewId})?$select=fetchxml,name,returnedtypecode,querytype`);
317
+ return this.view.getViewFetchXml(viewId);
318
+ }
319
+ async createView(name, entityLogicalName, fetchXml, layoutXml, options) {
320
+ const viewData = {
321
+ name,
322
+ returnedtypecode: entityLogicalName,
323
+ fetchxml: fetchXml,
324
+ layoutxml: layoutXml,
325
+ querytype: options?.queryType ?? 0,
326
+ };
327
+ if (options?.description) {
328
+ viewData.description = options.description;
329
+ }
330
+ if (options?.isDefault !== undefined) {
331
+ viewData.isdefault = options.isDefault;
332
+ }
333
+ return this.view.createView(viewData, options?.solutionUniqueName);
2180
334
  }
2181
- // ===== Phase 3: Advanced Customizations (Web Resources) =====
2182
- /**
2183
- * Create a web resource
2184
- */
2185
- async createWebResource(webResource, solutionUniqueName) {
2186
- const headers = solutionUniqueName ? { 'MSCRM.SolutionUniqueName': solutionUniqueName } : undefined;
2187
- return await this.makeRequest('api/data/v9.2/webresourceset', 'POST', webResource, headers);
335
+ async updateView(viewId, updates, solutionUniqueName) {
336
+ return this.view.updateView(viewId, updates, solutionUniqueName);
2188
337
  }
2189
- /**
2190
- * Update a web resource
2191
- */
2192
- async updateWebResource(webResourceId, updates, solutionUniqueName) {
2193
- const headers = solutionUniqueName ? { 'MSCRM.SolutionUniqueName': solutionUniqueName } : undefined;
2194
- await this.makeRequest(`api/data/v9.2/webresourceset(${webResourceId})`, 'PATCH', updates, headers);
338
+ async deleteView(viewId) {
339
+ return this.view.deleteView(viewId);
2195
340
  }
2196
- /**
2197
- * Delete a web resource
2198
- */
2199
- async deleteWebResource(webResourceId) {
2200
- await this.makeRequest(`api/data/v9.2/webresourceset(${webResourceId})`, 'DELETE');
341
+ async setDefaultView(viewId) {
342
+ return this.view.setDefaultView(viewId);
2201
343
  }
2202
- /**
2203
- * Get web resource
2204
- */
344
+ // =====================================================
345
+ // WEB RESOURCE CUSTOMIZATION METHODS
346
+ // =====================================================
2205
347
  async getWebResource(webResourceId) {
2206
- return await this.makeRequest(`api/data/v9.2/webresourceset(${webResourceId})`);
348
+ return this.webResource.getWebResource(webResourceId);
2207
349
  }
2208
- /**
2209
- * Get web resources by name pattern
2210
- */
2211
350
  async getWebResources(nameFilter) {
2212
- const filter = nameFilter ? `?$filter=contains(name,'${nameFilter}')` : '';
2213
- return await this.makeRequest(`api/data/v9.2/webresourceset${filter}`);
2214
- }
2215
- /**
2216
- * Get web resource content (base64 encoded)
2217
- * @param webResourceId The webresourceid (GUID)
2218
- * @returns The web resource with content field
2219
- */
2220
- async getWebResourceContent(webResourceId) {
2221
- return await this.makeRequest(`api/data/v9.2/webresourceset(${webResourceId})?$select=content,name,webresourcetype`);
351
+ return this.webResource.getWebResources(nameFilter);
352
+ }
353
+ async createWebResource(name, displayName, webResourceType, content, options) {
354
+ const webResourceData = {
355
+ name,
356
+ displayname: displayName,
357
+ webresourcetype: webResourceType,
358
+ content,
359
+ };
360
+ if (options?.description) {
361
+ webResourceData.description = options.description;
362
+ }
363
+ return this.webResource.createWebResource(webResourceData, options?.solutionUniqueName);
2222
364
  }
2223
- /**
2224
- * Get web resource dependencies
2225
- * @param webResourceId The webresourceid (GUID)
2226
- * @returns List of dependencies
2227
- */
2228
- async getWebResourceDependencies(webResourceId) {
2229
- return await this.makeRequest(`api/data/v9.2/webresourceset(${webResourceId})/dependencies`);
365
+ async updateWebResource(webResourceId, updates, solutionUniqueName) {
366
+ return this.webResource.updateWebResource(webResourceId, updates, solutionUniqueName);
2230
367
  }
2231
- // ===== Phase 4: Solution Management =====
2232
- /**
2233
- * Create a publisher
2234
- */
2235
- async createPublisher(publisher) {
2236
- return await this.makeRequest('api/data/v9.2/publishers', 'POST', publisher);
368
+ async deleteWebResource(webResourceId) {
369
+ return this.webResource.deleteWebResource(webResourceId);
2237
370
  }
2238
- /**
2239
- * Get publishers
2240
- */
371
+ // =====================================================
372
+ // SOLUTION CUSTOMIZATION METHODS
373
+ // =====================================================
2241
374
  async getPublishers() {
2242
- return await this.makeRequest('api/data/v9.2/publishers?$filter=isreadonly eq false');
2243
- }
2244
- /**
2245
- * Create a solution
2246
- */
2247
- async createSolution(solution) {
2248
- return await this.makeRequest('api/data/v9.2/solutions', 'POST', solution);
375
+ return this.solution.getPublishers();
376
+ }
377
+ async createPublisher(uniqueName, friendlyName, customizationPrefix, customizationOptionValuePrefix, description) {
378
+ const publisherData = {
379
+ uniquename: uniqueName,
380
+ friendlyname: friendlyName,
381
+ customizationprefix: customizationPrefix,
382
+ customizationoptionvalueprefix: customizationOptionValuePrefix,
383
+ };
384
+ if (description) {
385
+ publisherData.description = description;
386
+ }
387
+ return this.solution.createPublisher(publisherData);
2249
388
  }
2250
- /**
2251
- * Get solutions
2252
- */
2253
389
  async getSolutions() {
2254
- return await this.makeRequest('api/data/v9.2/solutions?$filter=isvisible eq true&$orderby=createdon desc');
390
+ return this.solution.getSolutions();
2255
391
  }
2256
- /**
2257
- * Get solution by unique name
2258
- */
2259
392
  async getSolution(uniqueName) {
2260
- const result = await this.makeRequest(`api/data/v9.2/solutions?$filter=uniquename eq '${uniqueName}'&$top=1`);
2261
- return result.value && result.value.length > 0 ? result.value[0] : null;
393
+ return this.solution.getSolution(uniqueName);
394
+ }
395
+ async createSolution(uniqueName, friendlyName, version, publisherId, description) {
396
+ const solutionData = {
397
+ uniquename: uniqueName,
398
+ friendlyname: friendlyName,
399
+ version: version,
400
+ 'publisherid@odata.bind': `/publishers(${publisherId})`,
401
+ };
402
+ if (description) {
403
+ solutionData.description = description;
404
+ }
405
+ return this.solution.createSolution(solutionData);
2262
406
  }
2263
- /**
2264
- * Add component to solution
2265
- */
2266
- async addComponentToSolution(solutionUniqueName, componentId, componentType, addRequiredComponents = true, includedComponentSettingsValues) {
2267
- await this.makeRequest('api/data/v9.2/AddSolutionComponent', 'POST', {
2268
- SolutionUniqueName: solutionUniqueName,
2269
- ComponentId: componentId,
2270
- ComponentType: componentType,
2271
- AddRequiredComponents: addRequiredComponents,
2272
- IncludedComponentSettingsValues: includedComponentSettingsValues
2273
- });
407
+ async getSolutionComponents(solutionUniqueName) {
408
+ return this.solution.getSolutionComponents(solutionUniqueName);
2274
409
  }
2275
- /**
2276
- * Remove component from solution
2277
- */
2278
- async removeComponentFromSolution(solutionUniqueName, componentId, componentType) {
2279
- await this.makeRequest('api/data/v9.2/RemoveSolutionComponent', 'POST', {
2280
- SolutionUniqueName: solutionUniqueName,
2281
- ComponentId: componentId,
2282
- ComponentType: componentType
2283
- });
410
+ async addComponentToSolution(solutionUniqueName, componentId, componentType, addRequiredComponents) {
411
+ return this.solution.addComponentToSolution(solutionUniqueName, componentId, componentType, addRequiredComponents);
2284
412
  }
2285
- /**
2286
- * Get solution components
2287
- */
2288
- async getSolutionComponents(solutionUniqueName) {
2289
- const solution = await this.getSolution(solutionUniqueName);
2290
- if (!solution) {
2291
- throw new Error(`Solution '${solutionUniqueName}' not found`);
2292
- }
2293
- return await this.makeRequest(`api/data/v9.2/solutioncomponents?$filter=_solutionid_value eq ${solution.solutionid}&$orderby=componenttype`);
413
+ async removeComponentFromSolution(solutionUniqueName, componentId, componentType) {
414
+ return this.solution.removeComponentFromSolution(solutionUniqueName, componentId, componentType);
2294
415
  }
2295
- /**
2296
- * Export solution
2297
- */
2298
- async exportSolution(solutionName, managed = false) {
2299
- return await this.makeRequest('api/data/v9.2/ExportSolution', 'POST', {
2300
- SolutionName: solutionName,
2301
- Managed: managed,
2302
- ExportAutoNumberingSettings: true,
2303
- ExportCalendarSettings: true,
2304
- ExportCustomizationSettings: true,
2305
- ExportEmailTrackingSettings: true,
2306
- ExportGeneralSettings: true,
2307
- ExportMarketingSettings: true,
2308
- ExportOutlookSynchronizationSettings: true,
2309
- ExportRelationshipRoles: true,
2310
- ExportIsvConfig: true,
2311
- ExportSales: true,
2312
- ExportExternalApplications: true
2313
- });
416
+ async exportSolution(solutionName, managed) {
417
+ return this.solution.exportSolution(solutionName, managed ?? false);
2314
418
  }
2315
- /**
2316
- * Import solution
2317
- */
2318
- async importSolution(customizationFile, publishWorkflows = true, overwriteUnmanagedCustomizations = false) {
2319
- return await this.makeRequest('api/data/v9.2/ImportSolution', 'POST', {
2320
- CustomizationFile: customizationFile,
2321
- PublishWorkflows: publishWorkflows,
2322
- OverwriteUnmanagedCustomizations: overwriteUnmanagedCustomizations,
2323
- SkipProductUpdateDependencies: false,
2324
- HoldingSolution: false,
2325
- ImportJobId: this.generateGuid()
2326
- });
419
+ async importSolution(customizationFile, overwriteUnmanagedCustomizations, publishWorkflows) {
420
+ return this.solution.importSolution(customizationFile, overwriteUnmanagedCustomizations, publishWorkflows);
2327
421
  }
2328
- /**
2329
- * Delete a solution
2330
- */
2331
- async deleteSolution(solutionId) {
2332
- await this.makeRequest(`api/data/v9.2/solutions(${solutionId})`, 'DELETE');
422
+ // =====================================================
423
+ // PUBLISHING METHODS
424
+ // =====================================================
425
+ async publishAllCustomizations() {
426
+ return this.publishing.publishAllCustomizations();
2333
427
  }
2334
- // ===== Phase 5: Publishing & Validation =====
2335
- /**
2336
- * Publish specific entity
2337
- */
2338
428
  async publishEntity(entityLogicalName) {
2339
- const parameterXml = `<importexportxml><entities><entity>${entityLogicalName}</entity></entities></importexportxml>`;
2340
- await this.publishXml(parameterXml);
429
+ return this.publishing.publishEntity(entityLogicalName);
2341
430
  }
2342
- /**
2343
- * Publish specific component
2344
- */
2345
- async publishComponent(componentId, componentType) {
2346
- const typeMap = {
2347
- 1: 'entity',
2348
- 2: 'attribute',
2349
- 9: 'optionset',
2350
- 24: 'form',
2351
- 26: 'savedquery',
2352
- 29: 'workflow',
2353
- 60: 'systemform',
2354
- 61: 'webresource'
2355
- };
2356
- const componentTypeName = typeMap[componentType] || 'component';
2357
- const parameterXml = `<importexportxml><${componentTypeName}s><${componentTypeName}>${componentId}</${componentTypeName}></${componentTypeName}s></importexportxml>`;
2358
- await this.publishXml(parameterXml);
431
+ // =====================================================
432
+ // DEPENDENCY METHODS
433
+ // =====================================================
434
+ async checkDependencies(componentId, componentType) {
435
+ return this.dependency.checkDependencies(componentId, componentType);
2359
436
  }
2360
- /**
2361
- * Check for unpublished customizations
2362
- */
2363
- async checkUnpublishedChanges() {
2364
- // Query for unpublished customizations using RetrieveUnpublished
2365
- return await this.makeRequest('api/data/v9.2/RetrieveUnpublished', 'POST', {});
437
+ async checkDeleteEligibility(componentId, componentType) {
438
+ return this.dependency.checkDeleteEligibility(componentId, componentType);
2366
439
  }
2367
- /**
2368
- * Check component dependencies
2369
- */
2370
- async checkDependencies(componentId, componentType) {
2371
- return await this.makeRequest('api/data/v9.2/RetrieveDependenciesForDelete', 'POST', {
2372
- ObjectId: componentId,
2373
- ComponentType: componentType
440
+ // =====================================================
441
+ // PLUGIN DEPLOYMENT METHODS
442
+ // =====================================================
443
+ async extractAssemblyVersion(assemblyPath) {
444
+ return this.pluginDeployment.extractAssemblyVersion(assemblyPath);
445
+ }
446
+ async queryPluginTypeByTypename(typename) {
447
+ return this.pluginDeployment.queryPluginTypeByTypename(typename);
448
+ }
449
+ async queryPluginAssemblyByName(assemblyName) {
450
+ return this.pluginDeployment.queryPluginAssemblyByName(assemblyName);
451
+ }
452
+ async getPluginTypesForAssembly(assemblyId) {
453
+ return this.pluginDeployment.getPluginTypesForAssembly(assemblyId);
454
+ }
455
+ async resolveSdkMessageAndFilter(messageName, primaryEntity) {
456
+ return this.pluginDeployment.resolveSdkMessageAndFilter(messageName, primaryEntity);
457
+ }
458
+ async createPluginAssembly(options) {
459
+ return this.pluginDeployment.createPluginAssembly({
460
+ name: options.name,
461
+ content: options.content,
462
+ version: options.version,
463
+ description: options.description,
464
+ isolationMode: options.isolationMode,
465
+ solutionUniqueName: options.solutionUniqueName,
2374
466
  });
2375
467
  }
2376
- /**
2377
- * Check if component can be deleted
2378
- */
2379
- async checkDeleteEligibility(componentId, componentType) {
2380
- try {
2381
- const result = await this.checkDependencies(componentId, componentType);
2382
- const dependencies = result.EntityCollection?.Entities || [];
2383
- return {
2384
- canDelete: dependencies.length === 0,
2385
- dependencies: dependencies
2386
- };
2387
- }
2388
- catch (error) {
2389
- return {
2390
- canDelete: false,
2391
- dependencies: []
2392
- };
2393
- }
468
+ async updatePluginAssembly(assemblyId, content, version, solutionUniqueName) {
469
+ return this.pluginDeployment.updatePluginAssembly(assemblyId, content, version, solutionUniqueName);
2394
470
  }
2395
- /**
2396
- * Preview unpublished changes
2397
- * Returns all components that have unpublished customizations
2398
- */
2399
- async previewUnpublishedChanges() {
2400
- // Use RetrieveUnpublished action to get unpublished changes
2401
- return await this.makeRequest('api/data/v9.2/RetrieveUnpublished', 'POST', {});
471
+ async deletePluginAssembly(assemblyId) {
472
+ return this.pluginDeployment.deletePluginAssembly(assemblyId);
2402
473
  }
2403
- /**
2404
- * Check dependencies for a specific component
2405
- * @param componentId The component ID (GUID)
2406
- * @param componentType The component type code
2407
- * @returns Dependency information
2408
- */
2409
- async checkComponentDependencies(componentId, componentType) {
2410
- // This is an alias for checkDependencies for consistency
2411
- return await this.checkDependencies(componentId, componentType);
474
+ async deletePluginStep(stepId) {
475
+ return this.pluginDeployment.deletePluginStep(stepId);
2412
476
  }
2413
- /**
2414
- * Validate solution integrity
2415
- * Checks for missing dependencies and other issues
2416
- * @param solutionUniqueName The unique name of the solution
2417
- * @returns Validation results
2418
- */
2419
- async validateSolutionIntegrity(solutionUniqueName) {
2420
- // Get solution components
2421
- const components = await this.getSolutionComponents(solutionUniqueName);
2422
- const issues = [];
2423
- const warnings = [];
2424
- // Check each component for dependencies
2425
- for (const component of components.value || []) {
2426
- try {
2427
- const deps = await this.checkDependencies(component.objectid, component.componenttype);
2428
- const dependencies = deps.EntityCollection?.Entities || [];
2429
- const missingDeps = dependencies.filter((d) => d.Attributes?.ismissing === true);
2430
- if (missingDeps.length > 0) {
2431
- issues.push({
2432
- componentId: component.objectid,
2433
- componentType: component.componenttype,
2434
- missingDependencies: missingDeps
2435
- });
2436
- }
2437
- }
2438
- catch (error) {
2439
- warnings.push({
2440
- componentId: component.objectid,
2441
- componentType: component.componenttype,
2442
- error: 'Could not check dependencies'
2443
- });
2444
- }
2445
- }
2446
- return {
2447
- isValid: issues.length === 0,
2448
- issues,
2449
- warnings
2450
- };
477
+ async registerPluginStep(config) {
478
+ return this.pluginDeployment.registerPluginStep(config);
2451
479
  }
2452
- /**
2453
- * Validate schema name
2454
- */
2455
- validateSchemaName(schemaName, prefix) {
2456
- const errors = [];
2457
- // Check if starts with prefix
2458
- if (!schemaName.startsWith(prefix)) {
2459
- errors.push(`Schema name must start with prefix '${prefix}'`);
2460
- }
2461
- // Check for invalid characters
2462
- if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(schemaName)) {
2463
- errors.push('Schema name must start with a letter or underscore and contain only letters, numbers, and underscores');
2464
- }
2465
- // Check length (max 64 characters for most components)
2466
- if (schemaName.length > 64) {
2467
- errors.push('Schema name must be 64 characters or less');
2468
- }
2469
- return {
2470
- valid: errors.length === 0,
2471
- errors
2472
- };
480
+ async registerPluginImage(config) {
481
+ return this.pluginDeployment.registerPluginImage(config);
2473
482
  }
2474
- /**
2475
- * Get entity customization information
2476
- */
2477
- async getEntityCustomizationInfo(entityLogicalName) {
2478
- return await this.makeRequest(`api/data/v9.2/EntityDefinitions(LogicalName='${entityLogicalName}')?$select=IsCustomizable,IsManaged,IsCustomEntity`);
483
+ // =====================================================
484
+ // APP MANAGEMENT METHODS
485
+ // =====================================================
486
+ async createSimpleSitemap(config, solutionUniqueName) {
487
+ // Cast config to the expected type - caller is responsible for correct structure
488
+ return this.appManagement.createSimpleSitemap(config, solutionUniqueName);
2479
489
  }
2480
- /**
2481
- * Check if entity has dependencies
2482
- */
2483
- async checkEntityDependencies(entityLogicalName) {
2484
- // First get the metadata ID
2485
- const entityMetadata = await this.getEntityMetadata(entityLogicalName);
2486
- if (!entityMetadata.MetadataId) {
2487
- throw new Error(`Could not find MetadataId for entity '${entityLogicalName}'`);
2488
- }
2489
- // Component type 1 = Entity
2490
- return await this.checkDependencies(entityMetadata.MetadataId, 1);
490
+ async addEntitiesToApp(appId, entityNames) {
491
+ return this.appManagement.addEntitiesToApp(appId, entityNames);
2491
492
  }
2492
- /**
2493
- * Helper to generate GUID
2494
- */
2495
- generateGuid() {
2496
- return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
2497
- const r = Math.random() * 16 | 0;
2498
- const v = c === 'x' ? r : (r & 0x3 | 0x8);
2499
- return v.toString(16);
2500
- });
493
+ async validateApp(appId) {
494
+ return this.appManagement.validateApp(appId);
2501
495
  }
2502
- // ============================================================
2503
- // PLUGIN DEPLOYMENT METHODS
2504
- // ============================================================
2505
- /**
2506
- * Extract assembly version from .NET DLL using PE header parsing
2507
- * @param assemblyPath - Path to the compiled .NET assembly (DLL)
2508
- * @returns Version string (e.g., "1.0.0.0")
2509
- */
2510
- async extractAssemblyVersion(assemblyPath) {
2511
- try {
2512
- // Dynamically import fs/promises for ESM compatibility
2513
- const fs = await import('fs/promises');
2514
- // Normalize path for cross-platform compatibility (Windows/WSL)
2515
- const normalizedPath = assemblyPath.replace(/\\/g, '/');
2516
- // Read the DLL file
2517
- const buffer = await fs.readFile(normalizedPath);
2518
- // Validate DLL format (should start with "MZ" header)
2519
- const header = buffer.toString('utf8', 0, 2);
2520
- if (header !== 'MZ') {
2521
- console.error('Invalid .NET assembly format - using default version');
2522
- return '1.0.0.0';
2523
- }
2524
- // Try to find version resource in PE header
2525
- // This is a simplified version - for production, you might want a more robust parser
2526
- // For now, we'll return a default version and let the user specify if needed
2527
- // Search for common version patterns in the assembly
2528
- const bufferStr = buffer.toString('utf16le');
2529
- const versionMatch = bufferStr.match(/\d+\.\d+\.\d+\.\d+/);
2530
- if (versionMatch) {
2531
- return versionMatch[0];
2532
- }
2533
- // Fallback to default version
2534
- console.error('Could not extract version from assembly - using default 1.0.0.0');
2535
- return '1.0.0.0';
2536
- }
2537
- catch (error) {
2538
- console.error('Error extracting assembly version:', error.message);
2539
- return '1.0.0.0';
2540
- }
496
+ async publishApp(appId) {
497
+ // publishApp needs a callback for publishXml
498
+ return this.appManagement.publishApp(appId, (parameterXml) => this.publishing.publishXml(parameterXml));
2541
499
  }
2542
- /**
2543
- * Query plugin type by typename
2544
- * @param typename - Plugin type typename (e.g., "MyNamespace.ContactPlugin")
2545
- * @returns Plugin type ID
2546
- */
2547
- async queryPluginTypeByTypename(typename) {
2548
- try {
2549
- const response = await this.makeRequest(`api/data/v9.2/plugintypes?$filter=typename eq '${typename}'&$select=plugintypeid`, 'GET');
2550
- if (!response.value || response.value.length === 0) {
2551
- throw new Error(`Plugin type '${typename}' not found. ` +
2552
- `Did you upload the assembly first? Use 'create-plugin-assembly' tool.`);
2553
- }
2554
- return response.value[0].plugintypeid;
2555
- }
2556
- catch (error) {
2557
- throw new Error(`Failed to query plugin type: ${error.message}`);
2558
- }
500
+ // =====================================================
501
+ // WORKFLOW MANAGEMENT METHODS
502
+ // =====================================================
503
+ async deactivateWorkflow(workflowId) {
504
+ return this.workflowManagement.deactivateWorkflow(workflowId);
2559
505
  }
2560
- /**
2561
- * Query plugin assembly by name
2562
- * @param assemblyName - Assembly name
2563
- * @returns Plugin assembly ID or null if not found
2564
- */
2565
- async queryPluginAssemblyByName(assemblyName) {
2566
- try {
2567
- const response = await this.makeRequest(`api/data/v9.2/pluginassemblies?$filter=name eq '${assemblyName}'&$select=pluginassemblyid`, 'GET');
2568
- if (!response.value || response.value.length === 0) {
2569
- return null;
2570
- }
2571
- return response.value[0].pluginassemblyid;
2572
- }
2573
- catch (error) {
2574
- throw new Error(`Failed to query plugin assembly: ${error.message}`);
2575
- }
506
+ async activateWorkflow(workflowId) {
507
+ return this.workflowManagement.activateWorkflow(workflowId);
2576
508
  }
2577
- /**
2578
- * Get plugin types for an existing assembly
2579
- * Used to retrieve type information after an assembly update
2580
- * @param assemblyId - Assembly GUID
2581
- * @returns Array of plugin type information
2582
- */
2583
- async getPluginTypesForAssembly(assemblyId) {
2584
- try {
2585
- const types = await this.makeRequest(`api/data/v9.2/plugintypes?$filter=_pluginassemblyid_value eq ${assemblyId}&$select=plugintypeid,typename,friendlyname`, 'GET');
2586
- return types.value.map((t) => ({
2587
- pluginTypeId: t.plugintypeid,
2588
- typeName: t.typename,
2589
- friendlyName: t.friendlyname
2590
- }));
2591
- }
2592
- catch (error) {
2593
- throw new Error(`Failed to get plugin types for assembly: ${error.message}`);
2594
- }
509
+ async updateWorkflowDescription(workflowId, description) {
510
+ return this.workflowManagement.updateWorkflowDescription(workflowId, description);
511
+ }
512
+ async updateFlowDescription(flowId, description) {
513
+ return this.workflowManagement.updateFlowDescription(flowId, description);
2595
514
  }
2596
515
  /**
2597
- * Resolve SDK Message and Filter IDs for plugin step registration
2598
- * @param messageName - SDK message name (e.g., "Create", "Update", "Delete")
2599
- * @param entityName - Entity logical name (e.g., "contact", "account")
2600
- * @returns Object containing messageId and filterId
516
+ * Adapter function to convert FlowService.parseFlowSummary output to expected format
2601
517
  */
2602
- async resolveSdkMessageAndFilter(messageName, entityName) {
2603
- const timer = auditLogger.startTimer();
2604
- try {
2605
- // Step 1: Get SDK Message ID
2606
- const messages = await this.makeRequest(`api/data/v9.2/sdkmessages?$filter=name eq '${messageName}'&$select=sdkmessageid,name`, 'GET');
2607
- if (!messages.value || messages.value.length === 0) {
2608
- throw new Error(`SDK message '${messageName}' not found. ` +
2609
- `Common messages: Create, Update, Delete, SetState, Assign, Merge, Retrieve, RetrieveMultiple`);
2610
- }
2611
- const messageId = messages.value[0].sdkmessageid;
2612
- // Step 2: Get SDK Message Filter ID
2613
- const filters = await this.makeRequest(`api/data/v9.2/sdkmessagefilters?$filter=sdkmessageid/sdkmessageid eq ${messageId} and primaryobjecttypecode eq '${entityName}'&$select=sdkmessagefilterid`, 'GET');
2614
- if (!filters.value || filters.value.length === 0) {
2615
- throw new Error(`SDK message filter not found for message '${messageName}' on entity '${entityName}'. ` +
2616
- `Verify the entity supports this message.`);
2617
- }
2618
- const filterId = filters.value[0].sdkmessagefilterid;
2619
- auditLogger.log({
2620
- operation: 'resolve-sdk-message-filter',
2621
- operationType: 'READ',
2622
- componentType: 'SdkMessage',
2623
- success: true,
2624
- parameters: { messageName, entityName, messageId, filterId },
2625
- executionTimeMs: timer(),
2626
- });
2627
- return { messageId, filterId };
2628
- }
2629
- catch (error) {
2630
- auditLogger.log({
2631
- operation: 'resolve-sdk-message-filter',
2632
- operationType: 'READ',
2633
- componentType: 'SdkMessage',
2634
- success: false,
2635
- error: error.message,
2636
- executionTimeMs: timer(),
2637
- });
2638
- throw error;
2639
- }
518
+ adaptFlowSummary(flowDef) {
519
+ const summary = this.flow.parseFlowSummary(flowDef);
520
+ return {
521
+ tablesModified: Array.from(summary.tablesModified || []),
522
+ triggerInfo: summary.triggerInfo || 'manual',
523
+ triggerFields: summary.triggerFields || [],
524
+ actions: summary.actions || [],
525
+ customApisCalled: Array.from(summary.customApisCalled || []),
526
+ };
2640
527
  }
2641
528
  /**
2642
- * Create a new plugin assembly in Dataverse
2643
- * @param options - Assembly creation options
2644
- * @returns Created assembly ID and plugin types
529
+ * Adapter function to convert WorkflowService.parseWorkflowXamlSummary output to expected format
2645
530
  */
2646
- async createPluginAssembly(options) {
2647
- const timer = auditLogger.startTimer();
2648
- try {
2649
- // Validate DLL size (16MB Dataverse limit)
2650
- const dllSize = Buffer.from(options.content, 'base64').length;
2651
- const maxSize = 16 * 1024 * 1024; // 16MB
2652
- if (dllSize > maxSize) {
2653
- throw new Error(`Assembly exceeds 16MB limit (current: ${(dllSize / 1024 / 1024).toFixed(2)}MB). ` +
2654
- `Remove unused dependencies or split into multiple assemblies.`);
2655
- }
2656
- // Create assembly payload
2657
- const assemblyData = {
2658
- name: options.name,
2659
- content: options.content,
2660
- version: options.version,
2661
- isolationmode: options.isolationMode ?? 2, // Default to Sandbox for security
2662
- sourcetype: options.sourceType ?? 0, // Default to Database
2663
- culture: 'neutral'
2664
- };
2665
- if (options.description) {
2666
- assemblyData.description = options.description;
2667
- }
2668
- // Upload assembly to Dataverse
2669
- const createResponse = await this.makeRequest('api/data/v9.2/pluginassemblies', 'POST', assemblyData, { 'Prefer': 'return=representation' });
2670
- // Extract assembly ID from response header or body
2671
- const pluginAssemblyId = createResponse.pluginassemblyid || createResponse.id;
2672
- if (!pluginAssemblyId) {
2673
- throw new Error('Plugin assembly created but ID not returned');
2674
- }
2675
- // Add to solution if specified
2676
- if (options.solutionUniqueName) {
2677
- await this.addComponentToSolution(options.solutionUniqueName, pluginAssemblyId, 91 // Component type 91 = PluginAssembly
2678
- );
2679
- }
2680
- // Poll for plugin types (Dataverse creates them asynchronously)
2681
- // Wait up to 60 seconds with 2-second intervals
2682
- const pluginTypes = [];
2683
- const maxAttempts = 30; // 30 attempts * 2 seconds = 60 seconds total
2684
- const pollInterval = 2000; // 2 seconds
2685
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
2686
- // Wait before polling (except first attempt)
2687
- if (attempt > 0) {
2688
- await new Promise(resolve => setTimeout(resolve, pollInterval));
2689
- }
2690
- // Query for plugin types
2691
- const types = await this.makeRequest(`api/data/v9.2/plugintypes?$filter=_pluginassemblyid_value eq ${pluginAssemblyId}&$select=plugintypeid,typename,friendlyname`, 'GET');
2692
- if (types.value && types.value.length > 0) {
2693
- pluginTypes.push(...types.value.map((t) => ({
2694
- pluginTypeId: t.plugintypeid,
2695
- typeName: t.typename,
2696
- friendlyName: t.friendlyname
2697
- })));
2698
- break;
2699
- }
2700
- }
2701
- if (pluginTypes.length === 0) {
2702
- throw new Error(`Plugin types not created after ${maxAttempts * pollInterval / 1000} seconds. ` +
2703
- `Dataverse may be experiencing delays processing the assembly. ` +
2704
- `Please check System Jobs in Power Platform admin center and retry. ` +
2705
- `Assembly ID: ${pluginAssemblyId}`);
2706
- }
2707
- auditLogger.log({
2708
- operation: 'create-plugin-assembly',
2709
- operationType: 'CREATE',
2710
- componentId: pluginAssemblyId,
2711
- componentType: 'PluginAssembly',
2712
- success: true,
2713
- parameters: {
2714
- name: options.name,
2715
- version: options.version,
2716
- size: dllSize,
2717
- pluginTypeCount: pluginTypes.length
2718
- },
2719
- executionTimeMs: timer(),
2720
- });
2721
- return {
2722
- pluginAssemblyId,
2723
- pluginTypes
2724
- };
2725
- }
2726
- catch (error) {
2727
- auditLogger.log({
2728
- operation: 'create-plugin-assembly',
2729
- operationType: 'CREATE',
2730
- componentName: options.name,
2731
- componentType: 'PluginAssembly',
2732
- success: false,
2733
- error: error.message,
2734
- executionTimeMs: timer(),
2735
- });
2736
- throw error;
2737
- }
531
+ adaptWorkflowSummary(xaml) {
532
+ const summary = this.workflow.parseWorkflowXamlSummary(xaml);
533
+ const activities = summary.activities || [];
534
+ const getCount = (type) => {
535
+ const activity = activities.find(a => a.type === type);
536
+ return activity?.count || 0;
537
+ };
538
+ return {
539
+ tablesModified: Array.from(summary.tablesModified || []),
540
+ triggerInfo: summary.triggerInfo || 'manual',
541
+ triggerFields: summary.triggerFields || [],
542
+ createEntityCount: getCount('CreateEntity'),
543
+ updateEntityCount: getCount('UpdateEntity'),
544
+ assignEntityCount: getCount('AssignEntity'),
545
+ setStateCount: getCount('SetState'),
546
+ };
547
+ }
548
+ async documentAutomation(automationId, type) {
549
+ return this.workflowManagement.documentAutomation(automationId, type, (def) => this.adaptFlowSummary(def), (xaml) => this.adaptWorkflowSummary(xaml));
550
+ }
551
+ async documentWorkflowSafe(workflowId, type) {
552
+ return this.workflowManagement.documentWorkflowSafe(workflowId, type, (def) => this.adaptFlowSummary(def), (xaml) => this.adaptWorkflowSummary(xaml));
2738
553
  }
2739
554
  /**
2740
- * Update an existing plugin assembly with new DLL content
2741
- * @param assemblyId - Assembly GUID
2742
- * @param content - Base64-encoded DLL content
2743
- * @param version - New version string
2744
- * @param solutionUniqueName - Optional solution context
555
+ * Create a new Power Automate flow from an existing template flow
2745
556
  */
2746
- async updatePluginAssembly(assemblyId, content, version, solutionUniqueName) {
2747
- const timer = auditLogger.startTimer();
2748
- try {
2749
- // Validate DLL size
2750
- const dllSize = Buffer.from(content, 'base64').length;
2751
- const maxSize = 16 * 1024 * 1024;
2752
- if (dllSize > maxSize) {
2753
- throw new Error(`Assembly exceeds 16MB limit (current: ${(dllSize / 1024 / 1024).toFixed(2)}MB)`);
2754
- }
2755
- // Update assembly
2756
- await this.makeRequest(`api/data/v9.2/pluginassemblies(${assemblyId})`, 'PATCH', {
2757
- content,
2758
- version
2759
- });
2760
- // Add to solution if specified
2761
- if (solutionUniqueName) {
2762
- await this.addComponentToSolution(solutionUniqueName, assemblyId, 91 // Component type 91 = PluginAssembly
2763
- );
2764
- }
2765
- auditLogger.log({
2766
- operation: 'update-plugin-assembly',
2767
- operationType: 'UPDATE',
2768
- componentId: assemblyId,
2769
- componentType: 'PluginAssembly',
2770
- success: true,
2771
- parameters: { assemblyId, version, size: dllSize },
2772
- executionTimeMs: timer(),
2773
- });
2774
- }
2775
- catch (error) {
2776
- auditLogger.log({
2777
- operation: 'update-plugin-assembly',
2778
- operationType: 'UPDATE',
2779
- componentId: assemblyId,
2780
- componentType: 'PluginAssembly',
2781
- success: false,
2782
- error: error.message,
2783
- executionTimeMs: timer(),
2784
- });
2785
- throw error;
2786
- }
557
+ async createFlow(name, templateFlowId, options) {
558
+ return this.workflowManagement.createFlow(name, templateFlowId, options);
2787
559
  }
2788
560
  /**
2789
- * Delete a plugin assembly and all its associated components
2790
- * Used for rollback on deployment failure
2791
- * @param assemblyId - Assembly GUID to delete
561
+ * Delete a Power Automate flow (permanent operation)
2792
562
  */
2793
- async deletePluginAssembly(assemblyId) {
2794
- const timer = auditLogger.startTimer();
2795
- try {
2796
- // Note: Dataverse cascade deletes associated plugin types, steps, and images
2797
- await this.makeRequest(`api/data/v9.2/pluginassemblies(${assemblyId})`, 'DELETE');
2798
- auditLogger.log({
2799
- operation: 'delete-plugin-assembly',
2800
- operationType: 'DELETE',
2801
- componentId: assemblyId,
2802
- componentType: 'PluginAssembly',
2803
- success: true,
2804
- executionTimeMs: timer(),
2805
- });
2806
- }
2807
- catch (error) {
2808
- auditLogger.log({
2809
- operation: 'delete-plugin-assembly',
2810
- operationType: 'DELETE',
2811
- componentId: assemblyId,
2812
- componentType: 'PluginAssembly',
2813
- success: false,
2814
- error: error.message,
2815
- executionTimeMs: timer(),
2816
- });
2817
- throw new Error(`Failed to delete plugin assembly: ${error.message}`);
2818
- }
563
+ async deleteFlow(flowId) {
564
+ return this.workflowManagement.deleteFlow(flowId);
2819
565
  }
2820
566
  /**
2821
- * Delete a plugin step
2822
- * Used for rollback on deployment failure
2823
- * @param stepId - Step GUID to delete
567
+ * Clone an existing flow with a new name
2824
568
  */
2825
- async deletePluginStep(stepId) {
2826
- const timer = auditLogger.startTimer();
2827
- try {
2828
- await this.makeRequest(`api/data/v9.2/sdkmessageprocessingsteps(${stepId})`, 'DELETE');
2829
- auditLogger.log({
2830
- operation: 'delete-plugin-step',
2831
- operationType: 'DELETE',
2832
- componentId: stepId,
2833
- componentType: 'PluginStep',
2834
- success: true,
2835
- executionTimeMs: timer(),
2836
- });
2837
- }
2838
- catch (error) {
2839
- auditLogger.log({
2840
- operation: 'delete-plugin-step',
2841
- operationType: 'DELETE',
2842
- componentId: stepId,
2843
- componentType: 'PluginStep',
2844
- success: false,
2845
- error: error.message,
2846
- executionTimeMs: timer(),
2847
- });
2848
- // Log but don't throw - rollback should continue even if individual deletes fail
2849
- console.error(`Warning: Failed to delete plugin step ${stepId}: ${error.message}`);
2850
- }
569
+ async cloneFlow(sourceFlowId, newName, options) {
570
+ return this.workflowManagement.cloneFlow(sourceFlowId, newName, options);
2851
571
  }
2852
572
  /**
2853
- * Register a plugin step on an SDK message
2854
- * @param options - Step registration options
2855
- * @returns Created step ID
573
+ * Activate a Power Automate flow (alias for activateWorkflow)
2856
574
  */
2857
- async registerPluginStep(options) {
2858
- const timer = auditLogger.startTimer();
2859
- try {
2860
- // Resolve SDK message and filter IDs
2861
- const { messageId, filterId } = await this.resolveSdkMessageAndFilter(options.messageName, options.primaryEntityName);
2862
- // Create step payload
2863
- const stepData = {
2864
- name: options.name,
2865
- 'plugintypeid@odata.bind': `/plugintypes(${options.pluginTypeId})`,
2866
- 'sdkmessageid@odata.bind': `/sdkmessages(${messageId})`,
2867
- 'sdkmessagefilterid@odata.bind': `/sdkmessagefilters(${filterId})`,
2868
- stage: options.stage,
2869
- mode: options.executionMode,
2870
- rank: options.rank ?? 1,
2871
- supporteddeployment: options.supportedDeployment ?? 0,
2872
- statuscode: 1 // Active
2873
- };
2874
- // Add optional fields
2875
- if (options.filteringAttributes) {
2876
- stepData.filteringattributes = options.filteringAttributes;
2877
- }
2878
- if (options.configuration) {
2879
- stepData.configuration = options.configuration;
2880
- }
2881
- // Register step
2882
- const createResponse = await this.makeRequest('api/data/v9.2/sdkmessageprocessingsteps', 'POST', stepData, { 'Prefer': 'return=representation' });
2883
- const stepId = createResponse.sdkmessageprocessingstepid || createResponse.id;
2884
- if (!stepId) {
2885
- throw new Error('Plugin step created but ID not returned');
2886
- }
2887
- // Add to solution if specified
2888
- if (options.solutionUniqueName) {
2889
- await this.addComponentToSolution(options.solutionUniqueName, stepId, 92 // Component type 92 = SDKMessageProcessingStep
2890
- );
2891
- }
2892
- auditLogger.log({
2893
- operation: 'register-plugin-step',
2894
- operationType: 'CREATE',
2895
- componentId: stepId,
2896
- componentType: 'PluginStep',
2897
- success: true,
2898
- parameters: {
2899
- name: options.name,
2900
- messageName: options.messageName,
2901
- primaryEntity: options.primaryEntityName,
2902
- stage: options.stage,
2903
- mode: options.executionMode
2904
- },
2905
- executionTimeMs: timer(),
2906
- });
2907
- return { stepId };
2908
- }
2909
- catch (error) {
2910
- auditLogger.log({
2911
- operation: 'register-plugin-step',
2912
- operationType: 'CREATE',
2913
- componentName: options.name,
2914
- componentType: 'PluginStep',
2915
- success: false,
2916
- error: error.message,
2917
- executionTimeMs: timer(),
2918
- });
2919
- throw error;
2920
- }
575
+ async activateFlow(flowId) {
576
+ return this.activateWorkflow(flowId);
2921
577
  }
2922
578
  /**
2923
- * Register a pre/post image for a plugin step
2924
- * @param options - Image registration options
2925
- * @returns Created image ID
579
+ * Deactivate a Power Automate flow (alias for deactivateWorkflow)
2926
580
  */
2927
- async registerPluginImage(options) {
2928
- const timer = auditLogger.startTimer();
2929
- try {
2930
- // Create image payload
2931
- const imageData = {
2932
- name: options.name,
2933
- 'sdkmessageprocessingstepid@odata.bind': `/sdkmessageprocessingsteps(${options.stepId})`,
2934
- imagetype: options.imageType,
2935
- entityalias: options.entityAlias,
2936
- messagepropertyname: options.messagePropertyName || 'Target'
2937
- };
2938
- // Add attributes if specified (empty string = all attributes)
2939
- if (options.attributes !== undefined) {
2940
- imageData.attributes = options.attributes;
2941
- }
2942
- // Register image
2943
- const createResponse = await this.makeRequest('api/data/v9.2/sdkmessageprocessingstepimages', 'POST', imageData, { 'Prefer': 'return=representation' });
2944
- const imageId = createResponse.sdkmessageprocessingstepimageid || createResponse.id;
2945
- if (!imageId) {
2946
- throw new Error('Plugin image created but ID not returned');
2947
- }
2948
- auditLogger.log({
2949
- operation: 'register-plugin-image',
2950
- operationType: 'CREATE',
2951
- componentId: imageId,
2952
- componentType: 'PluginImage',
2953
- success: true,
2954
- parameters: {
2955
- name: options.name,
2956
- imageType: options.imageType,
2957
- stepId: options.stepId
2958
- },
2959
- executionTimeMs: timer(),
2960
- });
2961
- return { imageId };
2962
- }
2963
- catch (error) {
2964
- auditLogger.log({
2965
- operation: 'register-plugin-image',
2966
- operationType: 'CREATE',
2967
- componentName: options.name,
2968
- componentType: 'PluginImage',
2969
- success: false,
2970
- error: error.message,
2971
- executionTimeMs: timer(),
2972
- });
2973
- throw error;
2974
- }
581
+ async deactivateFlow(flowId) {
582
+ return this.deactivateWorkflow(flowId);
2975
583
  }
2976
584
  }
2977
585
  //# sourceMappingURL=PowerPlatformService.js.map