@cyberismo/data-handler 0.0.13 → 0.0.15

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 (262) hide show
  1. package/dist/card-metadata-updater.js +1 -3
  2. package/dist/card-metadata-updater.js.map +1 -1
  3. package/dist/command-handler.js +13 -17
  4. package/dist/command-handler.js.map +1 -1
  5. package/dist/command-manager.d.ts +1 -1
  6. package/dist/command-manager.js +4 -3
  7. package/dist/command-manager.js.map +1 -1
  8. package/dist/commands/create.d.ts +3 -3
  9. package/dist/commands/create.js +20 -81
  10. package/dist/commands/create.js.map +1 -1
  11. package/dist/commands/edit.d.ts +12 -25
  12. package/dist/commands/edit.js +25 -74
  13. package/dist/commands/edit.js.map +1 -1
  14. package/dist/commands/export.js +4 -17
  15. package/dist/commands/export.js.map +1 -1
  16. package/dist/commands/fetch.js +2 -1
  17. package/dist/commands/fetch.js.map +1 -1
  18. package/dist/commands/import.js +3 -5
  19. package/dist/commands/import.js.map +1 -1
  20. package/dist/commands/move.d.ts +1 -2
  21. package/dist/commands/move.js +108 -146
  22. package/dist/commands/move.js.map +1 -1
  23. package/dist/commands/remove.js +15 -49
  24. package/dist/commands/remove.js.map +1 -1
  25. package/dist/commands/rename.d.ts +1 -0
  26. package/dist/commands/rename.js +13 -7
  27. package/dist/commands/rename.js.map +1 -1
  28. package/dist/commands/show.d.ts +7 -25
  29. package/dist/commands/show.js +39 -113
  30. package/dist/commands/show.js.map +1 -1
  31. package/dist/commands/transition.js +27 -30
  32. package/dist/commands/transition.js.map +1 -1
  33. package/dist/commands/update.d.ts +5 -3
  34. package/dist/commands/update.js +19 -5
  35. package/dist/commands/update.js.map +1 -1
  36. package/dist/commands/validate.d.ts +3 -3
  37. package/dist/commands/validate.js +20 -27
  38. package/dist/commands/validate.js.map +1 -1
  39. package/dist/containers/card-container.d.ts +87 -24
  40. package/dist/containers/card-container.js +183 -279
  41. package/dist/containers/card-container.js.map +1 -1
  42. package/dist/containers/project/calculation-engine.d.ts +6 -0
  43. package/dist/containers/project/calculation-engine.js +36 -29
  44. package/dist/containers/project/calculation-engine.js.map +1 -1
  45. package/dist/containers/project/card-cache.d.ts +146 -0
  46. package/dist/containers/project/card-cache.js +411 -0
  47. package/dist/containers/project/card-cache.js.map +1 -0
  48. package/dist/containers/project/resource-collector.d.ts +24 -1
  49. package/dist/containers/project/resource-collector.js +8 -1
  50. package/dist/containers/project/resource-collector.js.map +1 -1
  51. package/dist/containers/project.d.ts +119 -84
  52. package/dist/containers/project.js +423 -253
  53. package/dist/containers/project.js.map +1 -1
  54. package/dist/containers/template.d.ts +15 -31
  55. package/dist/containers/template.js +97 -104
  56. package/dist/containers/template.js.map +1 -1
  57. package/dist/index.d.ts +1 -0
  58. package/dist/index.js +1 -0
  59. package/dist/index.js.map +1 -1
  60. package/dist/interfaces/folder-content-interfaces.d.ts +12 -5
  61. package/dist/interfaces/folder-content-interfaces.js +5 -3
  62. package/dist/interfaces/folder-content-interfaces.js.map +1 -1
  63. package/dist/interfaces/macros.d.ts +1 -0
  64. package/dist/interfaces/macros.js +1 -1
  65. package/dist/interfaces/macros.js.map +1 -1
  66. package/dist/interfaces/project-interfaces.d.ts +16 -10
  67. package/dist/interfaces/project-interfaces.js +10 -8
  68. package/dist/interfaces/project-interfaces.js.map +1 -1
  69. package/dist/interfaces/resource-interfaces.d.ts +21 -22
  70. package/dist/interfaces/resource-interfaces.js +3 -0
  71. package/dist/interfaces/resource-interfaces.js.map +1 -1
  72. package/dist/macros/common.d.ts +10 -10
  73. package/dist/macros/createCards/index.d.ts +0 -13
  74. package/dist/macros/createCards/index.js.map +1 -1
  75. package/dist/macros/createCards/types.d.ts +44 -0
  76. package/dist/macros/createCards/types.js +15 -0
  77. package/dist/macros/createCards/types.js.map +1 -0
  78. package/dist/macros/graph/index.d.ts +2 -6
  79. package/dist/macros/graph/index.js +2 -2
  80. package/dist/macros/graph/index.js.map +1 -1
  81. package/dist/macros/graph/types.d.ts +23 -0
  82. package/dist/macros/graph/types.js +15 -0
  83. package/dist/macros/graph/types.js.map +1 -0
  84. package/dist/macros/image/index.d.ts +8 -16
  85. package/dist/macros/image/index.js +36 -33
  86. package/dist/macros/image/index.js.map +1 -1
  87. package/dist/macros/image/types.d.ts +38 -0
  88. package/dist/macros/image/types.js +15 -0
  89. package/dist/macros/image/types.js.map +1 -0
  90. package/dist/macros/include/index.d.ts +1 -6
  91. package/dist/macros/include/index.js +4 -7
  92. package/dist/macros/include/index.js.map +1 -1
  93. package/dist/macros/include/types.d.ts +31 -0
  94. package/dist/macros/include/types.js +15 -0
  95. package/dist/macros/include/types.js.map +1 -0
  96. package/dist/macros/percentage/index.d.ts +0 -6
  97. package/dist/macros/percentage/index.js.map +1 -1
  98. package/dist/macros/percentage/types.d.ts +31 -0
  99. package/dist/macros/percentage/types.js +15 -0
  100. package/dist/macros/percentage/types.js.map +1 -0
  101. package/dist/macros/report/index.d.ts +0 -3
  102. package/dist/macros/report/index.js.map +1 -1
  103. package/dist/macros/report/types.d.ts +19 -0
  104. package/dist/macros/report/types.js +15 -0
  105. package/dist/macros/report/types.js.map +1 -0
  106. package/dist/macros/scoreCard/index.d.ts +0 -6
  107. package/dist/macros/scoreCard/index.js.map +1 -1
  108. package/dist/macros/scoreCard/types.d.ts +31 -0
  109. package/dist/macros/scoreCard/types.js +15 -0
  110. package/dist/macros/scoreCard/types.js.map +1 -0
  111. package/dist/macros/types.d.ts +25 -0
  112. package/dist/macros/types.js +2 -0
  113. package/dist/macros/types.js.map +1 -0
  114. package/dist/macros/vega/index.d.ts +0 -4
  115. package/dist/macros/vega/index.js.map +1 -1
  116. package/dist/macros/vega/types.d.ts +20 -0
  117. package/dist/macros/vega/types.js +2 -0
  118. package/dist/macros/vega/types.js.map +1 -0
  119. package/dist/macros/vegalite/index.d.ts +0 -4
  120. package/dist/macros/vegalite/index.js.map +1 -1
  121. package/dist/macros/vegalite/types.d.ts +20 -0
  122. package/dist/macros/vegalite/types.js +15 -0
  123. package/dist/macros/vegalite/types.js.map +1 -0
  124. package/dist/macros/xref/index.d.ts +0 -3
  125. package/dist/macros/xref/index.js +5 -14
  126. package/dist/macros/xref/index.js.map +1 -1
  127. package/dist/macros/xref/types.d.ts +19 -0
  128. package/dist/macros/xref/types.js +15 -0
  129. package/dist/macros/xref/types.js.map +1 -0
  130. package/dist/module-manager.js +4 -4
  131. package/dist/module-manager.js.map +1 -1
  132. package/dist/project-settings.js.map +1 -1
  133. package/dist/resources/calculation-resource.d.ts +43 -0
  134. package/dist/resources/calculation-resource.js +75 -0
  135. package/dist/resources/calculation-resource.js.map +1 -0
  136. package/dist/resources/card-type-resource.d.ts +4 -21
  137. package/dist/resources/card-type-resource.js +13 -44
  138. package/dist/resources/card-type-resource.js.map +1 -1
  139. package/dist/resources/create-defaults.d.ts +13 -6
  140. package/dist/resources/create-defaults.js +19 -5
  141. package/dist/resources/create-defaults.js.map +1 -1
  142. package/dist/resources/field-type-resource.d.ts +4 -21
  143. package/dist/resources/field-type-resource.js +14 -38
  144. package/dist/resources/field-type-resource.js.map +1 -1
  145. package/dist/resources/file-resource.d.ts +12 -29
  146. package/dist/resources/file-resource.js +19 -287
  147. package/dist/resources/file-resource.js.map +1 -1
  148. package/dist/resources/folder-resource.d.ts +32 -51
  149. package/dist/resources/folder-resource.js +68 -96
  150. package/dist/resources/folder-resource.js.map +1 -1
  151. package/dist/resources/graph-model-resource.d.ts +5 -33
  152. package/dist/resources/graph-model-resource.js +8 -61
  153. package/dist/resources/graph-model-resource.js.map +1 -1
  154. package/dist/resources/graph-view-resource.d.ts +5 -28
  155. package/dist/resources/graph-view-resource.js +6 -45
  156. package/dist/resources/graph-view-resource.js.map +1 -1
  157. package/dist/resources/link-type-resource.d.ts +4 -21
  158. package/dist/resources/link-type-resource.js +6 -31
  159. package/dist/resources/link-type-resource.js.map +1 -1
  160. package/dist/resources/report-resource.d.ts +5 -17
  161. package/dist/resources/report-resource.js +6 -44
  162. package/dist/resources/report-resource.js.map +1 -1
  163. package/dist/resources/resource-object.d.ts +58 -23
  164. package/dist/resources/resource-object.js +307 -26
  165. package/dist/resources/resource-object.js.map +1 -1
  166. package/dist/resources/template-resource.d.ts +4 -15
  167. package/dist/resources/template-resource.js +10 -25
  168. package/dist/resources/template-resource.js.map +1 -1
  169. package/dist/resources/workflow-resource.d.ts +4 -23
  170. package/dist/resources/workflow-resource.js +12 -38
  171. package/dist/resources/workflow-resource.js.map +1 -1
  172. package/dist/utils/card-utils.d.ts +69 -19
  173. package/dist/utils/card-utils.js +179 -30
  174. package/dist/utils/card-utils.js.map +1 -1
  175. package/dist/utils/clingo-facts.js +11 -3
  176. package/dist/utils/clingo-facts.js.map +1 -1
  177. package/dist/utils/clingo-parser.js +1 -1
  178. package/dist/utils/clingo-parser.js.map +1 -1
  179. package/dist/utils/constants.d.ts +2 -0
  180. package/dist/utils/constants.js +5 -0
  181. package/dist/utils/constants.js.map +1 -1
  182. package/dist/utils/csv.js +1 -1
  183. package/dist/utils/csv.js.map +1 -1
  184. package/dist/utils/error-utils.d.ts +34 -0
  185. package/dist/utils/error-utils.js +56 -0
  186. package/dist/utils/error-utils.js.map +1 -0
  187. package/dist/utils/log-utils.d.ts +0 -27
  188. package/dist/utils/log-utils.js +0 -58
  189. package/dist/utils/log-utils.js.map +1 -1
  190. package/dist/utils/user-preferences.js +6 -3
  191. package/dist/utils/user-preferences.js.map +1 -1
  192. package/package.json +5 -5
  193. package/src/card-metadata-updater.ts +3 -5
  194. package/src/command-handler.ts +14 -19
  195. package/src/command-manager.ts +4 -3
  196. package/src/commands/create.ts +28 -112
  197. package/src/commands/edit.ts +27 -118
  198. package/src/commands/export.ts +8 -29
  199. package/src/commands/fetch.ts +2 -1
  200. package/src/commands/import.ts +4 -6
  201. package/src/commands/move.ts +144 -179
  202. package/src/commands/remove.ts +12 -54
  203. package/src/commands/rename.ts +22 -7
  204. package/src/commands/show.ts +51 -156
  205. package/src/commands/transition.ts +30 -33
  206. package/src/commands/update.ts +27 -9
  207. package/src/commands/validate.ts +22 -37
  208. package/src/containers/card-container.ts +200 -360
  209. package/src/containers/project/calculation-engine.ts +43 -33
  210. package/src/containers/project/card-cache.ts +497 -0
  211. package/src/containers/project/resource-collector.ts +9 -1
  212. package/src/containers/project.ts +533 -328
  213. package/src/containers/template.ts +109 -127
  214. package/src/index.ts +1 -0
  215. package/src/interfaces/folder-content-interfaces.ts +23 -5
  216. package/src/interfaces/macros.ts +2 -0
  217. package/src/interfaces/project-interfaces.ts +19 -10
  218. package/src/interfaces/resource-interfaces.ts +22 -24
  219. package/src/macros/createCards/index.ts +1 -12
  220. package/src/macros/createCards/types.ts +46 -0
  221. package/src/macros/graph/index.ts +3 -7
  222. package/src/macros/graph/types.ts +24 -0
  223. package/src/macros/image/index.ts +50 -61
  224. package/src/macros/image/types.ts +39 -0
  225. package/src/macros/include/index.ts +6 -15
  226. package/src/macros/include/types.ts +32 -0
  227. package/src/macros/percentage/index.ts +1 -7
  228. package/src/macros/percentage/types.ts +32 -0
  229. package/src/macros/report/index.ts +1 -4
  230. package/src/macros/report/types.ts +20 -0
  231. package/src/macros/scoreCard/index.ts +1 -7
  232. package/src/macros/scoreCard/types.ts +32 -0
  233. package/src/macros/types.ts +48 -0
  234. package/src/macros/vega/index.ts +1 -4
  235. package/src/macros/vega/types.ts +21 -0
  236. package/src/macros/vegalite/index.ts +1 -4
  237. package/src/macros/vegalite/types.ts +22 -0
  238. package/src/macros/xref/index.ts +6 -20
  239. package/src/macros/xref/types.ts +20 -0
  240. package/src/module-manager.ts +5 -5
  241. package/src/project-settings.ts +1 -1
  242. package/src/resources/calculation-resource.ts +101 -0
  243. package/src/resources/card-type-resource.ts +24 -59
  244. package/src/resources/create-defaults.ts +21 -5
  245. package/src/resources/field-type-resource.ts +22 -51
  246. package/src/resources/file-resource.ts +27 -403
  247. package/src/resources/folder-resource.ts +99 -125
  248. package/src/resources/graph-model-resource.ts +17 -74
  249. package/src/resources/graph-view-resource.ts +14 -54
  250. package/src/resources/link-type-resource.ts +13 -40
  251. package/src/resources/report-resource.ts +17 -57
  252. package/src/resources/resource-object.ts +454 -39
  253. package/src/resources/template-resource.ts +16 -29
  254. package/src/resources/workflow-resource.ts +26 -50
  255. package/src/utils/card-utils.ts +217 -31
  256. package/src/utils/clingo-facts.ts +13 -3
  257. package/src/utils/clingo-parser.ts +1 -1
  258. package/src/utils/constants.ts +7 -0
  259. package/src/utils/csv.ts +1 -1
  260. package/src/utils/error-utils.ts +62 -0
  261. package/src/utils/log-utils.ts +0 -68
  262. package/src/utils/user-preferences.ts +7 -3
@@ -13,8 +13,15 @@
13
13
  */
14
14
 
15
15
  // node
16
- import { dirname, join, resolve, sep } from 'node:path';
17
- import { readdir } from 'node:fs/promises';
16
+ import { basename, join, resolve } from 'node:path';
17
+ import {
18
+ constants as fsConstants,
19
+ copyFile,
20
+ mkdir,
21
+ readdir,
22
+ unlink,
23
+ writeFile,
24
+ } from 'node:fs/promises';
18
25
 
19
26
  import { CardContainer } from './card-container.js'; // base class
20
27
 
@@ -35,13 +42,18 @@ import {
35
42
  type Resource,
36
43
  type ResourceFolderType,
37
44
  } from '../interfaces/project-interfaces.js';
38
- import { getFilesSync, pathExists } from '../utils/file-utils.js';
45
+ import { pathExists } from '../utils/file-utils.js';
39
46
  import { generateRandomString } from '../utils/random.js';
40
- import { isTemplateCard } from '../utils/card-utils.js';
47
+ import {
48
+ cardPathParts,
49
+ isModulePath,
50
+ isTemplateCard,
51
+ } from '../utils/card-utils.js';
41
52
  import { ProjectConfiguration } from '../project-settings.js';
42
53
  import { ProjectPaths } from './project/project-paths.js';
43
54
  import { readJsonFile } from '../utils/json.js';
44
55
  import {
56
+ pathToResourceName,
45
57
  resourceName,
46
58
  type ResourceName,
47
59
  resourceNameToString,
@@ -53,6 +65,7 @@ import {
53
65
  import type { Template } from './template.js';
54
66
  import { Validate } from '../commands/validate.js';
55
67
 
68
+ import { CalculationResource } from '../resources/calculation-resource.js';
56
69
  import { CardTypeResource } from '../resources/card-type-resource.js';
57
70
  import { FieldTypeResource } from '../resources/field-type-resource.js';
58
71
  import { GraphModelResource } from '../resources/graph-model-resource.js';
@@ -63,7 +76,9 @@ import { TemplateResource } from '../resources/template-resource.js';
63
76
  import { WorkflowResource } from '../resources/workflow-resource.js';
64
77
 
65
78
  import { ContentWatcher } from './project/project-content-watcher.js';
66
- import { pathToResourceName } from '../utils/resource-utils.js';
79
+ import { getChildLogger } from '../utils/log-utils.js';
80
+
81
+ import { ROOT } from '../utils/constants.js';
67
82
 
68
83
  // Re-export this, so that classes that use Project do not need to have separate import.
69
84
  export { ResourcesFrom };
@@ -73,34 +88,44 @@ export { ResourcesFrom };
73
88
  */
74
89
  export class Project extends CardContainer {
75
90
  public calculationEngine: CalculationEngine;
76
- private resources: ResourceCollector;
77
- private projectPaths: ProjectPaths;
78
- private settings: ProjectConfiguration;
79
- private validator: Validate;
80
- private resourceWatcher: ContentWatcher | undefined;
81
-
82
91
  // Created resources are held in a cache.
83
92
  // In the cache, key is resource name, and data is resource metadata (as JSON).
84
93
  private createdResources = new Map<string, JSON>();
94
+ private logger = getChildLogger({ module: 'Project' });
95
+ private projectPaths: ProjectPaths;
96
+ private resources: ResourceCollector;
97
+ private resourceWatcher: ContentWatcher | undefined;
98
+ private settings: ProjectConfiguration;
99
+ private validator: Validate;
85
100
 
86
101
  constructor(
87
102
  path: string,
88
103
  private watchResourceChanges?: boolean,
89
104
  ) {
90
- super(path, '');
91
-
92
- this.calculationEngine = new CalculationEngine(this);
93
-
94
- this.settings = new ProjectConfiguration(
105
+ const settings = new ProjectConfiguration(
95
106
  join(path, '.cards', 'local', Project.projectConfigFileName),
96
107
  );
108
+ super(path, settings.cardKeyPrefix, '');
109
+ this.settings = settings;
110
+
111
+ this.logger.info({ path }, 'Initializing project');
112
+
113
+ this.calculationEngine = new CalculationEngine(this);
97
114
  this.projectPaths = new ProjectPaths(path);
98
115
  this.resources = new ResourceCollector(this);
99
116
 
100
117
  this.containerName = this.settings.name;
101
118
  // todo: implement project validation
102
119
  this.validator = Validate.getInstance();
120
+ this.logger.info(
121
+ { resourcesFolder: this.paths.resourcesFolder },
122
+ 'Collecting local resources',
123
+ );
103
124
  this.resources.collectLocalResources();
125
+ this.logger.info(
126
+ { name: this.containerName },
127
+ 'Project initialization complete',
128
+ );
104
129
 
105
130
  const ignoreRenameFileChanges = true;
106
131
 
@@ -134,17 +159,15 @@ export class Project extends CardContainer {
134
159
  }
135
160
  }
136
161
 
137
- // Removes current version of resource from cache.
138
- // Then re-creates the resource with current data and caches the value again.
139
- // If the value wasn't in the cache before, it will be added.
140
- private async replaceCacheValue(resourceName: string) {
141
- if (this.createdResources.has(resourceName)) {
142
- // First, remove the old version from cache
143
- this.createdResources.delete(resourceName);
162
+ // Changes a card's parent in the cache and updates all relationships.
163
+ private changeParent(updatedCard: Card, previousParent?: string) {
164
+ if (previousParent && previousParent !== ROOT) {
165
+ this.removeCachedChildren(previousParent, updatedCard.key);
144
166
  }
145
- const resourceData = await this.resource(resourceName);
146
- if (!resourceData) return;
147
- this.createdResources.set(resourceName, resourceData as JSON);
167
+ if (updatedCard.parent && updatedCard.parent !== ROOT) {
168
+ this.updateCachedChildren(updatedCard.parent, updatedCard);
169
+ }
170
+ this.cardCache.updateCard(updatedCard.key, updatedCard);
148
171
  }
149
172
 
150
173
  // Finds specific module.
@@ -154,8 +177,55 @@ export class Project extends CardContainer {
154
177
  );
155
178
  }
156
179
 
180
+ // Handles attachment changes after filesystem operations.
181
+ private async handleAttachmentChange(
182
+ cardKey: string,
183
+ operation: 'added' | 'removed' | 'refresh',
184
+ fileName: string,
185
+ ): Promise<void> {
186
+ if (operation === 'added') {
187
+ this.cardCache.addAttachment(cardKey, fileName);
188
+ } else if (operation === 'removed') {
189
+ this.cardCache.deleteAttachment(cardKey, fileName);
190
+ } else if (operation === 'refresh') {
191
+ const newAttachments = this.cardCache.getCardAttachments(cardKey);
192
+ if (newAttachments) {
193
+ this.cardCache.updateCardAttachments(cardKey, newAttachments);
194
+ }
195
+ }
196
+ }
197
+
198
+ // Determines the parent card key from a card's filesystem path.
199
+ private parentFromPath(cardPath: string): string {
200
+ return cardPathParts(this.projectPrefix, cardPath).parents.at(-1) || 'root';
201
+ }
202
+
203
+ // Remove children from a card in the card cache
204
+ private removeCachedChildren(parentKey: string, childKey: string) {
205
+ const parentCard = this.cardCache.getCard(parentKey);
206
+ if (parentCard && parentCard.children) {
207
+ parentCard.children = parentCard.children.filter(
208
+ (child) => child !== childKey,
209
+ );
210
+ this.cardCache.updateCard(parentCard.key, parentCard);
211
+ }
212
+ }
213
+
214
+ // Removes current version of a resource from the resource cache.
215
+ // Then re-creates the resource with current data and caches the value again.
216
+ // If the value wasn't in the cache before, it will be added.
217
+ private async replaceCacheValue(resourceName: string) {
218
+ if (this.createdResources.has(resourceName)) {
219
+ // First, remove the old version from cache
220
+ this.createdResources.delete(resourceName);
221
+ }
222
+ const resourceData = await this.resource(resourceName);
223
+ if (resourceData) {
224
+ this.createdResources.set(resourceName, resourceData as JSON);
225
+ }
226
+ }
227
+
157
228
  // Returns (local or all) resources of a given type.
158
- // @todo: if this would be public, we could remove cardTypes(), fieldTypes(), ... and similar APIs
159
229
  private async resourcesOfType(
160
230
  type: ResourceFolderType,
161
231
  from: ResourcesFrom = ResourcesFrom.localOnly,
@@ -163,6 +233,81 @@ export class Project extends CardContainer {
163
233
  return this.resources.resources(type, from);
164
234
  }
165
235
 
236
+ // Updates children in the card cache
237
+ private updateCachedChildren(parentKey: string, newChild: Card) {
238
+ const parentCard = this.cardCache.getCard(parentKey);
239
+ if (parentCard) {
240
+ // Add or update the child in the parent's children array
241
+ const existingChildIndex = parentCard.children?.findIndex(
242
+ (child) => child === newChild.key,
243
+ );
244
+ if (existingChildIndex === -1) {
245
+ parentCard.children.push(newChild.key);
246
+ }
247
+ this.cardCache.updateCard(parentCard.key, parentCard);
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Populate template cards into the card cache.
253
+ */
254
+ protected async populateTemplateCards(): Promise<void> {
255
+ try {
256
+ // Gets local & module templates
257
+ const templateResources = await this.templates();
258
+ const prefixes = await this.projectPrefixes();
259
+ const loadPromises = templateResources.map(async (template) => {
260
+ try {
261
+ this.validator.validResourceName(
262
+ 'templates',
263
+ template.name,
264
+ prefixes,
265
+ );
266
+ } catch (error) {
267
+ this.logger.warn(
268
+ { templateName: template.name, error },
269
+ `Template name '${template.name}' does not follow required format, skipping`,
270
+ );
271
+ return;
272
+ }
273
+
274
+ const templateResource = new TemplateResource(
275
+ this,
276
+ resourceName(template.name),
277
+ );
278
+
279
+ const templateObject = templateResource.templateObject();
280
+ const isCreated = templateObject && templateObject.isCreated();
281
+ if (!templateObject || !isCreated) {
282
+ return;
283
+ }
284
+
285
+ await this.cardCache.populateFromPath(
286
+ templateObject.templateCardsFolder(),
287
+ false,
288
+ );
289
+ });
290
+
291
+ await Promise.all(loadPromises);
292
+
293
+ // Once all templates have been fetched, build child-parent relationships.
294
+ this.cardCache.populateChildrenRelationships();
295
+ } catch (error) {
296
+ this.logger.error(
297
+ { error },
298
+ 'Failed to populate template cards into the card cache',
299
+ );
300
+ }
301
+ }
302
+
303
+ /**
304
+ * Populate both the project cards, and all template cards into card cache.
305
+ */
306
+ protected async populateCardsCache(): Promise<void> {
307
+ await this.cardCache.populateFromPath(this.paths.cardRootFolder);
308
+ await this.populateTemplateCards();
309
+ }
310
+
166
311
  /**
167
312
  * Add a given 'resource' to the local resource arrays.
168
313
  * @param resource Resource to add.
@@ -173,14 +318,52 @@ export class Project extends CardContainer {
173
318
  this.createdResources.set(resource.name, data);
174
319
  }
175
320
 
321
+ /**
322
+ * Returns all template cards from the project. This includes all module templates' cards.
323
+ * @returns all the template cards from the project
324
+ */
325
+ public allTemplateCards(): Card[] {
326
+ return this.cardCache.getAllTemplateCards();
327
+ }
328
+
176
329
  /**
177
330
  * Returns an array of all the attachments in the project card's (excluding ones in templates).
178
331
  * @returns all attachments in the project.
179
332
  */
180
- public async attachments(): Promise<CardAttachment[]> {
333
+ public attachments(): CardAttachment[] {
181
334
  return super.attachments(this.paths.cardRootFolder);
182
335
  }
183
336
 
337
+ /**
338
+ * Returns attachments from cards at a specific path using the card cache.
339
+ * This method allows templates to access attachments from the shared cache.
340
+ * @param path The path to get attachments from
341
+ * @returns Array of attachments from cards at the specified path
342
+ */
343
+ public attachmentsByPath(path: string): CardAttachment[] {
344
+ return super.attachments(path);
345
+ }
346
+
347
+ /**
348
+ * Returns all the attachments in the template cards.
349
+ * @returns all the attachments in the template cards.
350
+ */
351
+ public async attachmentsFromTemplates() {
352
+ const templateAttachments: CardAttachment[] = [];
353
+ const templates = await this.templates();
354
+ for (const template of templates) {
355
+ const templateResource = new TemplateResource(
356
+ this,
357
+ resourceName(template.name),
358
+ );
359
+ const templateObject = templateResource.templateObject();
360
+ if (templateObject) {
361
+ templateAttachments.push(...templateObject.attachments());
362
+ }
363
+ }
364
+ return templateAttachments;
365
+ }
366
+
184
367
  /**
185
368
  * Returns an array of all the calculation files (*.lp) in the project.
186
369
  * @param from Defines where resources are collected from.
@@ -193,59 +376,111 @@ export class Project extends CardContainer {
193
376
  }
194
377
 
195
378
  /**
196
- * Returns path to card's attachment folder.
379
+ * Returns path to a card's attachment folder.
197
380
  * @param cardKey card key
198
- * @returns path to card's attachment folder.
199
- * @throws if card path cannot be found
381
+ * @returns path to a card's attachment folder.
382
+ */
383
+ public cardAttachmentFolder(cardKey: string): string {
384
+ const pathToCard = this.findCard(cardKey).path;
385
+ return join(pathToCard, 'a');
386
+ }
387
+
388
+ /**
389
+ * Creates an attachment for a card.
390
+ * @param cardKey The card to add attachment to
391
+ * @param attachmentName The name for the attachment file
392
+ * @param attachmentData The attachment data (file path or buffer)
393
+ * @throws If trying to add attachment to module card, or if attachment is not found
200
394
  */
201
- public async cardAttachmentFolder(cardKey: string): Promise<string> {
202
- // Check if it is a template card.
203
- if (await this.isTemplateCard(cardKey)) {
204
- const cardPath = await this.cardFolder(cardKey);
205
- return join(cardPath, 'a');
395
+ public async createCardAttachment(
396
+ cardKey: string,
397
+ attachmentName: string,
398
+ attachmentData: string | Buffer,
399
+ ): Promise<void> {
400
+ const attachmentFolder = this.cardAttachmentFolder(cardKey);
401
+
402
+ // Check if this is a module template
403
+ if (isModulePath(attachmentFolder)) {
404
+ throw new Error(`Cannot modify imported module`);
206
405
  }
207
406
 
208
- const pathToProjectCard = this.pathToCard(cardKey);
209
- if (!pathToProjectCard) {
210
- throw new Error(`Card '${cardKey}' not found`);
407
+ // Create the attachment folder if it doesn't exist
408
+ await mkdir(attachmentFolder, { recursive: true });
409
+
410
+ const attachmentPath = join(attachmentFolder, basename(attachmentName));
411
+
412
+ if (Buffer.isBuffer(attachmentData)) {
413
+ await writeFile(attachmentPath, attachmentData, { flag: 'wx' });
414
+ } else {
415
+ try {
416
+ await copyFile(
417
+ attachmentData,
418
+ attachmentPath,
419
+ fsConstants.COPYFILE_EXCL,
420
+ );
421
+ } catch {
422
+ throw new Error(`Attachment file not found: ${attachmentData}`);
423
+ }
211
424
  }
212
- return join(this.paths.cardRootFolder, pathToProjectCard, 'a');
425
+
426
+ // Update cache
427
+ await this.handleAttachmentChange(
428
+ cardKey,
429
+ 'added',
430
+ basename(attachmentName),
431
+ );
213
432
  }
214
433
 
215
434
  /**
216
- * Returns details (as defined by cardDetails) of a card.
217
- * @param cardKey card key (project prefix and a number, e.g. test_1)
218
- * @param cardDetails which card details are returned.
219
- * @returns Card details, or undefined if the card cannot be found.
435
+ * Removes an attachment from a card.
436
+ * @param cardKey The card to remove attachment from
437
+ * @param fileName The name of the attachment file to remove
438
+ * @throws if trying to remove module card attachment, or the attachment was not found.
220
439
  */
221
- public async cardDetailsById(
440
+ public async removeCardAttachment(
222
441
  cardKey: string,
223
- cardDetails: ProjectFetchCardDetails,
224
- ): Promise<Card | undefined> {
225
- return this.findSpecificCard(cardKey, cardDetails);
442
+ fileName: string,
443
+ ): Promise<void> {
444
+ const attachmentFolder = this.cardAttachmentFolder(cardKey);
445
+
446
+ // Modules cannot be modified.
447
+ if (isModulePath(attachmentFolder)) {
448
+ throw new Error(`Cannot modify imported module`);
449
+ }
450
+
451
+ const attachmentPath = join(attachmentFolder, fileName);
452
+
453
+ try {
454
+ await unlink(attachmentPath);
455
+ } catch (error) {
456
+ this.logger.error({ error }, 'Removing card attachment');
457
+ throw new Error(`Attachment not found: ${fileName}`);
458
+ }
459
+ await this.handleAttachmentChange(cardKey, 'removed', fileName);
226
460
  }
227
461
 
228
462
  /**
229
- * Returns path to card's folder.
463
+ * Returns path to a card's folder.
230
464
  * @param cardKey card key
231
- * @returns path to card's folder.
465
+ * @returns path to a card's folder.
232
466
  */
233
467
  public async cardFolder(cardKey: string): Promise<string> {
234
- const found = await super.findCard(this.paths.cardRootFolder, cardKey);
468
+ const found = super.findCard(cardKey);
235
469
  if (found) {
236
470
  return found.path;
237
471
  }
238
472
 
239
473
  const templates = await this.templates();
240
- const templatePromises = templates.map(async (template) => {
474
+ const templatePromises = templates.map((template) => {
241
475
  const templateObject = new TemplateResource(
242
476
  this,
243
477
  resourceName(template.name),
244
478
  ).templateObject();
245
479
  const templateCard = templateObject
246
- ? await templateObject.findSpecificCard(cardKey)
480
+ ? templateObject.findCard(cardKey)
247
481
  : undefined;
248
- return templateCard ? templateCard.path : '';
482
+ const path = templateCard ? templateCard.path : '';
483
+ return path;
249
484
  });
250
485
 
251
486
  const templatePaths = await Promise.all(templatePromises);
@@ -253,78 +488,41 @@ export class Project extends CardContainer {
253
488
  }
254
489
 
255
490
  /**
256
- * Splits card path to parts. Returns the parts.
257
- * Returned parts are: prefix, card key, array of parents and template name. Template name is returned only for template cards.
258
- * @param cardPath path to a card
259
- * @returns card path logical parts
260
- * @throws when called with wrong path, or wrong card owner
261
- * todo: if prefix would be parameter; this could be static, or util method
491
+ * Fetches full Card data for given card keys
492
+ * @param cardIds array of card keys to fetch
493
+ * @returns Card data to the given card keys
262
494
  */
263
- public cardPathParts(cardPath: string) {
264
- const pathParts = cardPath.split(sep);
265
- const cardKey = pathParts.at(pathParts.length - 1);
266
- const parents = [];
267
- let prefix = this.projectPrefix;
268
- let template = '';
269
- let startIndex = -1;
270
- let templatesNameIndex = -1;
271
-
272
- const cardRootIndex = pathParts.indexOf('cardRoot');
273
- const projectInternalsIndex = pathParts.indexOf('.cards');
274
-
275
- if (projectInternalsIndex === -1 && cardRootIndex >= 0) {
276
- startIndex = projectInternalsIndex;
277
- } else if (projectInternalsIndex >= 0 && cardRootIndex === -1) {
278
- const templatesIndex = pathParts.indexOf('templates');
279
- startIndex = templatesIndex;
280
- if (templatesIndex === -1) {
281
- throw new Error(
282
- `Invalid card path. Template card must have 'templates' in path`,
283
- );
284
- }
285
- const modulesIndex = pathParts.indexOf('modules');
286
- if (modulesIndex !== -1) {
287
- prefix = pathParts.at(modulesIndex + 1) || '';
288
- }
289
- templatesNameIndex = templatesIndex + 1;
290
- template = `${prefix}/templates/${pathParts.at(templatesNameIndex)}`;
291
- } else {
292
- throw new Error(`Card must be either project card, or template card`);
293
- }
294
-
295
- // Look for parents in the path.
296
- let previousWasParent = false;
297
- for (let index = startIndex; index <= pathParts.length; index++) {
298
- if (previousWasParent) {
299
- previousWasParent = false;
300
- parents.push(pathParts.at(index - 2));
301
- }
302
- const cardsSubFolder = pathParts.at(index) === 'c';
303
- const ignoreOrNotTemplatesParent =
304
- index - 1 !== templatesNameIndex || templatesNameIndex === -1;
305
- if (cardsSubFolder && ignoreOrNotTemplatesParent) {
306
- previousWasParent = true;
495
+ public cardKeysToCards(cardIds: string[]): Card[] {
496
+ const cards: Card[] = [];
497
+ for (const cardId of cardIds) {
498
+ const card = this.cardCache.getCard(cardId);
499
+ if (card) {
500
+ cards.push(card);
307
501
  }
308
502
  }
503
+ return cards;
504
+ }
309
505
 
310
- return {
311
- cardKey: cardKey,
312
- parents: parents,
313
- prefix: prefix,
314
- template: template,
315
- };
506
+ /**
507
+ * Accessor for cards cache.
508
+ * Used by template container (it needs to access project's cache, not their own instance).
509
+ * @note Should not be used directly (other than Template).
510
+ */
511
+ public get cardsCache() {
512
+ return this.cardCache;
316
513
  }
317
514
 
318
515
  /**
319
- * Returns an array of all the cards in the project. Cards have content and metadata
320
- * @param path Optional path from which to fetch the cards. Generally it is best to fetch from Project root, e.g. Project.cardRootFolder
321
- * @param details Which details to include in the cards; by default only "content" and "metadata" are included.
516
+ * Returns an array of all the cards in the project.
517
+ * @note These are project cards only, by default (unless path dictates otherwise).
518
+ * @param path Path from which to fetch the cards. Generally it is best to fetch from Project root, e.g. Project.cardRootFolder
519
+ * @param details Which details to include in the cards; by default all details are included.
322
520
  * @returns all cards from the given path in the project.
323
521
  */
324
- public async cards(
522
+ public cards(
325
523
  path: string = this.paths.cardRootFolder,
326
- details: FetchCardDetails = { content: true, metadata: true },
327
- ): Promise<Card[]> {
524
+ details?: FetchCardDetails,
525
+ ): Card[] {
328
526
  return super.cards(path, details);
329
527
  }
330
528
 
@@ -339,6 +537,22 @@ export class Project extends CardContainer {
339
537
  return this.resources.resources('cardTypes', from);
340
538
  }
341
539
 
540
+ /**
541
+ * Returns children of a given card; as Card array
542
+ * @param card Parent card to fetch children from
543
+ * @returns children of a given card; as Card array
544
+ */
545
+ public childrenCards(card: Card): Card[] {
546
+ const cards: Card[] = [];
547
+ for (const child of card.children) {
548
+ const card = this.cardCache.getCard(child);
549
+ if (card) {
550
+ cards.push(card);
551
+ }
552
+ }
553
+ return cards;
554
+ }
555
+
342
556
  /**
343
557
  * Updates all local resources.
344
558
  */
@@ -349,8 +563,8 @@ export class Project extends CardContainer {
349
563
  /**
350
564
  * Updates all imported module resources.
351
565
  */
352
- public async collectModuleResources() {
353
- await this.resources.moduleImported();
566
+ public collectModuleResources() {
567
+ this.resources.moduleImported();
354
568
  }
355
569
 
356
570
  /**
@@ -370,7 +584,7 @@ export class Project extends CardContainer {
370
584
  if (!card || !card.path || !isTemplateCard(card)) {
371
585
  return undefined;
372
586
  }
373
- const { template } = this.cardPathParts(card.path);
587
+ const { template } = cardPathParts(this.projectPrefix, card.path);
374
588
  return new TemplateResource(this, resourceName(template)).templateObject();
375
589
  }
376
590
 
@@ -420,46 +634,8 @@ export class Project extends CardContainer {
420
634
  * @param details Defines which card details are included in the return values.
421
635
  * @returns specific card details, or undefined if card is not part of the project.
422
636
  */
423
- public async findSpecificCard(
424
- cardToFind: string,
425
- details: ProjectFetchCardDetails = {},
426
- ): Promise<Card | undefined> {
427
- let card;
428
-
429
- if (
430
- details.location === CardLocation.projectOnly ||
431
- details.location === CardLocation.all ||
432
- !details.location
433
- ) {
434
- card = await super.findCard(
435
- this.paths.cardRootFolder,
436
- cardToFind,
437
- details,
438
- );
439
- }
440
-
441
- if (
442
- !card &&
443
- (details.location === CardLocation.templatesOnly ||
444
- details.location === CardLocation.all ||
445
- !details.location)
446
- ) {
447
- const templates = await this.templates();
448
- for (const template of templates) {
449
- const templateObject = new TemplateResource(
450
- this,
451
- resourceName(template.name),
452
- ).templateObject();
453
- if (!templateObject) continue;
454
-
455
- // optimize: execute each find in template parallel
456
- card = await templateObject.findSpecificCard(cardToFind, details);
457
- if (card) {
458
- break;
459
- }
460
- }
461
- }
462
- return card;
637
+ public findCard(cardToFind: string, details?: ProjectFetchCardDetails): Card {
638
+ return super.findCard(cardToFind, details);
463
639
  }
464
640
 
465
641
  /**
@@ -489,6 +665,7 @@ export class Project extends CardContainer {
489
665
  * @param changedCard Card that was changed.
490
666
  */
491
667
  public async handleCardChanged(changedCard: Card) {
668
+ // Notify the calculation engine about the change
492
669
  return this.calculationEngine.handleCardChanged(changedCard);
493
670
  }
494
671
 
@@ -496,39 +673,86 @@ export class Project extends CardContainer {
496
673
  * When cards are removed.
497
674
  * @param deletedCard Card that is to be removed.
498
675
  */
499
- public async handleDeleteCard(deletedCard: Card) {
676
+ public async handleCardDeleted(deletedCard: Card) {
677
+ // Delete children from the cache first
678
+ if (deletedCard.children && deletedCard.children.length > 0) {
679
+ for (const child of deletedCard.children) {
680
+ try {
681
+ const childCard = this.findCard(child);
682
+ await this.handleCardDeleted(childCard);
683
+ } catch {
684
+ this.logger.warn(
685
+ `Accessing child '${child}' of '${deletedCard.key}' when deleting cards caused an exception`,
686
+ );
687
+ continue;
688
+ }
689
+ }
690
+ }
691
+ await super.removeCard(deletedCard.key);
500
692
  return this.calculationEngine.handleDeleteCard(deletedCard);
501
693
  }
502
694
 
503
695
  /**
504
- * When new cards are added.
505
- * @param cards Added cards.
696
+ * When card is moved.
697
+ * @param movedCard Card that moved
698
+ * @param newParentCard New parent for the 'movedCard'
699
+ * @param oldParentCard Previous parent of the 'movedCard'
506
700
  */
507
- public async handleNewCards(cards: Card[]) {
508
- return this.calculationEngine.handleNewCards(cards);
701
+ public async handleCardMoved(
702
+ movedCard: Card,
703
+ newParentCard?: Card,
704
+ oldParentCard?: Card,
705
+ ) {
706
+ if (newParentCard) {
707
+ this.cardCache.updateCard(newParentCard.key, newParentCard);
708
+ }
709
+ if (oldParentCard) {
710
+ this.cardCache.updateCard(oldParentCard.key, oldParentCard);
711
+ }
712
+ this.cardCache.updateCard(movedCard.key, movedCard);
713
+
714
+ // todo: it would be enough to just update parent, previous parent and changed card
715
+ this.cardCache.populateChildrenRelationships();
716
+ await this.handleCardChanged(movedCard);
717
+ await this.calculationEngine.handleCardMoved();
509
718
  }
510
719
 
511
720
  /**
512
- * Checks if a given card is part of this project.
513
- * @param cardKey card to check.
514
- * @returns true if a given card is found from project, false otherwise.
721
+ * When new cards are added.
722
+ * @param cards Added cards.
515
723
  */
516
- public hasCard(cardKey: string): boolean {
517
- return super.hasCard(cardKey, this.paths.cardRootFolder);
724
+ public async handleNewCards(cards: Card[]) {
725
+ // Add new cards to the card cache
726
+ cards.forEach((card) => {
727
+ const cardWithParent = {
728
+ ...card,
729
+ parent: card.parent || this.parentFromPath(card.path),
730
+ };
731
+
732
+ this.cardCache.updateCard(cardWithParent.key, cardWithParent);
733
+
734
+ // Update the parent's children list in the cache
735
+ if (cardWithParent.parent && cardWithParent.parent !== ROOT) {
736
+ this.updateCachedChildren(cardWithParent.parent, cardWithParent);
737
+ }
738
+ });
739
+ return this.calculationEngine.handleNewCards(cards);
518
740
  }
519
741
 
520
742
  /**
521
743
  * Adds a module from project.
522
- * @param module Name of the module
744
+ * @param module Module to add
523
745
  */
524
746
  public async importModule(module: ModuleSetting) {
525
747
  // Add module as a dependency.
526
748
  await this.configuration.addModule(module);
527
- await this.collectModuleResources();
749
+ this.collectModuleResources();
750
+ await this.populateTemplateCards();
751
+ this.logger.info(`Imported module '${module.name}'`);
528
752
  }
529
753
 
530
754
  /**
531
- * Checks if given path is a project.
755
+ * Checks if a given path is a project.
532
756
  * @param path Path to a project
533
757
  * @returns true, if in the given path there is a project; false otherwise
534
758
  */
@@ -536,17 +760,6 @@ export class Project extends CardContainer {
536
760
  return pathExists(join(path, 'cardRoot'));
537
761
  }
538
762
 
539
- /**
540
- * Returns whether card is a template card or not
541
- * @param cardKey card to check.
542
- * @todo: This is only used from 'remove'. Could it use the static checker?
543
- * @returns true, if card is template card; false otherwise
544
- */
545
- public async isTemplateCard(cardKey: string): Promise<boolean> {
546
- const templateCards = await this.allTemplateCards();
547
- return templateCards.find((card) => card.key === cardKey) != null;
548
- }
549
-
550
763
  /**
551
764
  * Returns an array of all the link types in the project.
552
765
  * @param from Defines where resources are collected from.
@@ -562,7 +775,7 @@ export class Project extends CardContainer {
562
775
  * Returns an array of cards in the project, in the templates or both.
563
776
  * Cards don't have content and nor metadata.
564
777
  * @param cardsFrom Where to return cards from (project, templates, or both)
565
- * @returns all cards in the project.
778
+ * @returns all cards in the project per container.
566
779
  */
567
780
  public async listCards(
568
781
  cardsFrom: CardLocation = CardLocation.all,
@@ -572,9 +785,9 @@ export class Project extends CardContainer {
572
785
  cardsFrom === CardLocation.all ||
573
786
  cardsFrom === CardLocation.projectOnly
574
787
  ) {
575
- const projectCards = (await super.cards(this.paths.cardRootFolder)).map(
576
- (item) => item.key,
577
- );
788
+ const projectCards = super
789
+ .cards(this.paths.cardRootFolder)
790
+ .map((item) => item.key);
578
791
  cardListContainer.push({
579
792
  name: this.projectName,
580
793
  type: 'project',
@@ -594,8 +807,8 @@ export class Project extends CardContainer {
594
807
  ).templateObject();
595
808
  if (templateObject) {
596
809
  // todo: optimization - do all this in parallel
597
- const templateCards = await templateObject.listCards();
598
- if (templateCards) {
810
+ const templateCards = templateObject.listCards();
811
+ if (templateCards.length) {
599
812
  cardListContainer.push({
600
813
  name: template.name,
601
814
  type: 'template',
@@ -617,69 +830,27 @@ export class Project extends CardContainer {
617
830
  public async listCardIds(
618
831
  cardsFrom: CardLocation = CardLocation.all,
619
832
  ): Promise<Set<string>> {
620
- const promises: Promise<Set<string>>[] = [];
621
- if (
622
- cardsFrom === CardLocation.all ||
623
- cardsFrom === CardLocation.projectOnly
624
- ) {
625
- promises.push(
626
- super
627
- .cards(this.paths.cardRootFolder)
628
- .then((cards) => new Set(cards.map((card) => card.key))),
629
- );
833
+ const cardContainers = await this.listCards(cardsFrom);
834
+ const allCardIDs = new Set<string>();
835
+ for (const container of cardContainers) {
836
+ const cards = container.cards;
837
+ cards.forEach((card) => allCardIDs.add(card));
630
838
  }
631
- if (
632
- cardsFrom === CardLocation.all ||
633
- cardsFrom === CardLocation.templatesOnly
634
- ) {
635
- promises.push(
636
- (async () => {
637
- const templates = await this.templates();
638
- const templateResources = templates.map(
639
- (template) =>
640
- new TemplateResource(this, resourceName(template.name)),
641
- );
642
- const templateObjectsResults =
643
- await Promise.allSettled(templateResources);
644
- const templateObjects = templateObjectsResults
645
- .filter(
646
- (result): result is PromiseFulfilledResult<TemplateResource> =>
647
- result.status === 'fulfilled' && result.value !== null,
648
- )
649
- .map((result) => result.value);
650
-
651
- const listCardsResults = await Promise.allSettled(
652
- templateObjects.map((obj) => obj.templateObject().listCards()),
653
- );
654
- const templateCardIds = new Set<string>();
655
- listCardsResults
656
- .filter(
657
- (result): result is PromiseFulfilledResult<Card[]> =>
658
- result.status === 'fulfilled',
659
- )
660
- .forEach((result) => {
661
- result.value.forEach((card) => templateCardIds.add(card.key));
662
- });
663
- return templateCardIds;
664
- })(),
665
- );
666
- }
667
- const allCardIdSets = await Promise.all(promises);
668
- return new Set(allCardIdSets.flatMap((set) => [...set]));
839
+ return allCardIDs;
669
840
  }
670
841
 
671
842
  /**
672
843
  * Returns details of a certain module.
673
844
  * @param moduleName Name of the module.
674
- * @returns module details, or undefined if workflow cannot be found.
845
+ * @returns module details, or undefined if module cannot be found.
675
846
  */
676
847
  public async module(moduleName: string): Promise<ModuleContent | undefined> {
677
848
  const module = await this.findModule(moduleName);
678
849
  if (module && module.path) {
679
850
  const modulePath = join(module.path, module.name);
680
- const moduleConfig = (await readJsonFile(
851
+ const moduleConfig = await readJsonFile(
681
852
  join(modulePath, Project.projectConfigFileName),
682
- )) as ModuleContent;
853
+ );
683
854
  return {
684
855
  name: moduleConfig.name,
685
856
  modules: moduleConfig.modules,
@@ -821,15 +992,12 @@ export class Project extends CardContainer {
821
992
  }
822
993
 
823
994
  /**
824
- * Returns full path to a given card.
825
- * @param cardKey card to check path for.
826
- * @returns path to a given card.
995
+ * Populates the card cache, if it has not been populated.
827
996
  */
828
- public pathToCard(cardKey: string): string {
829
- const allFiles = getFilesSync(this.paths.cardRootFolder);
830
- const cardIndexJsonFile = join(cardKey, Project.cardMetadataFile);
831
- const foundFile = allFiles.find((file) => file.includes(cardIndexJsonFile));
832
- return foundFile ? dirname(foundFile) : '';
997
+ public async populateCaches() {
998
+ if (!this.cardCache.isPopulated) {
999
+ await this.populateCardsCache();
1000
+ }
833
1001
  }
834
1002
 
835
1003
  /**
@@ -856,6 +1024,7 @@ export class Project extends CardContainer {
856
1024
  const prefixes: string[] = [this.projectPrefix];
857
1025
  let files;
858
1026
  try {
1027
+ // TODO: Could be optimized so that prefixes are stored once fetched.
859
1028
  files = await readdir(this.paths.modulesFolder, {
860
1029
  withFileTypes: true,
861
1030
  recursive: true,
@@ -873,13 +1042,48 @@ export class Project extends CardContainer {
873
1042
 
874
1043
  const configurationPrefixes = await Promise.all(configurationPromises);
875
1044
  prefixes.push(...configurationPrefixes);
876
- } catch {
877
- // do nothing if readdir throws // TODO: Log it
1045
+ } catch (error) {
1046
+ this.logger.error({ error }, 'Failed to collect prefixes in use');
878
1047
  }
879
1048
 
880
1049
  return prefixes;
881
1050
  }
882
1051
 
1052
+ /**
1053
+ * Removes a module from the project
1054
+ * @param module Module (name) to remove.
1055
+ */
1056
+ public async removeModule(moduleName: string) {
1057
+ const toBeRemovedTemplates = this.resources.moduleResources.resourceArray(
1058
+ 'templates',
1059
+ moduleName,
1060
+ );
1061
+ // First, remove cards from the cache
1062
+ for (const template of toBeRemovedTemplates) {
1063
+ this.cardCache.deleteCardsFromTemplate(template.name);
1064
+ }
1065
+
1066
+ // Then, remove module from project configuration
1067
+ await this.configuration.removeModule(moduleName);
1068
+ this.collectModuleResources();
1069
+
1070
+ this.logger.info(`Removed module '${moduleName}'`);
1071
+ }
1072
+
1073
+ /**
1074
+ * Removes a resource from Project.
1075
+ * @param resource Resource to remove.
1076
+ */
1077
+ public removeResource(resource: Resource) {
1078
+ // Template cards must be removed from the cache when resource is removed.
1079
+ if (resource.path.includes('templates')) {
1080
+ const templateName = resourceNameToString(resourceName(resource.name));
1081
+ this.cardCache.deleteCardsFromTemplate(templateName);
1082
+ }
1083
+ this.resources.remove(resource);
1084
+ this.createdResources.delete(resource.name);
1085
+ }
1086
+
883
1087
  /**
884
1088
  * Array of reports in the project.
885
1089
  * @param from Defines where resources are collected from.
@@ -907,21 +1111,12 @@ export class Project extends CardContainer {
907
1111
  return handleBarFiles;
908
1112
  }
909
1113
 
910
- /**
911
- * Removes a resource from Project.
912
- * @param resource Resource to remove.
913
- */
914
- public removeResource(resource: Resource) {
915
- this.resources.remove(resource);
916
- this.createdResources.delete(resource.name);
917
- }
918
-
919
1114
  /**
920
1115
  * Returns metadata from a given resource
921
1116
  * @param name Name of a resource
922
1117
  * @returns Metadata from the resource.
923
1118
  */
924
- public async resource<Type>(name: string): Promise<Type | undefined> {
1119
+ public resource<Type>(name: string): Type | undefined {
925
1120
  const resName = resourceName(name);
926
1121
  if (this.createdResources.has(resourceNameToString(resName))) {
927
1122
  const value = this.createdResources.get(
@@ -977,7 +1172,9 @@ export class Project extends CardContainer {
977
1172
  * @returns Created resource.
978
1173
  */
979
1174
  public static resourceObject(project: Project, name: ResourceName) {
980
- if (name.type === 'cardTypes') {
1175
+ if (name.type === 'calculations') {
1176
+ return new CalculationResource(project, name);
1177
+ } else if (name.type === 'cardTypes') {
981
1178
  return new CardTypeResource(project, name);
982
1179
  } else if (name.type === 'fieldTypes') {
983
1180
  return new FieldTypeResource(project, name);
@@ -1019,45 +1216,23 @@ export class Project extends CardContainer {
1019
1216
  * Show cards of a project.
1020
1217
  * @returns an array of all project cards in the project.
1021
1218
  */
1022
- public async showProjectCards(): Promise<Card[]> {
1219
+ public showProjectCards(): Card[] {
1023
1220
  return this.showCards(this.paths.cardRootFolder);
1024
1221
  }
1025
1222
 
1026
- /**
1027
- * Returns all template cards from the project. This includes all module templates' cards.
1028
- * @param cardDetails which details to fetch. Optional.
1029
- * @returns all the template cards from the project
1030
- */
1031
- public async allTemplateCards(
1032
- cardDetails?: FetchCardDetails,
1033
- ): Promise<Card[]> {
1034
- const templates = await this.templates();
1035
- const cards: Card[] = [];
1036
- for (const template of templates) {
1037
- const templateCards = await this.templateCards(
1038
- template.name,
1039
- cardDetails,
1040
- );
1041
- if (templateCards) cards.push(...templateCards);
1042
- }
1043
- return cards;
1044
- }
1045
-
1046
1223
  /**
1047
1224
  * Returns cards from single template.
1048
- * @param templateName Name of the template
1049
- * @param cardDetails Card information
1225
+ * @param templateName Name of the template (supports both full names like 'decision/templates/decision' and short names like 'decision')
1050
1226
  * @returns List of cards from template.
1051
1227
  */
1052
- public async templateCards(
1053
- templateName: string,
1054
- cardDetails?: FetchCardDetails,
1055
- ): Promise<Card[]> {
1056
- const templateObject = new TemplateResource(
1057
- this,
1058
- resourceName(templateName),
1059
- ).templateObject();
1060
- return await templateObject?.cards('', cardDetails);
1228
+ public templateCards(templateName: string): Card[] {
1229
+ const templateCards = this.cardCache.getAllTemplateCards();
1230
+ return templateCards.filter((cachedCard) => {
1231
+ if (cachedCard.location === 'project') {
1232
+ return false;
1233
+ }
1234
+ return cachedCard.location === templateName;
1235
+ });
1061
1236
  }
1062
1237
 
1063
1238
  /**
@@ -1072,20 +1247,21 @@ export class Project extends CardContainer {
1072
1247
  }
1073
1248
 
1074
1249
  /**
1075
- * Update card content.
1076
- * @param cardKey card's ID that is updated.
1250
+ * Update a card's content.
1251
+ * @param cardKey card key to update.
1077
1252
  * @param content changed content
1078
1253
  */
1079
1254
  public async updateCardContent(cardKey: string, content: string) {
1080
- const card = await this.findCard(this.basePath, cardKey, {
1081
- metadata: true,
1082
- content: true,
1083
- });
1084
- if (!card) {
1085
- throw new Error(`Card '${cardKey}' does not exist in the project`);
1086
- }
1255
+ const card = this.findCard(cardKey);
1087
1256
  card.content = content;
1257
+
1258
+ // Update lastUpdated timestamp in metadata
1259
+ if (card.metadata) {
1260
+ card.metadata.lastUpdated = new Date().toISOString();
1261
+ }
1262
+
1088
1263
  await this.saveCard(card);
1264
+ await this.handleCardChanged(card);
1089
1265
  }
1090
1266
 
1091
1267
  /**
@@ -1099,21 +1275,15 @@ export class Project extends CardContainer {
1099
1275
  changedKey: string,
1100
1276
  newValue: MetadataContent,
1101
1277
  ) {
1102
- const templateCard = await this.isTemplateCard(cardKey);
1103
- const card = await this.findCard(
1104
- templateCard ? this.paths.templatesFolder : this.paths.cardRootFolder,
1105
- cardKey,
1106
- {
1107
- metadata: true,
1108
- },
1109
- );
1110
- if (!card) {
1111
- throw new Error(`Card '${cardKey}' does not exist in the project`);
1112
- }
1113
-
1278
+ const card = this.findCard(cardKey);
1114
1279
  if (!card.metadata || card.metadata[changedKey] === newValue) {
1115
1280
  return;
1116
1281
  }
1282
+
1283
+ const isRankChange = changedKey === 'rank';
1284
+ const previousPath = isRankChange ? card.path : undefined;
1285
+ const previousParent = isRankChange ? card.parent : undefined;
1286
+
1117
1287
  const cardAsRecord: Record<string, MetadataContent> = card.metadata;
1118
1288
  cardAsRecord[changedKey] = newValue;
1119
1289
 
@@ -1124,35 +1294,70 @@ export class Project extends CardContainer {
1124
1294
  throw new Error(invalidCard);
1125
1295
  }
1126
1296
 
1127
- await this.saveCardMetadata(card);
1297
+ const updated = await this.saveCardMetadata(card);
1298
+ if (!updated) return;
1299
+
1300
+ // For rank changes, check if path changed (indicating a move)
1301
+ if (isRankChange) {
1302
+ const updatedCard = this.findCard(cardKey);
1303
+ if (updatedCard.path !== previousPath) {
1304
+ this.changeParent(updatedCard, previousParent);
1305
+ }
1306
+ }
1128
1307
  }
1129
1308
 
1130
1309
  /**
1131
- * Updates card metadata.
1310
+ * Updates the entire card in the card cache and handles any path/parent changes.
1311
+ * Also persists changes to content and metadata files.
1312
+ * @param card The card with updated information (path, parent, metadata, etc.)
1313
+ */
1314
+ public async updateCard(card: Card) {
1315
+ const cachedCard = this.cardCache.getCard(card.key);
1316
+ const pathChange = cachedCard && cachedCard.path !== card.path;
1317
+
1318
+ if (pathChange) {
1319
+ this.changeParent(card, cachedCard.parent);
1320
+ }
1321
+
1322
+ const metadataChanged =
1323
+ cachedCard &&
1324
+ JSON.stringify(cachedCard.metadata) !== JSON.stringify(card.metadata);
1325
+ if (metadataChanged) {
1326
+ await this.saveCardMetadata(card);
1327
+ }
1328
+
1329
+ const contentChanged = cachedCard && cachedCard.content !== card.content;
1330
+ if (contentChanged) {
1331
+ await this.saveCardContent(card);
1332
+ }
1333
+
1334
+ this.cardCache.updateCard(card.key, card);
1335
+ if (metadataChanged || contentChanged || pathChange) {
1336
+ await this.handleCardChanged(card);
1337
+ }
1338
+ }
1339
+
1340
+ /**
1341
+ * Updates a card's metadata.
1132
1342
  * @param card affected card
1133
1343
  * @param changedMetadata changed content for the card
1134
1344
  */
1135
1345
  public async updateCardMetadata(card: Card, changedMetadata: CardMetadata) {
1136
1346
  card.metadata = changedMetadata;
1137
- return this.saveCardMetadata(card);
1347
+ if (await this.saveCardMetadata(card)) {
1348
+ await this.handleCardChanged(card);
1349
+ }
1138
1350
  }
1139
1351
 
1140
- /**
1141
- * Validates that card's data is valid.
1142
- * @param card Card to validate.
1143
- * @returns validation errors, if any
1144
- */
1145
- public async validateCard(card: Card): Promise<string> {
1352
+ // Validates that card's data is valid.
1353
+ private async validateCard(card: Card): Promise<string> {
1146
1354
  const invalidCustomData = await this.validator.validateCustomFields(
1147
1355
  this,
1148
1356
  card,
1149
1357
  );
1150
- const invalidWorkFlow = await this.validator.validateWorkflowState(
1151
- this,
1152
- card,
1153
- );
1358
+ const invalidWorkFlow = this.validator.validateWorkflowState(this, card);
1154
1359
 
1155
- const invalidLabels = await this.validator.validateCardLabels(card);
1360
+ const invalidLabels = this.validator.validateCardLabels(card);
1156
1361
  if (
1157
1362
  invalidCustomData.length === 0 &&
1158
1363
  invalidWorkFlow.length === 0 &&