Skip to content

Commit 6efe471

Browse files
committed
Added "sf texei source flow convert" command
1 parent 88f409a commit 6efe471

File tree

6 files changed

+248
-4
lines changed

6 files changed

+248
-4
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# summary
2+
3+
Convert a Flow to a Subflow
4+
5+
# description
6+
7+
Convert a Flow to a Subflow
8+
9+
# flags.name.summary
10+
11+
Name of the Flow to convert
12+
13+
# flags.path.summary
14+
15+
Path of flows folder (default: force-app/main/default/flows)
16+
17+
# flags.path.save-to-file-name
18+
19+
Name of for new Flow file. If not provided, converted source Flow is overridden
20+
21+
# examples
22+
23+
- sf texei source flow convert --name My_Flow
24+
25+
# warning.beta
26+
27+
This command is in BETA, test the converted Flow, and report any issue at https://github.com/texei/texei-sfdx-plugin/issues
28+
29+
# warning.filters
30+
31+
The source Flow has Entry Conditions that can't be moved to Subflow, review them and add them either as a decision node or to the parent Flow according to the needs

package.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,17 @@
105105
},
106106
"data": {
107107
"description": "Commands to manage data"
108+
},
109+
"source": {
110+
"external": true,
111+
"subtopics": {
112+
"flow": {
113+
"description": "Convert a Flow to a Subflow"
114+
}
115+
}
108116
}
109-
}
117+
},
118+
"external": true
110119
}
111120
}
112121
},
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
export type FlowMetadataType = {
2+
Flow: Flow;
3+
};
4+
5+
export type Flow = {
6+
interviewLabel: string;
7+
label: string;
8+
status: string;
9+
start?: {
10+
locationX: number;
11+
locationY: number;
12+
connector: {
13+
targetReference: string;
14+
};
15+
object?: string;
16+
recordTriggerType?: string;
17+
triggerType?: string;
18+
filterFormula?: string;
19+
filters?: FlowRecordFilter[];
20+
};
21+
variables?: FlowVariable[];
22+
};
23+
24+
export type FlowVariable = {
25+
name: string;
26+
dataType: string;
27+
isCollection: boolean;
28+
isInput: boolean;
29+
isOutput: true;
30+
objectType: string;
31+
};
32+
33+
export type FlowRecordFilter = {
34+
field: string;
35+
operator: string;
36+
value: FlowElementReferenceOrValue;
37+
};
38+
39+
export type FlowElementReferenceOrValue = {
40+
apexValue?: string;
41+
booleanValue?: boolean;
42+
};
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/* eslint-disable no-console */
2+
import * as fs from 'node:fs';
3+
import * as path from 'node:path';
4+
import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
5+
import { Messages } from '@salesforce/core';
6+
import { XMLParser, XMLBuilder } from 'fast-xml-parser';
7+
import { getDefaultPackagePath } from '../../../../shared/sfdxProjectFolder';
8+
import { FlowMetadataType, FlowVariable } from './MetadataTypes';
9+
10+
Messages.importMessagesDirectory(__dirname);
11+
const messages = Messages.loadMessages('texei-sfdx-plugin', 'texei.source.flow.convert');
12+
13+
export type TexeiSourceFlowConvertResult = {
14+
convertedFlowPath: string;
15+
};
16+
17+
export default class TexeiSourceFlowConvert extends SfCommand<TexeiSourceFlowConvertResult> {
18+
public static readonly summary = messages.getMessage('summary');
19+
public static readonly description = messages.getMessage('description');
20+
public static readonly examples = messages.getMessages('examples');
21+
22+
// TODO: add flag for converted flow name
23+
public static readonly flags = {
24+
name: Flags.string({
25+
summary: messages.getMessage('flags.name.summary'),
26+
char: 'n',
27+
required: true,
28+
}),
29+
path: Flags.string({ char: 'p', required: false, summary: messages.getMessage('flags.path.summary') }),
30+
'save-to-file-name': Flags.string({
31+
char: 's',
32+
required: false,
33+
summary: messages.getMessage('flags.path.save-to-file-name'),
34+
}),
35+
};
36+
37+
public async run(): Promise<TexeiSourceFlowConvertResult> {
38+
const { flags } = await this.parse(TexeiSourceFlowConvert);
39+
40+
let savedFlowPath = '';
41+
42+
this.warn(messages.getMessage('warning.beta'));
43+
44+
const flowName = flags.name.endsWith('.flow-meta.xml') ? flags.name : `${flags.name}.flow-meta.xml`;
45+
const flowFolderPath: string = flags.path
46+
? path.join(flags.path, 'flows')
47+
: path.join(await getDefaultPackagePath(), 'flows');
48+
const flowPath: string = path.join(flowFolderPath, flowName);
49+
50+
// Check if flow exists
51+
if (fs.existsSync(flowPath)) {
52+
// Read data file
53+
const data = fs.readFileSync(flowPath, 'utf8');
54+
55+
// Parsing file
56+
const xmlParserBuilderOptions = {
57+
ignoreAttributes: false,
58+
removeNSPrefix: false,
59+
numberParseOptions: {
60+
hex: false,
61+
leadingZeros: false,
62+
skipLike: /\.[0-9]*0/,
63+
},
64+
format: true,
65+
};
66+
67+
const parser = new XMLParser(xmlParserBuilderOptions);
68+
let flowJson: FlowMetadataType = parser.parse(data) as FlowMetadataType;
69+
70+
const targetObject = flowJson?.Flow?.start?.object as string;
71+
72+
// Check if $Record or $ Record__Prior are used, if so create variables to replace them
73+
let flowAsString = JSON.stringify(flowJson);
74+
const variables: FlowVariable[] = [];
75+
76+
// Converting $Record__Prior to 'RecordPrior' variable if needed
77+
if (flowAsString.includes('$Record__Prior')) {
78+
const recordPriorVariable: FlowVariable = {
79+
name: 'RecordPrior',
80+
dataType: 'SObject',
81+
isCollection: false,
82+
isInput: true,
83+
isOutput: true,
84+
objectType: targetObject,
85+
};
86+
variables.push(recordPriorVariable);
87+
88+
// Replace $Record__Prior variable in Flow by new RecordPrior variable
89+
flowAsString = flowAsString.replaceAll('$Record__Prior', 'RecordPrior');
90+
}
91+
92+
// Converting $Record to 'Record' variable if needed
93+
if (flowAsString.includes('$Record')) {
94+
const recordVariable: FlowVariable = {
95+
name: 'Record',
96+
dataType: 'SObject',
97+
isCollection: false,
98+
isInput: true,
99+
isOutput: true,
100+
objectType: targetObject,
101+
};
102+
variables.push(recordVariable);
103+
104+
// Replace $Record variable in Flow by new Record variable
105+
flowAsString = flowAsString.replaceAll('$Record', 'Record');
106+
}
107+
108+
flowJson = JSON.parse(flowAsString) as FlowMetadataType;
109+
110+
if (variables) {
111+
// Check if variables were already part of the existing metadata
112+
if (flowJson.Flow?.variables === undefined) {
113+
// No variables, just add them
114+
flowJson.Flow.variables = variables;
115+
} else if (Array.isArray(flowJson.Flow?.variables)) {
116+
// variables are already an array, just add the new values to it
117+
flowJson.Flow.variables = flowJson.Flow.variables.concat(variables);
118+
} else {
119+
// there was only one variable, put them all together in an array
120+
variables.push(flowJson.Flow?.variables);
121+
flowJson.Flow.variables = variables;
122+
}
123+
}
124+
125+
// Deleting nodes not part of a subflow metadata
126+
delete flowJson.Flow?.start?.object;
127+
delete flowJson.Flow?.start?.recordTriggerType;
128+
delete flowJson.Flow?.start?.triggerType;
129+
130+
if (flowJson.Flow?.start?.filterFormula || flowJson.Flow?.start?.filters) {
131+
delete flowJson.Flow?.start?.filterFormula;
132+
delete flowJson.Flow?.start?.filters;
133+
134+
this.warn(messages.getMessage('warning.filters'));
135+
}
136+
137+
// Writing the new flow;
138+
const builder = new XMLBuilder(xmlParserBuilderOptions);
139+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
140+
const flowXml = builder.build(flowJson);
141+
142+
if (flags['save-to-file-name']) {
143+
const newFlowName = flags['save-to-file-name'].endsWith('.flow-meta.xml')
144+
? flags['save-to-file-name']
145+
: `${flags['save-to-file-name']}.flow-meta.xml`;
146+
savedFlowPath = path.join(flowFolderPath, newFlowName);
147+
} else {
148+
savedFlowPath = flowPath;
149+
}
150+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
151+
fs.writeFileSync(savedFlowPath, flowXml, 'utf8');
152+
} else {
153+
this.warn(`Flow ${flowPath} doesn't exist`);
154+
}
155+
156+
this.log(`Flow converted at ${savedFlowPath}`);
157+
158+
return {
159+
convertedFlowPath: savedFlowPath,
160+
};
161+
}
162+
}

test/helpers/init.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
const path = require('path')
2-
process.env.TS_NODE_PROJECT = path.resolve('test/tsconfig.json')
1+
const path = require('path');
2+
process.env.TS_NODE_PROJECT = path.resolve('test/tsconfig.json');

tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"rootDir": "src",
66
"strictNullChecks": true,
77
"skipLibCheck": true,
8-
"lib": ["dom"]
8+
"lib": ["dom", "ES2023"]
99
},
1010
"include": ["./src/**/*.ts"]
1111
}

0 commit comments

Comments
 (0)