@cyberismo/data-handler 0.0.14 → 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 (240) 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 +10 -16
  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.js +13 -59
  9. package/dist/commands/create.js.map +1 -1
  10. package/dist/commands/edit.d.ts +1 -15
  11. package/dist/commands/edit.js +15 -89
  12. package/dist/commands/edit.js.map +1 -1
  13. package/dist/commands/export.js +4 -17
  14. package/dist/commands/export.js.map +1 -1
  15. package/dist/commands/import.js +3 -5
  16. package/dist/commands/import.js.map +1 -1
  17. package/dist/commands/move.d.ts +1 -2
  18. package/dist/commands/move.js +108 -146
  19. package/dist/commands/move.js.map +1 -1
  20. package/dist/commands/remove.js +9 -44
  21. package/dist/commands/remove.js.map +1 -1
  22. package/dist/commands/rename.js +2 -7
  23. package/dist/commands/rename.js.map +1 -1
  24. package/dist/commands/show.d.ts +7 -25
  25. package/dist/commands/show.js +38 -102
  26. package/dist/commands/show.js.map +1 -1
  27. package/dist/commands/transition.js +27 -30
  28. package/dist/commands/transition.js.map +1 -1
  29. package/dist/commands/update.d.ts +5 -3
  30. package/dist/commands/update.js +19 -5
  31. package/dist/commands/update.js.map +1 -1
  32. package/dist/commands/validate.d.ts +3 -3
  33. package/dist/commands/validate.js +19 -26
  34. package/dist/commands/validate.js.map +1 -1
  35. package/dist/containers/card-container.d.ts +87 -24
  36. package/dist/containers/card-container.js +183 -279
  37. package/dist/containers/card-container.js.map +1 -1
  38. package/dist/containers/project/calculation-engine.d.ts +6 -0
  39. package/dist/containers/project/calculation-engine.js +19 -12
  40. package/dist/containers/project/calculation-engine.js.map +1 -1
  41. package/dist/containers/project/card-cache.d.ts +146 -0
  42. package/dist/containers/project/card-cache.js +411 -0
  43. package/dist/containers/project/card-cache.js.map +1 -0
  44. package/dist/containers/project/resource-collector.d.ts +24 -1
  45. package/dist/containers/project/resource-collector.js +8 -1
  46. package/dist/containers/project/resource-collector.js.map +1 -1
  47. package/dist/containers/project.d.ts +117 -83
  48. package/dist/containers/project.js +418 -252
  49. package/dist/containers/project.js.map +1 -1
  50. package/dist/containers/template.d.ts +15 -31
  51. package/dist/containers/template.js +97 -104
  52. package/dist/containers/template.js.map +1 -1
  53. package/dist/index.d.ts +1 -0
  54. package/dist/index.js +1 -0
  55. package/dist/index.js.map +1 -1
  56. package/dist/interfaces/folder-content-interfaces.d.ts +2 -1
  57. package/dist/interfaces/folder-content-interfaces.js.map +1 -1
  58. package/dist/interfaces/macros.d.ts +1 -0
  59. package/dist/interfaces/macros.js +1 -1
  60. package/dist/interfaces/macros.js.map +1 -1
  61. package/dist/interfaces/project-interfaces.d.ts +5 -1
  62. package/dist/interfaces/project-interfaces.js.map +1 -1
  63. package/dist/interfaces/resource-interfaces.d.ts +11 -21
  64. package/dist/interfaces/resource-interfaces.js +3 -0
  65. package/dist/interfaces/resource-interfaces.js.map +1 -1
  66. package/dist/macros/common.d.ts +10 -10
  67. package/dist/macros/createCards/index.d.ts +0 -13
  68. package/dist/macros/createCards/index.js.map +1 -1
  69. package/dist/macros/createCards/types.d.ts +44 -0
  70. package/dist/macros/createCards/types.js +15 -0
  71. package/dist/macros/createCards/types.js.map +1 -0
  72. package/dist/macros/graph/index.d.ts +2 -6
  73. package/dist/macros/graph/index.js +2 -2
  74. package/dist/macros/graph/index.js.map +1 -1
  75. package/dist/macros/graph/types.d.ts +23 -0
  76. package/dist/macros/graph/types.js +15 -0
  77. package/dist/macros/graph/types.js.map +1 -0
  78. package/dist/macros/image/index.d.ts +8 -16
  79. package/dist/macros/image/index.js +36 -33
  80. package/dist/macros/image/index.js.map +1 -1
  81. package/dist/macros/image/types.d.ts +38 -0
  82. package/dist/macros/image/types.js +15 -0
  83. package/dist/macros/image/types.js.map +1 -0
  84. package/dist/macros/include/index.d.ts +1 -6
  85. package/dist/macros/include/index.js +4 -7
  86. package/dist/macros/include/index.js.map +1 -1
  87. package/dist/macros/include/types.d.ts +31 -0
  88. package/dist/macros/include/types.js +15 -0
  89. package/dist/macros/include/types.js.map +1 -0
  90. package/dist/macros/percentage/index.d.ts +0 -6
  91. package/dist/macros/percentage/index.js.map +1 -1
  92. package/dist/macros/percentage/types.d.ts +31 -0
  93. package/dist/macros/percentage/types.js +15 -0
  94. package/dist/macros/percentage/types.js.map +1 -0
  95. package/dist/macros/report/index.d.ts +0 -3
  96. package/dist/macros/report/index.js.map +1 -1
  97. package/dist/macros/report/types.d.ts +19 -0
  98. package/dist/macros/report/types.js +15 -0
  99. package/dist/macros/report/types.js.map +1 -0
  100. package/dist/macros/scoreCard/index.d.ts +0 -6
  101. package/dist/macros/scoreCard/index.js.map +1 -1
  102. package/dist/macros/scoreCard/types.d.ts +31 -0
  103. package/dist/macros/scoreCard/types.js +15 -0
  104. package/dist/macros/scoreCard/types.js.map +1 -0
  105. package/dist/macros/types.d.ts +25 -0
  106. package/dist/macros/types.js +2 -0
  107. package/dist/macros/types.js.map +1 -0
  108. package/dist/macros/vega/index.d.ts +0 -4
  109. package/dist/macros/vega/index.js.map +1 -1
  110. package/dist/macros/vega/types.d.ts +20 -0
  111. package/dist/macros/vega/types.js +2 -0
  112. package/dist/macros/vega/types.js.map +1 -0
  113. package/dist/macros/vegalite/index.d.ts +0 -4
  114. package/dist/macros/vegalite/index.js.map +1 -1
  115. package/dist/macros/vegalite/types.d.ts +20 -0
  116. package/dist/macros/vegalite/types.js +15 -0
  117. package/dist/macros/vegalite/types.js.map +1 -0
  118. package/dist/macros/xref/index.d.ts +0 -3
  119. package/dist/macros/xref/index.js +5 -14
  120. package/dist/macros/xref/index.js.map +1 -1
  121. package/dist/macros/xref/types.d.ts +19 -0
  122. package/dist/macros/xref/types.js +15 -0
  123. package/dist/macros/xref/types.js.map +1 -0
  124. package/dist/module-manager.js +4 -4
  125. package/dist/module-manager.js.map +1 -1
  126. package/dist/project-settings.js.map +1 -1
  127. package/dist/resources/calculation-resource.d.ts +4 -32
  128. package/dist/resources/calculation-resource.js +0 -55
  129. package/dist/resources/calculation-resource.js.map +1 -1
  130. package/dist/resources/card-type-resource.d.ts +4 -21
  131. package/dist/resources/card-type-resource.js +13 -44
  132. package/dist/resources/card-type-resource.js.map +1 -1
  133. package/dist/resources/field-type-resource.d.ts +4 -21
  134. package/dist/resources/field-type-resource.js +14 -38
  135. package/dist/resources/field-type-resource.js.map +1 -1
  136. package/dist/resources/file-resource.d.ts +12 -29
  137. package/dist/resources/file-resource.js +19 -293
  138. package/dist/resources/file-resource.js.map +1 -1
  139. package/dist/resources/folder-resource.d.ts +31 -50
  140. package/dist/resources/folder-resource.js +68 -96
  141. package/dist/resources/folder-resource.js.map +1 -1
  142. package/dist/resources/graph-model-resource.d.ts +5 -33
  143. package/dist/resources/graph-model-resource.js +8 -61
  144. package/dist/resources/graph-model-resource.js.map +1 -1
  145. package/dist/resources/graph-view-resource.d.ts +5 -28
  146. package/dist/resources/graph-view-resource.js +6 -45
  147. package/dist/resources/graph-view-resource.js.map +1 -1
  148. package/dist/resources/link-type-resource.d.ts +4 -21
  149. package/dist/resources/link-type-resource.js +6 -31
  150. package/dist/resources/link-type-resource.js.map +1 -1
  151. package/dist/resources/report-resource.d.ts +5 -17
  152. package/dist/resources/report-resource.js +6 -44
  153. package/dist/resources/report-resource.js.map +1 -1
  154. package/dist/resources/resource-object.d.ts +58 -23
  155. package/dist/resources/resource-object.js +293 -24
  156. package/dist/resources/resource-object.js.map +1 -1
  157. package/dist/resources/template-resource.d.ts +4 -15
  158. package/dist/resources/template-resource.js +10 -25
  159. package/dist/resources/template-resource.js.map +1 -1
  160. package/dist/resources/workflow-resource.d.ts +4 -23
  161. package/dist/resources/workflow-resource.js +12 -38
  162. package/dist/resources/workflow-resource.js.map +1 -1
  163. package/dist/utils/card-utils.d.ts +69 -19
  164. package/dist/utils/card-utils.js +179 -30
  165. package/dist/utils/card-utils.js.map +1 -1
  166. package/dist/utils/clingo-facts.js +11 -3
  167. package/dist/utils/clingo-facts.js.map +1 -1
  168. package/dist/utils/clingo-parser.js +1 -1
  169. package/dist/utils/clingo-parser.js.map +1 -1
  170. package/dist/utils/constants.d.ts +2 -0
  171. package/dist/utils/constants.js +4 -0
  172. package/dist/utils/constants.js.map +1 -1
  173. package/dist/utils/csv.js +1 -1
  174. package/dist/utils/csv.js.map +1 -1
  175. package/package.json +5 -5
  176. package/src/card-metadata-updater.ts +3 -5
  177. package/src/command-handler.ts +11 -18
  178. package/src/command-manager.ts +4 -3
  179. package/src/commands/create.ts +17 -83
  180. package/src/commands/edit.ts +16 -132
  181. package/src/commands/export.ts +8 -29
  182. package/src/commands/import.ts +4 -6
  183. package/src/commands/move.ts +144 -179
  184. package/src/commands/remove.ts +9 -52
  185. package/src/commands/rename.ts +2 -7
  186. package/src/commands/show.ts +50 -143
  187. package/src/commands/transition.ts +30 -33
  188. package/src/commands/update.ts +27 -9
  189. package/src/commands/validate.ts +21 -36
  190. package/src/containers/card-container.ts +200 -360
  191. package/src/containers/project/calculation-engine.ts +21 -13
  192. package/src/containers/project/card-cache.ts +497 -0
  193. package/src/containers/project/resource-collector.ts +9 -1
  194. package/src/containers/project.ts +529 -327
  195. package/src/containers/template.ts +109 -127
  196. package/src/index.ts +1 -0
  197. package/src/interfaces/folder-content-interfaces.ts +7 -1
  198. package/src/interfaces/macros.ts +2 -0
  199. package/src/interfaces/project-interfaces.ts +7 -1
  200. package/src/interfaces/resource-interfaces.ts +12 -24
  201. package/src/macros/createCards/index.ts +1 -12
  202. package/src/macros/createCards/types.ts +46 -0
  203. package/src/macros/graph/index.ts +3 -7
  204. package/src/macros/graph/types.ts +24 -0
  205. package/src/macros/image/index.ts +50 -61
  206. package/src/macros/image/types.ts +39 -0
  207. package/src/macros/include/index.ts +6 -15
  208. package/src/macros/include/types.ts +32 -0
  209. package/src/macros/percentage/index.ts +1 -7
  210. package/src/macros/percentage/types.ts +32 -0
  211. package/src/macros/report/index.ts +1 -4
  212. package/src/macros/report/types.ts +20 -0
  213. package/src/macros/scoreCard/index.ts +1 -7
  214. package/src/macros/scoreCard/types.ts +32 -0
  215. package/src/macros/types.ts +48 -0
  216. package/src/macros/vega/index.ts +1 -4
  217. package/src/macros/vega/types.ts +21 -0
  218. package/src/macros/vegalite/index.ts +1 -4
  219. package/src/macros/vegalite/types.ts +22 -0
  220. package/src/macros/xref/index.ts +6 -20
  221. package/src/macros/xref/types.ts +20 -0
  222. package/src/module-manager.ts +5 -5
  223. package/src/project-settings.ts +1 -1
  224. package/src/resources/calculation-resource.ts +6 -76
  225. package/src/resources/card-type-resource.ts +24 -59
  226. package/src/resources/field-type-resource.ts +22 -51
  227. package/src/resources/file-resource.ts +27 -409
  228. package/src/resources/folder-resource.ts +98 -124
  229. package/src/resources/graph-model-resource.ts +17 -74
  230. package/src/resources/graph-view-resource.ts +14 -54
  231. package/src/resources/link-type-resource.ts +13 -40
  232. package/src/resources/report-resource.ts +17 -57
  233. package/src/resources/resource-object.ts +435 -32
  234. package/src/resources/template-resource.ts +16 -29
  235. package/src/resources/workflow-resource.ts +26 -50
  236. package/src/utils/card-utils.ts +217 -31
  237. package/src/utils/clingo-facts.ts +13 -3
  238. package/src/utils/clingo-parser.ts +1 -1
  239. package/src/utils/constants.ts +6 -0
  240. package/src/utils/csv.ts +1 -1
@@ -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,
@@ -64,7 +76,9 @@ import { TemplateResource } from '../resources/template-resource.js';
64
76
  import { WorkflowResource } from '../resources/workflow-resource.js';
65
77
 
66
78
  import { ContentWatcher } from './project/project-content-watcher.js';
67
- import { pathToResourceName } from '../utils/resource-utils.js';
79
+ import { getChildLogger } from '../utils/log-utils.js';
80
+
81
+ import { ROOT } from '../utils/constants.js';
68
82
 
69
83
  // Re-export this, so that classes that use Project do not need to have separate import.
70
84
  export { ResourcesFrom };
@@ -74,34 +88,44 @@ export { ResourcesFrom };
74
88
  */
75
89
  export class Project extends CardContainer {
76
90
  public calculationEngine: CalculationEngine;
77
- private resources: ResourceCollector;
78
- private projectPaths: ProjectPaths;
79
- private settings: ProjectConfiguration;
80
- private validator: Validate;
81
- private resourceWatcher: ContentWatcher | undefined;
82
-
83
91
  // Created resources are held in a cache.
84
92
  // In the cache, key is resource name, and data is resource metadata (as JSON).
85
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;
86
100
 
87
101
  constructor(
88
102
  path: string,
89
103
  private watchResourceChanges?: boolean,
90
104
  ) {
91
- super(path, '');
92
-
93
- this.calculationEngine = new CalculationEngine(this);
94
-
95
- this.settings = new ProjectConfiguration(
105
+ const settings = new ProjectConfiguration(
96
106
  join(path, '.cards', 'local', Project.projectConfigFileName),
97
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);
98
114
  this.projectPaths = new ProjectPaths(path);
99
115
  this.resources = new ResourceCollector(this);
100
116
 
101
117
  this.containerName = this.settings.name;
102
118
  // todo: implement project validation
103
119
  this.validator = Validate.getInstance();
120
+ this.logger.info(
121
+ { resourcesFolder: this.paths.resourcesFolder },
122
+ 'Collecting local resources',
123
+ );
104
124
  this.resources.collectLocalResources();
125
+ this.logger.info(
126
+ { name: this.containerName },
127
+ 'Project initialization complete',
128
+ );
105
129
 
106
130
  const ignoreRenameFileChanges = true;
107
131
 
@@ -135,17 +159,15 @@ export class Project extends CardContainer {
135
159
  }
136
160
  }
137
161
 
138
- // Removes current version of resource from cache.
139
- // Then re-creates the resource with current data and caches the value again.
140
- // If the value wasn't in the cache before, it will be added.
141
- private async replaceCacheValue(resourceName: string) {
142
- if (this.createdResources.has(resourceName)) {
143
- // First, remove the old version from cache
144
- 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);
145
166
  }
146
- const resourceData = await this.resource(resourceName);
147
- if (!resourceData) return;
148
- 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);
149
171
  }
150
172
 
151
173
  // Finds specific module.
@@ -155,8 +177,55 @@ export class Project extends CardContainer {
155
177
  );
156
178
  }
157
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
+
158
228
  // Returns (local or all) resources of a given type.
159
- // @todo: if this would be public, we could remove cardTypes(), fieldTypes(), ... and similar APIs
160
229
  private async resourcesOfType(
161
230
  type: ResourceFolderType,
162
231
  from: ResourcesFrom = ResourcesFrom.localOnly,
@@ -164,6 +233,81 @@ export class Project extends CardContainer {
164
233
  return this.resources.resources(type, from);
165
234
  }
166
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
+
167
311
  /**
168
312
  * Add a given 'resource' to the local resource arrays.
169
313
  * @param resource Resource to add.
@@ -174,14 +318,52 @@ export class Project extends CardContainer {
174
318
  this.createdResources.set(resource.name, data);
175
319
  }
176
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
+
177
329
  /**
178
330
  * Returns an array of all the attachments in the project card's (excluding ones in templates).
179
331
  * @returns all attachments in the project.
180
332
  */
181
- public async attachments(): Promise<CardAttachment[]> {
333
+ public attachments(): CardAttachment[] {
182
334
  return super.attachments(this.paths.cardRootFolder);
183
335
  }
184
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
+
185
367
  /**
186
368
  * Returns an array of all the calculation files (*.lp) in the project.
187
369
  * @param from Defines where resources are collected from.
@@ -194,59 +376,111 @@ export class Project extends CardContainer {
194
376
  }
195
377
 
196
378
  /**
197
- * Returns path to card's attachment folder.
379
+ * Returns path to a card's attachment folder.
198
380
  * @param cardKey card key
199
- * @returns path to card's attachment folder.
200
- * @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
201
394
  */
202
- public async cardAttachmentFolder(cardKey: string): Promise<string> {
203
- // Check if it is a template card.
204
- if (await this.isTemplateCard(cardKey)) {
205
- const cardPath = await this.cardFolder(cardKey);
206
- 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`);
207
405
  }
208
406
 
209
- const pathToProjectCard = this.pathToCard(cardKey);
210
- if (!pathToProjectCard) {
211
- 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
+ }
212
424
  }
213
- return join(this.paths.cardRootFolder, pathToProjectCard, 'a');
425
+
426
+ // Update cache
427
+ await this.handleAttachmentChange(
428
+ cardKey,
429
+ 'added',
430
+ basename(attachmentName),
431
+ );
214
432
  }
215
433
 
216
434
  /**
217
- * Returns details (as defined by cardDetails) of a card.
218
- * @param cardKey card key (project prefix and a number, e.g. test_1)
219
- * @param cardDetails which card details are returned.
220
- * @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.
221
439
  */
222
- public async cardDetailsById(
440
+ public async removeCardAttachment(
223
441
  cardKey: string,
224
- cardDetails: ProjectFetchCardDetails,
225
- ): Promise<Card | undefined> {
226
- 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);
227
460
  }
228
461
 
229
462
  /**
230
- * Returns path to card's folder.
463
+ * Returns path to a card's folder.
231
464
  * @param cardKey card key
232
- * @returns path to card's folder.
465
+ * @returns path to a card's folder.
233
466
  */
234
467
  public async cardFolder(cardKey: string): Promise<string> {
235
- const found = await super.findCard(this.paths.cardRootFolder, cardKey);
468
+ const found = super.findCard(cardKey);
236
469
  if (found) {
237
470
  return found.path;
238
471
  }
239
472
 
240
473
  const templates = await this.templates();
241
- const templatePromises = templates.map(async (template) => {
474
+ const templatePromises = templates.map((template) => {
242
475
  const templateObject = new TemplateResource(
243
476
  this,
244
477
  resourceName(template.name),
245
478
  ).templateObject();
246
479
  const templateCard = templateObject
247
- ? await templateObject.findSpecificCard(cardKey)
480
+ ? templateObject.findCard(cardKey)
248
481
  : undefined;
249
- return templateCard ? templateCard.path : '';
482
+ const path = templateCard ? templateCard.path : '';
483
+ return path;
250
484
  });
251
485
 
252
486
  const templatePaths = await Promise.all(templatePromises);
@@ -254,78 +488,41 @@ export class Project extends CardContainer {
254
488
  }
255
489
 
256
490
  /**
257
- * Splits card path to parts. Returns the parts.
258
- * Returned parts are: prefix, card key, array of parents and template name. Template name is returned only for template cards.
259
- * @param cardPath path to a card
260
- * @returns card path logical parts
261
- * @throws when called with wrong path, or wrong card owner
262
- * 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
263
494
  */
264
- public cardPathParts(cardPath: string) {
265
- const pathParts = cardPath.split(sep);
266
- const cardKey = pathParts.at(pathParts.length - 1);
267
- const parents = [];
268
- let prefix = this.projectPrefix;
269
- let template = '';
270
- let startIndex = -1;
271
- let templatesNameIndex = -1;
272
-
273
- const cardRootIndex = pathParts.indexOf('cardRoot');
274
- const projectInternalsIndex = pathParts.indexOf('.cards');
275
-
276
- if (projectInternalsIndex === -1 && cardRootIndex >= 0) {
277
- startIndex = projectInternalsIndex;
278
- } else if (projectInternalsIndex >= 0 && cardRootIndex === -1) {
279
- const templatesIndex = pathParts.indexOf('templates');
280
- startIndex = templatesIndex;
281
- if (templatesIndex === -1) {
282
- throw new Error(
283
- `Invalid card path. Template card must have 'templates' in path`,
284
- );
285
- }
286
- const modulesIndex = pathParts.indexOf('modules');
287
- if (modulesIndex !== -1) {
288
- prefix = pathParts.at(modulesIndex + 1) || '';
289
- }
290
- templatesNameIndex = templatesIndex + 1;
291
- template = `${prefix}/templates/${pathParts.at(templatesNameIndex)}`;
292
- } else {
293
- throw new Error(`Card must be either project card, or template card`);
294
- }
295
-
296
- // Look for parents in the path.
297
- let previousWasParent = false;
298
- for (let index = startIndex; index <= pathParts.length; index++) {
299
- if (previousWasParent) {
300
- previousWasParent = false;
301
- parents.push(pathParts.at(index - 2));
302
- }
303
- const cardsSubFolder = pathParts.at(index) === 'c';
304
- const ignoreOrNotTemplatesParent =
305
- index - 1 !== templatesNameIndex || templatesNameIndex === -1;
306
- if (cardsSubFolder && ignoreOrNotTemplatesParent) {
307
- 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);
308
501
  }
309
502
  }
503
+ return cards;
504
+ }
310
505
 
311
- return {
312
- cardKey: cardKey,
313
- parents: parents,
314
- prefix: prefix,
315
- template: template,
316
- };
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;
317
513
  }
318
514
 
319
515
  /**
320
- * Returns an array of all the cards in the project. Cards have content and metadata
321
- * @param path Optional path from which to fetch the cards. Generally it is best to fetch from Project root, e.g. Project.cardRootFolder
322
- * @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.
323
520
  * @returns all cards from the given path in the project.
324
521
  */
325
- public async cards(
522
+ public cards(
326
523
  path: string = this.paths.cardRootFolder,
327
- details: FetchCardDetails = { content: true, metadata: true },
328
- ): Promise<Card[]> {
524
+ details?: FetchCardDetails,
525
+ ): Card[] {
329
526
  return super.cards(path, details);
330
527
  }
331
528
 
@@ -340,6 +537,22 @@ export class Project extends CardContainer {
340
537
  return this.resources.resources('cardTypes', from);
341
538
  }
342
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
+
343
556
  /**
344
557
  * Updates all local resources.
345
558
  */
@@ -350,8 +563,8 @@ export class Project extends CardContainer {
350
563
  /**
351
564
  * Updates all imported module resources.
352
565
  */
353
- public async collectModuleResources() {
354
- await this.resources.moduleImported();
566
+ public collectModuleResources() {
567
+ this.resources.moduleImported();
355
568
  }
356
569
 
357
570
  /**
@@ -371,7 +584,7 @@ export class Project extends CardContainer {
371
584
  if (!card || !card.path || !isTemplateCard(card)) {
372
585
  return undefined;
373
586
  }
374
- const { template } = this.cardPathParts(card.path);
587
+ const { template } = cardPathParts(this.projectPrefix, card.path);
375
588
  return new TemplateResource(this, resourceName(template)).templateObject();
376
589
  }
377
590
 
@@ -421,46 +634,8 @@ export class Project extends CardContainer {
421
634
  * @param details Defines which card details are included in the return values.
422
635
  * @returns specific card details, or undefined if card is not part of the project.
423
636
  */
424
- public async findSpecificCard(
425
- cardToFind: string,
426
- details: ProjectFetchCardDetails = {},
427
- ): Promise<Card | undefined> {
428
- let card;
429
-
430
- if (
431
- details.location === CardLocation.projectOnly ||
432
- details.location === CardLocation.all ||
433
- !details.location
434
- ) {
435
- card = await super.findCard(
436
- this.paths.cardRootFolder,
437
- cardToFind,
438
- details,
439
- );
440
- }
441
-
442
- if (
443
- !card &&
444
- (details.location === CardLocation.templatesOnly ||
445
- details.location === CardLocation.all ||
446
- !details.location)
447
- ) {
448
- const templates = await this.templates();
449
- for (const template of templates) {
450
- const templateObject = new TemplateResource(
451
- this,
452
- resourceName(template.name),
453
- ).templateObject();
454
- if (!templateObject) continue;
455
-
456
- // optimize: execute each find in template parallel
457
- card = await templateObject.findSpecificCard(cardToFind, details);
458
- if (card) {
459
- break;
460
- }
461
- }
462
- }
463
- return card;
637
+ public findCard(cardToFind: string, details?: ProjectFetchCardDetails): Card {
638
+ return super.findCard(cardToFind, details);
464
639
  }
465
640
 
466
641
  /**
@@ -490,6 +665,7 @@ export class Project extends CardContainer {
490
665
  * @param changedCard Card that was changed.
491
666
  */
492
667
  public async handleCardChanged(changedCard: Card) {
668
+ // Notify the calculation engine about the change
493
669
  return this.calculationEngine.handleCardChanged(changedCard);
494
670
  }
495
671
 
@@ -497,39 +673,86 @@ export class Project extends CardContainer {
497
673
  * When cards are removed.
498
674
  * @param deletedCard Card that is to be removed.
499
675
  */
500
- 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);
501
692
  return this.calculationEngine.handleDeleteCard(deletedCard);
502
693
  }
503
694
 
504
695
  /**
505
- * When new cards are added.
506
- * @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'
507
700
  */
508
- public async handleNewCards(cards: Card[]) {
509
- 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();
510
718
  }
511
719
 
512
720
  /**
513
- * Checks if a given card is part of this project.
514
- * @param cardKey card to check.
515
- * @returns true if a given card is found from project, false otherwise.
721
+ * When new cards are added.
722
+ * @param cards Added cards.
516
723
  */
517
- public hasCard(cardKey: string): boolean {
518
- 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);
519
740
  }
520
741
 
521
742
  /**
522
743
  * Adds a module from project.
523
- * @param module Name of the module
744
+ * @param module Module to add
524
745
  */
525
746
  public async importModule(module: ModuleSetting) {
526
747
  // Add module as a dependency.
527
748
  await this.configuration.addModule(module);
528
- await this.collectModuleResources();
749
+ this.collectModuleResources();
750
+ await this.populateTemplateCards();
751
+ this.logger.info(`Imported module '${module.name}'`);
529
752
  }
530
753
 
531
754
  /**
532
- * Checks if given path is a project.
755
+ * Checks if a given path is a project.
533
756
  * @param path Path to a project
534
757
  * @returns true, if in the given path there is a project; false otherwise
535
758
  */
@@ -537,17 +760,6 @@ export class Project extends CardContainer {
537
760
  return pathExists(join(path, 'cardRoot'));
538
761
  }
539
762
 
540
- /**
541
- * Returns whether card is a template card or not
542
- * @param cardKey card to check.
543
- * @todo: This is only used from 'remove'. Could it use the static checker?
544
- * @returns true, if card is template card; false otherwise
545
- */
546
- public async isTemplateCard(cardKey: string): Promise<boolean> {
547
- const templateCards = await this.allTemplateCards();
548
- return templateCards.find((card) => card.key === cardKey) != null;
549
- }
550
-
551
763
  /**
552
764
  * Returns an array of all the link types in the project.
553
765
  * @param from Defines where resources are collected from.
@@ -563,7 +775,7 @@ export class Project extends CardContainer {
563
775
  * Returns an array of cards in the project, in the templates or both.
564
776
  * Cards don't have content and nor metadata.
565
777
  * @param cardsFrom Where to return cards from (project, templates, or both)
566
- * @returns all cards in the project.
778
+ * @returns all cards in the project per container.
567
779
  */
568
780
  public async listCards(
569
781
  cardsFrom: CardLocation = CardLocation.all,
@@ -573,9 +785,9 @@ export class Project extends CardContainer {
573
785
  cardsFrom === CardLocation.all ||
574
786
  cardsFrom === CardLocation.projectOnly
575
787
  ) {
576
- const projectCards = (await super.cards(this.paths.cardRootFolder)).map(
577
- (item) => item.key,
578
- );
788
+ const projectCards = super
789
+ .cards(this.paths.cardRootFolder)
790
+ .map((item) => item.key);
579
791
  cardListContainer.push({
580
792
  name: this.projectName,
581
793
  type: 'project',
@@ -595,8 +807,8 @@ export class Project extends CardContainer {
595
807
  ).templateObject();
596
808
  if (templateObject) {
597
809
  // todo: optimization - do all this in parallel
598
- const templateCards = await templateObject.listCards();
599
- if (templateCards) {
810
+ const templateCards = templateObject.listCards();
811
+ if (templateCards.length) {
600
812
  cardListContainer.push({
601
813
  name: template.name,
602
814
  type: 'template',
@@ -618,69 +830,27 @@ export class Project extends CardContainer {
618
830
  public async listCardIds(
619
831
  cardsFrom: CardLocation = CardLocation.all,
620
832
  ): Promise<Set<string>> {
621
- const promises: Promise<Set<string>>[] = [];
622
- if (
623
- cardsFrom === CardLocation.all ||
624
- cardsFrom === CardLocation.projectOnly
625
- ) {
626
- promises.push(
627
- super
628
- .cards(this.paths.cardRootFolder)
629
- .then((cards) => new Set(cards.map((card) => card.key))),
630
- );
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));
631
838
  }
632
- if (
633
- cardsFrom === CardLocation.all ||
634
- cardsFrom === CardLocation.templatesOnly
635
- ) {
636
- promises.push(
637
- (async () => {
638
- const templates = await this.templates();
639
- const templateResources = templates.map(
640
- (template) =>
641
- new TemplateResource(this, resourceName(template.name)),
642
- );
643
- const templateObjectsResults =
644
- await Promise.allSettled(templateResources);
645
- const templateObjects = templateObjectsResults
646
- .filter(
647
- (result): result is PromiseFulfilledResult<TemplateResource> =>
648
- result.status === 'fulfilled' && result.value !== null,
649
- )
650
- .map((result) => result.value);
651
-
652
- const listCardsResults = await Promise.allSettled(
653
- templateObjects.map((obj) => obj.templateObject().listCards()),
654
- );
655
- const templateCardIds = new Set<string>();
656
- listCardsResults
657
- .filter(
658
- (result): result is PromiseFulfilledResult<Card[]> =>
659
- result.status === 'fulfilled',
660
- )
661
- .forEach((result) => {
662
- result.value.forEach((card) => templateCardIds.add(card.key));
663
- });
664
- return templateCardIds;
665
- })(),
666
- );
667
- }
668
- const allCardIdSets = await Promise.all(promises);
669
- return new Set(allCardIdSets.flatMap((set) => [...set]));
839
+ return allCardIDs;
670
840
  }
671
841
 
672
842
  /**
673
843
  * Returns details of a certain module.
674
844
  * @param moduleName Name of the module.
675
- * @returns module details, or undefined if workflow cannot be found.
845
+ * @returns module details, or undefined if module cannot be found.
676
846
  */
677
847
  public async module(moduleName: string): Promise<ModuleContent | undefined> {
678
848
  const module = await this.findModule(moduleName);
679
849
  if (module && module.path) {
680
850
  const modulePath = join(module.path, module.name);
681
- const moduleConfig = (await readJsonFile(
851
+ const moduleConfig = await readJsonFile(
682
852
  join(modulePath, Project.projectConfigFileName),
683
- )) as ModuleContent;
853
+ );
684
854
  return {
685
855
  name: moduleConfig.name,
686
856
  modules: moduleConfig.modules,
@@ -822,15 +992,12 @@ export class Project extends CardContainer {
822
992
  }
823
993
 
824
994
  /**
825
- * Returns full path to a given card.
826
- * @param cardKey card to check path for.
827
- * @returns path to a given card.
995
+ * Populates the card cache, if it has not been populated.
828
996
  */
829
- public pathToCard(cardKey: string): string {
830
- const allFiles = getFilesSync(this.paths.cardRootFolder);
831
- const cardIndexJsonFile = join(cardKey, Project.cardMetadataFile);
832
- const foundFile = allFiles.find((file) => file.includes(cardIndexJsonFile));
833
- return foundFile ? dirname(foundFile) : '';
997
+ public async populateCaches() {
998
+ if (!this.cardCache.isPopulated) {
999
+ await this.populateCardsCache();
1000
+ }
834
1001
  }
835
1002
 
836
1003
  /**
@@ -857,6 +1024,7 @@ export class Project extends CardContainer {
857
1024
  const prefixes: string[] = [this.projectPrefix];
858
1025
  let files;
859
1026
  try {
1027
+ // TODO: Could be optimized so that prefixes are stored once fetched.
860
1028
  files = await readdir(this.paths.modulesFolder, {
861
1029
  withFileTypes: true,
862
1030
  recursive: true,
@@ -874,13 +1042,48 @@ export class Project extends CardContainer {
874
1042
 
875
1043
  const configurationPrefixes = await Promise.all(configurationPromises);
876
1044
  prefixes.push(...configurationPrefixes);
877
- } catch {
878
- // do nothing if readdir throws // TODO: Log it
1045
+ } catch (error) {
1046
+ this.logger.error({ error }, 'Failed to collect prefixes in use');
879
1047
  }
880
1048
 
881
1049
  return prefixes;
882
1050
  }
883
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
+
884
1087
  /**
885
1088
  * Array of reports in the project.
886
1089
  * @param from Defines where resources are collected from.
@@ -908,21 +1111,12 @@ export class Project extends CardContainer {
908
1111
  return handleBarFiles;
909
1112
  }
910
1113
 
911
- /**
912
- * Removes a resource from Project.
913
- * @param resource Resource to remove.
914
- */
915
- public removeResource(resource: Resource) {
916
- this.resources.remove(resource);
917
- this.createdResources.delete(resource.name);
918
- }
919
-
920
1114
  /**
921
1115
  * Returns metadata from a given resource
922
1116
  * @param name Name of a resource
923
1117
  * @returns Metadata from the resource.
924
1118
  */
925
- public async resource<Type>(name: string): Promise<Type | undefined> {
1119
+ public resource<Type>(name: string): Type | undefined {
926
1120
  const resName = resourceName(name);
927
1121
  if (this.createdResources.has(resourceNameToString(resName))) {
928
1122
  const value = this.createdResources.get(
@@ -1022,45 +1216,23 @@ export class Project extends CardContainer {
1022
1216
  * Show cards of a project.
1023
1217
  * @returns an array of all project cards in the project.
1024
1218
  */
1025
- public async showProjectCards(): Promise<Card[]> {
1219
+ public showProjectCards(): Card[] {
1026
1220
  return this.showCards(this.paths.cardRootFolder);
1027
1221
  }
1028
1222
 
1029
- /**
1030
- * Returns all template cards from the project. This includes all module templates' cards.
1031
- * @param cardDetails which details to fetch. Optional.
1032
- * @returns all the template cards from the project
1033
- */
1034
- public async allTemplateCards(
1035
- cardDetails?: FetchCardDetails,
1036
- ): Promise<Card[]> {
1037
- const templates = await this.templates();
1038
- const cards: Card[] = [];
1039
- for (const template of templates) {
1040
- const templateCards = await this.templateCards(
1041
- template.name,
1042
- cardDetails,
1043
- );
1044
- if (templateCards) cards.push(...templateCards);
1045
- }
1046
- return cards;
1047
- }
1048
-
1049
1223
  /**
1050
1224
  * Returns cards from single template.
1051
- * @param templateName Name of the template
1052
- * @param cardDetails Card information
1225
+ * @param templateName Name of the template (supports both full names like 'decision/templates/decision' and short names like 'decision')
1053
1226
  * @returns List of cards from template.
1054
1227
  */
1055
- public async templateCards(
1056
- templateName: string,
1057
- cardDetails?: FetchCardDetails,
1058
- ): Promise<Card[]> {
1059
- const templateObject = new TemplateResource(
1060
- this,
1061
- resourceName(templateName),
1062
- ).templateObject();
1063
- 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
+ });
1064
1236
  }
1065
1237
 
1066
1238
  /**
@@ -1075,20 +1247,21 @@ export class Project extends CardContainer {
1075
1247
  }
1076
1248
 
1077
1249
  /**
1078
- * Update card content.
1079
- * @param cardKey card's ID that is updated.
1250
+ * Update a card's content.
1251
+ * @param cardKey card key to update.
1080
1252
  * @param content changed content
1081
1253
  */
1082
1254
  public async updateCardContent(cardKey: string, content: string) {
1083
- const card = await this.findCard(this.basePath, cardKey, {
1084
- metadata: true,
1085
- content: true,
1086
- });
1087
- if (!card) {
1088
- throw new Error(`Card '${cardKey}' does not exist in the project`);
1089
- }
1255
+ const card = this.findCard(cardKey);
1090
1256
  card.content = content;
1257
+
1258
+ // Update lastUpdated timestamp in metadata
1259
+ if (card.metadata) {
1260
+ card.metadata.lastUpdated = new Date().toISOString();
1261
+ }
1262
+
1091
1263
  await this.saveCard(card);
1264
+ await this.handleCardChanged(card);
1092
1265
  }
1093
1266
 
1094
1267
  /**
@@ -1102,21 +1275,15 @@ export class Project extends CardContainer {
1102
1275
  changedKey: string,
1103
1276
  newValue: MetadataContent,
1104
1277
  ) {
1105
- const templateCard = await this.isTemplateCard(cardKey);
1106
- const card = await this.findCard(
1107
- templateCard ? this.paths.templatesFolder : this.paths.cardRootFolder,
1108
- cardKey,
1109
- {
1110
- metadata: true,
1111
- },
1112
- );
1113
- if (!card) {
1114
- throw new Error(`Card '${cardKey}' does not exist in the project`);
1115
- }
1116
-
1278
+ const card = this.findCard(cardKey);
1117
1279
  if (!card.metadata || card.metadata[changedKey] === newValue) {
1118
1280
  return;
1119
1281
  }
1282
+
1283
+ const isRankChange = changedKey === 'rank';
1284
+ const previousPath = isRankChange ? card.path : undefined;
1285
+ const previousParent = isRankChange ? card.parent : undefined;
1286
+
1120
1287
  const cardAsRecord: Record<string, MetadataContent> = card.metadata;
1121
1288
  cardAsRecord[changedKey] = newValue;
1122
1289
 
@@ -1127,35 +1294,70 @@ export class Project extends CardContainer {
1127
1294
  throw new Error(invalidCard);
1128
1295
  }
1129
1296
 
1130
- 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
+ }
1131
1307
  }
1132
1308
 
1133
1309
  /**
1134
- * 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.
1135
1342
  * @param card affected card
1136
1343
  * @param changedMetadata changed content for the card
1137
1344
  */
1138
1345
  public async updateCardMetadata(card: Card, changedMetadata: CardMetadata) {
1139
1346
  card.metadata = changedMetadata;
1140
- return this.saveCardMetadata(card);
1347
+ if (await this.saveCardMetadata(card)) {
1348
+ await this.handleCardChanged(card);
1349
+ }
1141
1350
  }
1142
1351
 
1143
- /**
1144
- * Validates that card's data is valid.
1145
- * @param card Card to validate.
1146
- * @returns validation errors, if any
1147
- */
1148
- public async validateCard(card: Card): Promise<string> {
1352
+ // Validates that card's data is valid.
1353
+ private async validateCard(card: Card): Promise<string> {
1149
1354
  const invalidCustomData = await this.validator.validateCustomFields(
1150
1355
  this,
1151
1356
  card,
1152
1357
  );
1153
- const invalidWorkFlow = await this.validator.validateWorkflowState(
1154
- this,
1155
- card,
1156
- );
1358
+ const invalidWorkFlow = this.validator.validateWorkflowState(this, card);
1157
1359
 
1158
- const invalidLabels = await this.validator.validateCardLabels(card);
1360
+ const invalidLabels = this.validator.validateCardLabels(card);
1159
1361
  if (
1160
1362
  invalidCustomData.length === 0 &&
1161
1363
  invalidWorkFlow.length === 0 &&