apn-app-manager 1.13.1 → 1.14.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -47,11 +47,11 @@ Used to duplicate all objects of an application, which replaces the namespace of
47
47
  - For example, an application with objects such as `SMP APP Artifacts` and `SMP_APP_displayUser` would have a namespace of `SMP_APP`
48
48
  - Note that this tool can only clone objects of one namepsace at a time with regular options.
49
49
  - Advanced options:
50
- 1. Select your application or package zip.
51
- 1. If cloning a package, select the zip file(s) that contain the entire application to ensure all references to objects not in the package are captured.
50
+ 1. Select your application or package zip you wish to clone.
51
+ 1. If cloning a package which is part of an application which is already cloned in the target environment, select the zip file(s) that your package from the first step contains references to, to ensure all references to objects not in the package are remapped. Use the same namespace configuration you did on the application(s) which were already cloned.
52
52
  1. Select your import customization properties file, if you have one.
53
53
  1. The tool will search through all object files and attempt to find every unique namespace, allowing you to specify cloning logic for each namepsace.
54
- 1. Database tables can be skipped during cloning.
54
+ 1. Select whether you want database table names (within CDTs and records) to be renamed or not during cloning.
55
55
  1. Access your cloned application in the generated `/out/` folder.
56
56
 
57
57
  #### Notes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apn-app-manager",
3
- "version": "1.13.1",
3
+ "version": "1.14.2",
4
4
  "description": "Appian App Manager",
5
5
  "license": "Apache-2.0",
6
6
  "preferGlobal": true,
@@ -22,6 +22,7 @@ const options = {
22
22
  const objectMaps = {};
23
23
  const uuidCollector = { uuids: [] };
24
24
  const namespaceCollector = {};
25
+ const problemWordCollector = { words: [] };
25
26
 
26
27
  let outZipPath;
27
28
  let icfPath;
@@ -37,7 +38,7 @@ function main(callback) {
37
38
  fileSelect.selectSingleFile("Select application or package zip", ["zip"], false, function (appZipPath) {
38
39
  outZipPath = deriveOutPath(appZipPath);
39
40
  fileSelect.selectMultipleFiles(
40
- "Select base application(s) and package(s) with objects that are referenced in the first zip selection",
41
+ "Select application(s) and package(s) that are referenced by the zip you specified to clone",
41
42
  ["zip"],
42
43
  [appZipPath],
43
44
  function (allAppZipPaths) {
@@ -99,36 +100,43 @@ function executeClone(callback) {
99
100
  }
100
101
  }
101
102
 
102
- util.writeJson(path.resolve(cons.outDir, `objects.json`), objectMaps);
103
-
104
103
  // Update the object maps we are cloning based on the application prefix
105
- util.print(`Cloning the objects files...`);
106
- obMapsUpdates.renameObjectMaps(objectMaps, uuidCollector, namespaceCollector, options, obProps.usageTypes.CLONE);
104
+ obMapsUpdates.renameObjectMaps(objectMaps, uuidCollector, namespaceCollector, problemWordCollector, options, obProps.usageTypes.CLONE);
107
105
 
108
- // These output files are helpful for debugging
109
- util.writeJson(path.resolve(cons.outDir, `objects.json`), objectMaps);
110
- util.writeJson(path.resolve(cons.outDir, `not-cloned-uuids.json`), uuidCollector);
106
+ // Prompts the user to tell us how to handle problem words
107
+ addressProblemWords(function () {
108
+ // These output files are helpful for debugging
109
+ util.writeJson(path.resolve(cons.outDir, `objects.json`), objectMaps);
110
+ util.writeJson(path.resolve(cons.outDir, `not-cloned-uuids.json`), uuidCollector);
111
+ if (!util.isBlank(namespaceCollector)) {
112
+ util.writeJson(path.resolve(cons.outDir, `namespaces.json`), namespaceCollector);
113
+ }
114
+ if (!util.isBlank(problemWordCollector.words)) {
115
+ util.writeJson(path.resolve(cons.outDir, `problem-terms.json`), problemWordCollector.words);
116
+ }
111
117
 
112
- // Modify the underlying object XML files & zip the package
113
- obModify.modifyObjectFiles(cons.tempDir, objectMaps, obProps.usageTypes.CLONE, []);
114
- util.zipDir(cons.tempDir, outZipPath, false);
115
- obModify.renameSingleFile(outZipPath, objectMaps, obProps.usageTypes.CLONE, []);
118
+ // Modify the underlying object XML files & zip the package
119
+ util.print(`Cloning the objects files...`);
120
+ obModify.modifyObjectFiles(cons.tempDir, objectMaps, problemWordCollector.words, obProps.usageTypes.CLONE, []);
121
+ util.zipDir(cons.tempDir, outZipPath, false);
122
+ obModify.renameSingleFile(outZipPath, objectMaps, problemWordCollector.words, obProps.usageTypes.CLONE, []);
116
123
 
117
- // Modify the ICF file
118
- obModify.modifySingleFile(icfPath, deriveOutPath(icfPath), objectMaps, obProps.usageTypes.CLONE, []);
124
+ // Modify the ICF file
125
+ obModify.modifySingleFile(icfPath, deriveOutPath(icfPath), objectMaps, problemWordCollector.words, obProps.usageTypes.CLONE, []);
119
126
 
120
- // Print warnings and output
121
- if (uuidCollector["uuids"].length > 0) {
127
+ // Print warnings and output
128
+ if (uuidCollector["uuids"].length > 0) {
129
+ util.printGap();
130
+ util.printWarning(
131
+ `${uuidCollector["uuids"].length} uuids were found in your zip but not parsed via cloning (see output file 'not-cloned-uuids.json'). Check the count of missing precedents from your app package in your source environment. If it is ${uuidCollector["uuids"].length} objects, then those are likely just the missing precedents, otherwise please reach out to the authors of this tool, since the XML structure of Appian objects has probably changed and this tool needs to be updated.`
132
+ );
133
+ }
122
134
  util.printGap();
123
- util.printWarning(
124
- `${uuidCollector["uuids"].length} uuids were found in your zip but not parsed via cloning (see output file 'not-cloned-uuids.json'). Check the count of missing precedents from your app package in your source environment. If it is ${uuidCollector["uuids"].length} objects, then those are likely just the missing precedents, otherwise please reach out to the authors of this tool, since the XML structure of Appian objects has probably changed and this tool needs to be updated.`
125
- );
126
- }
127
- util.printGap();
128
- util.print(`Cloning complete. Output can be found in the '/out/' folder (${path.resolve(cons.outDir)}).`);
135
+ util.print(`Cloning complete. Output can be found in the '/out/' folder (${path.resolve(cons.outDir)}).`);
129
136
 
130
- // Execute callback if passed
131
- util.execCallback(callback, outZipPath);
137
+ // Execute callback if passed
138
+ util.execCallback(callback, outZipPath);
139
+ });
132
140
  }
133
141
 
134
142
  // Returns the output path from an input path file
@@ -195,6 +203,55 @@ function mapNamespaces(onComplete) {
195
203
  obExtract.sortNamespaces(namespaceCollector).forEach(namespace => {
196
204
  util.print(` ${namespace} -> ${util.replaceBlank(namespaceCollector[namespace]["newNamespace"], `Will not clone`)}`, true);
197
205
  });
206
+ util.printGap();
207
+ onComplete();
208
+ }
209
+ }
210
+
211
+ // Prompts the user to tell us how to handle problem words
212
+ function addressProblemWords(onComplete) {
213
+ let problemWordsToDecide = lodash.filter(problemWordCollector.words, function (wordMap) {
214
+ return util.isBlank(wordMap["decision"]);
215
+ });
216
+ let isFirstIteration = problemWordsToDecide.length == problemWordCollector.words.length;
217
+ if (problemWordsToDecide.length > 0) {
218
+ let message;
219
+ if (isFirstIteration) {
220
+ message = `After analyzing the objects, ${problemWordCollector.words.length} potential problem terms were found. For each term, select what behavior you would like (remap or skip). The cloning tool assumes every reference to a term in the zip should be remapped, therefore it's recommended to skip terms that could also be XML tags or system object terms.`;
221
+ } else {
222
+ message = `Select another term`;
223
+ }
224
+ let problemWordsToDecideMap = {};
225
+ problemWordsToDecide.forEach(wordMap => {
226
+ let formattedMessage = `'${wordMap.current}' -> '${wordMap.new}' (object: '${wordMap.objectName}', property: '${wordMap.haulField}')`;
227
+ problemWordsToDecideMap[formattedMessage] = wordMap;
228
+ });
229
+ prompt.promptList(message, Object.keys(problemWordsToDecideMap), function (answer) {
230
+ let chosenProblemWordMap = problemWordsToDecideMap[answer];
231
+ let options = [];
232
+ Object.keys(cons.problemWordOptions).forEach(option => {
233
+ options.push(cons.problemWordOptions[option]);
234
+ });
235
+ let optionLabels = [];
236
+ options.forEach(option => {
237
+ optionLabels.push(option.label);
238
+ });
239
+ prompt.promptList(`Term: ${answer}. Choose what behavior to do when remapping this term.`, optionLabels, function (answer) {
240
+ let chosenDecision = lodash.filter(options, function (option) {
241
+ return option.label == answer;
242
+ })[0];
243
+ problemWordCollector.words.forEach(function (wordMap, index) {
244
+ if (wordMap.current == chosenProblemWordMap.current && wordMap.new == chosenProblemWordMap.new) {
245
+ problemWordCollector.words[index]["decision"] = chosenDecision.key;
246
+ }
247
+ });
248
+ addressProblemWords(onComplete);
249
+ });
250
+ });
251
+ } else {
252
+ if (problemWordCollector.words.length > 0) {
253
+ util.print(`All potential problem terms accounted for.`);
254
+ }
198
255
  onComplete();
199
256
  }
200
257
  }
package/src/cons.js CHANGED
@@ -44,4 +44,16 @@ module.exports = {
44
44
  ],
45
45
 
46
46
  appianUrlRegex: /^https:\/\/.*\/suite/g,
47
+
48
+ appianSystemObjectUuidRegex: /^SYSTEM_.*$/g,
49
+
50
+ // Problem words are words like "user" or "studio" which can easily map to
51
+ // existing platform terminology/XML tabs like <user></user> or #"SYSTEM_SYSRULES_studio_rule"
52
+ // Testing can be found here: https://regexr.com/827su
53
+ problemWordRegex: /^[a-z0-9]+$/g,
54
+
55
+ problemWordOptions: {
56
+ REMAP_ALL: { key: "REMAP_ALL", label: "Remap all references of this term" },
57
+ REMAP_NONE: { key: "REMAP_NONE", label: "Do not remap this term" },
58
+ },
47
59
  };
@@ -28,6 +28,9 @@ module.exports = {
28
28
  updateDatatypeUuids(objectMaps);
29
29
  },
30
30
 
31
+ // Returns the count of all objects in the objectMap
32
+ retrieveObjectMapCount: retrieveObjectMapCount,
33
+
31
34
  // Sorts the namespaces by their relative lengths
32
35
  sortNamespaces: function (namespaceCollector) {
33
36
  return lodash.flatten([
@@ -11,9 +11,9 @@ const { remove } = require("fs-extra");
11
11
  // Exports
12
12
  module.exports = {
13
13
  // Goes through all of the objectMaps and creates new name, uuid, etc.
14
- renameObjectMaps: function (objectMaps, uuidCollector, namespaceMap, options, usageType) {
14
+ renameObjectMaps: function (objectMaps, uuidCollector, namespaceMap, problemWordCollector, options, usageType) {
15
15
  // Update all objectMaps with new names
16
- renameObjectMaps(objectMaps, uuidCollector, namespaceMap, options, usageType);
16
+ renameObjectMaps(objectMaps, uuidCollector, namespaceMap, problemWordCollector, options, usageType);
17
17
  },
18
18
  };
19
19
 
@@ -22,13 +22,18 @@ module.exports = {
22
22
  // ---------------------------------------------------
23
23
 
24
24
  // Goes through all of the objectMaps and creates new name, uuid, etc.
25
- function renameObjectMaps(objectMaps, uuidCollector, namespaceMap, options, usageType) {
25
+ function renameObjectMaps(objectMaps, uuidCollector, namespaceMap, problemWordCollector, options, usageType) {
26
26
  obProps.haulTypes.forEach(haulType => {
27
27
  objectMaps[haulType.haulName].forEach(function (objectMap, objectMapIndex) {
28
28
  let curNamespaceForObject = findNamespaceForObject(objectMap, namespaceMap, haulType, objectMaps);
29
29
  let newNamespaceForObject;
30
- // Logic to determine if the object will be skipped, either not in the namespaceMap, or the newNamespace is null
31
- let objectWillNotBeCloned = Object.keys(namespaceMap).indexOf(curNamespaceForObject) < 0;
30
+ // Logic to determine if the object will be skipped, either not in the namespaceMap, or the newNamespace is null, or it's a system object
31
+ let uuidHaulField = lodash.filter(haulType.fields, function (haulField) {
32
+ return haulField.fieldName == obProps.fieldNames.UUID;
33
+ })[0];
34
+ let objectUuid = objectMap["current"][uuidHaulField.fieldName];
35
+ let objectWillNotBeCloned =
36
+ Object.keys(namespaceMap).indexOf(curNamespaceForObject) < 0 || objectUuid.match(cons.appianSystemObjectUuidRegex);
32
37
  if (!objectWillNotBeCloned) {
33
38
  newNamespaceForObject = namespaceMap[curNamespaceForObject]["newNamespace"];
34
39
  objectWillNotBeCloned = util.isBlank(newNamespaceForObject);
@@ -45,10 +50,14 @@ function renameObjectMaps(objectMaps, uuidCollector, namespaceMap, options, usag
45
50
  } else {
46
51
  // Object will be cloned
47
52
  objectMaps[haulType.haulName][objectMapIndex]["new"] = {};
53
+ let objectName = "";
48
54
  haulType.fields.forEach(haulField => {
49
55
  if (lodash.includes(haulField.usage, usageType)) {
50
56
  let newValue;
51
57
  let curValue = objectMap["current"][haulField.fieldName];
58
+ if (haulField.fieldName == obProps.fieldNames.NAME) {
59
+ objectName = curValue;
60
+ }
52
61
  removeFoundUuids(uuidCollector, haulField, curValue);
53
62
  if (haulField.fieldName == obProps.fieldNames.DB_TABLE && !options.renameDbTables) {
54
63
  newValue = curValue;
@@ -56,14 +65,17 @@ function renameObjectMaps(objectMaps, uuidCollector, namespaceMap, options, usag
56
65
  if (haulField.isArray) {
57
66
  let newValueArray = [];
58
67
  curValue.forEach(curValueIter => {
59
- newValueArray.push(renameObjectProp(curValueIter, haulField, curNamespaceForObject, newNamespaceForObject));
68
+ let newValueIter = renameObjectProp(curValueIter, haulField, curNamespaceForObject, newNamespaceForObject);
69
+ newValueArray.push(newValueIter);
70
+ checkProblemWord(curValueIter, newValueIter, objectName, haulField, haulType, problemWordCollector);
60
71
  });
61
72
  newValue = newValueArray;
62
73
  } else {
63
74
  newValue = renameObjectProp(curValue, haulField, curNamespaceForObject, newNamespaceForObject);
75
+ checkProblemWord(curValue, newValue, objectName, haulField, haulType, problemWordCollector);
64
76
  }
65
- objectMaps[haulType.haulName][objectMapIndex]["new"][haulField.fieldName] = newValue;
66
77
  }
78
+ objectMaps[haulType.haulName][objectMapIndex]["new"][haulField.fieldName] = newValue;
67
79
  }
68
80
  });
69
81
  }
@@ -174,6 +186,19 @@ function findNamespaceForObjectByName(objectName, namespaceMap) {
174
186
  return cons.noNamespace;
175
187
  }
176
188
 
189
+ // Check if a word might be a problem
190
+ function checkProblemWord(curValue, newValue, objectName, haulField, haulType, problemWordCollector) {
191
+ if (!util.isBlank(newValue) && curValue != newValue && curValue.match(cons.problemWordRegex)) {
192
+ problemWordCollector["words"].push({
193
+ current: curValue,
194
+ new: newValue,
195
+ objectName: objectName,
196
+ haulField: haulField.fieldName,
197
+ haulType: haulType.haulName,
198
+ });
199
+ }
200
+ }
201
+
177
202
  // Utility function to differenciate between debug logging and real logging
178
203
  function debug(input) {
179
204
  console.log(input);
@@ -18,10 +18,10 @@ module.exports = {
18
18
  modifyObjectFiles: modifyObjectFiles,
19
19
 
20
20
  // Modify a single file
21
- modifySingleFile: function (filePath, outPath, objectMaps, usageType, additionalReplacementMaps) {
21
+ modifySingleFile: function (filePath, outPath, objectMaps, problemWordsMaps, usageType, additionalReplacementMaps) {
22
22
  if (filePath) {
23
23
  // Convert the extracted names & uuids to a flat replacement array
24
- let replacementMaps = buildReplacementMaps(objectMaps, usageType, additionalReplacementMaps);
24
+ let replacementMaps = buildReplacementMaps(objectMaps, problemWordsMaps, usageType, additionalReplacementMaps);
25
25
  // Create the file we will update
26
26
  util.copyFile(filePath, outPath);
27
27
  // Actually modify the file
@@ -30,10 +30,10 @@ module.exports = {
30
30
  },
31
31
 
32
32
  // Renames a single file
33
- renameSingleFile: function (filePath, objectMaps, usageType, additionalReplacementMaps) {
33
+ renameSingleFile: function (filePath, objectMaps, problemWordsMaps, usageType, additionalReplacementMaps) {
34
34
  if (filePath) {
35
35
  // Convert the extracted names & uuids to a flat replacement array
36
- let replacementMaps = buildReplacementMaps(objectMaps, usageType, additionalReplacementMaps);
36
+ let replacementMaps = buildReplacementMaps(objectMaps, problemWordsMaps, usageType, additionalReplacementMaps);
37
37
  // Actually modify the file
38
38
  renameObjectFile(replacementMaps, filePath);
39
39
  }
@@ -45,9 +45,9 @@ module.exports = {
45
45
  // ---------------------------------------------------
46
46
 
47
47
  // Actually modifies all of the objects based on the objectMaps
48
- function modifyObjectFiles(appDir, objectMaps, usageType, additionalReplacementMaps) {
48
+ function modifyObjectFiles(appDir, objectMaps, problemWordsMaps, usageType, additionalReplacementMaps) {
49
49
  // Convert the extracted names & uuids to a flat replacement array
50
- let replacementMaps = buildReplacementMaps(objectMaps, usageType, additionalReplacementMaps);
50
+ let replacementMaps = buildReplacementMaps(objectMaps, problemWordsMaps, usageType, additionalReplacementMaps);
51
51
  // Determine how many files to change
52
52
  let totalCount = 0;
53
53
  util.handleDirFiles(appDir, ["xml", "xsd", "properties"], 3, 2, function (obPath) {
@@ -69,7 +69,7 @@ function modifyObjectFiles(appDir, objectMaps, usageType, additionalReplacementM
69
69
  }
70
70
 
71
71
  // Builds the replacementMaps object
72
- function buildReplacementMaps(objectMaps, usageType, additionalReplacementMaps) {
72
+ function buildReplacementMaps(objectMaps, problemWordsMaps, usageType, additionalReplacementMaps) {
73
73
  let replacementMaps = [];
74
74
  obProps.haulTypes.forEach(haulType => {
75
75
  haulType.fields.forEach(haulField => {
@@ -81,13 +81,19 @@ function buildReplacementMaps(objectMaps, usageType, additionalReplacementMaps)
81
81
  buildSingleReplacementMap(
82
82
  haulField,
83
83
  objectMap["current"][haulField.fieldName][index],
84
- objectMap["new"][haulField.fieldName][index]
84
+ objectMap["new"][haulField.fieldName][index],
85
+ problemWordsMaps
85
86
  )
86
87
  );
87
88
  });
88
89
  } else {
89
90
  replacementMaps.push(
90
- buildSingleReplacementMap(haulField, objectMap["current"][haulField.fieldName], objectMap["new"][haulField.fieldName])
91
+ buildSingleReplacementMap(
92
+ haulField,
93
+ objectMap["current"][haulField.fieldName],
94
+ objectMap["new"][haulField.fieldName],
95
+ problemWordsMaps
96
+ )
91
97
  );
92
98
  }
93
99
  });
@@ -104,9 +110,11 @@ function buildReplacementMaps(objectMaps, usageType, additionalReplacementMaps)
104
110
  }
105
111
 
106
112
  // Returns a single new value for the replacementMap
107
- function buildSingleReplacementMap(haulField, from, to) {
113
+ function buildSingleReplacementMap(haulField, from, to, problemWordsMaps) {
108
114
  if (to == undefined) {
109
115
  return null;
116
+ } else if (isReplacementAProblemWordtoSkip(from, to, problemWordsMaps)) {
117
+ return null;
110
118
  } else {
111
119
  let wordBounded = lodash.includes(obProps.wordBoundedReplacements, haulField.type);
112
120
  if (util.isBlank(haulField.replaceFormat)) {
@@ -121,6 +129,21 @@ function buildSingleReplacementMap(haulField, from, to) {
121
129
  }
122
130
  }
123
131
 
132
+ // Checks if a replacement is a problem word that should be skipped
133
+ function isReplacementAProblemWordtoSkip(from, to, problemWordsMaps) {
134
+ if (util.isBlank(problemWordsMaps)) {
135
+ return false;
136
+ }
137
+ let matches = lodash.filter(problemWordsMaps, function (map) {
138
+ return from == map["current"] && to == map["new"];
139
+ });
140
+ if (matches.length > 0) {
141
+ return matches[0]["decision"] == cons.problemWordOptions.REMAP_NONE["key"];
142
+ } else {
143
+ return false;
144
+ }
145
+ }
146
+
124
147
  // Modifies a single object files
125
148
  function modifyObjectFile(replacementMaps, fileCount, obPath, trackProgress) {
126
149
  let content = util.readFile(obPath);
@@ -138,7 +161,9 @@ function modifyObjectFile(replacementMaps, fileCount, obPath, trackProgress) {
138
161
 
139
162
  // Renames a single object file
140
163
  function renameObjectFile(replacementMaps, obPath) {
141
- util.renameFile(obPath, util.replaceMaps(obPath, replacementMaps));
164
+ let relativePath = path.relative(".", obPath).toString();
165
+ let newRelativePath = util.replaceMaps(relativePath, replacementMaps);
166
+ util.renameFile(obPath, path.resolve(newRelativePath));
142
167
  }
143
168
 
144
169
  // Utility function to differenciate between debug logging and real logging
@@ -9,7 +9,7 @@ const propTypes = {
9
9
  URL_STUB: "URL_STUB",
10
10
  };
11
11
 
12
- const wordBoundedReplacements = [propTypes.URL_STUB];
12
+ const wordBoundedReplacements = [propTypes.NAME, propTypes.URL_STUB];
13
13
 
14
14
  const obTypes = {
15
15
  DATATYPE: "datatype",
@@ -52,6 +52,7 @@ const fieldNames = {
52
52
  PAGE_UUIDS: "pageUuids",
53
53
  APP_PREFIX: "appPrefix",
54
54
  APP_ACTION_UUIDS: "appActionUuids",
55
+ RECORD_PLURAL_NAME: "pluralName",
55
56
  RECORD_ACTION_UUIDS: "recordActionUuids",
56
57
  RECORD_ACTION_REF_IDS: "recordActionRefIds",
57
58
  RECORD_FIELD_UUIDS: "fieldUuids",
@@ -286,6 +287,13 @@ const haulTypes = [
286
287
  usage: [usageTypes.CLONE, usageTypes.BUILD],
287
288
  isArray: true,
288
289
  },
290
+ {
291
+ fieldName: fieldNames.RECORD_PLURAL_NAME,
292
+ xpath: `recordTypeHaul/recordType/a:pluralName/node()`,
293
+ type: propTypes.TEXT,
294
+ usage: [usageTypes.CLONE, usageTypes.BUILD, usageTypes.RENAME],
295
+ replaceFormat: "<a:pluralName>{VALUE}</a:pluralName>",
296
+ },
289
297
  {
290
298
  fieldName: fieldNames.URL_STUB,
291
299
  xpath: `recordTypeHaul/recordType/a:urlStub/node()`,
package/src/util.js CHANGED
@@ -492,7 +492,9 @@ function replaceMaps(inStr, maps) {
492
492
  let replaceMaps = [];
493
493
 
494
494
  maps.forEach(map => {
495
- replaceMaps.push({ from: map.from, temp: buildNewUuid(), to: map.to, wordBounded: map.wordBounded });
495
+ if (!isBlank(map) && map.from != map.to) {
496
+ replaceMaps.push({ from: map.from, temp: buildNewUuid(), to: map.to, wordBounded: map.wordBounded });
497
+ }
496
498
  });
497
499
 
498
500
  replaceMaps = arrayByFieldLenth(replaceMaps, "from"); // Order the array by longest to shortest