@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.
- package/build/PowerPlatformService.d.ts +185 -735
- package/build/PowerPlatformService.d.ts.map +1 -1
- package/build/PowerPlatformService.js +451 -3178
- package/build/PowerPlatformService.js.map +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +207 -68
- package/build/index.js.map +1 -1
- package/package.json +3 -5
|
@@ -1,3312 +1,585 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
102
|
+
return this.metadata.getGlobalOptionSet(optionSetName);
|
|
216
103
|
}
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
826
|
-
|
|
827
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1637
|
-
|
|
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 '<';
|
|
1956
|
-
case '>': return '>';
|
|
1957
|
-
case '&': return '&';
|
|
1958
|
-
case '\'': return ''';
|
|
1959
|
-
case '"': return '"';
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2276
|
-
}
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
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
|
|
233
|
+
return this.attribute.createGlobalOptionSetAttribute(entityLogicalName, attributeDefinition, options?.solutionUniqueName);
|
|
2294
234
|
}
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2460
|
-
}
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
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
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
308
|
+
return this.form.deactivateForm(formId);
|
|
2500
309
|
}
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2539
|
-
|
|
344
|
+
// =====================================================
|
|
345
|
+
// WEB RESOURCE CUSTOMIZATION METHODS
|
|
346
|
+
// =====================================================
|
|
2540
347
|
async getWebResource(webResourceId) {
|
|
2541
|
-
return
|
|
348
|
+
return this.webResource.getWebResource(webResourceId);
|
|
2542
349
|
}
|
|
2543
|
-
/**
|
|
2544
|
-
* Get web resources by name pattern
|
|
2545
|
-
*/
|
|
2546
350
|
async getWebResources(nameFilter) {
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2575
|
-
|
|
371
|
+
// =====================================================
|
|
372
|
+
// SOLUTION CUSTOMIZATION METHODS
|
|
373
|
+
// =====================================================
|
|
2576
374
|
async getPublishers() {
|
|
2577
|
-
return
|
|
2578
|
-
}
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
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
|
|
390
|
+
return this.solution.getSolutions();
|
|
2590
391
|
}
|
|
2591
|
-
/**
|
|
2592
|
-
* Get solution by unique name
|
|
2593
|
-
*/
|
|
2594
392
|
async getSolution(uniqueName) {
|
|
2595
|
-
|
|
2596
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2665
|
-
|
|
2666
|
-
async
|
|
2667
|
-
|
|
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
|
-
|
|
2675
|
-
await this.publishXml(parameterXml);
|
|
429
|
+
return this.publishing.publishEntity(entityLogicalName);
|
|
2676
430
|
}
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
async
|
|
2681
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2704
|
-
|
|
2705
|
-
async
|
|
2706
|
-
return
|
|
2707
|
-
|
|
2708
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2811
|
-
|
|
2812
|
-
async
|
|
2813
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
2938
|
-
const
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
2982
|
-
const
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
const
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
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
|
-
*
|
|
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
|
|
3082
|
-
|
|
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
|
|
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
|
|
3129
|
-
|
|
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
|
-
*
|
|
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
|
|
3161
|
-
|
|
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
|
-
*
|
|
3189
|
-
* @param options - Step registration options
|
|
3190
|
-
* @returns Created step ID
|
|
573
|
+
* Activate a Power Automate flow (alias for activateWorkflow)
|
|
3191
574
|
*/
|
|
3192
|
-
async
|
|
3193
|
-
|
|
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
|
-
*
|
|
3259
|
-
* @param options - Image registration options
|
|
3260
|
-
* @returns Created image ID
|
|
579
|
+
* Deactivate a Power Automate flow (alias for deactivateWorkflow)
|
|
3261
580
|
*/
|
|
3262
|
-
async
|
|
3263
|
-
|
|
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
|