@cyberismo/data-handler 0.0.7 → 0.0.9

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.
Files changed (265) hide show
  1. package/dist/command-handler.d.ts +11 -2
  2. package/dist/command-handler.js +61 -18
  3. package/dist/command-handler.js.map +1 -1
  4. package/dist/command-manager.js +8 -8
  5. package/dist/command-manager.js.map +1 -1
  6. package/dist/commands/calculate.d.ts +7 -44
  7. package/dist/commands/calculate.js +8 -389
  8. package/dist/commands/calculate.js.map +1 -1
  9. package/dist/commands/create.d.ts +7 -4
  10. package/dist/commands/create.js +42 -15
  11. package/dist/commands/create.js.map +1 -1
  12. package/dist/commands/edit.d.ts +9 -3
  13. package/dist/commands/edit.js +33 -9
  14. package/dist/commands/edit.js.map +1 -1
  15. package/dist/commands/export.d.ts +13 -11
  16. package/dist/commands/export.js +80 -28
  17. package/dist/commands/export.js.map +1 -1
  18. package/dist/commands/import.d.ts +7 -0
  19. package/dist/commands/import.js +21 -2
  20. package/dist/commands/import.js.map +1 -1
  21. package/dist/commands/move.d.ts +11 -12
  22. package/dist/commands/move.js +12 -13
  23. package/dist/commands/move.js.map +1 -1
  24. package/dist/commands/remove.d.ts +2 -4
  25. package/dist/commands/remove.js +8 -16
  26. package/dist/commands/remove.js.map +1 -1
  27. package/dist/commands/rename.d.ts +1 -3
  28. package/dist/commands/rename.js +3 -6
  29. package/dist/commands/rename.js.map +1 -1
  30. package/dist/commands/show.d.ts +37 -5
  31. package/dist/commands/show.js +85 -7
  32. package/dist/commands/show.js.map +1 -1
  33. package/dist/commands/transition.d.ts +1 -3
  34. package/dist/commands/transition.js +3 -5
  35. package/dist/commands/transition.js.map +1 -1
  36. package/dist/commands/update.d.ts +5 -1
  37. package/dist/commands/update.js +7 -1
  38. package/dist/commands/update.js.map +1 -1
  39. package/dist/commands/validate.d.ts +7 -8
  40. package/dist/commands/validate.js +30 -35
  41. package/dist/commands/validate.js.map +1 -1
  42. package/dist/containers/card-container.d.ts +6 -0
  43. package/dist/containers/card-container.js +61 -0
  44. package/dist/containers/card-container.js.map +1 -1
  45. package/dist/containers/project/calculation-engine.d.ts +90 -0
  46. package/dist/containers/project/calculation-engine.js +402 -0
  47. package/dist/containers/project/calculation-engine.js.map +1 -0
  48. package/dist/containers/project/resource-collector.d.ts +2 -1
  49. package/dist/containers/project/resource-collector.js +41 -33
  50. package/dist/containers/project/resource-collector.js.map +1 -1
  51. package/dist/containers/project.d.ts +18 -6
  52. package/dist/containers/project.js +37 -72
  53. package/dist/containers/project.js.map +1 -1
  54. package/dist/containers/template.d.ts +5 -0
  55. package/dist/containers/template.js +9 -0
  56. package/dist/containers/template.js.map +1 -1
  57. package/dist/exceptions/index.d.ts +20 -0
  58. package/dist/exceptions/index.js +16 -0
  59. package/dist/exceptions/index.js.map +1 -1
  60. package/dist/index.d.ts +5 -2
  61. package/dist/index.js +5 -1
  62. package/dist/index.js.map +1 -1
  63. package/dist/interfaces/macros.d.ts +7 -2
  64. package/dist/interfaces/project-interfaces.d.ts +21 -0
  65. package/dist/interfaces/project-interfaces.js +4 -0
  66. package/dist/interfaces/project-interfaces.js.map +1 -1
  67. package/dist/interfaces/resource-interfaces.d.ts +1 -1
  68. package/dist/interfaces/resource-interfaces.js.map +1 -1
  69. package/dist/macros/base-macro.d.ts +2 -0
  70. package/dist/macros/base-macro.js +66 -19
  71. package/dist/macros/base-macro.js.map +1 -1
  72. package/dist/macros/common.d.ts +16 -9
  73. package/dist/macros/common.js +22 -9
  74. package/dist/macros/common.js.map +1 -1
  75. package/dist/macros/createCards/index.d.ts +1 -2
  76. package/dist/macros/createCards/index.js.map +1 -1
  77. package/dist/macros/graph/index.d.ts +1 -3
  78. package/dist/macros/graph/index.js +11 -9
  79. package/dist/macros/graph/index.js.map +1 -1
  80. package/dist/macros/image/index.d.ts +39 -0
  81. package/dist/macros/image/index.js +78 -0
  82. package/dist/macros/image/index.js.map +1 -0
  83. package/dist/macros/image/metadata.d.ts +18 -0
  84. package/dist/macros/image/metadata.js +22 -0
  85. package/dist/macros/image/metadata.js.map +1 -0
  86. package/dist/macros/include/index.d.ts +32 -0
  87. package/dist/macros/include/index.js +97 -0
  88. package/dist/macros/include/index.js.map +1 -0
  89. package/dist/macros/include/metadata.d.ts +15 -0
  90. package/dist/macros/include/metadata.js +19 -0
  91. package/dist/macros/include/metadata.js.map +1 -0
  92. package/dist/macros/index.d.ts +39 -31
  93. package/dist/macros/index.js +167 -73
  94. package/dist/macros/index.js.map +1 -1
  95. package/dist/macros/percentage/index.d.ts +29 -0
  96. package/dist/macros/percentage/index.js +36 -0
  97. package/dist/macros/percentage/index.js.map +1 -0
  98. package/dist/macros/percentage/metadata.d.ts +15 -0
  99. package/dist/macros/percentage/metadata.js +19 -0
  100. package/dist/macros/percentage/metadata.js.map +1 -0
  101. package/dist/macros/report/index.d.ts +2 -5
  102. package/dist/macros/report/index.js +20 -12
  103. package/dist/macros/report/index.js.map +1 -1
  104. package/dist/macros/scoreCard/index.d.ts +15 -15
  105. package/dist/macros/scoreCard/index.js +16 -17
  106. package/dist/macros/scoreCard/index.js.map +1 -1
  107. package/dist/macros/vega/index.d.ts +28 -0
  108. package/dist/macros/vega/index.js +27 -0
  109. package/dist/macros/vega/index.js.map +1 -0
  110. package/dist/macros/vega/metadata.d.ts +15 -0
  111. package/dist/macros/vega/metadata.js +7 -0
  112. package/dist/macros/vega/metadata.js.map +1 -0
  113. package/dist/macros/vegalite/index.d.ts +27 -0
  114. package/dist/macros/vegalite/index.js +27 -0
  115. package/dist/macros/vegalite/index.js.map +1 -0
  116. package/dist/macros/vegalite/metadata.d.ts +15 -0
  117. package/dist/macros/vegalite/metadata.js +7 -0
  118. package/dist/macros/vegalite/metadata.js.map +1 -0
  119. package/dist/macros/xref/index.d.ts +26 -0
  120. package/dist/macros/xref/index.js +53 -0
  121. package/dist/macros/xref/index.js.map +1 -0
  122. package/dist/macros/xref/metadata.d.ts +15 -0
  123. package/dist/macros/xref/metadata.js +19 -0
  124. package/dist/macros/xref/metadata.js.map +1 -0
  125. package/dist/module-manager.d.ts +17 -4
  126. package/dist/module-manager.js +192 -58
  127. package/dist/module-manager.js.map +1 -1
  128. package/dist/permissions/action-guard.d.ts +2 -2
  129. package/dist/permissions/action-guard.js +1 -1
  130. package/dist/permissions/action-guard.js.map +1 -1
  131. package/dist/project-settings.js +2 -8
  132. package/dist/project-settings.js.map +1 -1
  133. package/dist/resources/card-type-resource.d.ts +2 -0
  134. package/dist/resources/card-type-resource.js +63 -0
  135. package/dist/resources/card-type-resource.js.map +1 -1
  136. package/dist/resources/file-resource.d.ts +2 -0
  137. package/dist/resources/file-resource.js +12 -4
  138. package/dist/resources/file-resource.js.map +1 -1
  139. package/dist/resources/folder-resource.d.ts +19 -0
  140. package/dist/resources/folder-resource.js +51 -2
  141. package/dist/resources/folder-resource.js.map +1 -1
  142. package/dist/resources/graph-model-resource.js +1 -1
  143. package/dist/resources/graph-model-resource.js.map +1 -1
  144. package/dist/resources/graph-view-resource.js +1 -1
  145. package/dist/resources/graph-view-resource.js.map +1 -1
  146. package/dist/resources/report-resource.js +1 -1
  147. package/dist/resources/report-resource.js.map +1 -1
  148. package/dist/resources/resource-object.d.ts +8 -0
  149. package/dist/resources/resource-object.js +9 -0
  150. package/dist/resources/resource-object.js.map +1 -1
  151. package/dist/resources/template-resource.js +1 -1
  152. package/dist/resources/template-resource.js.map +1 -1
  153. package/dist/resources/workflow-resource.d.ts +2 -0
  154. package/dist/resources/workflow-resource.js +53 -9
  155. package/dist/resources/workflow-resource.js.map +1 -1
  156. package/dist/svg/index.d.ts +15 -0
  157. package/dist/svg/index.js +16 -0
  158. package/dist/svg/index.js.map +1 -0
  159. package/dist/svg/lib.d.ts +9 -0
  160. package/dist/svg/lib.js +26 -0
  161. package/dist/svg/lib.js.map +1 -0
  162. package/dist/svg/percentage.d.ts +25 -0
  163. package/dist/svg/percentage.js +90 -0
  164. package/dist/svg/percentage.js.map +1 -0
  165. package/dist/svg/scoreCard.d.ts +19 -0
  166. package/dist/svg/scoreCard.js +55 -0
  167. package/dist/svg/scoreCard.js.map +1 -0
  168. package/dist/types/queries.d.ts +8 -7
  169. package/dist/types/queries.js.map +1 -1
  170. package/dist/utils/card-utils.d.ts +6 -0
  171. package/dist/utils/card-utils.js +12 -0
  172. package/dist/utils/card-utils.js.map +1 -1
  173. package/dist/utils/clingo-facts.d.ts +2 -1
  174. package/dist/utils/clingo-facts.js +19 -2
  175. package/dist/utils/clingo-facts.js.map +1 -1
  176. package/dist/utils/clingo-parser.d.ts +1 -0
  177. package/dist/utils/clingo-parser.js +22 -100
  178. package/dist/utils/clingo-parser.js.map +1 -1
  179. package/dist/utils/constants.d.ts +7 -0
  180. package/dist/utils/constants.js +14 -0
  181. package/dist/utils/constants.js.map +1 -1
  182. package/dist/utils/csv.js.map +1 -1
  183. package/dist/utils/report.d.ts +17 -3
  184. package/dist/utils/report.js +38 -2
  185. package/dist/utils/report.js.map +1 -1
  186. package/dist/utils/resource-utils.d.ts +1 -0
  187. package/dist/utils/resource-utils.js +9 -0
  188. package/dist/utils/resource-utils.js.map +1 -1
  189. package/dist/utils/user-preferences.d.ts +1 -11
  190. package/dist/utils/user-preferences.js +30 -13
  191. package/dist/utils/user-preferences.js.map +1 -1
  192. package/dist/utils/validate.d.ts +2 -3
  193. package/dist/utils/validate.js +2 -2
  194. package/dist/utils/validate.js.map +1 -1
  195. package/package.json +8 -6
  196. package/src/command-handler.ts +96 -17
  197. package/src/command-manager.ts +8 -8
  198. package/src/commands/calculate.ts +11 -525
  199. package/src/commands/create.ts +53 -16
  200. package/src/commands/edit.ts +53 -11
  201. package/src/commands/export.ts +108 -34
  202. package/src/commands/import.ts +31 -2
  203. package/src/commands/move.ts +12 -15
  204. package/src/commands/remove.ts +10 -19
  205. package/src/commands/rename.ts +3 -12
  206. package/src/commands/show.ts +121 -8
  207. package/src/commands/transition.ts +3 -7
  208. package/src/commands/update.ts +6 -0
  209. package/src/commands/validate.ts +39 -47
  210. package/src/containers/card-container.ts +74 -0
  211. package/src/containers/project/calculation-engine.ts +535 -0
  212. package/src/containers/project/resource-collector.ts +45 -26
  213. package/src/containers/project.ts +66 -84
  214. package/src/containers/template.ts +16 -0
  215. package/src/exceptions/index.ts +36 -0
  216. package/src/index.ts +13 -2
  217. package/src/interfaces/macros.ts +7 -1
  218. package/src/interfaces/project-interfaces.ts +27 -0
  219. package/src/interfaces/resource-interfaces.ts +1 -0
  220. package/src/macros/base-macro.ts +89 -25
  221. package/src/macros/common.ts +22 -9
  222. package/src/macros/createCards/index.ts +1 -2
  223. package/src/macros/graph/index.ts +17 -12
  224. package/src/macros/image/index.ts +121 -0
  225. package/src/macros/image/metadata.ts +25 -0
  226. package/src/macros/include/index.ts +147 -0
  227. package/src/macros/include/metadata.ts +22 -0
  228. package/src/macros/index.ts +179 -100
  229. package/src/macros/percentage/index.ts +54 -0
  230. package/src/macros/percentage/metadata.ts +22 -0
  231. package/src/macros/report/index.ts +22 -17
  232. package/src/macros/scoreCard/index.ts +23 -23
  233. package/src/macros/vega/index.ts +55 -0
  234. package/src/macros/vega/metadata.ts +21 -0
  235. package/src/macros/vegalite/index.ts +50 -0
  236. package/src/macros/vegalite/metadata.ts +21 -0
  237. package/src/macros/xref/index.ts +73 -0
  238. package/src/macros/xref/metadata.ts +22 -0
  239. package/src/module-manager.ts +241 -69
  240. package/src/permissions/action-guard.ts +3 -3
  241. package/src/project-settings.ts +2 -11
  242. package/src/resources/card-type-resource.ts +100 -0
  243. package/src/resources/file-resource.ts +16 -4
  244. package/src/resources/folder-resource.ts +59 -2
  245. package/src/resources/graph-model-resource.ts +1 -1
  246. package/src/resources/graph-view-resource.ts +1 -1
  247. package/src/resources/report-resource.ts +1 -1
  248. package/src/resources/resource-object.ts +14 -0
  249. package/src/resources/template-resource.ts +1 -1
  250. package/src/resources/workflow-resource.ts +68 -13
  251. package/src/svg/index.ts +15 -0
  252. package/src/svg/lib.ts +31 -0
  253. package/src/svg/percentage.ts +97 -0
  254. package/src/svg/scoreCard.ts +88 -0
  255. package/src/types/queries.ts +8 -7
  256. package/src/types/string-pixel-width.d.ts +23 -0
  257. package/src/utils/card-utils.ts +13 -0
  258. package/src/utils/clingo-facts.ts +65 -3
  259. package/src/utils/clingo-parser.ts +31 -144
  260. package/src/utils/constants.ts +16 -0
  261. package/src/utils/csv.ts +1 -1
  262. package/src/utils/report.ts +45 -4
  263. package/src/utils/resource-utils.ts +9 -0
  264. package/src/utils/user-preferences.ts +32 -14
  265. package/src/utils/validate.ts +3 -3
@@ -29,10 +29,12 @@ import { readJsonFile } from './utils/json.js';
29
29
  import { Validate } from './commands/index.js';
30
30
 
31
31
  const FILE_PROTOCOL = 'file:';
32
- // todo: add support for git's default branch.
33
- const MAIN_BRANCH = 'main';
34
- // timeout in milliseconds for git client (no stdout / stderr activity)
35
- const DEFAULT_TIMEOUT = 10000;
32
+ const HTTPS_PROTOCOL = 'https:';
33
+
34
+ // When dependencies are built to a map, use map that has
35
+ // key: module name,
36
+ // value: list of unique prefixes
37
+ type DependencyGraph = Map<string, Set<string>>;
36
38
 
37
39
  /**
38
40
  * Class that handles module updates and imports.
@@ -40,6 +42,8 @@ const DEFAULT_TIMEOUT = 10000;
40
42
  export class ModuleManager {
41
43
  private modules: ModuleSetting[] = [];
42
44
  private tempModulesDir: string = '';
45
+ private defaultBranchCache: Map<string, string> = new Map();
46
+
43
47
  constructor(private project: Project) {
44
48
  this.tempModulesDir = join(this.project.paths.tempFolder, 'modules');
45
49
  }
@@ -53,6 +57,35 @@ export class ModuleManager {
53
57
  await this.project.collectModuleResources();
54
58
  }
55
59
 
60
+ // Creates a map of what dependencies each module depend from.
61
+ private async buildDependencyGraph(
62
+ dependencies: ModuleSetting[],
63
+ ): Promise<DependencyGraph> {
64
+ const dependencyNames = dependencies.map((item) => item.name);
65
+ const dependencyGraph = new Map<string, Set<string>>() as DependencyGraph;
66
+ for (const dependency of dependencyNames) {
67
+ dependencyGraph.set(
68
+ dependency,
69
+ await this.transientDependencies(dependency),
70
+ );
71
+ }
72
+ return dependencyGraph;
73
+ }
74
+
75
+ // Returns 'true' if 'moduleName' can be removed.
76
+ private canBeRemoved(
77
+ dependencies: DependencyGraph,
78
+ moduleName: string,
79
+ ): boolean {
80
+ let unused = true;
81
+ dependencies.forEach((transientDependencies, key) => {
82
+ if (key !== moduleName && transientDependencies.has(moduleName)) {
83
+ unused = false;
84
+ }
85
+ });
86
+ return unused;
87
+ }
88
+
56
89
  // Handles cloning of a repository.
57
90
  private async clone(
58
91
  module: ModuleSetting,
@@ -67,10 +100,15 @@ export class ModuleManager {
67
100
 
68
101
  let remote = module.location;
69
102
  if (module.private) {
70
- if (credentials && credentials?.username && credentials?.token) {
103
+ if (
104
+ credentials &&
105
+ credentials?.username &&
106
+ credentials?.token &&
107
+ module.location.startsWith(HTTPS_PROTOCOL)
108
+ ) {
71
109
  if (verbose) {
72
110
  console.log(
73
- `... Using credentials '${credentials?.username}' for cloning '${module.name}'`,
111
+ `... Using HTTPS with credentials '${credentials?.username}' for cloning '${module.name}'`,
74
112
  );
75
113
  }
76
114
  try {
@@ -83,21 +121,27 @@ export class ModuleManager {
83
121
  } catch {
84
122
  throw new Error(`Invalid repository URL: ${module.location}`);
85
123
  }
86
- } else {
124
+ } else if (module.location.startsWith('git@')) {
87
125
  if (verbose) {
88
- console.log(`... Not using credentials for cloning '${module.name}'`);
126
+ console.log(`... Using SSH for cloning '${module.name}'`);
89
127
  }
90
128
  }
129
+ } else {
130
+ if (verbose) {
131
+ console.log(
132
+ `... Using HTTPS without credentials for cloning '${module.name}'`,
133
+ );
134
+ }
91
135
  }
92
136
 
93
137
  try {
94
138
  await mkdir(this.tempModulesDir, { recursive: true });
95
- const cloneOptions = this.setCloneOptions(module);
139
+ const cloneOptions = await this.setCloneOptions(module);
96
140
  await rm(destinationPath, { recursive: true, force: true });
97
141
 
98
142
  const git: SimpleGit = simpleGit({
99
143
  timeout: {
100
- block: DEFAULT_TIMEOUT,
144
+ block: this.gitTimeout(),
101
145
  },
102
146
  });
103
147
 
@@ -110,7 +154,6 @@ export class ModuleManager {
110
154
  console.log(`... Cloned '${module.name}' to a temporary folder`);
111
155
  }
112
156
  } catch (error) {
113
- console.error(error);
114
157
  if (error instanceof Error)
115
158
  throw new Error(
116
159
  `Failed to clone module '${module.name}': ${error.message}`,
@@ -143,6 +186,61 @@ export class ModuleManager {
143
186
  }
144
187
  }
145
188
 
189
+ // Gets the default branch for a repository from remote or cache
190
+ private async defaultBranch(module: ModuleSetting): Promise<string> {
191
+ if (this.defaultBranchCache.has(module.location)) {
192
+ return this.defaultBranchCache.get(module.location)!;
193
+ }
194
+ // Set the default branch if branch was not specified
195
+ if (!module.branch) {
196
+ const destinationPath = join(this.tempModulesDir, module.name);
197
+ // Only return path after cloning
198
+ if (pathExists(destinationPath)) {
199
+ const git: SimpleGit = simpleGit({
200
+ timeout: {
201
+ block: this.gitTimeout(),
202
+ },
203
+ });
204
+ const options = ['--abbrev-ref', 'HEAD'];
205
+ const defaultBranch = await git.cwd(destinationPath).revparse(options);
206
+ this.defaultBranchCache.set(module.location, defaultBranch);
207
+ return defaultBranch;
208
+ }
209
+ }
210
+ // The actual default branch will be updated later (after cloning).
211
+ return '';
212
+ }
213
+
214
+ // Fetches direct dependencies of a module.
215
+ private async dependencies(moduleName: string): Promise<Set<string>> {
216
+ const allModules = await this.project.modules();
217
+ if (!allModules) return new Set();
218
+ const module = allModules.find((m) => m.name === moduleName);
219
+ if (!module) {
220
+ throw new Error(`Module '${moduleName}' not found`);
221
+ }
222
+ const modulePath = join(module.path, module.name, 'cardsConfig.json');
223
+ const moduleConfiguration = (await readJsonFile(
224
+ modulePath,
225
+ )) as ProjectConfiguration;
226
+ return moduleConfiguration.modules
227
+ ? new Set(moduleConfiguration.modules.map((m) => m.name))
228
+ : new Set();
229
+ }
230
+
231
+ // Increase timeout for CI environments and add platform-specific adjustments
232
+ private gitTimeout(): number {
233
+ const baseTimeout = 15000;
234
+ const isCI = process.env.CI;
235
+ const isWindows = process.platform === 'win32';
236
+
237
+ let timeout = baseTimeout;
238
+ if (isCI) timeout *= 2; // Double timeout in CI
239
+ if (isWindows) timeout *= 1.5; // 50% more time on Windows
240
+
241
+ return timeout;
242
+ }
243
+
146
244
  // Collects one module's dependency prefixes to 'this.modules'.
147
245
  // Note that there can be duplicate entries.
148
246
  private async doCollectModulePrefix(
@@ -167,16 +265,17 @@ export class ModuleManager {
167
265
 
168
266
  // Updates one module that is read from local file system.
169
267
  private async handleFileModule(module: ModuleSetting) {
170
- this.removeProtocolFromLocation(module);
268
+ this.stripProtocolFromLocation(module);
171
269
  await this.remove(module);
172
- await this.importFromFolder(module);
270
+ await this.importFromFolder(module.location, module.name);
173
271
  }
174
272
 
175
273
  // Updates one module that is received from Git.
176
274
  private async handleGitModule(module: ModuleSetting) {
177
275
  await this.clone(module);
276
+ const tempLocation = join(this.tempModulesDir, module.name);
178
277
  await this.remove(module);
179
- await this.importFromTemp(module);
278
+ await this.importFromFolder(tempLocation, module.name);
180
279
  }
181
280
 
182
281
  // Updates one module.
@@ -186,19 +285,11 @@ export class ModuleManager {
186
285
  : this.handleFileModule(module);
187
286
  }
188
287
 
189
- // Handles importing a module from module settings 'location'
190
- private async importFromFolder(module: ModuleSetting) {
191
- await this.importFileModule(module.location);
288
+ // Imports from a given folder. Is used both for .temp/<module name> and file locations.
289
+ private async importFromFolder(path: string, name: string) {
290
+ await this.importFileModule(path);
192
291
  console.log(
193
- `... Imported module '${module.name}' to '${this.project.configuration.name}'`,
194
- );
195
- }
196
-
197
- // Handles importing a module from '.temp' folder
198
- private async importFromTemp(module: ModuleSetting) {
199
- await this.importFileModule(join(this.tempModulesDir, module.name));
200
- console.log(
201
- `... Imported module '${module.name}' to '${this.project.configuration.name}'`,
292
+ `... Imported module '${name}' to '${this.project.configuration.name}'`,
202
293
  );
203
294
  }
204
295
 
@@ -211,7 +302,41 @@ export class ModuleManager {
211
302
  // Returns true if module is imported from git.
212
303
  private isGitModule(module: ModuleSetting): boolean {
213
304
  if (!module.location) return false;
214
- return module.location.startsWith('https:');
305
+ return (
306
+ module.location.startsWith('https:') || module.location.startsWith('git@')
307
+ );
308
+ }
309
+
310
+ // Collect modules that could be removed from .cards/modules when
311
+ // 'moduleName' is removed.
312
+ private async orphanedModules(
313
+ dependencies: DependencyGraph,
314
+ moduleName: string,
315
+ ): Promise<string[]> {
316
+ const projectModules = this.project.configuration.modules;
317
+ const removableTransientModules: string[] = [];
318
+ if (dependencies.has(moduleName)) {
319
+ const deps = dependencies.get(moduleName);
320
+ for (const dependency of deps!) {
321
+ const projectDependency = projectModules.some(
322
+ (item) => item.name === dependency,
323
+ );
324
+ if (projectDependency) continue;
325
+
326
+ let orphanModule = true;
327
+ dependencies.forEach((transientDependencies, key) => {
328
+ if (key === moduleName) return;
329
+ if (transientDependencies.has(dependency)) {
330
+ orphanModule = false;
331
+ }
332
+ });
333
+
334
+ if (orphanModule) {
335
+ removableTransientModules.push(dependency);
336
+ }
337
+ }
338
+ }
339
+ return removableTransientModules;
215
340
  }
216
341
 
217
342
  // Prepares '.temp/modules' for cloning
@@ -231,11 +356,6 @@ export class ModuleManager {
231
356
  }
232
357
  }
233
358
 
234
- // Returns whether to use git or file system for handling the module.
235
- private protocol(module: ModuleSetting) {
236
- return this.isFileModule(module) ? 'file' : 'git';
237
- }
238
-
239
359
  // Handles removing an imported module.
240
360
  private async remove(module: ModuleSetting) {
241
361
  try {
@@ -249,37 +369,19 @@ export class ModuleManager {
249
369
  }
250
370
  }
251
371
 
252
- // Remove module files.
253
- private async removeModuleFiles(moduleName: string) {
254
- const module = await this.project.module(moduleName);
255
- if (!module) {
256
- throw new Error(`Module '${moduleName}' not found`);
257
- }
258
- await deleteDir(module.path);
259
- }
260
-
261
- // Updates module's 'location' not to have 'protocol:' in the beginning (only for "file:" needed).
262
- private removeProtocolFromLocation(module: ModuleSetting) {
263
- const protocol = this.protocol(module);
264
- module.location = module.location.substring(
265
- protocol.length + 1,
266
- module.location.length,
267
- );
268
- }
269
-
270
372
  // Checks for duplicate ModuleSetting entries and throws an error if modules
271
373
  // with the same name have different branches or locations.
272
- // Treats undefined branch, empty string branch, and "main" branch as equivalent.
374
+ // Treats undefined branch, empty string branch, and default branch as equivalent.
273
375
  // Returns an array with duplicate entries removed
274
- private removeDuplicates(modules: ModuleSetting[]): ModuleSetting[] {
376
+ private async removeDuplicates(
377
+ modules: ModuleSetting[],
378
+ ): Promise<ModuleSetting[]> {
275
379
  const moduleMap = new Map<string, ModuleSetting>();
276
380
 
277
- // Assume that empty, or missing branch means 'main'
278
- const normalizeBranch = (branch: string | undefined): string => {
279
- if (!branch || branch === '' || branch === MAIN_BRANCH) {
280
- return MAIN_BRANCH;
281
- }
282
- return branch;
381
+ // Normalize branch names by checking against the default branch for each module
382
+
383
+ const normalizeBranch = async (module: ModuleSetting) => {
384
+ return module.branch ? module.branch! : await this.defaultBranch(module);
283
385
  };
284
386
 
285
387
  for (const module of modules) {
@@ -291,8 +393,8 @@ export class ModuleManager {
291
393
  ) {
292
394
  throw new Error(
293
395
  `Module conflict: '${module.name}' has different access:\n` +
294
- ` - ${existingModule.private === true ? 'true' : 'false'}\n` +
295
- ` - ${module.private === true ? 'true' : 'false'}`,
396
+ ` - ${Boolean(existingModule.private)}\n` +
397
+ ` - ${Boolean(module.private)}`,
296
398
  );
297
399
  }
298
400
  if (existingModule.location !== module.location) {
@@ -302,8 +404,8 @@ export class ModuleManager {
302
404
  ` - ${module.location}`,
303
405
  );
304
406
  }
305
- const existingBranch = normalizeBranch(existingModule.branch);
306
- const newBranch = normalizeBranch(module.branch);
407
+ const existingBranch = await normalizeBranch(existingModule);
408
+ const newBranch = await normalizeBranch(module);
307
409
 
308
410
  if (existingBranch !== newBranch) {
309
411
  throw new Error(
@@ -319,6 +421,15 @@ export class ModuleManager {
319
421
  return Array.from(moduleMap.values());
320
422
  }
321
423
 
424
+ // Remove module files.
425
+ private async removeModuleFiles(moduleName: string) {
426
+ const module = await this.project.module(moduleName);
427
+ if (!module) {
428
+ throw new Error(`Module '${moduleName}' not found`);
429
+ }
430
+ await deleteDir(module.path);
431
+ }
432
+
322
433
  // Gets repository name from gitUrl
323
434
  private repositoryName(gitUrl: string): string {
324
435
  const last = gitUrl.lastIndexOf('/');
@@ -326,19 +437,47 @@ export class ModuleManager {
326
437
  return repoName;
327
438
  }
328
439
 
329
- // Sets cloning options.
330
- private setCloneOptions(module: ModuleSetting) {
440
+ // Sets cloning options with support for default branch.
441
+ private async setCloneOptions(module: ModuleSetting): Promise<string[]> {
331
442
  const cloneOptions = ['--depth', '1'];
443
+ const defaultBranch = await this.defaultBranch(module);
444
+ // Only specify branch if it's different from the default branch
332
445
  if (
333
446
  module.branch &&
334
447
  module.branch !== '' &&
335
- module.branch !== MAIN_BRANCH
448
+ module.branch !== defaultBranch
336
449
  ) {
337
450
  cloneOptions.push('--branch', module.branch);
338
451
  }
339
452
  return cloneOptions;
340
453
  }
341
454
 
455
+ // Updates module's 'location' not to have 'protocol:' in the beginning (only for "file:" needed).
456
+ private stripProtocolFromLocation(module: ModuleSetting) {
457
+ const protocol = this.isFileModule(module) ? 'file' : 'git';
458
+ module.location = module.location.substring(
459
+ protocol.length + 1,
460
+ module.location.length,
461
+ );
462
+ }
463
+
464
+ // Fetches all dependencies for a module.
465
+ private async transientDependencies(
466
+ moduleName: string,
467
+ ): Promise<Set<string>> {
468
+ const dependencies = await this.dependencies(moduleName);
469
+ let transientDependencies: Set<string> = new Set(dependencies);
470
+ for (const dependency of dependencies) {
471
+ const depTransients = await this.transientDependencies(dependency);
472
+ transientDependencies = new Set([
473
+ ...transientDependencies,
474
+ ...depTransients,
475
+ ]);
476
+ }
477
+
478
+ return transientDependencies;
479
+ }
480
+
342
481
  // Updates modules in the project.
343
482
  private async update(module?: ModuleSetting, credentials?: Credentials) {
344
483
  // Prints dots every half second so that user knows that something is ongoing
@@ -350,7 +489,9 @@ export class ModuleManager {
350
489
  // Stops the above, and shows results
351
490
  function finished(interval: NodeJS.Timeout, modules: string[]) {
352
491
  clearInterval(interval);
353
- console.log(`\n... Found modules: ${modules.join(', ')}`);
492
+ if (modules.length > 0) {
493
+ console.log(`\n... Found modules: ${modules.join(', ')}`);
494
+ }
354
495
  }
355
496
 
356
497
  await this.prepare();
@@ -366,8 +507,7 @@ export class ModuleManager {
366
507
  let uniqueModules: ModuleSetting[] = [];
367
508
  try {
368
509
  await this.collectModulePrefixes(modules, credentials);
369
-
370
- uniqueModules = this.removeDuplicates(this.modules);
510
+ uniqueModules = await this.removeDuplicates(this.modules);
371
511
  } finally {
372
512
  finished(
373
513
  dotInterval,
@@ -380,7 +520,6 @@ export class ModuleManager {
380
520
  promises.push(this.handleModule(module)),
381
521
  );
382
522
  await Promise.all(promises);
383
-
384
523
  await deleteDir(this.tempModulesDir);
385
524
  await this.project.collectModuleResources();
386
525
  }
@@ -467,6 +606,39 @@ export class ModuleManager {
467
606
  return modulePrefix;
468
607
  }
469
608
 
609
+ /**
610
+ * Removed module from project.
611
+ * If module is not used by any other modules, then will remove the module from disk as well.
612
+ * Otherwise, only updates project configuration.
613
+ * @param moduleName Name of the module to remove
614
+ */
615
+ public async removeModule(moduleName: string) {
616
+ const projectModules = this.project.configuration.modules;
617
+ const dependencies = await this.buildDependencyGraph(projectModules);
618
+ const module = await this.project.module(moduleName);
619
+
620
+ if (!module) {
621
+ throw new Error(`Module '${moduleName}' not found`);
622
+ }
623
+
624
+ // Project module can always be removed from project configuration,
625
+ // but modules under .cards/modules must be checked not to be used by
626
+ // other modules.
627
+ if (this.canBeRemoved(dependencies, moduleName)) {
628
+ const orphans = await this.orphanedModules(dependencies, moduleName);
629
+ await deleteDir(module.path);
630
+ for (const moduleToDelete of orphans) {
631
+ const modulePath = join(
632
+ this.project.paths.modulesFolder,
633
+ moduleToDelete,
634
+ );
635
+ await deleteDir(modulePath);
636
+ }
637
+ await this.project.collectModuleResources();
638
+ }
639
+ await this.project.configuration.removeModule(moduleName);
640
+ }
641
+
470
642
  /**
471
643
  * Imports module from a local file path or a git URL.
472
644
  * @param module Module to update. If not provided, updates all modules.
@@ -10,7 +10,7 @@
10
10
  License along with this program. If not, see <https://www.gnu.org/licenses/>.
11
11
  */
12
12
 
13
- import type { Calculate } from '../commands/index.js';
13
+ import type { CalculationEngine } from '../containers/project/calculation-engine.js';
14
14
  import type { DeniedOperationCollection } from '../types/queries.js';
15
15
 
16
16
  export type Action = keyof DeniedOperationCollection;
@@ -25,7 +25,7 @@ function checkOperation<T extends { errorMessage: string }>(data: Array<T>) {
25
25
  * This class is used to guard actions from being used without permissions
26
26
  */
27
27
  export class ActionGuard {
28
- constructor(private calculate: Calculate) {}
28
+ constructor(private calculate: CalculationEngine) {}
29
29
 
30
30
  /**
31
31
  * Checks whether an action can be done
@@ -39,7 +39,7 @@ export class ActionGuard {
39
39
  param?: string,
40
40
  ) {
41
41
  await this.calculate.generate();
42
- const cards = await this.calculate.runQuery('card', {
42
+ const cards = await this.calculate.runQuery('card', 'localApp', {
43
43
  cardKey,
44
44
  });
45
45
  if (cards.length === 0) {
@@ -57,18 +57,9 @@ export class ProjectConfiguration implements ProjectSettings {
57
57
 
58
58
  // Sets configuration values from file.
59
59
  private readSettings() {
60
- let settings;
61
- try {
62
- settings = readJsonFileSync(this.settingPath) as ProjectConfiguration;
63
- } catch {
64
- throw new Error(
65
- `Invalid path '${this.settingPath}' to configuration file`,
66
- );
67
- }
60
+ const settings = readJsonFileSync(this.settingPath) as ProjectConfiguration;
68
61
  if (!settings) {
69
- throw new Error(
70
- `Invalid path '${this.settingPath}' to configuration file`,
71
- );
62
+ throw new Error(`File at '${this.settingPath}' is not a valid JSON file`);
72
63
  }
73
64
 
74
65
  const valid =
@@ -153,6 +153,50 @@ export class CardTypeResource extends FileResource {
153
153
  }
154
154
  }
155
155
 
156
+ // Apply state mapping to all cards using this card type.
157
+ // Checks that all states in the current workflow are updated.
158
+ private async handleWorkflowChange<Type>(
159
+ stateMapping: Record<string, string>,
160
+ op: ChangeOperation<Type>,
161
+ ) {
162
+ await this.verifyStateMapping(stateMapping, op);
163
+ const cards = await this.collectCards(
164
+ {
165
+ metadata: true,
166
+ content: true,
167
+ },
168
+ this.content.name,
169
+ );
170
+
171
+ const unmappedStates: string[] = [];
172
+
173
+ // Update each card's workflowState if it has a mapping
174
+ const updatePromises = cards.map(async (card) => {
175
+ if (card.metadata && card.metadata.workflowState) {
176
+ const currentState = card.metadata.workflowState as string;
177
+ const newState = stateMapping[currentState];
178
+
179
+ if (newState && newState !== currentState) {
180
+ this.logger.info(
181
+ `Updating card '${card.key}': ${currentState} -> ${newState}`,
182
+ );
183
+ card.metadata.workflowState = newState;
184
+ await this.project.updateCardMetadata(card, card.metadata);
185
+ } else if (!newState && !unmappedStates.includes(currentState)) {
186
+ unmappedStates.push(currentState);
187
+ }
188
+ }
189
+ });
190
+
191
+ await Promise.all(updatePromises);
192
+
193
+ if (unmappedStates.length > 0) {
194
+ this.logger.warn(
195
+ `Found unmapped states that were not updated: ${unmappedStates.join(', ')}`,
196
+ );
197
+ }
198
+ }
199
+
156
200
  // Checks if field type exists in this card type.
157
201
  private async hasFieldType(field: Partial<CustomField>): Promise<boolean> {
158
202
  return this.data.customFields.some((item) => item.name === field.name);
@@ -315,6 +359,57 @@ export class CardTypeResource extends FileResource {
315
359
  }
316
360
  }
317
361
 
362
+ // Verifies that:
363
+ // - all states in the current workflow are covered in the state mapping
364
+ // - the states are correct
365
+ private async verifyStateMapping<Type>(
366
+ stateMapping: Record<string, string>,
367
+ op: ChangeOperation<Type>,
368
+ ) {
369
+ const currentWorkflowName = op.target as string;
370
+ const currentWorkflow =
371
+ await this.project.resource<Workflow>(currentWorkflowName);
372
+ if (!currentWorkflow) {
373
+ throw new Error(
374
+ `Workflow '${currentWorkflowName}' does not exist in the project`,
375
+ );
376
+ }
377
+
378
+ const newWorkflow = await this.project.resource<Workflow>(op.to as string);
379
+ if (!newWorkflow) {
380
+ throw new Error(`Workflow '${op.to}' does not exist in the project`);
381
+ }
382
+
383
+ const currentWorkflowStates = currentWorkflow.states.map(
384
+ (state) => state.name,
385
+ );
386
+ const mappedSourceStates = Object.keys(stateMapping);
387
+ const unmappedCurrentStates = currentWorkflowStates.filter(
388
+ (stateName) => !mappedSourceStates.includes(stateName),
389
+ );
390
+
391
+ if (unmappedCurrentStates.length > 0) {
392
+ throw new Error(
393
+ `State mapping validation failed: The following states exist in the current workflow '${currentWorkflowName}' ` +
394
+ `but are not mapped from in the state mapping JSON file: ${unmappedCurrentStates.join(', ')}. ` +
395
+ `Please ensure all states in the current workflow are accounted for in the mapping to ensure all cards are properly updated.`,
396
+ );
397
+ }
398
+
399
+ // Also verify that all target states exist in the new workflow
400
+ const newWorkflowStates = newWorkflow.states.map((state) => state.name);
401
+ const mappedTargetStates = Object.values(stateMapping);
402
+ const invalidTargetStates = mappedTargetStates.filter(
403
+ (stateName) => !newWorkflowStates.includes(stateName),
404
+ );
405
+
406
+ if (invalidTargetStates.length > 0) {
407
+ throw new Error(
408
+ `State mapping validation failed: The following target states in the mapping do not exist in the new workflow '${op.to}': ${invalidTargetStates.join(', ')}.`,
409
+ );
410
+ }
411
+ }
412
+
318
413
  /**
319
414
  * Creates a new card type object. Base class writes the object to disk automatically.
320
415
  * @param workflowName Workflow name that this card type uses.
@@ -404,7 +499,12 @@ export class CardTypeResource extends FileResource {
404
499
  content.optionallyVisibleFields as Type[],
405
500
  ) as string[];
406
501
  } else if (key === 'workflow') {
502
+ const changeOp = op as ChangeOperation<string>;
503
+ const stateMapping = changeOp.mappingTable?.stateMapping || {};
407
504
  content.workflow = super.handleScalar(op) as string;
505
+ if (Object.keys(stateMapping).length > 0) {
506
+ await this.handleWorkflowChange(stateMapping, changeOp);
507
+ }
408
508
  } else if (key === 'customFields') {
409
509
  await this.validateFieldType(key, op);
410
510
  content.customFields = super.handleArray(
@@ -137,6 +137,16 @@ export class FileResource extends ResourceObject {
137
137
  return cards;
138
138
  }
139
139
 
140
+ // Get (resource folder) type name
141
+ protected get getType() {
142
+ return super.getType;
143
+ }
144
+
145
+ // Get logger instance
146
+ protected get logger() {
147
+ return super.getLogger(this.getType);
148
+ }
149
+
140
150
  // Initialize the resource.
141
151
  protected initialize() {
142
152
  if (this.resourceName.type === '') {
@@ -301,9 +311,9 @@ export class FileResource extends ResourceObject {
301
311
  ) {
302
312
  function toValue(op: Operation<Type>) {
303
313
  if (op.name === 'rank') return op.newIndex;
304
- if (op.name === 'add') return op.target;
305
- if (op.name === 'remove') return op.target;
306
- if (op.name === 'change') return op.to;
314
+ if (op.name === 'add') return JSON.stringify(op.target);
315
+ if (op.name === 'remove') return JSON.stringify(op.target);
316
+ if (op.name === 'change') return JSON.stringify(op.to);
307
317
  }
308
318
 
309
319
  // Check that new name is valid.
@@ -320,7 +330,9 @@ export class FileResource extends ResourceObject {
320
330
  } catch (error) {
321
331
  if (error instanceof Error) {
322
332
  const errorValue = typeof op === 'object' ? toValue(op) : op;
323
- throw new Error(`Cannot ${op.name} '${key}' --> '${errorValue}'`);
333
+ throw new Error(
334
+ `Cannot ${op.name} '${key}' --> '${errorValue}: ${error.message}'`,
335
+ );
324
336
  }
325
337
  }
326
338