apn-app-manager 1.7.0 → 1.8.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.
@@ -0,0 +1,9 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <module type="JAVA_MODULE" version="4">
3
+ <component name="NewModuleRootManager" inherit-compiler-output="true">
4
+ <exclude-output />
5
+ <content url="file://$MODULE_DIR$" />
6
+ <orderEntry type="inheritedJdk" />
7
+ <orderEntry type="sourceFolder" forTests="false" />
8
+ </component>
9
+ </module>
@@ -0,0 +1,20 @@
1
+ <component name="InspectionProjectProfileManager">
2
+ <profile version="1.0">
3
+ <option name="myName" value="Project Default" />
4
+ <inspection_tool class="DuplicateThrows" enabled="true" level="WARNING" enabled_by_default="true">
5
+ <scope name="Production" level="ERROR" enabled="true" />
6
+ </inspection_tool>
7
+ <inspection_tool class="LoggerInitializedWithForeignClass" enabled="false" level="WARNING" enabled_by_default="false">
8
+ <option name="loggerClassName" value="org.apache.log4j.Logger,org.slf4j.LoggerFactory,org.apache.commons.logging.LogFactory,java.util.logging.Logger" />
9
+ <option name="loggerFactoryMethodName" value="getLogger,getLogger,getLog,getLogger" />
10
+ </inspection_tool>
11
+ <inspection_tool class="OnDemandImport" enabled="true" level="ERROR" enabled_by_default="true" />
12
+ <inspection_tool class="RedundantThrows" enabled="true" level="WARNING" enabled_by_default="true">
13
+ <scope name="Production" level="ERROR" enabled="true" />
14
+ </inspection_tool>
15
+ <inspection_tool class="RedundantThrowsDeclaration" enabled="true" level="WARNING" enabled_by_default="true">
16
+ <scope name="Production" level="ERROR" enabled="true" />
17
+ </inspection_tool>
18
+ <inspection_tool class="UNUSED_IMPORT" enabled="true" level="ERROR" enabled_by_default="true" />
19
+ </profile>
20
+ </component>
@@ -0,0 +1,9 @@
1
+ <component name="libraryTable">
2
+ <library name="all apps">
3
+ <CLASSES>
4
+ <root url="jar://$PROJECT_DIR$/out/all apps.zip!/" />
5
+ </CLASSES>
6
+ <JAVADOC />
7
+ <SOURCES />
8
+ </library>
9
+ </component>
@@ -0,0 +1,11 @@
1
+ <component name="libraryTable">
2
+ <library name="app-manager">
3
+ <CLASSES>
4
+ <root url="jar://$PROJECT_DIR$/all apps.zip!/" />
5
+ <root url="jar://$PROJECT_DIR$/CT APP Clone Tester.zip!/" />
6
+ <root url="jar://$PROJECT_DIR$/IT i18n tobias.zip!/" />
7
+ </CLASSES>
8
+ <JAVADOC />
9
+ <SOURCES />
10
+ </library>
11
+ </component>
@@ -0,0 +1,9 @@
1
+ <component name="libraryTable">
2
+ <library name="appian-adm-import-client-2.5.17">
3
+ <CLASSES>
4
+ <root url="jar://$PROJECT_DIR$/lib/aim/lib/appian-adm-import-client-2.5.17.jar!/" />
5
+ </CLASSES>
6
+ <JAVADOC />
7
+ <SOURCES />
8
+ </library>
9
+ </component>
package/.idea/misc.xml ADDED
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="temurin-1.8" project-jdk-type="JavaSDK">
4
+ <output url="file://$PROJECT_DIR$/out" />
5
+ </component>
6
+ </project>
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ProjectModuleManager">
4
+ <modules>
5
+ <module fileurl="file://$PROJECT_DIR$/.idea/app-manager.iml" filepath="$PROJECT_DIR$/.idea/app-manager.iml" />
6
+ </modules>
7
+ </component>
8
+ </project>
package/.idea/vcs.xml ADDED
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="VcsDirectoryMappings">
4
+ <mapping directory="$PROJECT_DIR$/../.." vcs="Git" />
5
+ </component>
6
+ </project>
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  `apn-app-manager` is a command line tool that ingests an Appian application zip file, makes changes to the underlying XML files, and outputs a new Appian application zip file. It currently supports cloning an application.
4
4
 
5
+ Test beta version
6
+
5
7
  ## Why?
6
8
 
7
9
  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.
@@ -40,18 +42,21 @@ Used to duplicate all objects of an application, which replaces the namespace of
40
42
  1. Select `clone` for "What would you like to do?".
41
43
  1. Select your application zip.
42
44
  1. Select your import customization properties file, if you have one.
43
- 1. Enter your current namespace.
44
- - For example, an application with objects such as `SMP APP Artifacts` and `SMP_APP_displayUser` would have a namespace of `SMP_APP`
45
- 1. Enter your new namespace.
46
- 1. Do advanced options if necessary.
45
+ 1. Choose advanced options or not:
46
+ - Regular options:
47
+ 1. Enter your current namespace.
48
+ 1. Enter your new namespace.
49
+ - For example, an application with objects such as `SMP APP Artifacts` and `SMP_APP_displayUser` would have a namespace of `SMP_APP`
50
+ - Note that this tool can only clone objects of one namepsace at a time with regular options.
51
+ - Advanced options:
52
+ 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.
53
+ 1. Database tables can be skipped during cloning.
47
54
  1. Access your cloned application in the generated `/out/` folder.
48
55
 
49
56
  #### Notes
50
57
 
51
58
  1. Always inspect your cloned zip before importing to verify that what will be imported matches what you'd expect.
52
59
  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.
53
- 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.
54
- - 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
60
  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
61
  1. There are two other document generated in the `/out/` folder after cloning:
57
62
  - `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.7.0",
3
+ "version": "1.8.0",
4
4
  "description": "Appian App Manager",
5
5
  "license": "Apache-2.0",
6
6
  "preferGlobal": true,
@@ -19,55 +19,70 @@ const options = {
19
19
  renameDbTables: true,
20
20
  };
21
21
 
22
+ const objectMaps = {};
23
+ const uuidCollector = { uuids: [] };
24
+ const namespaceCollector = {};
25
+
22
26
  // Entry-point
23
27
  function main(callback) {
24
28
  util.delFile(cons.outDir);
25
29
  fileSelect.getAppPackage(function (appZipPath) {
26
30
  util.unzipDir(appZipPath, cons.tempDir);
27
31
  fileSelect.selectSingleFile("Select import customization properties file", ["properties"], true, function (icfPath) {
28
- promptNamespace("What is your current namespace?", function (curNamespace) {
29
- promptNamespace("What is your new namespace?", function (newNamespace) {
30
- const namespaceMap = {};
31
- namespaceMap[curNamespace] = { newNamespace: newNamespace };
32
- prompt.promptList("Advanced options?", ["No", "Yes"], function (enhancedOptions) {
33
- if (enhancedOptions == "Yes") {
34
- prompt.promptList("Rename database tables?", ["Yes", "No"], function (renameDbTables) {
35
- options["renameDbTables"] = renameDbTables == "Yes";
36
- executeClone(appZipPath, icfPath, namespaceMap, options, callback);
37
- });
38
- } else {
39
- executeClone(appZipPath, icfPath, namespaceMap, options, callback);
40
- }
32
+ prompt.promptList("Advanced options?", ["No", "Yes"], function (enhancedOptions) {
33
+ if (enhancedOptions == "Yes") {
34
+ promptAdvancedOptions(appZipPath, icfPath, executeClone(appZipPath, icfPath, callback));
35
+ } else {
36
+ promptNamespace("What is your current namespace?", function (curNamespace) {
37
+ promptNamespace("What is your new namespace?", function (newNamespace) {
38
+ namespaceCollector[curNamespace] = { newNamespace: newNamespace };
39
+ executeClone(appZipPath, icfPath, callback);
40
+ });
41
41
  });
42
- });
42
+ }
43
43
  });
44
44
  });
45
45
  });
46
46
  }
47
47
 
48
+ // Handles the advanced options menu
49
+ function promptAdvancedOptions(appZipPath, icfPath, callback) {
50
+ // Extract the metadata including the namespace from each object file
51
+ util.print(`Collecting all metadata from the objects...`);
52
+ obExtract.buildObjectMaps(objectMaps, uuidCollector, namespaceCollector, cons.tempDir, obProps.usageTypes.CLONE);
53
+
54
+ // Have the user map each namespace to its proper behavior
55
+ mapNamespaces(function () {
56
+ prompt.promptList("Rename database tables?", ["Yes", "No"], function (renameDbTables) {
57
+ options["renameDbTables"] = renameDbTables == "Yes";
58
+ executeClone(appZipPath, icfPath, callback);
59
+ });
60
+ });
61
+ }
62
+
48
63
  // Executes all the steps of the clone
49
- function executeClone(appZipPath, icfPath, namespaceMap, options, callback) {
64
+ function executeClone(appZipPath, icfPath, callback) {
50
65
  // Initialize output variables
51
66
  const outZipPath = path.resolve(cons.outDir, path.basename(appZipPath));
52
67
  const outIcfPath = icfPath ? path.resolve(cons.outDir, path.basename(icfPath)) : null;
53
68
 
54
69
  // Extract the metadata including the namespace from each object file
55
- const objectMaps = {};
56
- const uuidCollector = { uuids: [] };
57
- util.print(`Collecting all metadata from the objects...`);
58
- obExtract.buildObjectMaps(objectMaps, uuidCollector, cons.tempDir, obProps.usageTypes.CLONE);
59
- let nonMatchingObjectCount =
60
- obExtract.retrieveObjectMapCount(objectMaps) - obExtract.retrieveObjectMapCount(objectMaps, Object.keys(namespaceMap));
61
- if (nonMatchingObjectCount > 0) {
62
- util.printWarning(
63
- `${nonMatchingObjectCount} objects found not matching namespace ${Object.keys(namespaceMap)}, these objects won't be cloned`
64
- );
65
- util.printGap();
70
+ if (Object.keys(objectMaps).length == 0) {
71
+ util.print(`Collecting all metadata from the objects...`);
72
+ obExtract.buildObjectMaps(objectMaps, uuidCollector, null, cons.tempDir, obProps.usageTypes.CLONE);
73
+ let nonMatchingObjectCount =
74
+ obExtract.retrieveObjectMapCount(objectMaps) - obExtract.retrieveObjectMapCount(objectMaps, Object.keys(namespaceCollector));
75
+ if (nonMatchingObjectCount > 0) {
76
+ util.printWarning(
77
+ `${nonMatchingObjectCount} objects found not matching namespace ${Object.keys(namespaceCollector)}, these objects won't be cloned`
78
+ );
79
+ util.printGap();
80
+ }
66
81
  }
67
82
 
68
83
  // Update the object maps we are cloning based on the application prefix
69
84
  util.print(`Cloning the objects files...`);
70
- obMapsUpdates.renameObjectMaps(objectMaps, uuidCollector, namespaceMap, options, obProps.usageTypes.CLONE);
85
+ obMapsUpdates.renameObjectMaps(objectMaps, uuidCollector, namespaceCollector, options, obProps.usageTypes.CLONE);
71
86
 
72
87
  // These output files are helpful for debugging
73
88
  util.writeJson(path.resolve(cons.outDir, `objects.json`), objectMaps);
@@ -102,6 +117,62 @@ function promptNamespace(message, onAnswer) {
102
117
  });
103
118
  }
104
119
 
120
+ // Prompts the user to select a namespace
121
+ function mapNamespaces(onComplete) {
122
+ let namespacesToChooseFrom = lodash.filter(obExtract.sortNamespaces(namespaceCollector), function (namespace) {
123
+ return namespaceCollector[namespace]["newNamespace"] == null;
124
+ });
125
+ let isFirstIteration = namespacesToChooseFrom.length == Object.keys(namespaceCollector).length;
126
+ if (namespacesToChooseFrom.length > 0) {
127
+ let message;
128
+ if (isFirstIteration) {
129
+ message = `After analyzing the objects, ${
130
+ Object.keys(namespaceCollector).length
131
+ } 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.`;
132
+ } else {
133
+ message = `Select another namespace`;
134
+ }
135
+ let namespacesToChooseFromMap = {};
136
+ namespacesToChooseFrom.forEach(namespace => {
137
+ let subNamespaceCount =
138
+ lodash.filter(namespacesToChooseFrom, function (otherNamespace) {
139
+ return otherNamespace.startsWith(namespace);
140
+ }).length - 1;
141
+ let formattedNamespace =
142
+ namespace == cons.noNamespace
143
+ ? namespace
144
+ : `${" ".repeat(namespace.split("_").length)}${namespace} [including ${subNamespaceCount} matching sub-namespace(s)]`;
145
+ namespacesToChooseFromMap[formattedNamespace] = namespace;
146
+ });
147
+ prompt.promptList(message, Object.keys(namespacesToChooseFromMap), function (answer) {
148
+ let chosenNamespace = namespacesToChooseFromMap[answer];
149
+ promptNamespace(
150
+ `Namespace: ${chosenNamespace}. Enter a new namespace to clone these objects. Enter nothing to keep them as-is.`,
151
+ function (answer) {
152
+ Object.keys(namespaceCollector).forEach(namespace => {
153
+ if (namespaceCollector[namespace]["newNamespace"] == null) {
154
+ if (namespace.startsWith(chosenNamespace)) {
155
+ if (namespace == chosenNamespace) {
156
+ namespaceCollector[namespace]["newNamespace"] = answer;
157
+ } else {
158
+ delete namespaceCollector[namespace];
159
+ }
160
+ }
161
+ }
162
+ });
163
+ mapNamespaces(onComplete);
164
+ }
165
+ );
166
+ });
167
+ } else {
168
+ util.print(`All namespaces accounted for. See details below.`);
169
+ obExtract.sortNamespaces(namespaceCollector).forEach(namespace => {
170
+ util.print(` ${namespace} -> ${util.replaceBlank(namespaceCollector[namespace]["newNamespace"], `Will not clone`)}`, true);
171
+ });
172
+ onComplete();
173
+ }
174
+ }
175
+
105
176
  // Utility function to differenciate between debug logging and real logging
106
177
  function debug(input) {
107
178
  console.log(input);
@@ -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, appDir, usageType) {
14
+ buildObjectMaps: function (objectMaps, uuidCollector, namespaceCollector, 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, fileCount, obPath, usageType);
19
+ extractObProps(objectMaps, uuidCollector, namespaceCollector, fileCount, obPath, usageType);
20
20
  });
21
21
  // Set the datatype UUIDs from their name & namespace
22
22
  updateDatatypeUuids(objectMaps);
@@ -30,6 +30,16 @@ 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
+ },
33
43
  };
34
44
 
35
45
  // ---------------------------------------------------
@@ -44,7 +54,7 @@ function initObjectMaps(objectMaps) {
44
54
  }
45
55
 
46
56
  // Extracts the name, uuid, etc. from a single object file
47
- function extractObProps(objectMaps, uuidCollector, fileCount, obPath, usageType) {
57
+ function extractObProps(objectMaps, uuidCollector, namespaceCollector, fileCount, obPath, usageType) {
48
58
  fileCount.count = fileCount.count + 1;
49
59
  const obType = util.getFolderName(obPath);
50
60
  let isParsed = false;
@@ -85,6 +95,15 @@ function extractObProps(objectMaps, uuidCollector, fileCount, obPath, usageType)
85
95
  }
86
96
  });
87
97
 
98
+ // Get the namespace of the object
99
+ if (!util.isBlank(namespaceCollector)) {
100
+ let objectName = builder[obProps.fieldNames.NAME];
101
+ let objectNamespace = extractObjectNamespace(objectName);
102
+ if (Object.keys(namespaceCollector).indexOf(objectNamespace) == -1) {
103
+ namespaceCollector[objectNamespace] = { newNamespace: null };
104
+ }
105
+ }
106
+
88
107
  // Set the object metadata in the objectMaps
89
108
  objectMaps[haulType.haulName].push({ current: builder });
90
109
  isParsed = true;
@@ -93,7 +112,7 @@ function extractObProps(objectMaps, uuidCollector, fileCount, obPath, usageType)
93
112
 
94
113
  // Unknown file type
95
114
  if (!isParsed) {
96
- util.printWarning(`Unknown object type in package: ${obType}/${path.basename(obPath)}`);
115
+ util.printWarning(`WARNING: Unknown object type in package: ${obType}/${path.basename(obPath)}`);
97
116
  }
98
117
  }
99
118
 
@@ -140,6 +159,18 @@ function retrieveObjectMapCount(objectMaps, namespacesFilter) {
140
159
  return count;
141
160
  }
142
161
 
162
+ // Returns the namespace from an object name
163
+ function extractObjectNamespace(objectName) {
164
+ // See regex here with test cases: https://regexr.com/6trnd
165
+ let namespaceRegex = new RegExp(`^(?:[A-Z0-9]*${cons.namespaceDelimiterRegex}?){0,3}(?=${cons.namespaceDelimiterRegex})`, "g");
166
+ let namespaceMatch = util.firstMatch(objectName, namespaceRegex, null);
167
+ if (namespaceMatch) {
168
+ return namespaceMatch.replace(new RegExp(cons.namespaceDelimiterRegex, "g"), "_");
169
+ } else {
170
+ return cons.noNamespace;
171
+ }
172
+ }
173
+
143
174
  // Utility function to differenciate between debug logging and real logging
144
175
  function debug(input) {
145
176
  console.log(input);
@@ -26,7 +26,14 @@ function renameObjectMaps(objectMaps, uuidCollector, namespaceMap, options, usag
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
- if (Object.keys(namespaceMap).indexOf(curNamespaceForObject) < 0) {
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;
32
+ if (!objectWillNotBeCloned) {
33
+ newNamespaceForObject = namespaceMap[curNamespaceForObject]["newNamespace"];
34
+ objectWillNotBeCloned = util.isBlank(newNamespaceForObject);
35
+ }
36
+ if (objectWillNotBeCloned) {
30
37
  // Object will not be cloned
31
38
  objectMaps[haulType.haulName][objectMapIndex]["new"] = objectMap["current"];
32
39
  haulType.fields.forEach(haulField => {
@@ -37,7 +44,6 @@ function renameObjectMaps(objectMaps, uuidCollector, namespaceMap, options, usag
37
44
  });
38
45
  } else {
39
46
  // Object will be cloned
40
- let newNamespaceForObject = namespaceMap[curNamespaceForObject]["newNamespace"];
41
47
  objectMaps[haulType.haulName][objectMapIndex]["new"] = {};
42
48
  haulType.fields.forEach(haulField => {
43
49
  if (lodash.includes(haulField.usage, usageType)) {
package/src/util.js CHANGED
@@ -57,9 +57,7 @@ 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 (isBlank(namespace)) {
61
- return `Namespace cannot be null`;
62
- } else if (!namespace.match(/^\b(?:[A-Za-z0-9]+_?)+\b$/g) && !isBlank(namespace)) {
60
+ if (!namespace.match(/^\b(?:[A-Za-z0-9]+_?)+\b$/g) && !isBlank(namespace)) {
63
61
  return `Namespace can only be sets of uppercase letters and numbers separated by underscores`;
64
62
  } else {
65
63
  return true;