apn-app-manager 1.2.0-beta.3 → 1.2.0

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
@@ -2,8 +2,6 @@
2
2
 
3
3
  `apn-app-manager` is a command line tool for managing Appian applications.
4
4
 
5
- Test beta version
6
-
7
5
  ## Why?
8
6
 
9
7
  When app building you may want to do things in bulk that are easiest to do via an application's exported XML files and a re-import. This tool helps to automate some of those tasks.
@@ -51,7 +49,8 @@ Used to duplicate all objects of an application, which replaces the namespace of
51
49
 
52
50
  1. Always inspect your cloned zip before importing to verify that what will be imported matches what you'd expect.
53
51
  1. The cloning tool is idempotent meaning it will always return the same output zip run against the same input. This also means you can develop an application, clone it, then enhance the original application, re-clone it, and that second clone will deploy over the first clone with objects being recognized as "Changed" as if you had just modified the clone directly.
54
- 1. It is highly recommended that all objects in your application have the same prefix (including objects that generally aren't prefixed such as applications, groups, process models, etc). Otherwise, the cloning tool will not be able to generate a unique name for these objects, and certain objects are required to have unique names.
52
+ 1. It is recommended that all objects in your application have the same namespace (including objects that generally aren't prefixed such as applications, groups, process models, etc). Objects that do not match your entered namespace will not be cloned.
53
+ - If you have multiple namespaces across your objects, you can run the cloner tool recursively against the output package to account for each namespace.
55
54
  1. SQL files for your application are not supported for cloning at this time. SQL scripts will have to be adjusted manually to align with the cloned objects (i.e. changing table name references).
56
55
  1. There are two other document generated in the `/out/` folder after cloning:
57
56
  - `objects.json` - This contains every attribute of collected metadata from every object in the package, including its current value and new value.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apn-app-manager",
3
- "version": "1.2.0-beta.3",
3
+ "version": "1.2.0",
4
4
  "description": "Appian App Manager",
5
5
  "license": "Apache-2.0",
6
6
  "preferGlobal": true,
@@ -26,7 +26,7 @@
26
26
  "npm": "^8.18.0",
27
27
  "npm-run": "^5.0.1",
28
28
  "path": "^0.12.7",
29
- "prompts": "^2.3.0",
29
+ "prompts": "^2.4.2",
30
30
  "properties-parser": "^0.3.1",
31
31
  "set-immediate-shim": "^2.0.0",
32
32
  "uuid": "^9.0.0",
@@ -20,113 +20,65 @@ function main(callback) {
20
20
  fileSelect.getAppPackage(function (appZipPath) {
21
21
  util.unzipDir(appZipPath, cons.tempDir);
22
22
  fileSelect.selectSingleFile("Select import customization properties file", ["properties"], true, function (icfPath) {
23
- executeClone(appZipPath, icfPath, callback);
23
+ promptNamespace("What is your current namespace?", function (curNamespace) {
24
+ promptNamespace("What is your new namespace?", function (newNamespace) {
25
+ const namespaceMap = {};
26
+ namespaceMap[curNamespace] = { newNamespace: newNamespace };
27
+ executeClone(appZipPath, icfPath, namespaceMap, callback);
28
+ });
29
+ });
24
30
  });
25
31
  });
26
32
  }
27
33
 
28
34
  // Executes all the steps of the clone
29
- function executeClone(appZipPath, icfPath, callback) {
30
- // Initialize variables based on the new namespace
35
+ function executeClone(appZipPath, icfPath, namespaceMap, callback) {
36
+ // Initialize output variables
31
37
  const outZipPath = path.resolve(cons.outDir, path.basename(appZipPath));
32
38
  const outIcfPath = icfPath ? path.resolve(cons.outDir, path.basename(icfPath)) : null;
33
39
 
34
40
  // Extract the metadata including the namespace from each object file
35
41
  const objectMaps = {};
36
42
  const uuidCollector = { uuids: [] };
37
- const namespaceCollector = {};
38
43
  util.print(`Collecting all metadata from the objects...`);
39
- obExtract.buildObjectMaps(objectMaps, uuidCollector, namespaceCollector, cons.tempDir, obProps.usageTypes.CLONE);
40
-
41
- // Have the user map each namespace to its proper behavior
42
- mapNamespaces(namespaceCollector, function onComplete() {
44
+ obExtract.buildObjectMaps(objectMaps, uuidCollector, cons.tempDir, obProps.usageTypes.CLONE);
45
+ let nonMatchingObjectCount =
46
+ obExtract.retrieveObjectMapCount(objectMaps) - obExtract.retrieveObjectMapCount(objectMaps, Object.keys(namespaceMap));
47
+ if (nonMatchingObjectCount > 0) {
48
+ util.printWarning(
49
+ `${nonMatchingObjectCount} objects found not matching namespace ${Object.keys(namespaceMap)}, these objects won't be cloned`
50
+ );
43
51
  util.printGap();
44
- // Update the object maps we are cloning based on the application prefix
45
- util.print(`Cloning the objects files...`);
46
- obMapsUpdates.renameObjectMaps(objectMaps, uuidCollector, namespaceCollector, obProps.usageTypes.CLONE);
47
-
48
- // These output files are helpful for debugging
49
- util.writeJson(path.resolve(cons.outDir, `objects.json`), objectMaps);
50
- util.writeJson(path.resolve(cons.outDir, `not-cloned-uuids.json`), uuidCollector);
51
- util.writeJson(path.resolve(cons.outDir, `namespaces.json`), namespaceCollector);
52
+ }
52
53
 
53
- // Modify the underlying object XML files & zip the package
54
- obModify.modifyObjectFiles(cons.tempDir, objectMaps, obProps.usageTypes.CLONE, []);
55
- util.zipDir(cons.tempDir, outZipPath, false);
56
- obModify.renameSingleFile(outZipPath, objectMaps, obProps.usageTypes.CLONE, []);
54
+ // Update the object maps we are cloning based on the application prefix
55
+ util.print(`Cloning the objects files...`);
56
+ obMapsUpdates.renameObjectMaps(objectMaps, uuidCollector, namespaceMap, obProps.usageTypes.CLONE);
57
57
 
58
- // Modify the ICF file
59
- obModify.modifySingleFile(icfPath, outIcfPath, objectMaps, obProps.usageTypes.CLONE, []);
58
+ // These output files are helpful for debugging
59
+ util.writeJson(path.resolve(cons.outDir, `objects.json`), objectMaps);
60
+ util.writeJson(path.resolve(cons.outDir, `not-cloned-uuids.json`), uuidCollector);
60
61
 
61
- // Print warnings and output
62
- if (uuidCollector["uuids"].length > 0) {
63
- util.printGap();
64
- util.printWarning(
65
- `WARNING: ${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.`
66
- );
67
- }
68
- util.printGap();
69
- util.print(`Cloning complete. Output can be found in the '/out/' folder (${path.resolve(cons.outDir)}).`);
62
+ // Modify the underlying object XML files & zip the package
63
+ obModify.modifyObjectFiles(cons.tempDir, objectMaps, obProps.usageTypes.CLONE, []);
64
+ util.zipDir(cons.tempDir, outZipPath, false);
65
+ obModify.renameSingleFile(outZipPath, objectMaps, obProps.usageTypes.CLONE, []);
70
66
 
71
- // Execute callback if passed
72
- util.execCallback(callback, outZipPath);
73
- });
74
- }
67
+ // Modify the ICF file
68
+ obModify.modifySingleFile(icfPath, outIcfPath, objectMaps, obProps.usageTypes.CLONE, []);
75
69
 
76
- // Prompts the user to select a namespace
77
- function mapNamespaces(namespaceCollector, onComplete) {
78
- let namespacesToChooseFrom = lodash.filter(obExtract.sortNamespaces(namespaceCollector), function (namespace) {
79
- return namespaceCollector[namespace]["newNamespace"] == null;
80
- });
81
- let isFirstIteration = namespacesToChooseFrom.length == Object.keys(namespaceCollector).length;
82
- if (namespacesToChooseFrom.length > 0) {
83
- let message;
84
- if (isFirstIteration) {
85
- message = `After analyzing the objects, ${
86
- Object.keys(namespaceCollector).length
87
- } potential unique namespaces were found. For each namespace, select what behavior you would like (clone or keep as-is). There may be overlap with sub-prefixes, in which case selecting a shorter namespace that matches ones with more prefixes will result in that behavior applying to all longer matching namespaces. For objects without a namespace, a prefix will automatically be added if they will be cloned. All namespaces with more than 1 prefix are represented with '_' delimiters, although the proper delimiter will automatically be determined by the object type.`;
88
- } else {
89
- message = `Select another namespace`;
90
- }
91
- let namespacesToChooseFromMap = {};
92
- namespacesToChooseFrom.forEach(namespace => {
93
- let subNamespaceCount =
94
- lodash.filter(namespacesToChooseFrom, function (otherNamespace) {
95
- return otherNamespace.startsWith(namespace);
96
- }).length - 1;
97
- let formattedNamespace =
98
- namespace == cons.noNamespace
99
- ? namespace
100
- : `${" ".repeat(namespace.split("_").length)}${namespace} [including ${subNamespaceCount} matching sub-namespace(s)]`;
101
- namespacesToChooseFromMap[formattedNamespace] = namespace;
102
- });
103
- prompt.promptList(message, Object.keys(namespacesToChooseFromMap), function (answer) {
104
- let chosenNamespace = namespacesToChooseFromMap[answer];
105
- promptNamespace(
106
- `Namespace: ${chosenNamespace}. Enter a new namespace to clone these objects. Enter nothing to keep them as-is.`,
107
- function (answer) {
108
- Object.keys(namespaceCollector).forEach(namespace => {
109
- if (namespaceCollector[namespace]["newNamespace"] == null) {
110
- if (namespace.startsWith(chosenNamespace)) {
111
- if (namespace == chosenNamespace) {
112
- namespaceCollector[namespace]["newNamespace"] = answer;
113
- } else {
114
- delete namespaceCollector[namespace];
115
- }
116
- }
117
- }
118
- });
119
- mapNamespaces(namespaceCollector, onComplete);
120
- }
121
- );
122
- });
123
- } else {
124
- util.print(`All namespaces accounted for. See details below.`);
125
- obExtract.sortNamespaces(namespaceCollector).forEach(namespace => {
126
- util.print(` ${namespace} -> ${util.replaceBlank(namespaceCollector[namespace]["newNamespace"], `Will not clone`)}`, true);
127
- });
128
- onComplete();
70
+ // Print warnings and output
71
+ if (uuidCollector["uuids"].length > 0) {
72
+ util.printGap();
73
+ util.printWarning(
74
+ `${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.`
75
+ );
129
76
  }
77
+ util.printGap();
78
+ util.print(`Cloning complete. Output can be found in the '/out/' folder (${path.resolve(cons.outDir)}).`);
79
+
80
+ // Execute callback if passed
81
+ util.execCallback(callback, outZipPath);
130
82
  }
131
83
 
132
84
  // Prompts the user for a namespace
package/src/cons.js CHANGED
@@ -25,7 +25,7 @@ module.exports = {
25
25
  },
26
26
 
27
27
  yesNo: ["Yes", "No"],
28
- none: "None",
28
+ none: "[None]",
29
29
 
30
30
  // Testing of this regex can be found here: https://regexr.com/6sgu6
31
31
  // Note that we can assume any UUID that "must be cloned" must be wrapped in either >uuid< or "uuid"
@@ -21,10 +21,13 @@ module.exports = {
21
21
  };
22
22
 
23
23
  function selectFile(promptText, fileTypes, optional, callback) {
24
- let files = optional ? [cons.none] : [];
24
+ let files = [];
25
25
  util.handleDirFiles(cons.curDir, fileTypes, 1, 1, function (filePath) {
26
26
  files.push(path.basename(filePath));
27
27
  });
28
+ if (optional) {
29
+ files.push(cons.none);
30
+ }
28
31
  prompt.promptList(promptText, files, function (answer) {
29
32
  let answerPath = answer == cons.none ? null : path.resolve(cons.curDir, answer);
30
33
  util.execCallback(callback, answerPath);
@@ -11,12 +11,12 @@ const { split } = require("lodash");
11
11
  // Exports
12
12
  module.exports = {
13
13
  // Builds the objectMaps object from the application
14
- buildObjectMaps: function (objectMaps, uuidCollector, namespaceCollector, appDir, usageType) {
14
+ buildObjectMaps: function (objectMaps, uuidCollector, appDir, usageType) {
15
15
  let fileCount = { count: 0 };
16
16
  initObjectMaps(objectMaps);
17
17
  // Extract the name, uuid, etc. from each object file
18
18
  util.handleDirFiles(appDir, ["xml", "xsd"], 2, 2, function (obPath) {
19
- extractObProps(objectMaps, uuidCollector, namespaceCollector, fileCount, obPath, usageType);
19
+ extractObProps(objectMaps, uuidCollector, fileCount, obPath, usageType);
20
20
  });
21
21
  // Set the datatype UUIDs from their name & namespace
22
22
  updateDatatypeUuids(objectMaps);
@@ -30,16 +30,6 @@ module.exports = {
30
30
 
31
31
  // Returns the count of all objects in the objectMap
32
32
  retrieveObjectMapCount: retrieveObjectMapCount,
33
-
34
- // Sorts the namespaces by their relative lengths
35
- sortNamespaces: function (namespaceCollector) {
36
- return lodash.flatten([
37
- Object.keys(namespaceCollector).indexOf(cons.noNamespace) >= 0 ? cons.noNamespace : [],
38
- lodash.sortBy(lodash.difference(Object.keys(namespaceCollector), [cons.noNamespace]).sort(), function (namespace) {
39
- return namespace.split("_").length;
40
- }),
41
- ]);
42
- },
43
33
  };
44
34
 
45
35
  // ---------------------------------------------------
@@ -54,7 +44,7 @@ function initObjectMaps(objectMaps) {
54
44
  }
55
45
 
56
46
  // Extracts the name, uuid, etc. from a single object file
57
- function extractObProps(objectMaps, uuidCollector, namespaceCollector, fileCount, obPath, usageType) {
47
+ function extractObProps(objectMaps, uuidCollector, fileCount, obPath, usageType) {
58
48
  fileCount.count = fileCount.count + 1;
59
49
  const obType = util.getFolderName(obPath);
60
50
  let isParsed = false;
@@ -80,13 +70,6 @@ function extractObProps(objectMaps, uuidCollector, namespaceCollector, fileCount
80
70
  }
81
71
  });
82
72
 
83
- // Get the namespace of the object
84
- let objectName = builder[obProps.fieldNames.NAME];
85
- let objectNamespace = extractObjectNamespace(objectName);
86
- if (Object.keys(namespaceCollector).indexOf(objectNamespace) == -1) {
87
- namespaceCollector[objectNamespace] = { newNamespace: null };
88
- }
89
-
90
73
  // Set the object metadata in the objectMaps
91
74
  objectMaps[haulType.haulName].push({ current: builder });
92
75
  isParsed = true;
@@ -95,7 +78,7 @@ function extractObProps(objectMaps, uuidCollector, namespaceCollector, fileCount
95
78
 
96
79
  // Unknown file type
97
80
  if (!isParsed) {
98
- util.printWarning(`WARNING: Unknown object type in package: ${obType}/${path.basename(obPath)}`);
81
+ util.printWarning(`Unknown object type in package: ${obType}/${path.basename(obPath)}`);
99
82
  }
100
83
  }
101
84
 
@@ -116,26 +99,28 @@ function updateDatatypeUuids(objectMaps) {
116
99
  }
117
100
 
118
101
  // Returns the count of all objects in the objectMap
119
- function retrieveObjectMapCount(objectMaps) {
102
+ function retrieveObjectMapCount(objectMaps, namespacesFilter) {
120
103
  let count = 0;
121
104
  Object.keys(objectMaps).forEach(key => {
122
- count = count + objectMaps[key].length;
105
+ if (util.isBlank(namespacesFilter)) {
106
+ count = count + objectMaps[key].length;
107
+ } else {
108
+ let countMatchingObjects = lodash.filter(objectMaps[key], function (objectMap) {
109
+ let isMatch = false;
110
+ for (const namespace of namespacesFilter) {
111
+ if (util.doesNameMatchNamespace(objectMap["current"][obProps.fieldNames.NAME], namespace)) {
112
+ isMatch = true;
113
+ return true;
114
+ }
115
+ return isMatch;
116
+ }
117
+ }).length;
118
+ count = count + countMatchingObjects;
119
+ }
123
120
  });
124
121
  return count;
125
122
  }
126
123
 
127
- // Returns the namespace from an object name
128
- function extractObjectNamespace(objectName) {
129
- // See regex here with test cases: https://regexr.com/6trnd
130
- let namespaceRegex = new RegExp(`^(?:[A-Z0-9]*${cons.namespaceDelimiterRegex}?){0,3}(?=${cons.namespaceDelimiterRegex})`, "g");
131
- let namespaceMatch = util.firstMatch(objectName, namespaceRegex, null);
132
- if (namespaceMatch) {
133
- return namespaceMatch.replace(new RegExp(cons.namespaceDelimiterRegex, "g"), "_");
134
- } else {
135
- return cons.noNamespace;
136
- }
137
- }
138
-
139
124
  // Utility function to differenciate between debug logging and real logging
140
125
  function debug(input) {
141
126
  console.log(input);
@@ -5,15 +5,15 @@ const lodash = require("lodash");
5
5
  const util = require("../util");
6
6
  const cons = require("../cons");
7
7
  const obProps = require("../objectProps");
8
- const obExtract = require("./objectExtractHandler");
8
+ const obExtract = require("../handlers/objectExtractHandler");
9
9
  const { remove } = require("fs-extra");
10
10
 
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, namespaceCollector, usageType) {
14
+ renameObjectMaps: function (objectMaps, uuidCollector, namespaceMap, usageType) {
15
15
  // Update all objectMaps with new names
16
- renameObjectMaps(objectMaps, uuidCollector, namespaceCollector, usageType);
16
+ renameObjectMaps(objectMaps, uuidCollector, namespaceMap, usageType);
17
17
  },
18
18
  };
19
19
 
@@ -22,12 +22,11 @@ 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, namespaceCollector, usageType) {
25
+ function renameObjectMaps(objectMaps, uuidCollector, namespaceMap, usageType) {
26
26
  obProps.haulTypes.forEach(haulType => {
27
27
  objectMaps[haulType.haulName].forEach(function (objectMap, objectMapIndex) {
28
- let curNamespaceForObject = findNamespaceForObject(objectMap["current"][obProps.fieldNames.NAME], namespaceCollector);
29
- let newNamespaceForObject = namespaceCollector[curNamespaceForObject]["newNamespace"];
30
- if (util.isBlank(newNamespaceForObject)) {
28
+ let curNamespaceForObject = findNamespaceForObject(objectMap["current"][obProps.fieldNames.NAME], namespaceMap);
29
+ if (Object.keys(namespaceMap).indexOf(curNamespaceForObject) < 0) {
31
30
  // Object will not be cloned
32
31
  objectMaps[haulType.haulName][objectMapIndex]["new"] = objectMap["current"];
33
32
  haulType.fields.forEach(haulField => {
@@ -37,6 +36,7 @@ function renameObjectMaps(objectMaps, uuidCollector, namespaceCollector, usageTy
37
36
  }
38
37
  });
39
38
  } else {
39
+ let newNamespaceForObject = namespaceMap[curNamespaceForObject]["newNamespace"];
40
40
  // Object will be cloned
41
41
  objectMaps[haulType.haulName][objectMapIndex]["new"] = {};
42
42
  haulType.fields.forEach(haulField => {
@@ -129,9 +129,9 @@ function buildNamespaceReplacementRegex(curNamespace, newNamespace, regexTags) {
129
129
  }
130
130
 
131
131
  // Returns the namespace for an object
132
- function findNamespaceForObject(objectName, namespaceCollector) {
133
- for (const namespace of obExtract.sortNamespaces(namespaceCollector).reverse()) {
134
- if (objectName.replace(new RegExp(cons.namespaceDelimiterRegex, "g"), "_").startsWith(namespace)) {
132
+ function findNamespaceForObject(objectName, namespaceMap) {
133
+ for (const namespace of util.sortNamespaces(Object.keys(namespaceMap)).reverse()) {
134
+ if (util.doesNameMatchNamespace(objectName, namespace)) {
135
135
  return namespace;
136
136
  }
137
137
  }
package/src/util.js CHANGED
@@ -57,7 +57,9 @@ module.exports = {
57
57
 
58
58
  // Validates the new namespace is only letters and numbers with no whitespace
59
59
  validateNamespace: function (namespace) {
60
- if (!namespace.match(/^\b(?:[A-Za-z0-9]+_?)+\b$/g) && !isBlank(namespace)) {
60
+ if (isBlank(namespace)) {
61
+ return `Namespace cannot be null`;
62
+ } else if (!namespace.match(/^\b(?:[A-Za-z0-9]+_?)+\b$/g) && !isBlank(namespace)) {
61
63
  return `Namespace can only be sets of uppercase letters and numbers separated by underscores`;
62
64
  } else {
63
65
  return true;
@@ -308,7 +310,19 @@ module.exports = {
308
310
 
309
311
  // Prints a wanring in color
310
312
  printWarning: function (log) {
311
- colorlog.color("yellow").log(log);
313
+ colorlog.color("yellow").log(`WARNING: ${log}`);
314
+ },
315
+
316
+ // Returns true if the string matches the namespace
317
+ doesNameMatchNamespace: function (name, namespace) {
318
+ return name.replace(new RegExp(cons.namespaceDelimiterRegex, "g"), "_").startsWith(namespace);
319
+ },
320
+
321
+ // Sorts an array of namespaces by their delimited length, then alphabetically
322
+ sortNamespaces: function (namespaces) {
323
+ return lodash.sortBy(namespaces.sort(), function (namespace) {
324
+ return namespace.split("_").length;
325
+ });
312
326
  },
313
327
  };
314
328