@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
@@ -12,8 +12,7 @@
12
12
  */
13
13
 
14
14
  // node
15
- import { basename, join, resolve } from 'node:path';
16
- import { readFile, writeFile } from 'node:fs/promises';
15
+ import { writeFile } from 'node:fs/promises';
17
16
 
18
17
  import { sanitizeSvgBase64 } from '../../utils/sanitize-svg.js';
19
18
  import { instance } from '@viz-js/viz';
@@ -26,7 +25,6 @@ import type {
26
25
  } from '../../types/queries.js';
27
26
  import type { Card, Context } from '../../interfaces/project-interfaces.js';
28
27
  import ClingoParser from '../../utils/clingo-parser.js';
29
- import { pathExists } from '../../utils/file-utils.js';
30
28
  import { Mutex } from 'async-mutex';
31
29
  import Handlebars from 'handlebars';
32
30
  import { type Project, ResourcesFrom } from '../../containers/project.js';
@@ -52,6 +50,7 @@ import type {
52
50
  TemplateMetadata,
53
51
  Workflow,
54
52
  } from '../../interfaces/resource-interfaces.js';
53
+ import { CalculationResource } from '../../resources/calculation-resource.js';
55
54
  import {
56
55
  removeAllPrograms,
57
56
  solve,
@@ -62,6 +61,7 @@ import {
62
61
  import { generateReportContent } from '../../utils/report.js';
63
62
  import { lpFiles, graphvizReport } from '@cyberismo/assets';
64
63
  import {
64
+ resourceName,
65
65
  type ResourceName,
66
66
  resourceNameToString,
67
67
  } from '../../utils/resource-utils.js';
@@ -102,12 +102,7 @@ export class CalculationEngine {
102
102
  * @returns The logic program content for the card
103
103
  */
104
104
  public async cardLogicProgram(cardKey: string): Promise<string> {
105
- const card = await this.project.findSpecificCard(cardKey, {
106
- metadata: true,
107
- });
108
- if (!card) {
109
- throw new Error(`Card '${cardKey}' does not exist in the project`);
110
- }
105
+ const card = this.project.findCard(cardKey);
111
106
  return createCardFacts(card, this.project);
112
107
  }
113
108
 
@@ -123,7 +118,7 @@ export class CalculationEngine {
123
118
  query?: QueryName,
124
119
  ) {
125
120
  let logicProgram = query ? this.queryContent(query) : '';
126
- logicProgram += await buildProgram('', programs);
121
+ logicProgram += buildProgram('', programs);
127
122
  await writeFile(destination, logicProgram);
128
123
  }
129
124
 
@@ -137,8 +132,7 @@ export class CalculationEngine {
137
132
 
138
133
  // Generate card tree content
139
134
  private async setCardTreeContent() {
140
- const cards = await this.getCards(undefined);
141
-
135
+ const cards = this.getCards(undefined);
142
136
  for (const card of cards) {
143
137
  await this.setCardContent(card);
144
138
  }
@@ -245,7 +239,7 @@ export class CalculationEngine {
245
239
  if (!template) continue;
246
240
 
247
241
  const templateContent = createTemplateFacts(template);
248
- const cards = await this.getCards(template.name);
242
+ const cards = this.getCards(template.name);
249
243
  for (const card of cards) {
250
244
  const cardContent = await createCardFacts(card, this.project);
251
245
  setProgram(card.key, cardContent, [ALL_CATEGORY]);
@@ -259,35 +253,39 @@ export class CalculationEngine {
259
253
  const calculations = await this.project.calculations(ResourcesFrom.all);
260
254
 
261
255
  for (const calculationFile of calculations) {
262
- if (calculationFile.path) {
263
- const moduleLogicFile = resolve(
264
- join(calculationFile.path, basename(calculationFile.name)),
256
+ try {
257
+ const calculationResource = new CalculationResource(
258
+ this.project,
259
+ resourceName(calculationFile.name),
265
260
  );
266
-
267
- const filePath = moduleLogicFile.endsWith('.lp')
268
- ? moduleLogicFile
269
- : moduleLogicFile + '.lp';
270
-
271
- if (pathExists(filePath)) {
272
- try {
273
- const moduleContent = await readFile(filePath, 'utf-8');
274
- setProgram(calculationFile.name, moduleContent, [ALL_CATEGORY]);
275
- } catch (error) {
276
- this.logger.warn(
277
- error,
278
- `Failed to read calculation ${calculationFile.name}`,
261
+ if (calculationResource) {
262
+ const resource = await calculationResource.show();
263
+ if (!resource?.content.calculation) {
264
+ this.logger.info(
265
+ `Calculation resource '${resource.name}' does not have calculation file`,
279
266
  );
267
+ continue;
280
268
  }
269
+ setProgram(calculationFile.name, resource.content.calculation, [
270
+ ALL_CATEGORY,
271
+ ]);
281
272
  }
273
+ } catch (error) {
274
+ this.logger.warn(
275
+ error,
276
+ `Failed to read calculation ${calculationFile.name}`,
277
+ );
282
278
  }
283
279
  }
284
280
  }
285
281
 
286
282
  // Gets either all the cards (no parent), or a subtree.
287
- private async getCards(templateName?: string): Promise<Card[]> {
288
- return templateName
289
- ? this.project.templateCards(templateName)
290
- : this.project.cards();
283
+ private getCards(templateName?: string): Card[] {
284
+ if (templateName) {
285
+ return this.project.templateCards(templateName);
286
+ }
287
+
288
+ return this.project.cards();
291
289
  }
292
290
 
293
291
  // Checks that Clingo successfully returned result.
@@ -385,6 +383,18 @@ export class CalculationEngine {
385
383
  });
386
384
  }
387
385
 
386
+ /**
387
+ * When card is moved, rebuild the entire card tree structure.
388
+ * Moving cards changes parent-child relationships, so we need to rebuild
389
+ * the complete card tree facts to ensure consistency.
390
+ */
391
+ public async handleCardMoved() {
392
+ await CalculationEngine.mutex.runExclusive(async () => {
393
+ // Rebuild entire tree structure from scratch to ensure all relationships are correct
394
+ await this.setCardTreeContent();
395
+ });
396
+ }
397
+
388
398
  /**
389
399
  * When cards are removed, automatically remove card-specific calculations.
390
400
  * @param deletedCard Card that is to be removed.
@@ -0,0 +1,497 @@
1
+ /**
2
+ Cyberismo
3
+ Copyright © Cyberismo Ltd and contributors 2025
4
+ This program is free software: you can redistribute it and/or modify it under
5
+ the terms of the GNU Affero General Public License version 3 as published by
6
+ the Free Software Foundation. This program is distributed in the hope that it
7
+ will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
8
+ of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
9
+ See the GNU Affero General Public License for more details.
10
+ You should have received a copy of the GNU Affero General Public
11
+ License along with this program. If not, see <https://www.gnu.org/licenses/>.
12
+ */
13
+
14
+ // node
15
+ import type { Dirent } from 'node:fs';
16
+ import { basename, join } from 'node:path';
17
+ import { readdir, readFile } from 'node:fs/promises';
18
+
19
+ import type {
20
+ Card,
21
+ CardAttachment,
22
+ CardMetadata,
23
+ } from '../../interfaces/project-interfaces.js';
24
+ import { CardNameRegEx } from '../../interfaces/project-interfaces.js';
25
+ import { cardPathParts, parentCard } from '../../utils/card-utils.js';
26
+ import { getChildLogger } from '../../utils/log-utils.js';
27
+ import { pathExists } from '../../utils/file-utils.js';
28
+
29
+ import mime from 'mime-types';
30
+
31
+ /**
32
+ * Extended card interface that includes location metadata.
33
+ * For project cards: location = 'project'
34
+ * For template cards: location = template name (e.g., 'decision/templates/decision')
35
+ */
36
+ interface CachedCard extends Card {
37
+ location: string;
38
+ }
39
+
40
+ const cardMetadataFile = 'index.json';
41
+ const cardContentFile = 'index.adoc';
42
+
43
+ /**
44
+ *
45
+ */
46
+ export class CardCache {
47
+ private cardCache: Map<string, CachedCard> = new Map();
48
+ private cachePopulated: boolean = false;
49
+ constructor(private prefix: string) {}
50
+
51
+ // Recursively builds children relationships for all cards in the cache.
52
+ private buildChildrenRelationshipsRecursively() {
53
+ // Helper function to recursively populate children for a card
54
+ const populateChildren = (cardKey: string, cardCopy: Card) => {
55
+ const directChildren: string[] = [];
56
+ for (const potentialChild of this.getCards()) {
57
+ if (potentialChild.parent === cardKey) {
58
+ directChildren.push(potentialChild.key);
59
+ }
60
+ }
61
+ cardCopy.children = directChildren;
62
+ };
63
+
64
+ // Populate children for all cards in the cache
65
+ for (const card of this.getCards()) {
66
+ populateChildren(card.key, card);
67
+ }
68
+ }
69
+
70
+ // Determines the location from a given path: 'project' for project cards, template name for template cards
71
+ private determineLocationFromPath(path: string): string {
72
+ return cardPathParts(this.prefix, path).template || 'project';
73
+ }
74
+
75
+ // Gets all directory entries recursively.
76
+ private async entries(path: string): Promise<Dirent[]> {
77
+ try {
78
+ return await readdir(path, { withFileTypes: true, recursive: true });
79
+ } catch (error) {
80
+ CardCache.logger.error({ error }, 'Reading entries');
81
+ return [];
82
+ }
83
+ }
84
+
85
+ // Gets attachments from disk.
86
+ private async fetchAttachments(
87
+ currentPath: string,
88
+ ): Promise<CardAttachment[]> {
89
+ const attachmentPath = join(currentPath, 'a');
90
+ if (!pathExists(attachmentPath)) {
91
+ CardCache.logger.info(`No attachment path for ${currentPath}`);
92
+ return [];
93
+ }
94
+
95
+ const fileAttachments = await this.entries(attachmentPath);
96
+ const attachments: CardAttachment[] = [];
97
+ const seenAttachments = new Set<string>();
98
+
99
+ fileAttachments.forEach((attachment) => {
100
+ const cardKey = basename(currentPath);
101
+ const attachmentKey = `${cardKey}:${attachment.parentPath}:${attachment.name}`;
102
+
103
+ // Skip duplicate attachments based on card, path, and filename
104
+ if (!seenAttachments.has(attachmentKey)) {
105
+ seenAttachments.add(attachmentKey);
106
+ attachments.push({
107
+ card: cardKey,
108
+ fileName: attachment.name,
109
+ path: attachment.parentPath,
110
+ mimeType: mime.lookup(attachment.name) || null,
111
+ });
112
+ } else {
113
+ CardCache.logger.warn(
114
+ `Duplicate attachment found during cache population: ${attachment.name} for card ${cardKey}`,
115
+ );
116
+ }
117
+ });
118
+
119
+ return attachments;
120
+ }
121
+
122
+ // Gets content from disk.
123
+ private async fetchContent(
124
+ currentPath: string,
125
+ ): Promise<string | CardAttachment[] | Card[]> {
126
+ return readFile(join(currentPath, cardContentFile), {
127
+ encoding: 'utf-8',
128
+ });
129
+ }
130
+
131
+ // Gets metadata from disk.
132
+ private async fetchMetadata(currentPath: string): Promise<string> {
133
+ function injectLinksIfMissing(metadata: string): string {
134
+ if (metadata !== '' && !metadata.includes('"links":')) {
135
+ const end = metadata.lastIndexOf('}');
136
+ metadata = metadata.slice(0, end - 1) + ',\n "links": []\n' + '}';
137
+ }
138
+ return metadata;
139
+ }
140
+ let metadata = await readFile(join(currentPath, cardMetadataFile), {
141
+ encoding: 'utf-8',
142
+ });
143
+ metadata = injectLinksIfMissing(metadata);
144
+ return metadata;
145
+ }
146
+
147
+ // Builds the card cache from filesystem.
148
+ private async fetchFileEntries(path: string) {
149
+ const allEntries = await this.entries(path);
150
+ const cardEntries = allEntries.filter(
151
+ (entry) => entry.isDirectory() && CardNameRegEx.test(entry.name),
152
+ );
153
+
154
+ // Process all card entries in parallel
155
+ const cardPromises = cardEntries.map(async (entry) => {
156
+ const currentPath = join(entry.parentPath, entry.name);
157
+ const location = this.determineLocationFromPath(currentPath);
158
+
159
+ const [cardContent, cardMetadata, cardAttachments] = await Promise.all([
160
+ this.fetchContent(currentPath),
161
+ this.fetchMetadata(currentPath),
162
+ this.fetchAttachments(currentPath),
163
+ ]);
164
+
165
+ return {
166
+ key: entry.name,
167
+ path: currentPath,
168
+ children: [],
169
+ attachments: Array.isArray(cardAttachments) ? cardAttachments : [],
170
+ content: typeof cardContent === 'string' ? cardContent : '',
171
+ metadata: JSON.parse(cardMetadata),
172
+ parent: parentCard(currentPath),
173
+ location: location,
174
+ };
175
+ });
176
+
177
+ // Wait for all cards to be processed and add them to the cards array
178
+ const processedCards = await Promise.all(cardPromises);
179
+ return processedCards;
180
+ }
181
+
182
+ // Populates the cache from the given array of cards
183
+ private populateFromCards(cards: CachedCard[], buildRelationships = true) {
184
+ const newMap = new Map(
185
+ cards?.map((card): [string, CachedCard] => {
186
+ return [card.key, card];
187
+ }),
188
+ );
189
+ this.cardCache = new Map([...this.cardCache, ...newMap]);
190
+
191
+ // Remove possible duplicates, card IDs must be unique
192
+ const cardIds = cards.map((item) => item.key);
193
+ const duplicates = cardIds.reduce<string[]>(
194
+ (acc, v, i, arr) =>
195
+ arr.indexOf(v) === i || acc.includes(v) ? acc : acc.concat(v),
196
+ [],
197
+ );
198
+ if (duplicates.length > 0) {
199
+ throw new Error(`Duplicate card keys found: ${duplicates}`);
200
+ }
201
+
202
+ if (buildRelationships) {
203
+ this.populateChildrenRelationships();
204
+ }
205
+ this.cachePopulated = true;
206
+ CardCache.logger.info(`Card cache populated`);
207
+ }
208
+
209
+ // Returns instance of logger.
210
+ private static get logger() {
211
+ return getChildLogger({
212
+ module: 'cardCache',
213
+ });
214
+ }
215
+
216
+ /**
217
+ * Adds attachment to a card in the cache.
218
+ * @param cardKey card key for which to add new attachment
219
+ * @param fileName attachment fileName
220
+ * @returns true, if attachment was added to the cache; false otherwise.
221
+ */
222
+ public addAttachment(cardKey: string, fileName: string) {
223
+ const card = this.cardCache.get(cardKey);
224
+ if (!card) {
225
+ CardCache.logger.warn(
226
+ `Cannot add attachment to card '${cardKey}. Card does not exist.'`,
227
+ );
228
+ return false;
229
+ }
230
+ const attachmentFolder = join(card.path, 'a');
231
+ const attachment: CardAttachment = {
232
+ card: cardKey,
233
+ path: attachmentFolder,
234
+ fileName: fileName,
235
+ mimeType: mime.lookup(fileName!) || null,
236
+ };
237
+
238
+ // Check for duplicate attachments based on card, path, and filename
239
+ const isDuplicate = card.attachments.some(
240
+ (existingAttachment) =>
241
+ existingAttachment.card === attachment.card &&
242
+ existingAttachment.path === attachment.path &&
243
+ existingAttachment.fileName === attachment.fileName,
244
+ );
245
+
246
+ if (isDuplicate) {
247
+ CardCache.logger.warn(
248
+ `Duplicate attachment prevented: ${attachment.fileName} for card ${cardKey}`,
249
+ );
250
+ return false;
251
+ }
252
+
253
+ card.attachments.push(attachment);
254
+ this.cardCache.set(cardKey, card);
255
+ return true;
256
+ }
257
+
258
+ /**
259
+ * Empties the cache.
260
+ */
261
+ public clear() {
262
+ CardCache.logger.info(`Card cache cleared`);
263
+ this.cachePopulated = false;
264
+ this.cardCache.clear();
265
+ }
266
+
267
+ /**
268
+ * Removes a card from the cache.
269
+ * @param cardKey card key to remove
270
+ * @returns true, if card was removed from the cache; false otherwise
271
+ */
272
+ public deleteCard(cardKey: string) {
273
+ const cardExists = this.cardCache.has(cardKey);
274
+ if (cardExists) {
275
+ this.cardCache.delete(cardKey);
276
+ }
277
+ return cardExists;
278
+ }
279
+
280
+ /**
281
+ * Removes attachment from a card in the cache.
282
+ * @param cardKey card key of card from which attachment is to be removed
283
+ * @param filename attachment filename to remove
284
+ * @returns true, if attachment was removed from the cache; false otherwise
285
+ */
286
+ public deleteAttachment(cardKey: string, filename: string): boolean {
287
+ const cachedCard = this.cardCache.get(cardKey);
288
+ if (cachedCard && cachedCard.attachments) {
289
+ const attachmentExists = cachedCard.attachments.find(
290
+ (item) => item.fileName === filename,
291
+ );
292
+ cachedCard.attachments = cachedCard.attachments.filter(
293
+ (attachment) => attachment.fileName !== filename,
294
+ );
295
+ return attachmentExists ? true : false;
296
+ }
297
+ return false;
298
+ }
299
+
300
+ /**
301
+ * Removes template's cards from the cache.
302
+ * @param templateName Name of the template
303
+ */
304
+ public deleteCardsFromTemplate(templateName: string) {
305
+ for (const card of this.cardCache.values()) {
306
+ if (card.location === templateName) {
307
+ this.deleteCard(card.key);
308
+ }
309
+ }
310
+ }
311
+
312
+ /**
313
+ * Returns all the template cards in the cache.
314
+ * @returns all the template cards in the cache.
315
+ */
316
+ public getAllTemplateCards(): CachedCard[] {
317
+ return Array.from(this.cardCache.values()).filter(
318
+ (item) => item.location !== 'project',
319
+ );
320
+ }
321
+
322
+ /**
323
+ * Returns a card from the cache.
324
+ * @param cardKey card key to find
325
+ * @returns card from the cache; if not found then returns undefined.
326
+ */
327
+ public getCard(cardKey: string): CachedCard | undefined {
328
+ return this.cardCache.get(cardKey);
329
+ }
330
+
331
+ /**
332
+ * Returns all the cards in the cache.
333
+ * @returns all the cards in the cache
334
+ */
335
+ public getCards(): CachedCard[] {
336
+ return Array.from(this.cardCache.values());
337
+ }
338
+
339
+ /**
340
+ * Returns all the attachments in the cache for a given card key.
341
+ * @param cardKey Card key for which to fetch all the attachments for.
342
+ * @returns all the attachments in the cache for a given card key.
343
+ */
344
+ public getCardAttachments(cardKey: string): CardAttachment[] | undefined {
345
+ const card = this.cardCache.get(cardKey);
346
+ if (!card) {
347
+ CardCache.logger.warn(`Card '${cardKey}' not found`);
348
+ return undefined;
349
+ }
350
+ return card.attachments;
351
+ }
352
+
353
+ /**
354
+ * Checks if card is in the cache; false otherwise.
355
+ * @param cardKey card key to check
356
+ * @returns true if card is in the cache; false otherwise
357
+ */
358
+ public hasCard(cardKey: string): boolean {
359
+ return this.cardCache.has(cardKey);
360
+ }
361
+
362
+ /**
363
+ * Checks if card in cache has attachment.
364
+ * @param cardKey card key to check
365
+ * @param filename attachment file name to find
366
+ * @returns true, if card in cache has attachment; false otherwise.
367
+ */
368
+ public hasCardAttachment(cardKey: string, filename: string): boolean {
369
+ const card = this.cardCache.get(cardKey);
370
+ if (!card) {
371
+ CardCache.logger.warn(`Card '${cardKey}' not found`);
372
+ return false;
373
+ }
374
+ const attachment = card.attachments.find(
375
+ (item) => item.fileName === filename,
376
+ );
377
+ return attachment ? true : false;
378
+ }
379
+
380
+ /**
381
+ * Checks if cache has been already populated.
382
+ * @returns true if cache has already been populated; otherwise false
383
+ */
384
+ public get isPopulated(): boolean {
385
+ return this.cachePopulated;
386
+ }
387
+
388
+ /**
389
+ * Re-builds the existing cache's parent-child relationships info.
390
+ */
391
+ public populateChildrenRelationships() {
392
+ CardCache.logger.info(`Card children relationships re-built`);
393
+ for (const card of this.getCards()) {
394
+ card.children = [];
395
+ }
396
+ this.buildChildrenRelationshipsRecursively();
397
+ }
398
+
399
+ /**
400
+ * Populates the cache from a given path
401
+ * @param path File system path where the cache should be built from.
402
+ * @param buildRelationships Whether to build parent-child relationships immediately
403
+ */
404
+ public async populateFromPath(path: string, buildRelationships = true) {
405
+ const cards = await this.fetchFileEntries(path);
406
+ this.populateFromCards(cards, buildRelationships);
407
+ }
408
+
409
+ /**
410
+ * Updates, or adds a card to cache.
411
+ * @param cardKey Card key
412
+ * @param cardData Updated data.
413
+ */
414
+ public updateCard(cardKey: string, cardData: Card) {
415
+ const card = this.cardCache.get(cardKey);
416
+ if (!card) {
417
+ const targetLocation = this.determineLocationFromPath(cardData.path);
418
+ const extendedCard: CachedCard = {
419
+ ...cardData,
420
+ location: targetLocation,
421
+ };
422
+ this.cardCache.set(cardKey, extendedCard);
423
+ return;
424
+ }
425
+ if (!card?.path) {
426
+ CardCache.logger.info(`Missing path for a card '${cardKey}'`);
427
+ throw new Error(`No path in card!`);
428
+ }
429
+ if (!cardData?.path) {
430
+ CardCache.logger.info(`Missing path for a card data '${cardKey}'`);
431
+ throw new Error(`No path in card data!`);
432
+ }
433
+ const targetLocation = this.determineLocationFromPath(cardData.path);
434
+
435
+ const extendedCard: CachedCard = {
436
+ ...card, // Existing card data
437
+ ...cardData, // Override with new data
438
+ location: targetLocation,
439
+ // Explicitly preserve certain data if it exists in the cached card but not in cardData
440
+ metadata: cardData.metadata ?? card.metadata,
441
+ content: cardData.content ?? card.content,
442
+ attachments: cardData.attachments ?? card.attachments,
443
+ };
444
+ this.cardCache.set(cardKey, extendedCard);
445
+ }
446
+
447
+ /**
448
+ * Updates card's attachments.
449
+ * @param cardKey card key of a card to update
450
+ * @param attachments Attachments to use in the card.
451
+ * @returns true, if update succeeded; false otherwise.
452
+ */
453
+ public updateCardAttachments(cardKey: string, attachments: CardAttachment[]) {
454
+ const card = this.cardCache.get(cardKey);
455
+ if (!card) {
456
+ CardCache.logger.warn(`Card '${cardKey}' not found`);
457
+ return false;
458
+ }
459
+ card.attachments = attachments;
460
+ this.cardCache.set(cardKey, card);
461
+ return true;
462
+ }
463
+
464
+ /**
465
+ * Updates card's content in the cache.
466
+ * @param cardKey card key of a card to update.
467
+ * @param content New content for the card.
468
+ * @returns true, if update succeeded; false otherwise.
469
+ */
470
+ public updateCardContent(cardKey: string, content: string) {
471
+ const card = this.cardCache.get(cardKey);
472
+ if (!card) {
473
+ CardCache.logger.warn(`Card '${cardKey}' not found`);
474
+ return false;
475
+ }
476
+ card.content = content;
477
+ this.cardCache.set(cardKey, card);
478
+ return true;
479
+ }
480
+
481
+ /**
482
+ * Updates card's metadata in the cache.
483
+ * @param cardKey card key of a card to update.
484
+ * @param metadata New metadata for the card.
485
+ * @returns true, if update succeeded; false otherwise.
486
+ */
487
+ public updateCardMetadata(cardKey: string, metadata: CardMetadata) {
488
+ const card = this.cardCache.get(cardKey);
489
+ if (!card) {
490
+ CardCache.logger.warn(`Card '${cardKey}' not found`);
491
+ return false;
492
+ }
493
+ card.metadata = metadata;
494
+ this.cardCache.set(cardKey, card);
495
+ return true;
496
+ }
497
+ }
@@ -55,6 +55,7 @@ class ResourceCollection {
55
55
  /**
56
56
  * Returns resource array of a give type.
57
57
  * @param type Resource array type to return.
58
+ * @param moduleName Name of a module. If given, returns just resources from that module.
58
59
  * @returns resource array of a give type.
59
60
  */
60
61
  public resourceArray(
@@ -341,10 +342,17 @@ export class ResourceCollector {
341
342
  /**
342
343
  * Re-collects imported module resources.
343
344
  */
344
- public async moduleImported() {
345
+ public moduleImported() {
345
346
  this.modulesCollected = false;
346
347
  }
347
348
 
349
+ /**
350
+ * Getter. Provide access for owner to browse module resources.
351
+ */
352
+ public get moduleResources() {
353
+ return this.modules;
354
+ }
355
+
348
356
  /**
349
357
  * Removes a resource from Project.
350
358
  * @param resource Resource to remove.