powerplatform-mcp
Version:
PowerPlatform Model Context Protocol server
331 lines (330 loc) • 14.7 kB
JavaScript
/**
* WorkflowService
*
* Read-only service for classic Dynamics workflows.
*/
const WORKFLOW_CATEGORIES = {
0: 'Background',
1: 'On-Demand',
2: 'Business Rule',
3: 'Action',
4: 'Business Process Flow',
};
export class WorkflowService {
client;
constructor(client) {
this.client = client;
}
/**
* Get all classic Dynamics workflows in the environment
*/
async getWorkflows(activeOnly = false, maxRecords = 25) {
const stateFilter = activeOnly ? ' and statecode eq 1' : '';
const requestLimit = maxRecords + 1;
const workflows = await this.client.get(`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,_ownerid_value&$expand=modifiedby($select=fullname)&$orderby=modifiedon desc&$top=${requestLimit}`);
const hasMore = workflows.value.length > maxRecords;
const trimmedWorkflows = hasMore
? workflows.value.slice(0, maxRecords)
: workflows.value;
const formattedWorkflows = trimmedWorkflows.map((workflow) => ({
workflowid: workflow.workflowid,
name: workflow.name,
description: workflow.description,
state: workflow.statecode === 0
? 'Draft'
: workflow.statecode === 1
? 'Activated'
: 'Suspended',
statecode: workflow.statecode,
statuscode: workflow.statuscode,
type: workflow.type === 1
? 'Definition'
: workflow.type === 2
? 'Activation'
: 'Template',
mode: workflow.mode === 0 ? 'Background' : 'Real-time',
primaryEntity: workflow.primaryentity,
isManaged: workflow.ismanaged,
isOnDemand: workflow.ondemand,
triggerOnCreate: workflow.triggeroncreate,
triggerOnDelete: workflow.triggerondelete,
isSubprocess: workflow.subprocess,
ownerId: workflow._ownerid_value,
modifiedOn: workflow.modifiedon,
modifiedBy: workflow.modifiedby?.fullname,
createdOn: workflow.createdon,
}));
return {
totalCount: formattedWorkflows.length,
hasMore,
requestedMax: maxRecords,
workflows: formattedWorkflows,
};
}
/**
* Get all non-cloud-flow workflows (background, on-demand, business rules, actions, BPFs).
* Useful for cross-environment comparison.
*/
async getOotbWorkflows(options) {
const { maxRecords = 500, categories } = options ?? {};
let categoryFilter;
if (categories?.length) {
categoryFilter = categories.map((c) => `category eq ${c}`).join(' or ');
categoryFilter = `(${categoryFilter})`;
}
else {
categoryFilter = 'category ne 5';
}
const response = await this.client.get(`api/data/v9.2/workflows?$filter=${categoryFilter}&$select=workflowid,name,category,statecode,statuscode,ismanaged,mode,primaryentity,modifiedon&$orderby=name&$top=${maxRecords}`);
const workflows = response.value.map((wf) => ({
workflowid: wf.workflowid,
name: wf.name,
category: wf.category,
categoryName: WORKFLOW_CATEGORIES[wf.category] ?? `Other (${wf.category})`,
state: wf.statecode === 0 ? 'Draft'
: wf.statecode === 1 ? 'Activated'
: 'Suspended',
statecode: wf.statecode,
mode: wf.mode === 0 ? 'Background' : 'Real-time',
primaryEntity: wf.primaryentity ?? null,
isManaged: wf.ismanaged,
modifiedOn: wf.modifiedon,
}));
const byCategoryCount = {};
for (const wf of workflows) {
byCategoryCount[wf.categoryName] = (byCategoryCount[wf.categoryName] ?? 0) + 1;
}
return {
totalCount: workflows.length,
byCategoryCount,
workflows,
};
}
/**
* Get a specific classic workflow with its complete XAML definition
*/
async getWorkflowDefinition(workflowId, summary = false) {
const workflow = await this.client.get(`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,_ownerid_value&$expand=modifiedby($select=fullname),createdby($select=fullname)`);
const baseResult = {
workflowid: workflow.workflowid,
name: workflow.name,
description: workflow.description,
state: workflow.statecode === 0
? 'Draft'
: workflow.statecode === 1
? 'Activated'
: 'Suspended',
statecode: workflow.statecode,
statuscode: workflow.statuscode,
type: workflow.type === 1
? 'Definition'
: workflow.type === 2
? 'Activation'
: 'Template',
category: workflow.category,
mode: workflow.mode === 0 ? 'Background' : 'Real-time',
primaryEntity: workflow.primaryentity,
isManaged: workflow.ismanaged,
isOnDemand: workflow.ondemand,
triggerOnCreate: workflow.triggeroncreate,
triggerOnDelete: workflow.triggerondelete,
triggerOnUpdateAttributes: workflow.triggeronupdateattributelist
? workflow.triggeronupdateattributelist.split(',')
: [],
isSubprocess: workflow.subprocess,
syncWorkflowLogOnFailure: workflow.syncworkflowlogonfailure,
ownerId: workflow._ownerid_value,
createdOn: workflow.createdon,
createdBy: workflow.createdby?.fullname,
modifiedOn: workflow.modifiedon,
modifiedBy: workflow.modifiedby?.fullname,
};
if (summary) {
const workflowSummary = this.parseWorkflowXamlSummary(workflow.xaml);
return {
...baseResult,
summary: workflowSummary,
note: 'Use summary=false to get the full XAML definition',
};
}
return {
...baseResult,
xaml: workflow.xaml,
};
}
/**
* Parse workflow XAML to extract a structured summary
*/
parseWorkflowXamlSummary(xaml) {
if (!xaml) {
return { error: 'No XAML definition available' };
}
const summary = {
activities: [],
activityCount: 0,
hasConditions: false,
hasWaitConditions: false,
sendsEmail: false,
createsRecords: false,
updatesRecords: false,
assignsRecords: false,
callsChildWorkflows: false,
variables: [],
xamlSize: xaml.length,
tablesModified: new Set(),
triggerInfo: 'manual',
triggerFields: [],
};
try {
// SendEmail activities
const emailPattern = /<(SendEmail|mxswa:SendEmail)[^>]*>/gi;
const emailCount = (xaml.match(emailPattern) || []).length;
if (emailCount > 0) {
summary.sendsEmail = true;
summary.activities.push({
type: 'SendEmail',
count: emailCount,
});
}
// CreateEntity activities
const createPattern = /<(CreateEntity|mxswa:CreateEntity)[^>]*>/gi;
const createCount = (xaml.match(createPattern) || []).length;
if (createCount > 0) {
summary.createsRecords = true;
summary.activities.push({
type: 'CreateEntity',
count: createCount,
});
}
// UpdateEntity activities
const updatePattern = /<(UpdateEntity|mxswa:UpdateEntity)[^>]*>/gi;
const updateCount = (xaml.match(updatePattern) || []).length;
if (updateCount > 0) {
summary.updatesRecords = true;
summary.activities.push({
type: 'UpdateEntity',
count: updateCount,
});
}
// AssignEntity activities
const assignPattern = /<(AssignEntity|mxswa:AssignEntity)[^>]*>/gi;
const assignCount = (xaml.match(assignPattern) || []).length;
if (assignCount > 0) {
summary.assignsRecords = true;
summary.activities.push({
type: 'AssignEntity',
count: assignCount,
});
}
// SetState activities
const setStatePattern = /<(SetState|mxswa:SetState)[^>]*>/gi;
const setStateCount = (xaml.match(setStatePattern) || []).length;
if (setStateCount > 0) {
summary.activities.push({
type: 'SetState',
count: setStateCount,
});
}
// Extract tables modified
const createEntityMatches = xaml.matchAll(/<CreateEntity[^>]+EntitySchemaName="([^"]+)"/g);
for (const match of createEntityMatches) {
summary.tablesModified.add(match[1].toLowerCase());
}
const updateEntityMatches = xaml.matchAll(/<UpdateEntity[^>]+EntitySchemaName="([^"]+)"/g);
for (const match of updateEntityMatches) {
summary.tablesModified.add(match[1].toLowerCase());
}
const assignEntityMatches = xaml.matchAll(/<AssignEntity[^>]+EntitySchemaName="([^"]+)"/g);
for (const match of assignEntityMatches) {
summary.tablesModified.add(match[1].toLowerCase());
}
const setStateEntityMatches = xaml.matchAll(/<SetState[^>]+EntitySchemaName="([^"]+)"/g);
for (const match of setStateEntityMatches) {
summary.tablesModified.add(match[1].toLowerCase());
}
// Conditions
const conditionPattern = /<(Condition|mxswa:ConditionBranch|If|Switch)[^>]*>/gi;
const conditionCount = (xaml.match(conditionPattern) || []).length;
if (conditionCount > 0) {
summary.hasConditions = true;
summary.activities.push({
type: 'Condition',
count: conditionCount,
});
}
// Wait conditions
const waitPattern = /<(Wait|mxswa:Wait|WaitConditionBranch)[^>]*>/gi;
const waitCount = (xaml.match(waitPattern) || []).length;
if (waitCount > 0) {
summary.hasWaitConditions = true;
summary.activities.push({
type: 'Wait',
count: waitCount,
});
}
// Child workflow calls
const childWorkflowPattern = /<(ExecuteWorkflow|mxswa:ExecuteWorkflow)[^>]*>/gi;
const childWorkflowCount = (xaml.match(childWorkflowPattern) || [])
.length;
if (childWorkflowCount > 0) {
summary.callsChildWorkflows = true;
summary.activities.push({
type: 'ExecuteWorkflow',
count: childWorkflowCount,
});
}
// Custom workflow activities
const customActivityPattern = /<(mxswa:ActivityReference|WorkflowActivity)[^>]*TypeName="([^"]*)"[^>]*>/gi;
const customMatches = xaml.matchAll(customActivityPattern);
for (const match of customMatches) {
summary.activities.push({
type: 'CustomActivity',
typeName: match[2],
});
}
// Extract variables
const variablePattern = /<Variable[^>]*x:Name="([^"]*)"[^>]*x:TypeArguments="([^"]*)"[^>]*>/gi;
const varMatches = xaml.matchAll(variablePattern);
for (const match of varMatches) {
summary.variables.push({ name: match[1], type: match[2] });
}
// Extract trigger information
const triggerOnCreateMatch = xaml.match(/<TriggerOnCreate>([^<]+)<\/TriggerOnCreate>/);
const triggerOnDeleteMatch = xaml.match(/<TriggerOnDelete>([^<]+)<\/TriggerOnDelete>/);
const primaryEntityMatch = xaml.match(/<PrimaryEntity>([^<]+)<\/PrimaryEntity>/);
if (primaryEntityMatch) {
const primaryEntity = primaryEntityMatch[1];
if (triggerOnCreateMatch && triggerOnCreateMatch[1] === 'true') {
summary.triggerInfo = `${primaryEntity}.create`;
}
else if (triggerOnDeleteMatch && triggerOnDeleteMatch[1] === 'true') {
summary.triggerInfo = `${primaryEntity}.delete`;
}
else {
const triggerOnUpdateMatch = xaml.match(/<TriggerOnUpdate>([^<]+)<\/TriggerOnUpdate>/);
if (triggerOnUpdateMatch && triggerOnUpdateMatch[1] === 'true') {
summary.triggerInfo = `${primaryEntity}.update`;
const filteringAttributesMatch = xaml.match(/<FilteringAttributes>([^<]+)<\/FilteringAttributes>/);
if (filteringAttributesMatch) {
const attrs = filteringAttributesMatch[1]
.split(',')
.map((a) => a.trim())
.filter((a) => a);
summary.triggerFields = attrs;
}
}
else {
summary.triggerInfo = 'manual';
}
}
}
summary.activityCount = summary.activities.length;
// Convert Set to array
summary.tablesModified = Array.from(summary.tablesModified);
}
catch {
summary.parseError = 'Partial parse - some information may be missing';
}
return summary;
}
}