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

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,3312 +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}')`);
151
- }
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}`);
96
+ return this.metadata.getEntityAttribute(entityName, attributeName);
194
97
  }
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
- }
277
- }
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
- }
113
+ async getPluginAssemblyComplete(assemblyName, includeDisabled) {
114
+ return this.plugin.getPluginAssemblyComplete(assemblyName, includeDisabled);
325
115
  }
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
- }
116
+ async getEntityPluginPipeline(entityName, messageFilter, includeDisabled) {
117
+ return this.plugin.getEntityPluginPipeline(entityName, messageFilter, includeDisabled);
365
118
  }
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
- };
587
- }
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;
616
- }
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
- };
650
- }
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
- };
687
- }
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
- };
727
- }
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
- };
765
- }
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
- };
120
+ return this.plugin.getPluginTraceLogs(options);
823
121
  }
824
- /**
825
- * Deactivate a workflow (set to Draft state)
826
- * @param workflowId GUID of the workflow
827
- * @returns Previous and new state information
828
- */
829
- async deactivateWorkflow(workflowId) {
830
- // First, get current state for audit trail
831
- const currentWorkflow = await this.makeRequest(`api/data/v9.2/workflows(${workflowId})?$select=workflowid,name,statecode,statuscode`);
832
- const previousState = currentWorkflow.statecode === 0 ? 'Draft' :
833
- currentWorkflow.statecode === 1 ? 'Activated' : 'Suspended';
834
- // If already in Draft state, return success without making API call
835
- if (currentWorkflow.statecode === 0) {
836
- auditLogger.log({
837
- operation: 'deactivate-workflow',
838
- operationType: 'UPDATE',
839
- componentType: 'workflow',
840
- componentName: currentWorkflow.name,
841
- componentId: workflowId,
842
- parameters: { previousState, result: 'already-draft' },
843
- success: true,
844
- });
845
- return {
846
- success: true,
847
- workflowId,
848
- workflowName: currentWorkflow.name,
849
- previousState,
850
- newState: 'Draft',
851
- };
852
- }
853
- // Deactivate workflow using SetState action
854
- // State: 0=Draft, Status: 1=Draft
855
- try {
856
- await this.makeRequest(`api/data/v9.2/workflows(${workflowId})/Microsoft.Dynamics.CRM.SetState`, 'POST', {
857
- State: 0,
858
- Status: 1
859
- });
860
- auditLogger.log({
861
- operation: 'deactivate-workflow',
862
- operationType: 'UPDATE',
863
- componentType: 'workflow',
864
- componentName: currentWorkflow.name,
865
- componentId: workflowId,
866
- parameters: { previousState, newState: 'Draft' },
867
- success: true,
868
- });
869
- return {
870
- success: true,
871
- workflowId,
872
- workflowName: currentWorkflow.name,
873
- previousState,
874
- newState: 'Draft',
875
- };
876
- }
877
- catch (error) {
878
- // Handle expected error codes
879
- if (error.response?.data?.error?.code === '0x80045003') {
880
- // Workflow is already in target state - treat as success
881
- auditLogger.log({
882
- operation: 'deactivate-workflow',
883
- operationType: 'UPDATE',
884
- componentType: 'workflow',
885
- componentName: currentWorkflow.name,
886
- componentId: workflowId,
887
- parameters: { previousState, result: 'already-draft' },
888
- success: true,
889
- });
890
- return {
891
- success: true,
892
- workflowId,
893
- workflowName: currentWorkflow.name,
894
- previousState,
895
- newState: 'Draft',
896
- };
897
- }
898
- // Re-throw unexpected errors
899
- throw error;
900
- }
122
+ // =====================================================
123
+ // FLOW METHODS (Read-only)
124
+ // =====================================================
125
+ async getFlows(options) {
126
+ return this.flow.getFlows(options);
901
127
  }
902
- /**
903
- * Activate a workflow (set to Activated state)
904
- * @param workflowId GUID of the workflow
905
- * @returns Previous and new state information
906
- */
907
- async activateWorkflow(workflowId) {
908
- // First, get current state for audit trail
909
- const currentWorkflow = await this.makeRequest(`api/data/v9.2/workflows(${workflowId})?$select=workflowid,name,statecode,statuscode`);
910
- const previousState = currentWorkflow.statecode === 0 ? 'Draft' :
911
- currentWorkflow.statecode === 1 ? 'Activated' : 'Suspended';
912
- // If already in Activated state, return success without making API call
913
- if (currentWorkflow.statecode === 1) {
914
- auditLogger.log({
915
- operation: 'activate-workflow',
916
- operationType: 'UPDATE',
917
- componentType: 'workflow',
918
- componentName: currentWorkflow.name,
919
- componentId: workflowId,
920
- parameters: { previousState, result: 'already-activated' },
921
- success: true,
922
- });
923
- return {
924
- success: true,
925
- workflowId,
926
- workflowName: currentWorkflow.name,
927
- previousState,
928
- newState: 'Activated',
929
- };
930
- }
931
- // Activate workflow using SetState action
932
- // State: 1=Activated, Status: 2=Activated
933
- try {
934
- await this.makeRequest(`api/data/v9.2/workflows(${workflowId})/Microsoft.Dynamics.CRM.SetState`, 'POST', {
935
- State: 1,
936
- Status: 2
937
- });
938
- auditLogger.log({
939
- operation: 'activate-workflow',
940
- operationType: 'UPDATE',
941
- componentType: 'workflow',
942
- componentName: currentWorkflow.name,
943
- componentId: workflowId,
944
- parameters: { previousState, newState: 'Activated' },
945
- success: true,
946
- });
947
- return {
948
- success: true,
949
- workflowId,
950
- workflowName: currentWorkflow.name,
951
- previousState,
952
- newState: 'Activated',
953
- };
954
- }
955
- catch (error) {
956
- // Handle expected error codes
957
- if (error.response?.data?.error?.code === '0x80045003') {
958
- // Workflow is already in target state - treat as success
959
- auditLogger.log({
960
- operation: 'activate-workflow',
961
- operationType: 'UPDATE',
962
- componentType: 'workflow',
963
- componentName: currentWorkflow.name,
964
- componentId: workflowId,
965
- parameters: { previousState, result: 'already-activated' },
966
- success: true,
967
- });
968
- return {
969
- success: true,
970
- workflowId,
971
- workflowName: currentWorkflow.name,
972
- previousState,
973
- newState: 'Activated',
974
- };
975
- }
976
- // Re-throw unexpected errors
977
- throw error;
978
- }
128
+ async getFlowDefinition(flowId, summary) {
129
+ return this.flow.getFlowDefinition(flowId, summary);
979
130
  }
980
- /**
981
- * Document a workflow safely by deactivating, updating description, and reactivating
982
- * Atomic operation with rollback on failure
983
- * @param workflowId GUID of the workflow
984
- * @param type Type of automation ('flow' or 'workflow'), auto-detected if not provided
985
- * @returns Documentation result with state management summary
986
- */
987
- async documentWorkflowSafe(workflowId, type) {
988
- // Step 1: Get initial workflow state
989
- const currentWorkflow = await this.makeRequest(`api/data/v9.2/workflows(${workflowId})?$select=workflowid,name,statecode,statuscode`);
990
- const initialState = currentWorkflow.statecode === 0 ? 'Draft' :
991
- currentWorkflow.statecode === 1 ? 'Activated' : 'Suspended';
992
- const wasInitiallyActive = currentWorkflow.statecode === 1;
993
- auditLogger.log({
994
- operation: 'document-workflow-safe-start',
995
- operationType: 'UPDATE',
996
- componentType: 'workflow',
997
- componentName: currentWorkflow.name,
998
- componentId: workflowId,
999
- parameters: { initialState },
1000
- success: true,
1001
- });
1002
- let wasDeactivated = false;
1003
- let wasReactivated = false;
1004
- try {
1005
- // Step 2: Deactivate workflow if needed
1006
- if (wasInitiallyActive) {
1007
- const deactivateResult = await this.deactivateWorkflow(workflowId);
1008
- wasDeactivated = deactivateResult.success;
1009
- if (!wasDeactivated) {
1010
- throw new Error('Failed to deactivate workflow');
1011
- }
1012
- }
1013
- // Step 3: Document the automation
1014
- let documentResult;
1015
- try {
1016
- documentResult = await this.documentAutomation(workflowId, type);
1017
- }
1018
- catch (documentError) {
1019
- // Documentation failed - attempt to restore initial state
1020
- if (wasDeactivated && wasInitiallyActive) {
1021
- auditLogger.log({
1022
- operation: 'document-workflow-safe-rollback',
1023
- operationType: 'UPDATE',
1024
- componentType: 'workflow',
1025
- componentName: currentWorkflow.name,
1026
- componentId: workflowId,
1027
- parameters: { reason: 'documentation-failed' },
1028
- success: false,
1029
- error: documentError.message,
1030
- });
1031
- try {
1032
- await this.activateWorkflow(workflowId);
1033
- }
1034
- catch (rollbackError) {
1035
- // Log rollback failure but throw original error
1036
- auditLogger.log({
1037
- operation: 'document-workflow-safe-rollback-failed',
1038
- operationType: 'UPDATE',
1039
- componentType: 'workflow',
1040
- componentName: currentWorkflow.name,
1041
- componentId: workflowId,
1042
- success: false,
1043
- error: rollbackError.message,
1044
- });
1045
- }
1046
- }
1047
- throw new Error(`Documentation failed: ${documentError.message}`);
1048
- }
1049
- // Step 4: Reactivate workflow if it was initially active
1050
- if (wasInitiallyActive) {
1051
- try {
1052
- const activateResult = await this.activateWorkflow(workflowId);
1053
- wasReactivated = activateResult.success;
1054
- if (!wasReactivated) {
1055
- // Critical: Documentation succeeded but reactivation failed
1056
- auditLogger.log({
1057
- operation: 'document-workflow-safe-reactivation-failed',
1058
- operationType: 'UPDATE',
1059
- componentType: 'workflow',
1060
- componentName: currentWorkflow.name,
1061
- componentId: workflowId,
1062
- parameters: { warning: 'Workflow documented but remains deactivated - manual reactivation required' },
1063
- success: false,
1064
- });
1065
- return {
1066
- success: true, // Documentation succeeded even if reactivation failed
1067
- workflowId,
1068
- workflowName: currentWorkflow.name,
1069
- analysis: documentResult.analysis,
1070
- descriptionUpdated: documentResult.descriptionUpdated,
1071
- previousDescription: documentResult.previousDescription,
1072
- newDescription: documentResult.newDescription,
1073
- stateManagement: {
1074
- initialState,
1075
- wasDeactivated,
1076
- wasReactivated: false,
1077
- finalState: 'Draft (⚠️ Manual reactivation required)',
1078
- },
1079
- };
1080
- }
1081
- }
1082
- catch (reactivationError) {
1083
- // Log reactivation failure
1084
- auditLogger.log({
1085
- operation: 'document-workflow-safe-reactivation-error',
1086
- operationType: 'UPDATE',
1087
- componentType: 'workflow',
1088
- componentName: currentWorkflow.name,
1089
- componentId: workflowId,
1090
- success: false,
1091
- error: reactivationError.message,
1092
- });
1093
- return {
1094
- success: true, // Documentation succeeded even if reactivation failed
1095
- workflowId,
1096
- workflowName: currentWorkflow.name,
1097
- analysis: documentResult.analysis,
1098
- descriptionUpdated: documentResult.descriptionUpdated,
1099
- previousDescription: documentResult.previousDescription,
1100
- newDescription: documentResult.newDescription,
1101
- stateManagement: {
1102
- initialState,
1103
- wasDeactivated,
1104
- wasReactivated: false,
1105
- finalState: 'Draft (⚠️ Manual reactivation required)',
1106
- },
1107
- };
1108
- }
1109
- }
1110
- // Success - all steps completed
1111
- const finalState = wasInitiallyActive ? 'Activated' : 'Draft';
1112
- auditLogger.log({
1113
- operation: 'document-workflow-safe-complete',
1114
- operationType: 'UPDATE',
1115
- componentType: 'workflow',
1116
- componentName: currentWorkflow.name,
1117
- componentId: workflowId,
1118
- parameters: { initialState, finalState, wasDeactivated, wasReactivated },
1119
- success: true,
1120
- });
1121
- return {
1122
- success: true,
1123
- workflowId,
1124
- workflowName: currentWorkflow.name,
1125
- analysis: documentResult.analysis,
1126
- descriptionUpdated: documentResult.descriptionUpdated,
1127
- previousDescription: documentResult.previousDescription,
1128
- newDescription: documentResult.newDescription,
1129
- stateManagement: {
1130
- initialState,
1131
- wasDeactivated,
1132
- wasReactivated,
1133
- finalState,
1134
- },
1135
- };
1136
- }
1137
- catch (error) {
1138
- auditLogger.log({
1139
- operation: 'document-workflow-safe-error',
1140
- operationType: 'UPDATE',
1141
- componentType: 'workflow',
1142
- componentName: currentWorkflow.name,
1143
- componentId: workflowId,
1144
- success: false,
1145
- error: error.message,
1146
- });
1147
- throw error;
1148
- }
1149
- }
1150
- /**
1151
- * Generate YAML metadata block for automation documentation
1152
- */
1153
- generateAutomationYaml(analysis) {
1154
- const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
1155
- return `[AUTO-DOCS:v1]
1156
- tables_modified: ${analysis.tablesModified.join(', ') || 'none'}
1157
- trigger: ${analysis.trigger}
1158
- trigger_fields: ${analysis.triggerFields.join(', ') || 'none'}
1159
- custom_apis_called: ${analysis.customApisCalled && analysis.customApisCalled.length > 0 ? analysis.customApisCalled.join(', ') : 'none'}
1160
- action_count: ${analysis.actionCount}
1161
- analyzed: ${today}
1162
- ---`;
1163
- }
1164
- /**
1165
- * Merge YAML metadata with existing description, preserving manual notes
1166
- */
1167
- mergeDescriptionWithYaml(yamlBlock, existingDescription) {
1168
- // Case 1: Empty description
1169
- if (!existingDescription || existingDescription.trim() === '') {
1170
- return `${yamlBlock}\n[Manual notes below this line are preserved on re-analysis]`;
1171
- }
1172
- // Case 2: Has [AUTO-DOCS: tag - extract and preserve content after ---
1173
- const autoDocsMatch = existingDescription.match(/\[AUTO-DOCS:v\d+\]([\s\S]*?)^---$/m);
1174
- if (autoDocsMatch) {
1175
- const separatorIndex = existingDescription.indexOf('---', autoDocsMatch.index);
1176
- const manualNotes = existingDescription.substring(separatorIndex + 3).trim();
1177
- if (manualNotes) {
1178
- return `${yamlBlock}\n${manualNotes}`;
1179
- }
1180
- else {
1181
- return `${yamlBlock}\n[Manual notes below this line are preserved on re-analysis]`;
1182
- }
1183
- }
1184
- // Case 3: Has content but no [AUTO-DOCS: tag - treat entire content as manual notes
1185
- return `${yamlBlock}\n${existingDescription.trim()}`;
131
+ async getFlowRuns(flowId, maxRecords) {
132
+ return this.flow.getFlowRuns(flowId, maxRecords);
1186
133
  }
1187
- /**
1188
- * Analyze a flow or workflow and document it with YAML metadata
1189
- * @param automationId GUID of the flow or workflow
1190
- * @param type Type of automation ('flow' or 'workflow'), auto-detected if not provided
1191
- * @returns Analysis and description update result
1192
- */
1193
- async documentAutomation(automationId, type) {
1194
- // Fetch automation (flow or workflow)
1195
- const accessToken = await this.getAccessToken();
1196
- const response = await fetch(`${this.config.organizationUrl}/api/data/v9.2/workflows(${automationId})?$select=category,clientdata,description`, {
1197
- headers: {
1198
- 'Authorization': `Bearer ${accessToken}`,
1199
- 'Accept': 'application/json',
1200
- 'OData-MaxVersion': '4.0',
1201
- 'OData-Version': '4.0'
1202
- }
1203
- });
1204
- if (!response.ok) {
1205
- throw new Error(`Failed to fetch automation: ${response.status} ${response.statusText}`);
1206
- }
1207
- const automation = await response.json();
1208
- const previousDescription = automation.description || '';
1209
- // Detect type if not provided (category 5 = flow, 0 = workflow)
1210
- const detectedType = type || (automation.category === 5 ? 'flow' : 'workflow');
1211
- let analysis;
1212
- if (detectedType === 'flow') {
1213
- // Parse flow definition from clientdata
1214
- if (!automation.clientdata) {
1215
- throw new Error('Flow has no clientdata (definition)');
1216
- }
1217
- const flowDefinition = JSON.parse(automation.clientdata);
1218
- // Use enhanced parser from base package
1219
- const readOnlyService = new PowerPlatformReadOnlyService({
1220
- organizationUrl: this.config.organizationUrl,
1221
- clientId: this.config.clientId,
1222
- clientSecret: this.config.clientSecret,
1223
- tenantId: this.config.tenantId
1224
- });
1225
- // Call public method from base package
1226
- const summary = readOnlyService.parseFlowSummary(flowDefinition);
1227
- // Convert action objects to action names for display
1228
- const actionNames = Array.isArray(summary.actions)
1229
- ? summary.actions.map((a) => a.name || a.type || 'unknown').slice(0, 10)
1230
- : [];
1231
- analysis = {
1232
- tablesModified: summary.tablesModified,
1233
- trigger: summary.triggerInfo,
1234
- triggerFields: summary.triggerFields,
1235
- actions: actionNames,
1236
- actionCount: Array.isArray(summary.actions) ? summary.actions.length : 0,
1237
- customApisCalled: summary.customApisCalled || []
1238
- };
1239
- }
1240
- else {
1241
- // Fetch workflow XAML
1242
- const xamlResponse = await fetch(`${this.config.organizationUrl}/api/data/v9.2/workflows(${automationId})?$select=xaml`, {
1243
- headers: {
1244
- 'Authorization': `Bearer ${accessToken}`,
1245
- 'Accept': 'application/json',
1246
- 'OData-MaxVersion': '4.0',
1247
- 'OData-Version': '4.0'
1248
- }
1249
- });
1250
- if (!xamlResponse.ok) {
1251
- throw new Error(`Failed to fetch workflow XAML: ${xamlResponse.status}`);
1252
- }
1253
- const xamlData = await xamlResponse.json();
1254
- if (!xamlData.xaml) {
1255
- throw new Error('Workflow has no XAML definition');
1256
- }
1257
- // Use enhanced parser from base package
1258
- const readOnlyService = new PowerPlatformReadOnlyService({
1259
- organizationUrl: this.config.organizationUrl,
1260
- clientId: this.config.clientId,
1261
- clientSecret: this.config.clientSecret,
1262
- tenantId: this.config.tenantId
1263
- });
1264
- const summary = readOnlyService.parseWorkflowXamlSummary(xamlData.xaml);
1265
- // Get action types from XAML
1266
- const actions = [];
1267
- if (summary.createEntityCount > 0)
1268
- actions.push('create_record');
1269
- if (summary.updateEntityCount > 0)
1270
- actions.push('update_record');
1271
- if (summary.assignEntityCount > 0)
1272
- actions.push('assign_record');
1273
- if (summary.setStateCount > 0)
1274
- actions.push('set_state');
1275
- analysis = {
1276
- tablesModified: summary.tablesModified,
1277
- trigger: summary.triggerInfo,
1278
- triggerFields: summary.triggerFields,
1279
- actions,
1280
- actionCount: actions.length,
1281
- customApisCalled: [] // Classic workflows don't support Custom APIs directly
1282
- };
1283
- }
1284
- // Generate YAML and merge with existing description
1285
- const yamlBlock = this.generateAutomationYaml(analysis);
1286
- const newDescription = this.mergeDescriptionWithYaml(yamlBlock, previousDescription);
1287
- // Update description
1288
- const updateResult = await this.updateWorkflowDescription(automationId, newDescription);
1289
- return {
1290
- analysis,
1291
- descriptionUpdated: updateResult.success,
1292
- previousDescription: updateResult.previousDescription,
1293
- newDescription: updateResult.newDescription
1294
- };
134
+ // =====================================================
135
+ // WORKFLOW METHODS (Read-only)
136
+ // =====================================================
137
+ async getWorkflows(activeOnly, maxRecords) {
138
+ return this.workflow.getWorkflows(activeOnly, maxRecords);
1295
139
  }
1296
- /**
1297
- * Update a Power Automate flow's description field
1298
- * @param flowId GUID of the flow (workflowid in workflows entity)
1299
- * @param description New description content
1300
- * @returns Previous and new description
1301
- */
1302
- async updateFlowDescription(flowId, description) {
1303
- // Flows and workflows share the same entity and update mechanism
1304
- // The only difference is category (5 = flow, 0 = workflow)
1305
- return this.updateWorkflowDescription(flowId, description);
140
+ async getWorkflowDefinition(workflowId, summary) {
141
+ return this.workflow.getWorkflowDefinition(workflowId, summary);
1306
142
  }
1307
- /**
1308
- * Get all business rules in the environment
1309
- * @param activeOnly Only return activated business rules (default: false)
1310
- * @param maxRecords Maximum number of business rules to return (default: 100)
1311
- * @returns List of business rules with basic information
1312
- */
1313
- async getBusinessRules(activeOnly = false, maxRecords = 100) {
1314
- // Category 2 = Business Rule
1315
- // StateCode: 0=Draft, 1=Activated, 2=Suspended
1316
- // Type: 1=Definition
1317
- const stateFilter = activeOnly ? ' and statecode eq 1' : '';
1318
- 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}`);
1319
- // Format the results for better readability
1320
- const formattedBusinessRules = businessRules.value.map((rule) => ({
1321
- workflowid: rule.workflowid,
1322
- name: rule.name,
1323
- description: rule.description,
1324
- state: rule.statecode === 0 ? 'Draft' : rule.statecode === 1 ? 'Activated' : 'Suspended',
1325
- statecode: rule.statecode,
1326
- statuscode: rule.statuscode,
1327
- type: rule.type === 1 ? 'Definition' : rule.type === 2 ? 'Activation' : 'Template',
1328
- primaryEntity: rule.primaryentity,
1329
- isManaged: rule.ismanaged,
1330
- owner: rule.ownerid?.fullname,
1331
- modifiedOn: rule.modifiedon,
1332
- modifiedBy: rule.modifiedby?.fullname,
1333
- createdOn: rule.createdon
1334
- }));
1335
- return {
1336
- totalCount: formattedBusinessRules.length,
1337
- businessRules: formattedBusinessRules
1338
- };
143
+ // =====================================================
144
+ // BUSINESS RULE METHODS (Read-only)
145
+ // =====================================================
146
+ async getBusinessRules(activeOnly, maxRecords) {
147
+ return this.businessRule.getBusinessRules(activeOnly, maxRecords);
1339
148
  }
1340
- /**
1341
- * Get a specific business rule with its complete XAML definition
1342
- * @param workflowId The GUID of the business rule (workflowid)
1343
- * @returns Complete business rule information including the XAML definition
1344
- */
1345
149
  async getBusinessRule(workflowId) {
1346
- 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)`);
1347
- // Verify it's actually a business rule
1348
- if (businessRule.category !== 2) {
1349
- throw new Error(`Workflow ${workflowId} is not a business rule (category: ${businessRule.category})`);
1350
- }
1351
- return {
1352
- workflowid: businessRule.workflowid,
1353
- name: businessRule.name,
1354
- description: businessRule.description,
1355
- state: businessRule.statecode === 0 ? 'Draft' : businessRule.statecode === 1 ? 'Activated' : 'Suspended',
1356
- statecode: businessRule.statecode,
1357
- statuscode: businessRule.statuscode,
1358
- type: businessRule.type === 1 ? 'Definition' : businessRule.type === 2 ? 'Activation' : 'Template',
1359
- category: businessRule.category,
1360
- primaryEntity: businessRule.primaryentity,
1361
- isManaged: businessRule.ismanaged,
1362
- owner: businessRule.ownerid?.fullname,
1363
- createdOn: businessRule.createdon,
1364
- createdBy: businessRule.createdby?.fullname,
1365
- modifiedOn: businessRule.modifiedon,
1366
- modifiedBy: businessRule.modifiedby?.fullname,
1367
- xaml: businessRule.xaml
1368
- };
150
+ return this.businessRule.getBusinessRule(workflowId);
1369
151
  }
1370
- // ==================== MODEL-DRIVEN APP OPERATIONS ====================
1371
- /**
1372
- * Get all model-driven apps in the environment
1373
- * @param activeOnly Only return active apps (default: false)
1374
- * @param maxRecords Maximum number of apps to return (default: 100)
1375
- * @returns List of model-driven apps with basic information
1376
- */
1377
- async getApps(activeOnly = false, maxRecords = 100, includeUnpublished = true, solutionUniqueName) {
1378
- // Build filter conditions
1379
- const filters = [];
1380
- // StateCode: 0=Active, 1=Inactive
1381
- if (activeOnly) {
1382
- filters.push('statecode eq 0');
1383
- }
1384
- // Published status: publishedon null = unpublished
1385
- if (!includeUnpublished) {
1386
- filters.push('publishedon ne null');
1387
- }
1388
- const filterString = filters.length > 0 ? `&$filter=${filters.join(' and ')}` : '';
1389
- 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}`);
1390
- // If solution filter specified, filter results by solution
1391
- let filteredApps = apps.value;
1392
- if (solutionUniqueName) {
1393
- // Query solution components to find apps in the specified solution
1394
- const solution = await this.makeRequest(`api/data/v9.2/solutions?$filter=uniquename eq '${solutionUniqueName}'&$select=solutionid`);
1395
- if (solution.value.length > 0) {
1396
- const solutionId = solution.value[0].solutionid;
1397
- // Query solution components for app modules
1398
- const solutionComponents = await this.makeRequest(`api/data/v9.2/solutioncomponents?$filter=_solutionid_value eq ${solutionId} and componenttype eq 80&$select=objectid`);
1399
- const appIdsInSolution = new Set(solutionComponents.value.map((c) => c.objectid.toLowerCase()));
1400
- filteredApps = apps.value.filter((app) => appIdsInSolution.has(app.appmoduleid.toLowerCase()));
1401
- }
1402
- }
1403
- // Format the results for better readability
1404
- const formattedApps = filteredApps.map((app) => ({
1405
- appmoduleid: app.appmoduleid,
1406
- name: app.name,
1407
- uniquename: app.uniquename,
1408
- description: app.description,
1409
- webresourceid: app.webresourceid,
1410
- clienttype: app.clienttype,
1411
- formfactor: app.formfactor,
1412
- navigationtype: app.navigationtype,
1413
- url: app.url,
1414
- isfeatured: app.isfeatured,
1415
- isdefault: app.isdefault,
1416
- state: app.statecode === 0 ? 'Active' : 'Inactive',
1417
- statecode: app.statecode,
1418
- statuscode: app.statuscode,
1419
- publishedon: app.publishedon,
1420
- published: app.publishedon ? true : false,
1421
- publisherid: app._publisherid_value || null,
1422
- createdon: app.createdon,
1423
- modifiedon: app.modifiedon
1424
- }));
1425
- return {
1426
- totalCount: formattedApps.length,
1427
- apps: formattedApps,
1428
- filters: {
1429
- activeOnly,
1430
- includeUnpublished,
1431
- solutionUniqueName: solutionUniqueName || 'all'
1432
- }
1433
- };
152
+ // =====================================================
153
+ // APP METHODS (Read-only)
154
+ // =====================================================
155
+ async getApps(activeOnly, maxRecords, includeUnpublished, solutionUniqueName) {
156
+ return this.app.getApps(activeOnly, maxRecords, includeUnpublished, solutionUniqueName);
1434
157
  }
1435
- /**
1436
- * Get a specific model-driven app by ID
1437
- * @param appId The GUID of the app (appmoduleid)
1438
- * @returns Complete app information including publisher details
1439
- */
1440
158
  async getApp(appId) {
1441
- 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`);
1442
- return {
1443
- appmoduleid: app.appmoduleid,
1444
- name: app.name,
1445
- uniquename: app.uniquename,
1446
- description: app.description,
1447
- webresourceid: app.webresourceid,
1448
- clienttype: app.clienttype,
1449
- formfactor: app.formfactor,
1450
- navigationtype: app.navigationtype === 0 ? 'Single Session' : 'Multi Session',
1451
- url: app.url,
1452
- isfeatured: app.isfeatured,
1453
- isdefault: app.isdefault,
1454
- state: app.statecode === 0 ? 'Active' : 'Inactive',
1455
- statecode: app.statecode,
1456
- statuscode: app.statuscode,
1457
- publishedon: app.publishedon,
1458
- createdon: app.createdon,
1459
- modifiedon: app.modifiedon,
1460
- createdBy: app._createdby_value || null,
1461
- modifiedBy: app._modifiedby_value || null,
1462
- publisherid: app._publisherid_value || null
1463
- };
159
+ return this.app.getApp(appId);
1464
160
  }
1465
- /**
1466
- * Get all components (entities, forms, views, sitemaps) associated with an app
1467
- * @param appId The GUID of the app (appmoduleid)
1468
- * @returns List of app components with type information
1469
- */
1470
161
  async getAppComponents(appId) {
1471
- 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`);
1472
- // Map component type numbers to friendly names
1473
- const componentTypeMap = {
1474
- 1: 'Entity',
1475
- 24: 'Form',
1476
- 26: 'View',
1477
- 29: 'Business Process Flow',
1478
- 48: 'Ribbon Command',
1479
- 59: 'Chart/Dashboard',
1480
- 60: 'System Form',
1481
- 62: 'SiteMap'
1482
- };
1483
- const formattedComponents = components.value.map((component) => ({
1484
- appmodulecomponentid: component.appmodulecomponentid,
1485
- objectid: component.objectid,
1486
- componenttype: component.componenttype,
1487
- componenttypeName: componentTypeMap[component.componenttype] || `Unknown (${component.componenttype})`,
1488
- rootappmodulecomponentid: component.rootappmodulecomponentid,
1489
- createdon: component.createdon,
1490
- modifiedon: component.modifiedon
1491
- }));
1492
- // Group by component type for easier reading
1493
- const groupedByType = {};
1494
- formattedComponents.forEach((comp) => {
1495
- const typeName = comp.componenttypeName;
1496
- if (!groupedByType[typeName]) {
1497
- groupedByType[typeName] = [];
1498
- }
1499
- groupedByType[typeName].push(comp);
1500
- });
1501
- return {
1502
- totalCount: formattedComponents.length,
1503
- components: formattedComponents,
1504
- groupedByType
1505
- };
162
+ return this.app.getAppComponents(appId);
1506
163
  }
1507
- /**
1508
- * Get the sitemap for a specific app
1509
- * @param appId The GUID of the app (appmoduleid)
1510
- * @returns Sitemap information including XML
1511
- */
1512
164
  async getAppSitemap(appId) {
1513
- // First get the app components to find the sitemap
1514
- const components = await this.makeRequest(`api/data/v9.2/appmodulecomponents?$filter=_appmoduleidunique_value eq ${appId} and componenttype eq 62&$select=objectid`);
1515
- if (components.value.length === 0) {
1516
- return {
1517
- hasSitemap: false,
1518
- message: 'No sitemap found for this app'
1519
- };
1520
- }
1521
- // Get the sitemap details
1522
- const sitemapId = components.value[0].objectid;
1523
- const sitemap = await this.makeRequest(`api/data/v9.2/sitemaps(${sitemapId})?$select=sitemapid,sitemapname,sitemapnameunique,sitemapxml,isappaware,enablecollapsiblegroups,showhome,showpinned,showrecents,ismanaged,createdon,modifiedon`);
1524
- return {
1525
- hasSitemap: true,
1526
- sitemapid: sitemap.sitemapid,
1527
- sitemapname: sitemap.sitemapname,
1528
- sitemapnameunique: sitemap.sitemapnameunique,
1529
- sitemapxml: sitemap.sitemapxml,
1530
- isappaware: sitemap.isappaware,
1531
- enablecollapsiblegroups: sitemap.enablecollapsiblegroups,
1532
- showhome: sitemap.showhome,
1533
- showpinned: sitemap.showpinned,
1534
- showrecents: sitemap.showrecents,
1535
- ismanaged: sitemap.ismanaged,
1536
- createdon: sitemap.createdon,
1537
- modifiedon: sitemap.modifiedon
1538
- };
1539
- }
1540
- /**
1541
- * Create a new model-driven app
1542
- * @param appDefinition The app definition object
1543
- * @param solutionUniqueName Optional solution to add the app to
1544
- * @returns The created app information including ID
1545
- */
1546
- async createApp(appDefinition, solutionUniqueName) {
1547
- const startTime = Date.now();
1548
- try {
1549
- // Validate uniquename format (English chars/numbers only, no spaces)
1550
- const uniquename = appDefinition.uniquename;
1551
- if (!/^[a-zA-Z0-9_]+$/.test(uniquename)) {
1552
- throw new Error('App uniquename must contain only English letters, numbers, and underscores (no spaces)');
1553
- }
1554
- // Set defaults
1555
- const appRequest = {
1556
- name: appDefinition.name,
1557
- uniquename: appDefinition.uniquename,
1558
- description: appDefinition.description || '',
1559
- webresourceid: appDefinition.webresourceid || '953b9fac-1e5e-e611-80d6-00155ded156f', // Default icon
1560
- welcomepageid: '00000000-0000-0000-0000-000000000000', // Required: empty GUID for no welcome page
1561
- clienttype: appDefinition.clienttype || 4, // UCI
1562
- formfactor: appDefinition.formfactor || 1, // Unknown/All
1563
- navigationtype: appDefinition.navigationtype !== undefined ? appDefinition.navigationtype : 0, // Single session
1564
- isfeatured: appDefinition.isfeatured || false,
1565
- isdefault: appDefinition.isdefault || false,
1566
- url: appDefinition.url || ''
1567
- };
1568
- // Headers with solution context and return representation
1569
- const headers = {
1570
- 'Prefer': 'return=representation'
1571
- };
1572
- if (solutionUniqueName) {
1573
- headers['MSCRM.SolutionUniqueName'] = solutionUniqueName;
1574
- }
1575
- // Execute with rate limiting
1576
- const response = await rateLimiter.execute(async () => {
1577
- return await this.makeRequest('api/data/v9.2/appmodules', 'POST', appRequest, headers);
1578
- });
1579
- // Extract app ID from response (now returned due to Prefer header)
1580
- const appId = response.appmoduleid;
1581
- if (!appId) {
1582
- throw new Error('App creation response missing appmoduleid. Full response: ' + JSON.stringify(response));
1583
- }
1584
- // Verify the app is queryable (retry with delay if needed)
1585
- let appVerified = false;
1586
- let retryCount = 0;
1587
- const maxRetries = 3;
1588
- const retryDelayMs = 2000;
1589
- while (!appVerified && retryCount < maxRetries) {
1590
- try {
1591
- await this.makeRequest(`api/data/v9.2/appmodules(${appId})?$select=appmoduleid,name,uniquename`);
1592
- appVerified = true;
1593
- }
1594
- catch (error) {
1595
- retryCount++;
1596
- if (retryCount < maxRetries) {
1597
- await new Promise(resolve => setTimeout(resolve, retryDelayMs));
1598
- }
1599
- }
1600
- }
1601
- // Audit log success
1602
- auditLogger.log({
1603
- operation: 'createApp',
1604
- operationType: 'CREATE',
1605
- componentType: 'AppModule',
1606
- componentName: appDefinition.name,
1607
- componentId: appId,
1608
- success: true,
1609
- executionTimeMs: Date.now() - startTime
1610
- });
1611
- return {
1612
- appId,
1613
- name: appDefinition.name,
1614
- uniquename: appDefinition.uniquename,
1615
- verified: appVerified,
1616
- message: appVerified
1617
- ? 'App created successfully and verified. Remember to add entities, validate, and publish.'
1618
- : `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.`
1619
- };
1620
- }
1621
- catch (error) {
1622
- // Audit log failure
1623
- auditLogger.log({
1624
- operation: 'createApp',
1625
- operationType: 'CREATE',
1626
- componentType: 'AppModule',
1627
- componentName: appDefinition.name,
1628
- success: false,
1629
- error: error.message,
1630
- executionTimeMs: Date.now() - startTime
1631
- });
1632
- throw new Error(`Failed to create app: ${error.message}`);
1633
- }
165
+ return this.app.getAppSitemap(appId);
1634
166
  }
1635
- /**
1636
- * Create a sitemap from simplified configuration (no XML knowledge required)
1637
- * @param config Simplified sitemap configuration
1638
- * @param solutionUniqueName Optional solution to add the sitemap to
1639
- * @returns The created sitemap information including ID and XML
1640
- */
1641
- async createSimpleSitemap(config, solutionUniqueName) {
1642
- const startTime = Date.now();
1643
- try {
1644
- // Generate sitemap XML from simplified configuration
1645
- let xml = '<SiteMap>';
1646
- config.areas.forEach((area) => {
1647
- xml += `<Area Id="${area.id}"`;
1648
- if (area.icon) {
1649
- xml += ` Icon="${area.icon}"`;
1650
- }
1651
- if (area.showGroups !== undefined) {
1652
- xml += ` ShowGroups="${area.showGroups}"`;
1653
- }
1654
- xml += '>';
1655
- xml += `<Titles><Title LCID="1033" Title="${this.escapeXml(area.title)}" /></Titles>`;
1656
- if (area.description) {
1657
- xml += `<Descriptions><Description LCID="1033" Description="${this.escapeXml(area.description)}" /></Descriptions>`;
1658
- }
1659
- area.groups.forEach((group) => {
1660
- xml += `<Group Id="${group.id}"`;
1661
- if (group.isProfile !== undefined) {
1662
- xml += ` IsProfile="${group.isProfile}"`;
1663
- }
1664
- xml += '>';
1665
- xml += `<Titles><Title LCID="1033" Title="${this.escapeXml(group.title)}" /></Titles>`;
1666
- if (group.description) {
1667
- xml += `<Descriptions><Description LCID="1033" Description="${this.escapeXml(group.description)}" /></Descriptions>`;
1668
- }
1669
- group.subareas.forEach((subarea) => {
1670
- xml += `<SubArea Id="${subarea.id}"`;
1671
- if (subarea.entity) {
1672
- xml += ` Entity="${subarea.entity}"`;
1673
- }
1674
- if (subarea.url) {
1675
- xml += ` Url="${subarea.url}"`;
1676
- }
1677
- if (subarea.icon) {
1678
- xml += ` Icon="${subarea.icon}"`;
1679
- }
1680
- if (subarea.availableOffline !== undefined) {
1681
- xml += ` AvailableOffline="${subarea.availableOffline}"`;
1682
- }
1683
- if (subarea.passParams !== undefined) {
1684
- xml += ` PassParams="${subarea.passParams}"`;
1685
- }
1686
- xml += '>';
1687
- xml += `<Titles><Title LCID="1033" Title="${this.escapeXml(subarea.title)}" /></Titles>`;
1688
- if (subarea.description) {
1689
- xml += `<Descriptions><Description LCID="1033" Description="${this.escapeXml(subarea.description)}" /></Descriptions>`;
1690
- }
1691
- xml += '</SubArea>';
1692
- });
1693
- xml += '</Group>';
1694
- });
1695
- xml += '</Area>';
1696
- });
1697
- xml += '</SiteMap>';
1698
- // Create sitemap entity
1699
- const sitemapRequest = {
1700
- sitemapname: config.name,
1701
- sitemapxml: xml,
1702
- isappaware: true,
1703
- enablecollapsiblegroups: config.enableCollapsibleGroups !== undefined ? config.enableCollapsibleGroups : false,
1704
- showhome: config.showHome !== undefined ? config.showHome : true,
1705
- showpinned: config.showPinned !== undefined ? config.showPinned : true,
1706
- showrecents: config.showRecents !== undefined ? config.showRecents : true
1707
- };
1708
- // Headers with solution context
1709
- const headers = {};
1710
- if (solutionUniqueName) {
1711
- headers['MSCRM.SolutionUniqueName'] = solutionUniqueName;
1712
- }
1713
- // Execute with rate limiting
1714
- const response = await rateLimiter.execute(async () => {
1715
- return await this.makeRequest('api/data/v9.2/sitemaps', 'POST', sitemapRequest, headers);
1716
- });
1717
- // Extract sitemap ID from response
1718
- const sitemapId = response.sitemapid;
1719
- // Audit log success
1720
- auditLogger.log({
1721
- operation: 'createSimpleSitemap',
1722
- operationType: 'CREATE',
1723
- componentType: 'SiteMap',
1724
- componentName: config.name,
1725
- componentId: sitemapId,
1726
- success: true,
1727
- executionTimeMs: Date.now() - startTime
1728
- });
1729
- return {
1730
- sitemapId,
1731
- sitemapName: config.name,
1732
- sitemapXml: xml,
1733
- message: 'Sitemap created successfully. Add it to your app using add-entities-to-app or add specific components.'
1734
- };
1735
- }
1736
- catch (error) {
1737
- // Audit log failure
1738
- auditLogger.log({
1739
- operation: 'createSimpleSitemap',
1740
- operationType: 'CREATE',
1741
- componentType: 'SiteMap',
1742
- componentName: config.name,
1743
- success: false,
1744
- error: error.message,
1745
- executionTimeMs: Date.now() - startTime
1746
- });
1747
- throw new Error(`Failed to create sitemap: ${error.message}`);
1748
- }
1749
- }
1750
- /**
1751
- * Add entities to an app by modifying the sitemap XML
1752
- * @param appId The GUID of the app
1753
- * @param entityNames Array of entity logical names to add
1754
- * @returns Result of the operation
1755
- */
1756
- async addEntitiesToApp(appId, entityNames) {
1757
- const startTime = Date.now();
1758
- try {
1759
- // Get app details
1760
- const app = await this.makeRequest(`api/data/v9.2/appmodules(${appId})?$select=appmoduleid,name,uniquename`);
1761
- // Validate entities exist and get their display names
1762
- const entityPromises = entityNames.map(name => this.makeRequest(`api/data/v9.2/EntityDefinitions(LogicalName='${name}')?$select=LogicalName,DisplayName,MetadataId`));
1763
- const entities = await Promise.all(entityPromises);
1764
- // Try to get the app's sitemap via components first
1765
- let sitemapInfo = await this.getAppSitemap(appId);
1766
- // If not found via components, try to find by matching name
1767
- if (!sitemapInfo.hasSitemap) {
1768
- const sitemapQuery = await this.makeRequest(`api/data/v9.2/sitemaps?$filter=sitemapnameunique eq '${app.uniquename}'&$select=sitemapid,sitemapname,sitemapnameunique,sitemapxml`);
1769
- if (sitemapQuery.value.length > 0) {
1770
- const sitemap = sitemapQuery.value[0];
1771
- sitemapInfo = {
1772
- hasSitemap: true,
1773
- sitemapid: sitemap.sitemapid,
1774
- sitemapname: sitemap.sitemapname,
1775
- sitemapnameunique: sitemap.sitemapnameunique,
1776
- sitemapxml: sitemap.sitemapxml
1777
- };
1778
- }
1779
- else {
1780
- throw new Error(`App '${app.name}' does not have a sitemap. Cannot add entities without a sitemap.`);
1781
- }
1782
- }
1783
- // Parse sitemap XML
1784
- let sitemapXml = sitemapInfo.sitemapxml;
1785
- // Find or create a "Tables" area and group
1786
- // Check if <Area> with Id="Area_Tables" exists
1787
- const areaRegex = /<Area[^>]+Id="Area_Tables"[^>]*>/;
1788
- const hasTablesArea = areaRegex.test(sitemapXml);
1789
- if (!hasTablesArea) {
1790
- // Add a new Area for tables before the closing </SiteMap>
1791
- const newArea = `
1792
- <Area Id="Area_Tables" Title="Tables" ShowGroups="true">
1793
- <Group Id="Group_Tables" Title="Custom Tables">
1794
- </Group>
1795
- </Area>`;
1796
- sitemapXml = sitemapXml.replace('</SiteMap>', newArea + '\n</SiteMap>');
1797
- }
1798
- // Add SubArea elements for each entity within Group_Tables
1799
- for (const entity of entities) {
1800
- const displayName = entity.DisplayName?.UserLocalizedLabel?.Label || entity.LogicalName;
1801
- const subAreaId = `SubArea_${entity.LogicalName}`;
1802
- // Check if SubArea already exists
1803
- const subAreaRegex = new RegExp(`<SubArea[^>]+Id="${subAreaId}"[^>]*>`);
1804
- if (subAreaRegex.test(sitemapXml)) {
1805
- continue; // Skip if already exists
1806
- }
1807
- // Add SubArea within Group_Tables
1808
- const newSubArea = `
1809
- <SubArea Id="${subAreaId}" Entity="${entity.LogicalName}" Title="${displayName}" />`;
1810
- // Find the Group_Tables closing tag and add before it
1811
- sitemapXml = sitemapXml.replace(/<\/Group>/, newSubArea + '\n </Group>');
1812
- }
1813
- // Update the sitemap
1814
- await rateLimiter.execute(async () => {
1815
- return await this.makeRequest(`api/data/v9.2/sitemaps(${sitemapInfo.sitemapid})`, 'PATCH', {
1816
- sitemapxml: sitemapXml
1817
- });
1818
- });
1819
- // CRITICAL: Also add entity components to app for Advanced Find/Search
1820
- // Use deep insert via appmodule_appmodulecomponent collection navigation property
1821
- for (const entity of entities) {
1822
- try {
1823
- await rateLimiter.execute(async () => {
1824
- return await this.makeRequest(`api/data/v9.2/appmodules(${appId})/appmodule_appmodulecomponent`, 'POST', {
1825
- componenttype: 1, // Entity
1826
- objectid: entity.MetadataId
1827
- });
1828
- });
1829
- }
1830
- catch (componentError) {
1831
- // If deep insert fails, try to continue with other entities
1832
- auditLogger.log({
1833
- operation: 'addEntitiesToApp',
1834
- operationType: 'CREATE',
1835
- componentType: 'AppModuleComponent',
1836
- componentName: entity.LogicalName,
1837
- success: false,
1838
- error: `Failed to add ${entity.LogicalName} as app component: ${componentError.message}`,
1839
- executionTimeMs: Date.now() - startTime
1840
- });
1841
- }
1842
- }
1843
- // Audit log success
1844
- auditLogger.log({
1845
- operation: 'addEntitiesToApp',
1846
- operationType: 'UPDATE',
1847
- componentType: 'AppModule',
1848
- componentId: appId,
1849
- success: true,
1850
- executionTimeMs: Date.now() - startTime
1851
- });
1852
- return {
1853
- appId,
1854
- sitemapId: sitemapInfo.sitemapid,
1855
- entitiesAdded: entityNames,
1856
- message: `Successfully added ${entityNames.length} entities to app sitemap. Remember to publish the app.`
1857
- };
1858
- }
1859
- catch (error) {
1860
- // Audit log failure
1861
- auditLogger.log({
1862
- operation: 'addEntitiesToApp',
1863
- operationType: 'UPDATE',
1864
- componentType: 'AppModule',
1865
- componentId: appId,
1866
- success: false,
1867
- error: error.message,
1868
- executionTimeMs: Date.now() - startTime
1869
- });
1870
- throw new Error(`Failed to add entities to app: ${error.message}`);
1871
- }
1872
- }
1873
- /**
1874
- * Validate an app before publishing
1875
- * @param appId The GUID of the app
1876
- * @returns Validation result with any issues found
1877
- */
1878
- async validateApp(appId) {
1879
- try {
1880
- const response = await this.makeRequest(`api/data/v9.2/ValidateApp(AppModuleId=${appId})`);
1881
- const validationResponse = response.AppValidationResponse;
1882
- const isValid = validationResponse.ValidationSuccess;
1883
- const issues = validationResponse.ValidationIssueList || [];
1884
- return {
1885
- appId,
1886
- isValid,
1887
- issueCount: issues.length,
1888
- issues: issues.map((issue) => ({
1889
- errorType: issue.ErrorType,
1890
- message: issue.Message,
1891
- componentId: issue.ComponentId,
1892
- componentType: issue.ComponentType
1893
- })),
1894
- message: isValid
1895
- ? 'App validation passed. Ready to publish.'
1896
- : `App validation found ${issues.length} issue(s). Fix them before publishing.`
1897
- };
1898
- }
1899
- catch (error) {
1900
- throw new Error(`Failed to validate app: ${error.message}`);
1901
- }
1902
- }
1903
- /**
1904
- * Publish an app to make it available to users
1905
- * @param appId The GUID of the app
1906
- * @returns Result of the publish operation
1907
- */
1908
- async publishApp(appId) {
1909
- const startTime = Date.now();
1910
- try {
1911
- // First validate the app
1912
- const validation = await this.validateApp(appId);
1913
- if (!validation.isValid) {
1914
- throw new Error(`Cannot publish app with validation errors: ${JSON.stringify(validation.issues)}`);
1915
- }
1916
- // Publish using PublishXml with app parameter
1917
- const parameterXml = `<importexportxml><appmodules><appmodule>${appId}</appmodule></appmodules></importexportxml>`;
1918
- await rateLimiter.execute(async () => {
1919
- return await this.publishXml(parameterXml);
1920
- });
1921
- // Audit log success
1922
- auditLogger.log({
1923
- operation: 'publishApp',
1924
- operationType: 'PUBLISH',
1925
- componentType: 'AppModule',
1926
- componentId: appId,
1927
- success: true,
1928
- executionTimeMs: Date.now() - startTime
1929
- });
1930
- return {
1931
- appId,
1932
- message: 'App published successfully. It is now available to users with appropriate security roles.'
1933
- };
1934
- }
1935
- catch (error) {
1936
- // Audit log failure
1937
- auditLogger.log({
1938
- operation: 'publishApp',
1939
- operationType: 'PUBLISH',
1940
- componentType: 'AppModule',
1941
- componentId: appId,
1942
- success: false,
1943
- error: error.message,
1944
- executionTimeMs: Date.now() - startTime
1945
- });
1946
- throw new Error(`Failed to publish app: ${error.message}`);
1947
- }
1948
- }
1949
- /**
1950
- * Helper to escape XML special characters
1951
- */
1952
- escapeXml(unsafe) {
1953
- return unsafe.replace(/[<>&'"]/g, (c) => {
1954
- switch (c) {
1955
- case '<': return '&lt;';
1956
- case '>': return '&gt;';
1957
- case '&': return '&amp;';
1958
- case '\'': return '&apos;';
1959
- case '"': return '&quot;';
1960
- default: return c;
1961
- }
1962
- });
1963
- }
1964
- // ==================== CUSTOMIZATION WRITE OPERATIONS ====================
1965
- /**
1966
- * Create a new custom entity (table)
1967
- * @param entityDefinition The entity definition object
1968
- * @param solutionUniqueName Optional solution to add the entity to
1969
- * @returns The created entity metadata
1970
- */
167
+ // =====================================================
168
+ // ENTITY CUSTOMIZATION METHODS
169
+ // =====================================================
1971
170
  async createEntity(entityDefinition, solutionUniqueName) {
1972
- const startTime = Date.now();
1973
- try {
1974
- // Validate entity name against best practices
1975
- const schemaName = entityDefinition.SchemaName || entityDefinition.LogicalName;
1976
- const isRefData = schemaName?.toLowerCase().includes('ref_') || false;
1977
- const nameValidation = bestPracticesValidator.validateEntityName(schemaName, isRefData);
1978
- if (!nameValidation.isValid) {
1979
- const error = `Entity name validation failed: ${nameValidation.issues.join(', ')}`;
1980
- auditLogger.log({
1981
- operation: 'createEntity',
1982
- operationType: 'CREATE',
1983
- componentType: 'Entity',
1984
- componentName: schemaName,
1985
- success: false,
1986
- error,
1987
- executionTimeMs: Date.now() - startTime
1988
- });
1989
- throw new Error(error);
1990
- }
1991
- // Log warnings if any
1992
- if (nameValidation.warnings.length > 0) {
1993
- console.error(`[WARNING] Entity name warnings: ${nameValidation.warnings.join(', ')}`);
1994
- }
1995
- // Validate ownership type
1996
- const ownershipType = entityDefinition.OwnershipType;
1997
- if (ownershipType) {
1998
- const ownershipValidation = bestPracticesValidator.validateOwnershipType(ownershipType);
1999
- if (!ownershipValidation.isValid) {
2000
- console.error(`[WARNING] ${ownershipValidation.issues.join(', ')}`);
2001
- }
2002
- }
2003
- // Check for required columns
2004
- const requiredColumnsValidation = bestPracticesValidator.validateRequiredColumns([], isRefData);
2005
- if (requiredColumnsValidation.missingColumns && requiredColumnsValidation.missingColumns.length > 0) {
2006
- console.error('[WARNING] Entity will need required columns added after creation:', requiredColumnsValidation.missingColumns.map(c => c.schemaName).join(', '));
2007
- }
2008
- const headers = {};
2009
- if (solutionUniqueName) {
2010
- headers['MSCRM.SolutionUniqueName'] = solutionUniqueName;
2011
- }
2012
- // Execute with rate limiting
2013
- const response = await rateLimiter.execute(async () => {
2014
- return await this.makeRequest('api/data/v9.2/EntityDefinitions', 'POST', entityDefinition, headers);
2015
- });
2016
- // Log success
2017
- auditLogger.log({
2018
- operation: 'createEntity',
2019
- operationType: 'CREATE',
2020
- componentType: 'Entity',
2021
- componentName: schemaName,
2022
- success: true,
2023
- executionTimeMs: Date.now() - startTime
2024
- });
2025
- return response;
2026
- }
2027
- catch (error) {
2028
- // Log failure
2029
- auditLogger.log({
2030
- operation: 'createEntity',
2031
- operationType: 'CREATE',
2032
- componentType: 'Entity',
2033
- componentName: entityDefinition.SchemaName || entityDefinition.LogicalName,
2034
- success: false,
2035
- error: error.message,
2036
- executionTimeMs: Date.now() - startTime
2037
- });
2038
- throw error;
2039
- }
171
+ return this.entity.createEntity(entityDefinition, solutionUniqueName);
2040
172
  }
2041
- /**
2042
- * Update an existing entity
2043
- * @param metadataId The MetadataId of the entity
2044
- * @param updates The properties to update
2045
- * @param solutionUniqueName Optional solution context
2046
- */
2047
173
  async updateEntity(metadataId, updates, solutionUniqueName) {
2048
- const headers = {
2049
- 'MSCRM.MergeLabels': 'true'
2050
- };
2051
- if (solutionUniqueName) {
2052
- headers['MSCRM.SolutionUniqueName'] = solutionUniqueName;
2053
- }
2054
- await this.makeRequest(`api/data/v9.2/EntityDefinitions(${metadataId})`, 'PUT', updates, headers);
174
+ return this.entity.updateEntity(metadataId, updates, solutionUniqueName);
2055
175
  }
2056
- /**
2057
- * Delete a custom entity
2058
- * @param metadataId The MetadataId of the entity to delete
2059
- */
2060
176
  async deleteEntity(metadataId) {
2061
- await this.makeRequest(`api/data/v9.2/EntityDefinitions(${metadataId})`, 'DELETE');
177
+ return this.entity.deleteEntity(metadataId);
2062
178
  }
2063
- /**
2064
- * Update entity icon using Fluent UI System Icon
2065
- * @param entityLogicalName The logical name of the entity
2066
- * @param iconFileName The Fluent UI icon file name (e.g., 'people_community_24_filled.svg')
2067
- * @param solutionUniqueName Optional solution to add the web resource to
2068
- * @returns Result with web resource ID and icon vector name
2069
- */
2070
179
  async updateEntityIcon(entityLogicalName, iconFileName, solutionUniqueName) {
2071
- const startTime = Date.now();
2072
- try {
2073
- // Step 1: Get entity metadata to retrieve schema name and metadata ID
2074
- const entityMetadata = await this.getEntityMetadata(entityLogicalName);
2075
- const entitySchemaName = entityMetadata.SchemaName;
2076
- const metadataId = entityMetadata.MetadataId;
2077
- if (!metadataId) {
2078
- throw new Error(`Could not find MetadataId for entity '${entityLogicalName}'`);
2079
- }
2080
- // Note: No need to clear existing IconVectorName - PowerPlatform will override it
2081
- // when we set the new icon. This avoids potential API errors from setting null values.
2082
- // Step 2: Fetch the icon SVG from Fluent UI GitHub
2083
- const svgContent = await iconManager.fetchIcon(iconFileName);
2084
- // Step 3: Validate the SVG
2085
- const validation = iconManager.validateIconSvg(svgContent);
2086
- if (!validation.valid) {
2087
- throw new Error(`Invalid SVG: ${validation.error}`);
2088
- }
2089
- // Step 4: Convert SVG to base64
2090
- const base64Content = Buffer.from(svgContent).toString('base64');
2091
- // Step 5: Generate web resource name
2092
- const webResourceName = iconManager.generateWebResourceName(entitySchemaName, iconFileName.replace('.svg', ''));
2093
- // Step 6: Check if web resource already exists (use exact name match)
2094
- const existingResourcesResponse = await this.makeRequest(`api/data/v9.2/webresourceset?$filter=name eq '${webResourceName}'&$select=webresourceid,name`);
2095
- let webResourceId;
2096
- if (existingResourcesResponse.value && existingResourcesResponse.value.length > 0) {
2097
- // Web resource exists, update it
2098
- const existing = existingResourcesResponse.value[0];
2099
- webResourceId = existing.webresourceid;
2100
- const webResourceUpdates = {
2101
- displayname: `Icon for ${entityMetadata.DisplayName?.UserLocalizedLabel?.Label || entityLogicalName}`,
2102
- content: base64Content,
2103
- description: `Fluent UI icon (${iconFileName}) for ${entityLogicalName} entity`
2104
- };
2105
- await this.updateWebResource(webResourceId, webResourceUpdates, solutionUniqueName);
2106
- }
2107
- else {
2108
- // Web resource doesn't exist, create new
2109
- const webResource = {
2110
- name: webResourceName,
2111
- displayname: `Icon for ${entityMetadata.DisplayName?.UserLocalizedLabel?.Label || entityLogicalName}`,
2112
- webresourcetype: 11, // SVG
2113
- content: base64Content,
2114
- description: `Fluent UI icon (${iconFileName}) for ${entityLogicalName} entity`
2115
- };
2116
- const webResourceResult = await this.createWebResource(webResource, solutionUniqueName);
2117
- webResourceId = webResourceResult.webresourceid;
2118
- }
2119
- // Step 7: Generate icon vector name
2120
- const iconVectorName = iconManager.generateIconVectorName(webResourceName);
2121
- // Step 8: Update entity metadata with icon reference
2122
- const entityUpdates = {
2123
- '@odata.type': 'Microsoft.Dynamics.CRM.EntityMetadata',
2124
- IconVectorName: iconVectorName
2125
- };
2126
- await this.updateEntity(metadataId, entityUpdates, solutionUniqueName);
2127
- // Step 9: Publish the web resource (component type 61)
2128
- await this.publishComponent(webResourceId, 61);
2129
- // Step 10: Publish the entity (component type 1)
2130
- await this.publishComponent(metadataId, 1);
2131
- // Log success
2132
- auditLogger.log({
2133
- operation: 'updateEntityIcon',
2134
- operationType: 'UPDATE',
2135
- componentType: 'Entity',
2136
- componentName: entityLogicalName,
2137
- success: true,
2138
- parameters: {
2139
- iconFileName,
2140
- webResourceName,
2141
- webResourceId,
2142
- iconVectorName
2143
- },
2144
- executionTimeMs: Date.now() - startTime
2145
- });
2146
- return {
2147
- success: true,
2148
- entityLogicalName,
2149
- entitySchemaName,
2150
- iconFileName,
2151
- webResourceId,
2152
- webResourceName,
2153
- iconVectorName,
2154
- message: 'Entity icon updated and published successfully. The icon should now be visible in the UI.'
2155
- };
2156
- }
2157
- catch (error) {
2158
- // Log failure
2159
- auditLogger.log({
2160
- operation: 'updateEntityIcon',
2161
- operationType: 'UPDATE',
2162
- componentType: 'Entity',
2163
- componentName: entityLogicalName,
2164
- success: false,
2165
- error: error.message,
2166
- executionTimeMs: Date.now() - startTime
2167
- });
2168
- throw error;
2169
- }
2170
- }
2171
- /**
2172
- * Create a new attribute on an entity
2173
- * @param entityLogicalName The logical name of the entity
2174
- * @param attributeDefinition The attribute definition object
2175
- * @param solutionUniqueName Optional solution to add the attribute to
2176
- * @returns The created attribute metadata
2177
- */
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
+ // =====================================================
2178
190
  async createAttribute(entityLogicalName, attributeDefinition, solutionUniqueName) {
2179
- const startTime = Date.now();
2180
- try {
2181
- // Validate attribute name against best practices
2182
- const schemaName = attributeDefinition.SchemaName || attributeDefinition.LogicalName;
2183
- const isLookup = attributeDefinition['@odata.type'] === 'Microsoft.Dynamics.CRM.LookupAttributeMetadata';
2184
- const nameValidation = bestPracticesValidator.validateAttributeName(schemaName, isLookup);
2185
- if (!nameValidation.isValid) {
2186
- const error = `Attribute name validation failed: ${nameValidation.issues.join(', ')}`;
2187
- auditLogger.log({
2188
- operation: 'createAttribute',
2189
- operationType: 'CREATE',
2190
- componentType: 'Attribute',
2191
- componentName: `${entityLogicalName}.${schemaName}`,
2192
- success: false,
2193
- error,
2194
- executionTimeMs: Date.now() - startTime
2195
- });
2196
- throw new Error(error);
2197
- }
2198
- // Log warnings if any
2199
- if (nameValidation.warnings.length > 0) {
2200
- console.error(`[WARNING] Attribute name warnings: ${nameValidation.warnings.join(', ')}`);
2201
- }
2202
- // Validate boolean usage (best practice is to avoid booleans)
2203
- const isBoolean = attributeDefinition['@odata.type'] === 'Microsoft.Dynamics.CRM.BooleanAttributeMetadata';
2204
- if (isBoolean) {
2205
- const booleanValidation = bestPracticesValidator.validateBooleanUsage('Boolean', schemaName);
2206
- if (!booleanValidation.isValid) {
2207
- console.error(`[WARNING] ${booleanValidation.warnings.join(', ')}`);
2208
- }
2209
- }
2210
- const headers = {};
2211
- if (solutionUniqueName) {
2212
- headers['MSCRM.SolutionUniqueName'] = solutionUniqueName;
2213
- }
2214
- // Execute with rate limiting
2215
- const response = await rateLimiter.execute(async () => {
2216
- return await this.makeRequest(`api/data/v9.2/EntityDefinitions(LogicalName='${entityLogicalName}')/Attributes`, 'POST', attributeDefinition, headers);
2217
- });
2218
- // Log success
2219
- auditLogger.log({
2220
- operation: 'createAttribute',
2221
- operationType: 'CREATE',
2222
- componentType: 'Attribute',
2223
- componentName: `${entityLogicalName}.${schemaName}`,
2224
- success: true,
2225
- executionTimeMs: Date.now() - startTime
2226
- });
2227
- return response;
2228
- }
2229
- catch (error) {
2230
- // Log failure
2231
- const schemaName = attributeDefinition.SchemaName || attributeDefinition.LogicalName;
2232
- auditLogger.log({
2233
- operation: 'createAttribute',
2234
- operationType: 'CREATE',
2235
- componentType: 'Attribute',
2236
- componentName: `${entityLogicalName}.${schemaName}`,
2237
- success: false,
2238
- error: error.message,
2239
- executionTimeMs: Date.now() - startTime
2240
- });
2241
- throw error;
2242
- }
191
+ return this.attribute.createAttribute(entityLogicalName, attributeDefinition, solutionUniqueName);
2243
192
  }
2244
- /**
2245
- * Update an existing attribute
2246
- * @param entityLogicalName The logical name of the entity
2247
- * @param attributeLogicalName The logical name of the attribute
2248
- * @param updates The properties to update
2249
- * @param solutionUniqueName Optional solution context
2250
- */
2251
193
  async updateAttribute(entityLogicalName, attributeLogicalName, updates, solutionUniqueName) {
2252
- const headers = {
2253
- 'MSCRM.MergeLabels': 'true'
2254
- };
2255
- if (solutionUniqueName) {
2256
- headers['MSCRM.SolutionUniqueName'] = solutionUniqueName;
2257
- }
2258
- // First, get the existing attribute to retrieve its @odata.type and merge updates
2259
- const existingAttribute = await this.getEntityAttribute(entityLogicalName, attributeLogicalName);
2260
- // Merge the updates with required fields
2261
- const payload = {
2262
- ...updates,
2263
- '@odata.type': existingAttribute['@odata.type'],
2264
- LogicalName: attributeLogicalName,
2265
- AttributeType: existingAttribute.AttributeType
2266
- };
2267
- 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);
2268
195
  }
2269
- /**
2270
- * Delete an attribute
2271
- * @param entityLogicalName The logical name of the entity
2272
- * @param attributeMetadataId The MetadataId of the attribute to delete
2273
- */
2274
196
  async deleteAttribute(entityLogicalName, attributeMetadataId) {
2275
- await this.makeRequest(`api/data/v9.2/EntityDefinitions(LogicalName='${entityLogicalName}')/Attributes(${attributeMetadataId})`, 'DELETE');
2276
- }
2277
- /**
2278
- * Create a picklist attribute using a global option set
2279
- * @param entityLogicalName The logical name of the entity
2280
- * @param attributeDefinition The attribute definition (must reference a global option set)
2281
- * @param solutionUniqueName Optional solution to add the attribute to
2282
- * @returns The created attribute metadata
2283
- */
2284
- async createGlobalOptionSetAttribute(entityLogicalName, attributeDefinition, solutionUniqueName) {
2285
- const headers = {};
2286
- if (solutionUniqueName) {
2287
- headers['MSCRM.SolutionUniqueName'] = solutionUniqueName;
2288
- }
2289
- // Ensure the attribute is of type PicklistAttributeMetadata with GlobalOptionSet
2290
- if (!attributeDefinition['@odata.type']) {
2291
- attributeDefinition['@odata.type'] = 'Microsoft.Dynamics.CRM.PicklistAttributeMetadata';
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
+ };
2292
232
  }
2293
- return await this.makeRequest(`api/data/v9.2/EntityDefinitions(LogicalName='${entityLogicalName}')/Attributes`, 'POST', attributeDefinition, headers);
233
+ return this.attribute.createGlobalOptionSetAttribute(entityLogicalName, attributeDefinition, options?.solutionUniqueName);
2294
234
  }
2295
- /**
2296
- * Create a one-to-many relationship
2297
- * @param relationshipDefinition The relationship definition
2298
- * @param solutionUniqueName Optional solution to add the relationship to
2299
- */
2300
- async createOneToManyRelationship(relationshipDefinition, solutionUniqueName) {
2301
- const headers = {};
2302
- if (solutionUniqueName) {
2303
- headers['MSCRM.SolutionUniqueName'] = solutionUniqueName;
2304
- }
2305
- const response = await this.makeRequest('api/data/v9.2/RelationshipDefinitions', 'POST', relationshipDefinition, headers);
2306
- return response;
235
+ // =====================================================
236
+ // RELATIONSHIP CUSTOMIZATION METHODS
237
+ // =====================================================
238
+ async createOneToManyRelationship(definition, solutionUniqueName) {
239
+ return this.relationship.createOneToManyRelationship(definition, solutionUniqueName);
2307
240
  }
2308
- /**
2309
- * Create a many-to-many relationship
2310
- * @param relationshipDefinition The relationship definition
2311
- * @param solutionUniqueName Optional solution to add the relationship to
2312
- */
2313
- async createManyToManyRelationship(relationshipDefinition, solutionUniqueName) {
2314
- const headers = {};
2315
- if (solutionUniqueName) {
2316
- headers['MSCRM.SolutionUniqueName'] = solutionUniqueName;
2317
- }
2318
- const response = await this.makeRequest('api/data/v9.2/RelationshipDefinitions', 'POST', relationshipDefinition, headers);
2319
- return response;
241
+ async createManyToManyRelationship(definition, solutionUniqueName) {
242
+ return this.relationship.createManyToManyRelationship(definition, solutionUniqueName);
2320
243
  }
2321
- /**
2322
- * Delete a relationship
2323
- * @param metadataId The MetadataId of the relationship to delete
2324
- */
2325
244
  async deleteRelationship(metadataId) {
2326
- await this.makeRequest(`api/data/v9.2/RelationshipDefinitions(${metadataId})`, 'DELETE');
2327
- }
2328
- /**
2329
- * Update a relationship
2330
- * Note: Most relationship properties are immutable, only labels can be updated
2331
- * @param metadataId The MetadataId of the relationship
2332
- * @param updates The properties to update (typically labels)
2333
- */
2334
- async updateRelationship(metadataId, updates) {
2335
- await this.makeRequest(`api/data/v9.2/RelationshipDefinitions(${metadataId})`, 'PUT', updates, { 'MSCRM.MergeLabels': 'true' });
2336
- }
2337
- /**
2338
- * Get detailed information about a relationship
2339
- * @param metadataId The MetadataId of the relationship
2340
- * @returns The relationship metadata
2341
- */
2342
- async getRelationshipDetails(metadataId) {
2343
- return await this.makeRequest(`api/data/v9.2/RelationshipDefinitions(${metadataId})`);
2344
- }
2345
- /**
2346
- * Publish all customizations
2347
- */
2348
- async publishAllCustomizations() {
2349
- await this.makeRequest('api/data/v9.2/PublishAllXml', 'POST', {});
2350
- }
2351
- /**
2352
- * Publish specific customizations
2353
- * @param parameterXml The ParameterXml specifying what to publish
2354
- */
2355
- async publishXml(parameterXml) {
2356
- await this.makeRequest('api/data/v9.2/PublishXml', 'POST', { ParameterXml: parameterXml });
2357
- }
2358
- /**
2359
- * Create a global option set
2360
- * @param optionSetDefinition The option set definition
2361
- * @param solutionUniqueName Optional solution to add the option set to
2362
- */
2363
- async createGlobalOptionSet(optionSetDefinition, solutionUniqueName) {
2364
- const headers = {};
2365
- if (solutionUniqueName) {
2366
- headers['MSCRM.SolutionUniqueName'] = solutionUniqueName;
2367
- }
2368
- const response = await this.makeRequest('api/data/v9.2/GlobalOptionSetDefinitions', 'POST', optionSetDefinition, headers);
2369
- return response;
245
+ return this.relationship.deleteRelationship(metadataId);
2370
246
  }
2371
- /**
2372
- * Delete a global option set
2373
- * @param metadataId The MetadataId of the option set to delete
2374
- */
2375
- async deleteGlobalOptionSet(metadataId) {
2376
- await this.makeRequest(`api/data/v9.2/GlobalOptionSetDefinitions(${metadataId})`, 'DELETE');
247
+ async updateRelationship(metadataId, updates) {
248
+ return this.relationship.updateRelationship(metadataId, updates);
249
+ }
250
+ // =====================================================
251
+ // OPTION SET CUSTOMIZATION METHODS
252
+ // =====================================================
253
+ async createGlobalOptionSet(definition, solutionUniqueName) {
254
+ return this.optionSet.createGlobalOptionSet(definition, solutionUniqueName);
2377
255
  }
2378
- // ===== Phase 2: UI Components (Forms, Views, Option Sets) =====
2379
- /**
2380
- * Update a global option set
2381
- */
2382
256
  async updateGlobalOptionSet(metadataId, updates, solutionUniqueName) {
2383
- const headers = solutionUniqueName ? { 'MSCRM.SolutionUniqueName': solutionUniqueName } : undefined;
2384
- await this.makeRequest(`api/data/v9.2/GlobalOptionSetDefinitions(${metadataId})`, 'PUT', updates, headers);
257
+ return this.optionSet.updateGlobalOptionSet(metadataId, updates, solutionUniqueName);
2385
258
  }
2386
- /**
2387
- * Add a value to a global option set
2388
- */
2389
259
  async addOptionSetValue(optionSetName, value, label, solutionUniqueName) {
2390
- const headers = solutionUniqueName ? { 'MSCRM.SolutionUniqueName': solutionUniqueName } : undefined;
2391
- return await this.makeRequest(`api/data/v9.2/InsertOptionValue`, 'POST', {
2392
- OptionSetName: optionSetName,
2393
- Value: value,
2394
- Label: {
2395
- LocalizedLabels: [{ Label: label, LanguageCode: 1033 }]
2396
- }
2397
- }, headers);
260
+ return this.optionSet.addOptionSetValue(optionSetName, value, label, solutionUniqueName);
2398
261
  }
2399
- /**
2400
- * Update an option set value
2401
- */
2402
262
  async updateOptionSetValue(optionSetName, value, label, solutionUniqueName) {
2403
- const headers = { 'MSCRM.MergeLabels': 'true' };
2404
- if (solutionUniqueName) {
2405
- headers['MSCRM.SolutionUniqueName'] = solutionUniqueName;
2406
- }
2407
- await this.makeRequest(`api/data/v9.2/UpdateOptionValue`, 'POST', {
2408
- OptionSetName: optionSetName,
2409
- Value: value,
2410
- Label: {
2411
- LocalizedLabels: [{ Label: label, LanguageCode: 1033 }]
2412
- },
2413
- MergeLabels: true
2414
- }, headers);
263
+ return this.optionSet.updateOptionSetValue(optionSetName, value, label, solutionUniqueName);
2415
264
  }
2416
- /**
2417
- * Delete an option set value
2418
- */
2419
265
  async deleteOptionSetValue(optionSetName, value) {
2420
- await this.makeRequest(`api/data/v9.2/DeleteOptionValue`, 'POST', {
2421
- OptionSetName: optionSetName,
2422
- Value: value
2423
- });
266
+ return this.optionSet.deleteOptionSetValue(optionSetName, value);
2424
267
  }
2425
- /**
2426
- * Reorder option set values
2427
- */
2428
268
  async reorderOptionSetValues(optionSetName, values, solutionUniqueName) {
2429
- const headers = solutionUniqueName ? { 'MSCRM.SolutionUniqueName': solutionUniqueName } : undefined;
2430
- await this.makeRequest(`api/data/v9.2/OrderOption`, 'POST', {
2431
- OptionSetName: optionSetName,
2432
- Values: values
2433
- }, headers);
2434
- }
2435
- /**
2436
- * Create a form (systemform)
2437
- */
2438
- async createForm(form, solutionUniqueName) {
2439
- const headers = solutionUniqueName ? { 'MSCRM.SolutionUniqueName': solutionUniqueName } : undefined;
2440
- return await this.makeRequest('api/data/v9.2/systemforms', 'POST', form, headers);
2441
- }
2442
- /**
2443
- * Update a form
2444
- */
2445
- async updateForm(formId, updates, solutionUniqueName) {
2446
- const headers = solutionUniqueName ? { 'MSCRM.SolutionUniqueName': solutionUniqueName } : undefined;
2447
- await this.makeRequest(`api/data/v9.2/systemforms(${formId})`, 'PATCH', updates, headers);
269
+ return this.optionSet.reorderOptionSetValues(optionSetName, values, solutionUniqueName);
2448
270
  }
2449
- /**
2450
- * Delete a form
2451
- */
2452
- async deleteForm(formId) {
2453
- await this.makeRequest(`api/data/v9.2/systemforms(${formId})`, 'DELETE');
2454
- }
2455
- /**
2456
- * Get forms for an entity
2457
- */
271
+ // =====================================================
272
+ // FORM CUSTOMIZATION METHODS
273
+ // =====================================================
2458
274
  async getForms(entityLogicalName) {
2459
- return await this.makeRequest(`api/data/v9.2/systemforms?$filter=objecttypecode eq '${entityLogicalName}'&$orderby=type`);
2460
- }
2461
- /**
2462
- * Create a view (savedquery)
2463
- */
2464
- async createView(view, solutionUniqueName) {
2465
- const headers = solutionUniqueName ? { 'MSCRM.SolutionUniqueName': solutionUniqueName } : undefined;
2466
- 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);
2467
288
  }
2468
- /**
2469
- * Update a view
2470
- */
2471
- async updateView(viewId, updates, solutionUniqueName) {
2472
- const headers = solutionUniqueName ? { 'MSCRM.SolutionUniqueName': solutionUniqueName } : undefined;
2473
- 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;
2474
297
  }
2475
- /**
2476
- * Delete a view
2477
- */
2478
- async deleteView(viewId) {
2479
- await this.makeRequest(`api/data/v9.2/savedqueries(${viewId})`, 'DELETE');
298
+ async updateForm(formId, updates, solutionUniqueName) {
299
+ return this.form.updateForm(formId, updates, solutionUniqueName);
2480
300
  }
2481
- /**
2482
- * Get views for an entity
2483
- */
2484
- async getViews(entityLogicalName) {
2485
- 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);
2486
303
  }
2487
- /**
2488
- * Activate a form (set statecode=1)
2489
- * @param formId The systemformid (GUID)
2490
- */
2491
304
  async activateForm(formId) {
2492
- await this.makeRequest(`api/data/v9.2/systemforms(${formId})`, 'PATCH', { statecode: 1, statuscode: 1 });
305
+ return this.form.activateForm(formId);
2493
306
  }
2494
- /**
2495
- * Deactivate a form (set statecode=0)
2496
- * @param formId The systemformid (GUID)
2497
- */
2498
307
  async deactivateForm(formId) {
2499
- await this.makeRequest(`api/data/v9.2/systemforms(${formId})`, 'PATCH', { statecode: 0, statuscode: 2 });
308
+ return this.form.deactivateForm(formId);
2500
309
  }
2501
- /**
2502
- * Set a view as the default view for its entity
2503
- * @param viewId The savedqueryid (GUID)
2504
- */
2505
- async setDefaultView(viewId) {
2506
- 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);
2507
315
  }
2508
- /**
2509
- * Get the FetchXML from a view
2510
- * @param viewId The savedqueryid (GUID)
2511
- * @returns The view with FetchXML
2512
- */
2513
316
  async getViewFetchXml(viewId) {
2514
- 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);
2515
334
  }
2516
- // ===== Phase 3: Advanced Customizations (Web Resources) =====
2517
- /**
2518
- * Create a web resource
2519
- */
2520
- async createWebResource(webResource, solutionUniqueName) {
2521
- const headers = solutionUniqueName ? { 'MSCRM.SolutionUniqueName': solutionUniqueName } : undefined;
2522
- 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);
2523
337
  }
2524
- /**
2525
- * Update a web resource
2526
- */
2527
- async updateWebResource(webResourceId, updates, solutionUniqueName) {
2528
- const headers = solutionUniqueName ? { 'MSCRM.SolutionUniqueName': solutionUniqueName } : undefined;
2529
- await this.makeRequest(`api/data/v9.2/webresourceset(${webResourceId})`, 'PATCH', updates, headers);
338
+ async deleteView(viewId) {
339
+ return this.view.deleteView(viewId);
2530
340
  }
2531
- /**
2532
- * Delete a web resource
2533
- */
2534
- async deleteWebResource(webResourceId) {
2535
- await this.makeRequest(`api/data/v9.2/webresourceset(${webResourceId})`, 'DELETE');
341
+ async setDefaultView(viewId) {
342
+ return this.view.setDefaultView(viewId);
2536
343
  }
2537
- /**
2538
- * Get web resource
2539
- */
344
+ // =====================================================
345
+ // WEB RESOURCE CUSTOMIZATION METHODS
346
+ // =====================================================
2540
347
  async getWebResource(webResourceId) {
2541
- return await this.makeRequest(`api/data/v9.2/webresourceset(${webResourceId})`);
348
+ return this.webResource.getWebResource(webResourceId);
2542
349
  }
2543
- /**
2544
- * Get web resources by name pattern
2545
- */
2546
350
  async getWebResources(nameFilter) {
2547
- const filter = nameFilter ? `?$filter=contains(name,'${nameFilter}')` : '';
2548
- return await this.makeRequest(`api/data/v9.2/webresourceset${filter}`);
2549
- }
2550
- /**
2551
- * Get web resource content (base64 encoded)
2552
- * @param webResourceId The webresourceid (GUID)
2553
- * @returns The web resource with content field
2554
- */
2555
- async getWebResourceContent(webResourceId) {
2556
- 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);
2557
364
  }
2558
- /**
2559
- * Get web resource dependencies
2560
- * @param webResourceId The webresourceid (GUID)
2561
- * @returns List of dependencies
2562
- */
2563
- async getWebResourceDependencies(webResourceId) {
2564
- 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);
2565
367
  }
2566
- // ===== Phase 4: Solution Management =====
2567
- /**
2568
- * Create a publisher
2569
- */
2570
- async createPublisher(publisher) {
2571
- return await this.makeRequest('api/data/v9.2/publishers', 'POST', publisher);
368
+ async deleteWebResource(webResourceId) {
369
+ return this.webResource.deleteWebResource(webResourceId);
2572
370
  }
2573
- /**
2574
- * Get publishers
2575
- */
371
+ // =====================================================
372
+ // SOLUTION CUSTOMIZATION METHODS
373
+ // =====================================================
2576
374
  async getPublishers() {
2577
- return await this.makeRequest('api/data/v9.2/publishers?$filter=isreadonly eq false');
2578
- }
2579
- /**
2580
- * Create a solution
2581
- */
2582
- async createSolution(solution) {
2583
- 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);
2584
388
  }
2585
- /**
2586
- * Get solutions
2587
- */
2588
389
  async getSolutions() {
2589
- return await this.makeRequest('api/data/v9.2/solutions?$filter=isvisible eq true&$orderby=createdon desc');
390
+ return this.solution.getSolutions();
2590
391
  }
2591
- /**
2592
- * Get solution by unique name
2593
- */
2594
392
  async getSolution(uniqueName) {
2595
- const result = await this.makeRequest(`api/data/v9.2/solutions?$filter=uniquename eq '${uniqueName}'&$top=1`);
2596
- 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);
2597
406
  }
2598
- /**
2599
- * Add component to solution
2600
- */
2601
- async addComponentToSolution(solutionUniqueName, componentId, componentType, addRequiredComponents = true, includedComponentSettingsValues) {
2602
- await this.makeRequest('api/data/v9.2/AddSolutionComponent', 'POST', {
2603
- SolutionUniqueName: solutionUniqueName,
2604
- ComponentId: componentId,
2605
- ComponentType: componentType,
2606
- AddRequiredComponents: addRequiredComponents,
2607
- IncludedComponentSettingsValues: includedComponentSettingsValues
2608
- });
407
+ async getSolutionComponents(solutionUniqueName) {
408
+ return this.solution.getSolutionComponents(solutionUniqueName);
2609
409
  }
2610
- /**
2611
- * Remove component from solution
2612
- */
2613
- async removeComponentFromSolution(solutionUniqueName, componentId, componentType) {
2614
- await this.makeRequest('api/data/v9.2/RemoveSolutionComponent', 'POST', {
2615
- SolutionUniqueName: solutionUniqueName,
2616
- ComponentId: componentId,
2617
- ComponentType: componentType
2618
- });
410
+ async addComponentToSolution(solutionUniqueName, componentId, componentType, addRequiredComponents) {
411
+ return this.solution.addComponentToSolution(solutionUniqueName, componentId, componentType, addRequiredComponents);
2619
412
  }
2620
- /**
2621
- * Get solution components
2622
- */
2623
- async getSolutionComponents(solutionUniqueName) {
2624
- const solution = await this.getSolution(solutionUniqueName);
2625
- if (!solution) {
2626
- throw new Error(`Solution '${solutionUniqueName}' not found`);
2627
- }
2628
- 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);
2629
415
  }
2630
- /**
2631
- * Export solution
2632
- */
2633
- async exportSolution(solutionName, managed = false) {
2634
- return await this.makeRequest('api/data/v9.2/ExportSolution', 'POST', {
2635
- SolutionName: solutionName,
2636
- Managed: managed,
2637
- ExportAutoNumberingSettings: true,
2638
- ExportCalendarSettings: true,
2639
- ExportCustomizationSettings: true,
2640
- ExportEmailTrackingSettings: true,
2641
- ExportGeneralSettings: true,
2642
- ExportMarketingSettings: true,
2643
- ExportOutlookSynchronizationSettings: true,
2644
- ExportRelationshipRoles: true,
2645
- ExportIsvConfig: true,
2646
- ExportSales: true,
2647
- ExportExternalApplications: true
2648
- });
416
+ async exportSolution(solutionName, managed) {
417
+ return this.solution.exportSolution(solutionName, managed ?? false);
2649
418
  }
2650
- /**
2651
- * Import solution
2652
- */
2653
- async importSolution(customizationFile, publishWorkflows = true, overwriteUnmanagedCustomizations = false) {
2654
- return await this.makeRequest('api/data/v9.2/ImportSolution', 'POST', {
2655
- CustomizationFile: customizationFile,
2656
- PublishWorkflows: publishWorkflows,
2657
- OverwriteUnmanagedCustomizations: overwriteUnmanagedCustomizations,
2658
- SkipProductUpdateDependencies: false,
2659
- HoldingSolution: false,
2660
- ImportJobId: this.generateGuid()
2661
- });
419
+ async importSolution(customizationFile, overwriteUnmanagedCustomizations, publishWorkflows) {
420
+ return this.solution.importSolution(customizationFile, overwriteUnmanagedCustomizations, publishWorkflows);
2662
421
  }
2663
- /**
2664
- * Delete a solution
2665
- */
2666
- async deleteSolution(solutionId) {
2667
- await this.makeRequest(`api/data/v9.2/solutions(${solutionId})`, 'DELETE');
422
+ // =====================================================
423
+ // PUBLISHING METHODS
424
+ // =====================================================
425
+ async publishAllCustomizations() {
426
+ return this.publishing.publishAllCustomizations();
2668
427
  }
2669
- // ===== Phase 5: Publishing & Validation =====
2670
- /**
2671
- * Publish specific entity
2672
- */
2673
428
  async publishEntity(entityLogicalName) {
2674
- const parameterXml = `<importexportxml><entities><entity>${entityLogicalName}</entity></entities></importexportxml>`;
2675
- await this.publishXml(parameterXml);
429
+ return this.publishing.publishEntity(entityLogicalName);
2676
430
  }
2677
- /**
2678
- * Publish specific component
2679
- */
2680
- async publishComponent(componentId, componentType) {
2681
- const typeMap = {
2682
- 1: 'entity',
2683
- 2: 'attribute',
2684
- 9: 'optionset',
2685
- 24: 'form',
2686
- 26: 'savedquery',
2687
- 29: 'workflow',
2688
- 60: 'systemform',
2689
- 61: 'webresource'
2690
- };
2691
- const componentTypeName = typeMap[componentType] || 'component';
2692
- const parameterXml = `<importexportxml><${componentTypeName}s><${componentTypeName}>${componentId}</${componentTypeName}></${componentTypeName}s></importexportxml>`;
2693
- await this.publishXml(parameterXml);
431
+ // =====================================================
432
+ // DEPENDENCY METHODS
433
+ // =====================================================
434
+ async checkDependencies(componentId, componentType) {
435
+ return this.dependency.checkDependencies(componentId, componentType);
2694
436
  }
2695
- /**
2696
- * Check for unpublished customizations
2697
- */
2698
- async checkUnpublishedChanges() {
2699
- // Query for unpublished customizations using RetrieveUnpublished
2700
- return await this.makeRequest('api/data/v9.2/RetrieveUnpublished', 'POST', {});
437
+ async checkDeleteEligibility(componentId, componentType) {
438
+ return this.dependency.checkDeleteEligibility(componentId, componentType);
2701
439
  }
2702
- /**
2703
- * Check component dependencies
2704
- */
2705
- async checkDependencies(componentId, componentType) {
2706
- return await this.makeRequest('api/data/v9.2/RetrieveDependenciesForDelete', 'POST', {
2707
- ObjectId: componentId,
2708
- 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,
2709
466
  });
2710
467
  }
2711
- /**
2712
- * Check if component can be deleted
2713
- */
2714
- async checkDeleteEligibility(componentId, componentType) {
2715
- try {
2716
- const result = await this.checkDependencies(componentId, componentType);
2717
- const dependencies = result.EntityCollection?.Entities || [];
2718
- return {
2719
- canDelete: dependencies.length === 0,
2720
- dependencies: dependencies
2721
- };
2722
- }
2723
- catch (error) {
2724
- return {
2725
- canDelete: false,
2726
- dependencies: []
2727
- };
2728
- }
468
+ async updatePluginAssembly(assemblyId, content, version, solutionUniqueName) {
469
+ return this.pluginDeployment.updatePluginAssembly(assemblyId, content, version, solutionUniqueName);
2729
470
  }
2730
- /**
2731
- * Preview unpublished changes
2732
- * Returns all components that have unpublished customizations
2733
- */
2734
- async previewUnpublishedChanges() {
2735
- // Use RetrieveUnpublished action to get unpublished changes
2736
- return await this.makeRequest('api/data/v9.2/RetrieveUnpublished', 'POST', {});
471
+ async deletePluginAssembly(assemblyId) {
472
+ return this.pluginDeployment.deletePluginAssembly(assemblyId);
2737
473
  }
2738
- /**
2739
- * Check dependencies for a specific component
2740
- * @param componentId The component ID (GUID)
2741
- * @param componentType The component type code
2742
- * @returns Dependency information
2743
- */
2744
- async checkComponentDependencies(componentId, componentType) {
2745
- // This is an alias for checkDependencies for consistency
2746
- return await this.checkDependencies(componentId, componentType);
474
+ async deletePluginStep(stepId) {
475
+ return this.pluginDeployment.deletePluginStep(stepId);
2747
476
  }
2748
- /**
2749
- * Validate solution integrity
2750
- * Checks for missing dependencies and other issues
2751
- * @param solutionUniqueName The unique name of the solution
2752
- * @returns Validation results
2753
- */
2754
- async validateSolutionIntegrity(solutionUniqueName) {
2755
- // Get solution components
2756
- const components = await this.getSolutionComponents(solutionUniqueName);
2757
- const issues = [];
2758
- const warnings = [];
2759
- // Check each component for dependencies
2760
- for (const component of components.value || []) {
2761
- try {
2762
- const deps = await this.checkDependencies(component.objectid, component.componenttype);
2763
- const dependencies = deps.EntityCollection?.Entities || [];
2764
- const missingDeps = dependencies.filter((d) => d.Attributes?.ismissing === true);
2765
- if (missingDeps.length > 0) {
2766
- issues.push({
2767
- componentId: component.objectid,
2768
- componentType: component.componenttype,
2769
- missingDependencies: missingDeps
2770
- });
2771
- }
2772
- }
2773
- catch (error) {
2774
- warnings.push({
2775
- componentId: component.objectid,
2776
- componentType: component.componenttype,
2777
- error: 'Could not check dependencies'
2778
- });
2779
- }
2780
- }
2781
- return {
2782
- isValid: issues.length === 0,
2783
- issues,
2784
- warnings
2785
- };
477
+ async registerPluginStep(config) {
478
+ return this.pluginDeployment.registerPluginStep(config);
2786
479
  }
2787
- /**
2788
- * Validate schema name
2789
- */
2790
- validateSchemaName(schemaName, prefix) {
2791
- const errors = [];
2792
- // Check if starts with prefix
2793
- if (!schemaName.startsWith(prefix)) {
2794
- errors.push(`Schema name must start with prefix '${prefix}'`);
2795
- }
2796
- // Check for invalid characters
2797
- if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(schemaName)) {
2798
- errors.push('Schema name must start with a letter or underscore and contain only letters, numbers, and underscores');
2799
- }
2800
- // Check length (max 64 characters for most components)
2801
- if (schemaName.length > 64) {
2802
- errors.push('Schema name must be 64 characters or less');
2803
- }
2804
- return {
2805
- valid: errors.length === 0,
2806
- errors
2807
- };
480
+ async registerPluginImage(config) {
481
+ return this.pluginDeployment.registerPluginImage(config);
2808
482
  }
2809
- /**
2810
- * Get entity customization information
2811
- */
2812
- async getEntityCustomizationInfo(entityLogicalName) {
2813
- 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);
2814
489
  }
2815
- /**
2816
- * Check if entity has dependencies
2817
- */
2818
- async checkEntityDependencies(entityLogicalName) {
2819
- // First get the metadata ID
2820
- const entityMetadata = await this.getEntityMetadata(entityLogicalName);
2821
- if (!entityMetadata.MetadataId) {
2822
- throw new Error(`Could not find MetadataId for entity '${entityLogicalName}'`);
2823
- }
2824
- // Component type 1 = Entity
2825
- return await this.checkDependencies(entityMetadata.MetadataId, 1);
490
+ async addEntitiesToApp(appId, entityNames) {
491
+ return this.appManagement.addEntitiesToApp(appId, entityNames);
2826
492
  }
2827
- /**
2828
- * Helper to generate GUID
2829
- */
2830
- generateGuid() {
2831
- return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
2832
- const r = Math.random() * 16 | 0;
2833
- const v = c === 'x' ? r : (r & 0x3 | 0x8);
2834
- return v.toString(16);
2835
- });
493
+ async validateApp(appId) {
494
+ return this.appManagement.validateApp(appId);
2836
495
  }
2837
- // ============================================================
2838
- // PLUGIN DEPLOYMENT METHODS
2839
- // ============================================================
2840
- /**
2841
- * Extract assembly version from .NET DLL using PE header parsing
2842
- * @param assemblyPath - Path to the compiled .NET assembly (DLL)
2843
- * @returns Version string (e.g., "1.0.0.0")
2844
- */
2845
- async extractAssemblyVersion(assemblyPath) {
2846
- try {
2847
- // Dynamically import fs/promises for ESM compatibility
2848
- const fs = await import('fs/promises');
2849
- // Normalize path for cross-platform compatibility (Windows/WSL)
2850
- const normalizedPath = assemblyPath.replace(/\\/g, '/');
2851
- // Read the DLL file
2852
- const buffer = await fs.readFile(normalizedPath);
2853
- // Validate DLL format (should start with "MZ" header)
2854
- const header = buffer.toString('utf8', 0, 2);
2855
- if (header !== 'MZ') {
2856
- console.error('Invalid .NET assembly format - using default version');
2857
- return '1.0.0.0';
2858
- }
2859
- // Try to find version resource in PE header
2860
- // This is a simplified version - for production, you might want a more robust parser
2861
- // For now, we'll return a default version and let the user specify if needed
2862
- // Search for common version patterns in the assembly
2863
- const bufferStr = buffer.toString('utf16le');
2864
- const versionMatch = bufferStr.match(/\d+\.\d+\.\d+\.\d+/);
2865
- if (versionMatch) {
2866
- return versionMatch[0];
2867
- }
2868
- // Fallback to default version
2869
- console.error('Could not extract version from assembly - using default 1.0.0.0');
2870
- return '1.0.0.0';
2871
- }
2872
- catch (error) {
2873
- console.error('Error extracting assembly version:', error.message);
2874
- return '1.0.0.0';
2875
- }
496
+ async publishApp(appId) {
497
+ // publishApp needs a callback for publishXml
498
+ return this.appManagement.publishApp(appId, (parameterXml) => this.publishing.publishXml(parameterXml));
2876
499
  }
2877
- /**
2878
- * Query plugin type by typename
2879
- * @param typename - Plugin type typename (e.g., "MyNamespace.ContactPlugin")
2880
- * @returns Plugin type ID
2881
- */
2882
- async queryPluginTypeByTypename(typename) {
2883
- try {
2884
- const response = await this.makeRequest(`api/data/v9.2/plugintypes?$filter=typename eq '${typename}'&$select=plugintypeid`, 'GET');
2885
- if (!response.value || response.value.length === 0) {
2886
- throw new Error(`Plugin type '${typename}' not found. ` +
2887
- `Did you upload the assembly first? Use 'create-plugin-assembly' tool.`);
2888
- }
2889
- return response.value[0].plugintypeid;
2890
- }
2891
- catch (error) {
2892
- throw new Error(`Failed to query plugin type: ${error.message}`);
2893
- }
500
+ // =====================================================
501
+ // WORKFLOW MANAGEMENT METHODS
502
+ // =====================================================
503
+ async deactivateWorkflow(workflowId) {
504
+ return this.workflowManagement.deactivateWorkflow(workflowId);
2894
505
  }
2895
- /**
2896
- * Query plugin assembly by name
2897
- * @param assemblyName - Assembly name
2898
- * @returns Plugin assembly ID or null if not found
2899
- */
2900
- async queryPluginAssemblyByName(assemblyName) {
2901
- try {
2902
- const response = await this.makeRequest(`api/data/v9.2/pluginassemblies?$filter=name eq '${assemblyName}'&$select=pluginassemblyid`, 'GET');
2903
- if (!response.value || response.value.length === 0) {
2904
- return null;
2905
- }
2906
- return response.value[0].pluginassemblyid;
2907
- }
2908
- catch (error) {
2909
- throw new Error(`Failed to query plugin assembly: ${error.message}`);
2910
- }
506
+ async activateWorkflow(workflowId) {
507
+ return this.workflowManagement.activateWorkflow(workflowId);
2911
508
  }
2912
- /**
2913
- * Get plugin types for an existing assembly
2914
- * Used to retrieve type information after an assembly update
2915
- * @param assemblyId - Assembly GUID
2916
- * @returns Array of plugin type information
2917
- */
2918
- async getPluginTypesForAssembly(assemblyId) {
2919
- try {
2920
- const types = await this.makeRequest(`api/data/v9.2/plugintypes?$filter=_pluginassemblyid_value eq ${assemblyId}&$select=plugintypeid,typename,friendlyname`, 'GET');
2921
- return types.value.map((t) => ({
2922
- pluginTypeId: t.plugintypeid,
2923
- typeName: t.typename,
2924
- friendlyName: t.friendlyname
2925
- }));
2926
- }
2927
- catch (error) {
2928
- throw new Error(`Failed to get plugin types for assembly: ${error.message}`);
2929
- }
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);
2930
514
  }
2931
515
  /**
2932
- * Resolve SDK Message and Filter IDs for plugin step registration
2933
- * @param messageName - SDK message name (e.g., "Create", "Update", "Delete")
2934
- * @param entityName - Entity logical name (e.g., "contact", "account")
2935
- * @returns Object containing messageId and filterId
516
+ * Adapter function to convert FlowService.parseFlowSummary output to expected format
2936
517
  */
2937
- async resolveSdkMessageAndFilter(messageName, entityName) {
2938
- const timer = auditLogger.startTimer();
2939
- try {
2940
- // Step 1: Get SDK Message ID
2941
- const messages = await this.makeRequest(`api/data/v9.2/sdkmessages?$filter=name eq '${messageName}'&$select=sdkmessageid,name`, 'GET');
2942
- if (!messages.value || messages.value.length === 0) {
2943
- throw new Error(`SDK message '${messageName}' not found. ` +
2944
- `Common messages: Create, Update, Delete, SetState, Assign, Merge, Retrieve, RetrieveMultiple`);
2945
- }
2946
- const messageId = messages.value[0].sdkmessageid;
2947
- // Step 2: Get SDK Message Filter ID
2948
- const filters = await this.makeRequest(`api/data/v9.2/sdkmessagefilters?$filter=sdkmessageid/sdkmessageid eq ${messageId} and primaryobjecttypecode eq '${entityName}'&$select=sdkmessagefilterid`, 'GET');
2949
- if (!filters.value || filters.value.length === 0) {
2950
- throw new Error(`SDK message filter not found for message '${messageName}' on entity '${entityName}'. ` +
2951
- `Verify the entity supports this message.`);
2952
- }
2953
- const filterId = filters.value[0].sdkmessagefilterid;
2954
- auditLogger.log({
2955
- operation: 'resolve-sdk-message-filter',
2956
- operationType: 'READ',
2957
- componentType: 'SdkMessage',
2958
- success: true,
2959
- parameters: { messageName, entityName, messageId, filterId },
2960
- executionTimeMs: timer(),
2961
- });
2962
- return { messageId, filterId };
2963
- }
2964
- catch (error) {
2965
- auditLogger.log({
2966
- operation: 'resolve-sdk-message-filter',
2967
- operationType: 'READ',
2968
- componentType: 'SdkMessage',
2969
- success: false,
2970
- error: error.message,
2971
- executionTimeMs: timer(),
2972
- });
2973
- throw error;
2974
- }
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
+ };
2975
527
  }
2976
528
  /**
2977
- * Create a new plugin assembly in Dataverse
2978
- * @param options - Assembly creation options
2979
- * @returns Created assembly ID and plugin types
529
+ * Adapter function to convert WorkflowService.parseWorkflowXamlSummary output to expected format
2980
530
  */
2981
- async createPluginAssembly(options) {
2982
- const timer = auditLogger.startTimer();
2983
- try {
2984
- // Validate DLL size (16MB Dataverse limit)
2985
- const dllSize = Buffer.from(options.content, 'base64').length;
2986
- const maxSize = 16 * 1024 * 1024; // 16MB
2987
- if (dllSize > maxSize) {
2988
- throw new Error(`Assembly exceeds 16MB limit (current: ${(dllSize / 1024 / 1024).toFixed(2)}MB). ` +
2989
- `Remove unused dependencies or split into multiple assemblies.`);
2990
- }
2991
- // Create assembly payload
2992
- const assemblyData = {
2993
- name: options.name,
2994
- content: options.content,
2995
- version: options.version,
2996
- isolationmode: options.isolationMode ?? 2, // Default to Sandbox for security
2997
- sourcetype: options.sourceType ?? 0, // Default to Database
2998
- culture: 'neutral'
2999
- };
3000
- if (options.description) {
3001
- assemblyData.description = options.description;
3002
- }
3003
- // Upload assembly to Dataverse
3004
- const createResponse = await this.makeRequest('api/data/v9.2/pluginassemblies', 'POST', assemblyData, { 'Prefer': 'return=representation' });
3005
- // Extract assembly ID from response header or body
3006
- const pluginAssemblyId = createResponse.pluginassemblyid || createResponse.id;
3007
- if (!pluginAssemblyId) {
3008
- throw new Error('Plugin assembly created but ID not returned');
3009
- }
3010
- // Add to solution if specified
3011
- if (options.solutionUniqueName) {
3012
- await this.addComponentToSolution(options.solutionUniqueName, pluginAssemblyId, 91 // Component type 91 = PluginAssembly
3013
- );
3014
- }
3015
- // Poll for plugin types (Dataverse creates them asynchronously)
3016
- // Wait up to 60 seconds with 2-second intervals
3017
- const pluginTypes = [];
3018
- const maxAttempts = 30; // 30 attempts * 2 seconds = 60 seconds total
3019
- const pollInterval = 2000; // 2 seconds
3020
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
3021
- // Wait before polling (except first attempt)
3022
- if (attempt > 0) {
3023
- await new Promise(resolve => setTimeout(resolve, pollInterval));
3024
- }
3025
- // Query for plugin types
3026
- const types = await this.makeRequest(`api/data/v9.2/plugintypes?$filter=_pluginassemblyid_value eq ${pluginAssemblyId}&$select=plugintypeid,typename,friendlyname`, 'GET');
3027
- if (types.value && types.value.length > 0) {
3028
- pluginTypes.push(...types.value.map((t) => ({
3029
- pluginTypeId: t.plugintypeid,
3030
- typeName: t.typename,
3031
- friendlyName: t.friendlyname
3032
- })));
3033
- break;
3034
- }
3035
- }
3036
- if (pluginTypes.length === 0) {
3037
- throw new Error(`Plugin types not created after ${maxAttempts * pollInterval / 1000} seconds. ` +
3038
- `Dataverse may be experiencing delays processing the assembly. ` +
3039
- `Please check System Jobs in Power Platform admin center and retry. ` +
3040
- `Assembly ID: ${pluginAssemblyId}`);
3041
- }
3042
- auditLogger.log({
3043
- operation: 'create-plugin-assembly',
3044
- operationType: 'CREATE',
3045
- componentId: pluginAssemblyId,
3046
- componentType: 'PluginAssembly',
3047
- success: true,
3048
- parameters: {
3049
- name: options.name,
3050
- version: options.version,
3051
- size: dllSize,
3052
- pluginTypeCount: pluginTypes.length
3053
- },
3054
- executionTimeMs: timer(),
3055
- });
3056
- return {
3057
- pluginAssemblyId,
3058
- pluginTypes
3059
- };
3060
- }
3061
- catch (error) {
3062
- auditLogger.log({
3063
- operation: 'create-plugin-assembly',
3064
- operationType: 'CREATE',
3065
- componentName: options.name,
3066
- componentType: 'PluginAssembly',
3067
- success: false,
3068
- error: error.message,
3069
- executionTimeMs: timer(),
3070
- });
3071
- throw error;
3072
- }
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));
3073
553
  }
3074
554
  /**
3075
- * Update an existing plugin assembly with new DLL content
3076
- * @param assemblyId - Assembly GUID
3077
- * @param content - Base64-encoded DLL content
3078
- * @param version - New version string
3079
- * @param solutionUniqueName - Optional solution context
555
+ * Create a new Power Automate flow from an existing template flow
3080
556
  */
3081
- async updatePluginAssembly(assemblyId, content, version, solutionUniqueName) {
3082
- const timer = auditLogger.startTimer();
3083
- try {
3084
- // Validate DLL size
3085
- const dllSize = Buffer.from(content, 'base64').length;
3086
- const maxSize = 16 * 1024 * 1024;
3087
- if (dllSize > maxSize) {
3088
- throw new Error(`Assembly exceeds 16MB limit (current: ${(dllSize / 1024 / 1024).toFixed(2)}MB)`);
3089
- }
3090
- // Update assembly
3091
- await this.makeRequest(`api/data/v9.2/pluginassemblies(${assemblyId})`, 'PATCH', {
3092
- content,
3093
- version
3094
- });
3095
- // Add to solution if specified
3096
- if (solutionUniqueName) {
3097
- await this.addComponentToSolution(solutionUniqueName, assemblyId, 91 // Component type 91 = PluginAssembly
3098
- );
3099
- }
3100
- auditLogger.log({
3101
- operation: 'update-plugin-assembly',
3102
- operationType: 'UPDATE',
3103
- componentId: assemblyId,
3104
- componentType: 'PluginAssembly',
3105
- success: true,
3106
- parameters: { assemblyId, version, size: dllSize },
3107
- executionTimeMs: timer(),
3108
- });
3109
- }
3110
- catch (error) {
3111
- auditLogger.log({
3112
- operation: 'update-plugin-assembly',
3113
- operationType: 'UPDATE',
3114
- componentId: assemblyId,
3115
- componentType: 'PluginAssembly',
3116
- success: false,
3117
- error: error.message,
3118
- executionTimeMs: timer(),
3119
- });
3120
- throw error;
3121
- }
557
+ async createFlow(name, templateFlowId, options) {
558
+ return this.workflowManagement.createFlow(name, templateFlowId, options);
3122
559
  }
3123
560
  /**
3124
- * Delete a plugin assembly and all its associated components
3125
- * Used for rollback on deployment failure
3126
- * @param assemblyId - Assembly GUID to delete
561
+ * Delete a Power Automate flow (permanent operation)
3127
562
  */
3128
- async deletePluginAssembly(assemblyId) {
3129
- const timer = auditLogger.startTimer();
3130
- try {
3131
- // Note: Dataverse cascade deletes associated plugin types, steps, and images
3132
- await this.makeRequest(`api/data/v9.2/pluginassemblies(${assemblyId})`, 'DELETE');
3133
- auditLogger.log({
3134
- operation: 'delete-plugin-assembly',
3135
- operationType: 'DELETE',
3136
- componentId: assemblyId,
3137
- componentType: 'PluginAssembly',
3138
- success: true,
3139
- executionTimeMs: timer(),
3140
- });
3141
- }
3142
- catch (error) {
3143
- auditLogger.log({
3144
- operation: 'delete-plugin-assembly',
3145
- operationType: 'DELETE',
3146
- componentId: assemblyId,
3147
- componentType: 'PluginAssembly',
3148
- success: false,
3149
- error: error.message,
3150
- executionTimeMs: timer(),
3151
- });
3152
- throw new Error(`Failed to delete plugin assembly: ${error.message}`);
3153
- }
563
+ async deleteFlow(flowId) {
564
+ return this.workflowManagement.deleteFlow(flowId);
3154
565
  }
3155
566
  /**
3156
- * Delete a plugin step
3157
- * Used for rollback on deployment failure
3158
- * @param stepId - Step GUID to delete
567
+ * Clone an existing flow with a new name
3159
568
  */
3160
- async deletePluginStep(stepId) {
3161
- const timer = auditLogger.startTimer();
3162
- try {
3163
- await this.makeRequest(`api/data/v9.2/sdkmessageprocessingsteps(${stepId})`, 'DELETE');
3164
- auditLogger.log({
3165
- operation: 'delete-plugin-step',
3166
- operationType: 'DELETE',
3167
- componentId: stepId,
3168
- componentType: 'PluginStep',
3169
- success: true,
3170
- executionTimeMs: timer(),
3171
- });
3172
- }
3173
- catch (error) {
3174
- auditLogger.log({
3175
- operation: 'delete-plugin-step',
3176
- operationType: 'DELETE',
3177
- componentId: stepId,
3178
- componentType: 'PluginStep',
3179
- success: false,
3180
- error: error.message,
3181
- executionTimeMs: timer(),
3182
- });
3183
- // Log but don't throw - rollback should continue even if individual deletes fail
3184
- console.error(`Warning: Failed to delete plugin step ${stepId}: ${error.message}`);
3185
- }
569
+ async cloneFlow(sourceFlowId, newName, options) {
570
+ return this.workflowManagement.cloneFlow(sourceFlowId, newName, options);
3186
571
  }
3187
572
  /**
3188
- * Register a plugin step on an SDK message
3189
- * @param options - Step registration options
3190
- * @returns Created step ID
573
+ * Activate a Power Automate flow (alias for activateWorkflow)
3191
574
  */
3192
- async registerPluginStep(options) {
3193
- const timer = auditLogger.startTimer();
3194
- try {
3195
- // Resolve SDK message and filter IDs
3196
- const { messageId, filterId } = await this.resolveSdkMessageAndFilter(options.messageName, options.primaryEntityName);
3197
- // Create step payload
3198
- const stepData = {
3199
- name: options.name,
3200
- 'plugintypeid@odata.bind': `/plugintypes(${options.pluginTypeId})`,
3201
- 'sdkmessageid@odata.bind': `/sdkmessages(${messageId})`,
3202
- 'sdkmessagefilterid@odata.bind': `/sdkmessagefilters(${filterId})`,
3203
- stage: options.stage,
3204
- mode: options.executionMode,
3205
- rank: options.rank ?? 1,
3206
- supporteddeployment: options.supportedDeployment ?? 0,
3207
- statuscode: 1 // Active
3208
- };
3209
- // Add optional fields
3210
- if (options.filteringAttributes) {
3211
- stepData.filteringattributes = options.filteringAttributes;
3212
- }
3213
- if (options.configuration) {
3214
- stepData.configuration = options.configuration;
3215
- }
3216
- // Register step
3217
- const createResponse = await this.makeRequest('api/data/v9.2/sdkmessageprocessingsteps', 'POST', stepData, { 'Prefer': 'return=representation' });
3218
- const stepId = createResponse.sdkmessageprocessingstepid || createResponse.id;
3219
- if (!stepId) {
3220
- throw new Error('Plugin step created but ID not returned');
3221
- }
3222
- // Add to solution if specified
3223
- if (options.solutionUniqueName) {
3224
- await this.addComponentToSolution(options.solutionUniqueName, stepId, 92 // Component type 92 = SDKMessageProcessingStep
3225
- );
3226
- }
3227
- auditLogger.log({
3228
- operation: 'register-plugin-step',
3229
- operationType: 'CREATE',
3230
- componentId: stepId,
3231
- componentType: 'PluginStep',
3232
- success: true,
3233
- parameters: {
3234
- name: options.name,
3235
- messageName: options.messageName,
3236
- primaryEntity: options.primaryEntityName,
3237
- stage: options.stage,
3238
- mode: options.executionMode
3239
- },
3240
- executionTimeMs: timer(),
3241
- });
3242
- return { stepId };
3243
- }
3244
- catch (error) {
3245
- auditLogger.log({
3246
- operation: 'register-plugin-step',
3247
- operationType: 'CREATE',
3248
- componentName: options.name,
3249
- componentType: 'PluginStep',
3250
- success: false,
3251
- error: error.message,
3252
- executionTimeMs: timer(),
3253
- });
3254
- throw error;
3255
- }
575
+ async activateFlow(flowId) {
576
+ return this.activateWorkflow(flowId);
3256
577
  }
3257
578
  /**
3258
- * Register a pre/post image for a plugin step
3259
- * @param options - Image registration options
3260
- * @returns Created image ID
579
+ * Deactivate a Power Automate flow (alias for deactivateWorkflow)
3261
580
  */
3262
- async registerPluginImage(options) {
3263
- const timer = auditLogger.startTimer();
3264
- try {
3265
- // Create image payload
3266
- const imageData = {
3267
- name: options.name,
3268
- 'sdkmessageprocessingstepid@odata.bind': `/sdkmessageprocessingsteps(${options.stepId})`,
3269
- imagetype: options.imageType,
3270
- entityalias: options.entityAlias,
3271
- messagepropertyname: options.messagePropertyName || 'Target'
3272
- };
3273
- // Add attributes if specified (empty string = all attributes)
3274
- if (options.attributes !== undefined) {
3275
- imageData.attributes = options.attributes;
3276
- }
3277
- // Register image
3278
- const createResponse = await this.makeRequest('api/data/v9.2/sdkmessageprocessingstepimages', 'POST', imageData, { 'Prefer': 'return=representation' });
3279
- const imageId = createResponse.sdkmessageprocessingstepimageid || createResponse.id;
3280
- if (!imageId) {
3281
- throw new Error('Plugin image created but ID not returned');
3282
- }
3283
- auditLogger.log({
3284
- operation: 'register-plugin-image',
3285
- operationType: 'CREATE',
3286
- componentId: imageId,
3287
- componentType: 'PluginImage',
3288
- success: true,
3289
- parameters: {
3290
- name: options.name,
3291
- imageType: options.imageType,
3292
- stepId: options.stepId
3293
- },
3294
- executionTimeMs: timer(),
3295
- });
3296
- return { imageId };
3297
- }
3298
- catch (error) {
3299
- auditLogger.log({
3300
- operation: 'register-plugin-image',
3301
- operationType: 'CREATE',
3302
- componentName: options.name,
3303
- componentType: 'PluginImage',
3304
- success: false,
3305
- error: error.message,
3306
- executionTimeMs: timer(),
3307
- });
3308
- throw error;
3309
- }
581
+ async deactivateFlow(flowId) {
582
+ return this.deactivateWorkflow(flowId);
3310
583
  }
3311
584
  }
3312
585
  //# sourceMappingURL=PowerPlatformService.js.map