@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
@@ -12,18 +12,18 @@
12
12
  License along with this program. If not, see <https://www.gnu.org/licenses/>.
13
13
  */
14
14
  // node
15
- import { dirname, join, resolve, sep } from 'node:path';
16
- import { readdir } from 'node:fs/promises';
15
+ import { basename, join, resolve } from 'node:path';
16
+ import { constants as fsConstants, copyFile, mkdir, readdir, unlink, writeFile, } from 'node:fs/promises';
17
17
  import { CardContainer } from './card-container.js'; // base class
18
18
  import { CalculationEngine } from './project/calculation-engine.js';
19
19
  import { CardLocation, } from '../interfaces/project-interfaces.js';
20
- import { getFilesSync, pathExists } from '../utils/file-utils.js';
20
+ import { pathExists } from '../utils/file-utils.js';
21
21
  import { generateRandomString } from '../utils/random.js';
22
- import { isTemplateCard } from '../utils/card-utils.js';
22
+ import { cardPathParts, isModulePath, isTemplateCard, } from '../utils/card-utils.js';
23
23
  import { ProjectConfiguration } from '../project-settings.js';
24
24
  import { ProjectPaths } from './project/project-paths.js';
25
25
  import { readJsonFile } from '../utils/json.js';
26
- import { resourceName, resourceNameToString, } from '../utils/resource-utils.js';
26
+ import { pathToResourceName, resourceName, resourceNameToString, } from '../utils/resource-utils.js';
27
27
  import { ResourcesFrom, ResourceCollector, } from './project/resource-collector.js';
28
28
  import { Validate } from '../commands/validate.js';
29
29
  import { CalculationResource } from '../resources/calculation-resource.js';
@@ -36,7 +36,8 @@ import { ReportResource } from '../resources/report-resource.js';
36
36
  import { TemplateResource } from '../resources/template-resource.js';
37
37
  import { WorkflowResource } from '../resources/workflow-resource.js';
38
38
  import { ContentWatcher } from './project/project-content-watcher.js';
39
- import { pathToResourceName } from '../utils/resource-utils.js';
39
+ import { getChildLogger } from '../utils/log-utils.js';
40
+ import { ROOT } from '../utils/constants.js';
40
41
  // Re-export this, so that classes that use Project do not need to have separate import.
41
42
  export { ResourcesFrom };
42
43
  /**
@@ -45,25 +46,30 @@ export { ResourcesFrom };
45
46
  export class Project extends CardContainer {
46
47
  watchResourceChanges;
47
48
  calculationEngine;
48
- resources;
49
- projectPaths;
50
- settings;
51
- validator;
52
- resourceWatcher;
53
49
  // Created resources are held in a cache.
54
50
  // In the cache, key is resource name, and data is resource metadata (as JSON).
55
51
  createdResources = new Map();
52
+ logger = getChildLogger({ module: 'Project' });
53
+ projectPaths;
54
+ resources;
55
+ resourceWatcher;
56
+ settings;
57
+ validator;
56
58
  constructor(path, watchResourceChanges) {
57
- super(path, '');
59
+ const settings = new ProjectConfiguration(join(path, '.cards', 'local', Project.projectConfigFileName));
60
+ super(path, settings.cardKeyPrefix, '');
58
61
  this.watchResourceChanges = watchResourceChanges;
62
+ this.settings = settings;
63
+ this.logger.info({ path }, 'Initializing project');
59
64
  this.calculationEngine = new CalculationEngine(this);
60
- this.settings = new ProjectConfiguration(join(path, '.cards', 'local', Project.projectConfigFileName));
61
65
  this.projectPaths = new ProjectPaths(path);
62
66
  this.resources = new ResourceCollector(this);
63
67
  this.containerName = this.settings.name;
64
68
  // todo: implement project validation
65
69
  this.validator = Validate.getInstance();
70
+ this.logger.info({ resourcesFolder: this.paths.resourcesFolder }, 'Collecting local resources');
66
71
  this.resources.collectLocalResources();
72
+ this.logger.info({ name: this.containerName }, 'Project initialization complete');
67
73
  const ignoreRenameFileChanges = true;
68
74
  // Watch changes in .cards if there are multiple instances of Project being
69
75
  // run concurrently.
@@ -88,7 +94,48 @@ export class Project extends CardContainer {
88
94
  });
89
95
  }
90
96
  }
91
- // Removes current version of resource from cache.
97
+ // Changes a card's parent in the cache and updates all relationships.
98
+ changeParent(updatedCard, previousParent) {
99
+ if (previousParent && previousParent !== ROOT) {
100
+ this.removeCachedChildren(previousParent, updatedCard.key);
101
+ }
102
+ if (updatedCard.parent && updatedCard.parent !== ROOT) {
103
+ this.updateCachedChildren(updatedCard.parent, updatedCard);
104
+ }
105
+ this.cardCache.updateCard(updatedCard.key, updatedCard);
106
+ }
107
+ // Finds specific module.
108
+ async findModule(moduleName) {
109
+ return (await this.resources.resources('modules')).find((item) => item.name === moduleName && item.path);
110
+ }
111
+ // Handles attachment changes after filesystem operations.
112
+ async handleAttachmentChange(cardKey, operation, fileName) {
113
+ if (operation === 'added') {
114
+ this.cardCache.addAttachment(cardKey, fileName);
115
+ }
116
+ else if (operation === 'removed') {
117
+ this.cardCache.deleteAttachment(cardKey, fileName);
118
+ }
119
+ else if (operation === 'refresh') {
120
+ const newAttachments = this.cardCache.getCardAttachments(cardKey);
121
+ if (newAttachments) {
122
+ this.cardCache.updateCardAttachments(cardKey, newAttachments);
123
+ }
124
+ }
125
+ }
126
+ // Determines the parent card key from a card's filesystem path.
127
+ parentFromPath(cardPath) {
128
+ return cardPathParts(this.projectPrefix, cardPath).parents.at(-1) || 'root';
129
+ }
130
+ // Remove children from a card in the card cache
131
+ removeCachedChildren(parentKey, childKey) {
132
+ const parentCard = this.cardCache.getCard(parentKey);
133
+ if (parentCard && parentCard.children) {
134
+ parentCard.children = parentCard.children.filter((child) => child !== childKey);
135
+ this.cardCache.updateCard(parentCard.key, parentCard);
136
+ }
137
+ }
138
+ // Removes current version of a resource from the resource cache.
92
139
  // Then re-creates the resource with current data and caches the value again.
93
140
  // If the value wasn't in the cache before, it will be added.
94
141
  async replaceCacheValue(resourceName) {
@@ -97,19 +144,65 @@ export class Project extends CardContainer {
97
144
  this.createdResources.delete(resourceName);
98
145
  }
99
146
  const resourceData = await this.resource(resourceName);
100
- if (!resourceData)
101
- return;
102
- this.createdResources.set(resourceName, resourceData);
103
- }
104
- // Finds specific module.
105
- async findModule(moduleName) {
106
- return (await this.resources.resources('modules')).find((item) => item.name === moduleName && item.path);
147
+ if (resourceData) {
148
+ this.createdResources.set(resourceName, resourceData);
149
+ }
107
150
  }
108
151
  // Returns (local or all) resources of a given type.
109
- // @todo: if this would be public, we could remove cardTypes(), fieldTypes(), ... and similar APIs
110
152
  async resourcesOfType(type, from = ResourcesFrom.localOnly) {
111
153
  return this.resources.resources(type, from);
112
154
  }
155
+ // Updates children in the card cache
156
+ updateCachedChildren(parentKey, newChild) {
157
+ const parentCard = this.cardCache.getCard(parentKey);
158
+ if (parentCard) {
159
+ // Add or update the child in the parent's children array
160
+ const existingChildIndex = parentCard.children?.findIndex((child) => child === newChild.key);
161
+ if (existingChildIndex === -1) {
162
+ parentCard.children.push(newChild.key);
163
+ }
164
+ this.cardCache.updateCard(parentCard.key, parentCard);
165
+ }
166
+ }
167
+ /**
168
+ * Populate template cards into the card cache.
169
+ */
170
+ async populateTemplateCards() {
171
+ try {
172
+ // Gets local & module templates
173
+ const templateResources = await this.templates();
174
+ const prefixes = await this.projectPrefixes();
175
+ const loadPromises = templateResources.map(async (template) => {
176
+ try {
177
+ this.validator.validResourceName('templates', template.name, prefixes);
178
+ }
179
+ catch (error) {
180
+ this.logger.warn({ templateName: template.name, error }, `Template name '${template.name}' does not follow required format, skipping`);
181
+ return;
182
+ }
183
+ const templateResource = new TemplateResource(this, resourceName(template.name));
184
+ const templateObject = templateResource.templateObject();
185
+ const isCreated = templateObject && templateObject.isCreated();
186
+ if (!templateObject || !isCreated) {
187
+ return;
188
+ }
189
+ await this.cardCache.populateFromPath(templateObject.templateCardsFolder(), false);
190
+ });
191
+ await Promise.all(loadPromises);
192
+ // Once all templates have been fetched, build child-parent relationships.
193
+ this.cardCache.populateChildrenRelationships();
194
+ }
195
+ catch (error) {
196
+ this.logger.error({ error }, 'Failed to populate template cards into the card cache');
197
+ }
198
+ }
199
+ /**
200
+ * Populate both the project cards, and all template cards into card cache.
201
+ */
202
+ async populateCardsCache() {
203
+ await this.cardCache.populateFromPath(this.paths.cardRootFolder);
204
+ await this.populateTemplateCards();
205
+ }
113
206
  /**
114
207
  * Add a given 'resource' to the local resource arrays.
115
208
  * @param resource Resource to add.
@@ -119,13 +212,45 @@ export class Project extends CardContainer {
119
212
  this.resources.add(resource);
120
213
  this.createdResources.set(resource.name, data);
121
214
  }
215
+ /**
216
+ * Returns all template cards from the project. This includes all module templates' cards.
217
+ * @returns all the template cards from the project
218
+ */
219
+ allTemplateCards() {
220
+ return this.cardCache.getAllTemplateCards();
221
+ }
122
222
  /**
123
223
  * Returns an array of all the attachments in the project card's (excluding ones in templates).
124
224
  * @returns all attachments in the project.
125
225
  */
126
- async attachments() {
226
+ attachments() {
127
227
  return super.attachments(this.paths.cardRootFolder);
128
228
  }
229
+ /**
230
+ * Returns attachments from cards at a specific path using the card cache.
231
+ * This method allows templates to access attachments from the shared cache.
232
+ * @param path The path to get attachments from
233
+ * @returns Array of attachments from cards at the specified path
234
+ */
235
+ attachmentsByPath(path) {
236
+ return super.attachments(path);
237
+ }
238
+ /**
239
+ * Returns all the attachments in the template cards.
240
+ * @returns all the attachments in the template cards.
241
+ */
242
+ async attachmentsFromTemplates() {
243
+ const templateAttachments = [];
244
+ const templates = await this.templates();
245
+ for (const template of templates) {
246
+ const templateResource = new TemplateResource(this, resourceName(template.name));
247
+ const templateObject = templateResource.templateObject();
248
+ if (templateObject) {
249
+ templateAttachments.push(...templateObject.attachments());
250
+ }
251
+ }
252
+ return templateAttachments;
253
+ }
129
254
  /**
130
255
  * Returns an array of all the calculation files (*.lp) in the project.
131
256
  * @param from Defines where resources are collected from.
@@ -135,117 +260,119 @@ export class Project extends CardContainer {
135
260
  return this.resources.resources('calculations', from);
136
261
  }
137
262
  /**
138
- * Returns path to card's attachment folder.
263
+ * Returns path to a card's attachment folder.
139
264
  * @param cardKey card key
140
- * @returns path to card's attachment folder.
141
- * @throws if card path cannot be found
265
+ * @returns path to a card's attachment folder.
142
266
  */
143
- async cardAttachmentFolder(cardKey) {
144
- // Check if it is a template card.
145
- if (await this.isTemplateCard(cardKey)) {
146
- const cardPath = await this.cardFolder(cardKey);
147
- return join(cardPath, 'a');
267
+ cardAttachmentFolder(cardKey) {
268
+ const pathToCard = this.findCard(cardKey).path;
269
+ return join(pathToCard, 'a');
270
+ }
271
+ /**
272
+ * Creates an attachment for a card.
273
+ * @param cardKey The card to add attachment to
274
+ * @param attachmentName The name for the attachment file
275
+ * @param attachmentData The attachment data (file path or buffer)
276
+ * @throws If trying to add attachment to module card, or if attachment is not found
277
+ */
278
+ async createCardAttachment(cardKey, attachmentName, attachmentData) {
279
+ const attachmentFolder = this.cardAttachmentFolder(cardKey);
280
+ // Check if this is a module template
281
+ if (isModulePath(attachmentFolder)) {
282
+ throw new Error(`Cannot modify imported module`);
148
283
  }
149
- const pathToProjectCard = this.pathToCard(cardKey);
150
- if (!pathToProjectCard) {
151
- throw new Error(`Card '${cardKey}' not found`);
284
+ // Create the attachment folder if it doesn't exist
285
+ await mkdir(attachmentFolder, { recursive: true });
286
+ const attachmentPath = join(attachmentFolder, basename(attachmentName));
287
+ if (Buffer.isBuffer(attachmentData)) {
288
+ await writeFile(attachmentPath, attachmentData, { flag: 'wx' });
152
289
  }
153
- return join(this.paths.cardRootFolder, pathToProjectCard, 'a');
290
+ else {
291
+ try {
292
+ await copyFile(attachmentData, attachmentPath, fsConstants.COPYFILE_EXCL);
293
+ }
294
+ catch {
295
+ throw new Error(`Attachment file not found: ${attachmentData}`);
296
+ }
297
+ }
298
+ // Update cache
299
+ await this.handleAttachmentChange(cardKey, 'added', basename(attachmentName));
154
300
  }
155
301
  /**
156
- * Returns details (as defined by cardDetails) of a card.
157
- * @param cardKey card key (project prefix and a number, e.g. test_1)
158
- * @param cardDetails which card details are returned.
159
- * @returns Card details, or undefined if the card cannot be found.
302
+ * Removes an attachment from a card.
303
+ * @param cardKey The card to remove attachment from
304
+ * @param fileName The name of the attachment file to remove
305
+ * @throws if trying to remove module card attachment, or the attachment was not found.
160
306
  */
161
- async cardDetailsById(cardKey, cardDetails) {
162
- return this.findSpecificCard(cardKey, cardDetails);
307
+ async removeCardAttachment(cardKey, fileName) {
308
+ const attachmentFolder = this.cardAttachmentFolder(cardKey);
309
+ // Modules cannot be modified.
310
+ if (isModulePath(attachmentFolder)) {
311
+ throw new Error(`Cannot modify imported module`);
312
+ }
313
+ const attachmentPath = join(attachmentFolder, fileName);
314
+ try {
315
+ await unlink(attachmentPath);
316
+ }
317
+ catch (error) {
318
+ this.logger.error({ error }, 'Removing card attachment');
319
+ throw new Error(`Attachment not found: ${fileName}`);
320
+ }
321
+ await this.handleAttachmentChange(cardKey, 'removed', fileName);
163
322
  }
164
323
  /**
165
- * Returns path to card's folder.
324
+ * Returns path to a card's folder.
166
325
  * @param cardKey card key
167
- * @returns path to card's folder.
326
+ * @returns path to a card's folder.
168
327
  */
169
328
  async cardFolder(cardKey) {
170
- const found = await super.findCard(this.paths.cardRootFolder, cardKey);
329
+ const found = super.findCard(cardKey);
171
330
  if (found) {
172
331
  return found.path;
173
332
  }
174
333
  const templates = await this.templates();
175
- const templatePromises = templates.map(async (template) => {
334
+ const templatePromises = templates.map((template) => {
176
335
  const templateObject = new TemplateResource(this, resourceName(template.name)).templateObject();
177
336
  const templateCard = templateObject
178
- ? await templateObject.findSpecificCard(cardKey)
337
+ ? templateObject.findCard(cardKey)
179
338
  : undefined;
180
- return templateCard ? templateCard.path : '';
339
+ const path = templateCard ? templateCard.path : '';
340
+ return path;
181
341
  });
182
342
  const templatePaths = await Promise.all(templatePromises);
183
343
  return templatePaths.find((path) => path !== '') || '';
184
344
  }
185
345
  /**
186
- * Splits card path to parts. Returns the parts.
187
- * Returned parts are: prefix, card key, array of parents and template name. Template name is returned only for template cards.
188
- * @param cardPath path to a card
189
- * @returns card path logical parts
190
- * @throws when called with wrong path, or wrong card owner
191
- * todo: if prefix would be parameter; this could be static, or util method
192
- */
193
- cardPathParts(cardPath) {
194
- const pathParts = cardPath.split(sep);
195
- const cardKey = pathParts.at(pathParts.length - 1);
196
- const parents = [];
197
- let prefix = this.projectPrefix;
198
- let template = '';
199
- let startIndex = -1;
200
- let templatesNameIndex = -1;
201
- const cardRootIndex = pathParts.indexOf('cardRoot');
202
- const projectInternalsIndex = pathParts.indexOf('.cards');
203
- if (projectInternalsIndex === -1 && cardRootIndex >= 0) {
204
- startIndex = projectInternalsIndex;
205
- }
206
- else if (projectInternalsIndex >= 0 && cardRootIndex === -1) {
207
- const templatesIndex = pathParts.indexOf('templates');
208
- startIndex = templatesIndex;
209
- if (templatesIndex === -1) {
210
- throw new Error(`Invalid card path. Template card must have 'templates' in path`);
211
- }
212
- const modulesIndex = pathParts.indexOf('modules');
213
- if (modulesIndex !== -1) {
214
- prefix = pathParts.at(modulesIndex + 1) || '';
215
- }
216
- templatesNameIndex = templatesIndex + 1;
217
- template = `${prefix}/templates/${pathParts.at(templatesNameIndex)}`;
218
- }
219
- else {
220
- throw new Error(`Card must be either project card, or template card`);
221
- }
222
- // Look for parents in the path.
223
- let previousWasParent = false;
224
- for (let index = startIndex; index <= pathParts.length; index++) {
225
- if (previousWasParent) {
226
- previousWasParent = false;
227
- parents.push(pathParts.at(index - 2));
228
- }
229
- const cardsSubFolder = pathParts.at(index) === 'c';
230
- const ignoreOrNotTemplatesParent = index - 1 !== templatesNameIndex || templatesNameIndex === -1;
231
- if (cardsSubFolder && ignoreOrNotTemplatesParent) {
232
- previousWasParent = true;
346
+ * Fetches full Card data for given card keys
347
+ * @param cardIds array of card keys to fetch
348
+ * @returns Card data to the given card keys
349
+ */
350
+ cardKeysToCards(cardIds) {
351
+ const cards = [];
352
+ for (const cardId of cardIds) {
353
+ const card = this.cardCache.getCard(cardId);
354
+ if (card) {
355
+ cards.push(card);
233
356
  }
234
357
  }
235
- return {
236
- cardKey: cardKey,
237
- parents: parents,
238
- prefix: prefix,
239
- template: template,
240
- };
358
+ return cards;
241
359
  }
242
360
  /**
243
- * Returns an array of all the cards in the project. Cards have content and metadata
244
- * @param path Optional path from which to fetch the cards. Generally it is best to fetch from Project root, e.g. Project.cardRootFolder
245
- * @param details Which details to include in the cards; by default only "content" and "metadata" are included.
361
+ * Accessor for cards cache.
362
+ * Used by template container (it needs to access project's cache, not their own instance).
363
+ * @note Should not be used directly (other than Template).
364
+ */
365
+ get cardsCache() {
366
+ return this.cardCache;
367
+ }
368
+ /**
369
+ * Returns an array of all the cards in the project.
370
+ * @note These are project cards only, by default (unless path dictates otherwise).
371
+ * @param path Path from which to fetch the cards. Generally it is best to fetch from Project root, e.g. Project.cardRootFolder
372
+ * @param details Which details to include in the cards; by default all details are included.
246
373
  * @returns all cards from the given path in the project.
247
374
  */
248
- async cards(path = this.paths.cardRootFolder, details = { content: true, metadata: true }) {
375
+ cards(path = this.paths.cardRootFolder, details) {
249
376
  return super.cards(path, details);
250
377
  }
251
378
  /**
@@ -256,6 +383,21 @@ export class Project extends CardContainer {
256
383
  async cardTypes(from = ResourcesFrom.all) {
257
384
  return this.resources.resources('cardTypes', from);
258
385
  }
386
+ /**
387
+ * Returns children of a given card; as Card array
388
+ * @param card Parent card to fetch children from
389
+ * @returns children of a given card; as Card array
390
+ */
391
+ childrenCards(card) {
392
+ const cards = [];
393
+ for (const child of card.children) {
394
+ const card = this.cardCache.getCard(child);
395
+ if (card) {
396
+ cards.push(card);
397
+ }
398
+ }
399
+ return cards;
400
+ }
259
401
  /**
260
402
  * Updates all local resources.
261
403
  */
@@ -265,8 +407,8 @@ export class Project extends CardContainer {
265
407
  /**
266
408
  * Updates all imported module resources.
267
409
  */
268
- async collectModuleResources() {
269
- await this.resources.moduleImported();
410
+ collectModuleResources() {
411
+ this.resources.moduleImported();
270
412
  }
271
413
  /**
272
414
  * Returns project configuration.
@@ -284,7 +426,7 @@ export class Project extends CardContainer {
284
426
  if (!card || !card.path || !isTemplateCard(card)) {
285
427
  return undefined;
286
428
  }
287
- const { template } = this.cardPathParts(card.path);
429
+ const { template } = cardPathParts(this.projectPrefix, card.path);
288
430
  return new TemplateResource(this, resourceName(template)).templateObject();
289
431
  }
290
432
  /**
@@ -326,30 +468,8 @@ export class Project extends CardContainer {
326
468
  * @param details Defines which card details are included in the return values.
327
469
  * @returns specific card details, or undefined if card is not part of the project.
328
470
  */
329
- async findSpecificCard(cardToFind, details = {}) {
330
- let card;
331
- if (details.location === CardLocation.projectOnly ||
332
- details.location === CardLocation.all ||
333
- !details.location) {
334
- card = await super.findCard(this.paths.cardRootFolder, cardToFind, details);
335
- }
336
- if (!card &&
337
- (details.location === CardLocation.templatesOnly ||
338
- details.location === CardLocation.all ||
339
- !details.location)) {
340
- const templates = await this.templates();
341
- for (const template of templates) {
342
- const templateObject = new TemplateResource(this, resourceName(template.name)).templateObject();
343
- if (!templateObject)
344
- continue;
345
- // optimize: execute each find in template parallel
346
- card = await templateObject.findSpecificCard(cardToFind, details);
347
- if (card) {
348
- break;
349
- }
350
- }
351
- }
352
- return card;
471
+ findCard(cardToFind, details) {
472
+ return super.findCard(cardToFind, details);
353
473
  }
354
474
  /**
355
475
  * Returns an array of all the graph models in the project.
@@ -372,57 +492,87 @@ export class Project extends CardContainer {
372
492
  * @param changedCard Card that was changed.
373
493
  */
374
494
  async handleCardChanged(changedCard) {
495
+ // Notify the calculation engine about the change
375
496
  return this.calculationEngine.handleCardChanged(changedCard);
376
497
  }
377
498
  /**
378
499
  * When cards are removed.
379
500
  * @param deletedCard Card that is to be removed.
380
501
  */
381
- async handleDeleteCard(deletedCard) {
502
+ async handleCardDeleted(deletedCard) {
503
+ // Delete children from the cache first
504
+ if (deletedCard.children && deletedCard.children.length > 0) {
505
+ for (const child of deletedCard.children) {
506
+ try {
507
+ const childCard = this.findCard(child);
508
+ await this.handleCardDeleted(childCard);
509
+ }
510
+ catch {
511
+ this.logger.warn(`Accessing child '${child}' of '${deletedCard.key}' when deleting cards caused an exception`);
512
+ continue;
513
+ }
514
+ }
515
+ }
516
+ await super.removeCard(deletedCard.key);
382
517
  return this.calculationEngine.handleDeleteCard(deletedCard);
383
518
  }
519
+ /**
520
+ * When card is moved.
521
+ * @param movedCard Card that moved
522
+ * @param newParentCard New parent for the 'movedCard'
523
+ * @param oldParentCard Previous parent of the 'movedCard'
524
+ */
525
+ async handleCardMoved(movedCard, newParentCard, oldParentCard) {
526
+ if (newParentCard) {
527
+ this.cardCache.updateCard(newParentCard.key, newParentCard);
528
+ }
529
+ if (oldParentCard) {
530
+ this.cardCache.updateCard(oldParentCard.key, oldParentCard);
531
+ }
532
+ this.cardCache.updateCard(movedCard.key, movedCard);
533
+ // todo: it would be enough to just update parent, previous parent and changed card
534
+ this.cardCache.populateChildrenRelationships();
535
+ await this.handleCardChanged(movedCard);
536
+ await this.calculationEngine.handleCardMoved();
537
+ }
384
538
  /**
385
539
  * When new cards are added.
386
540
  * @param cards Added cards.
387
541
  */
388
542
  async handleNewCards(cards) {
543
+ // Add new cards to the card cache
544
+ cards.forEach((card) => {
545
+ const cardWithParent = {
546
+ ...card,
547
+ parent: card.parent || this.parentFromPath(card.path),
548
+ };
549
+ this.cardCache.updateCard(cardWithParent.key, cardWithParent);
550
+ // Update the parent's children list in the cache
551
+ if (cardWithParent.parent && cardWithParent.parent !== ROOT) {
552
+ this.updateCachedChildren(cardWithParent.parent, cardWithParent);
553
+ }
554
+ });
389
555
  return this.calculationEngine.handleNewCards(cards);
390
556
  }
391
- /**
392
- * Checks if a given card is part of this project.
393
- * @param cardKey card to check.
394
- * @returns true if a given card is found from project, false otherwise.
395
- */
396
- hasCard(cardKey) {
397
- return super.hasCard(cardKey, this.paths.cardRootFolder);
398
- }
399
557
  /**
400
558
  * Adds a module from project.
401
- * @param module Name of the module
559
+ * @param module Module to add
402
560
  */
403
561
  async importModule(module) {
404
562
  // Add module as a dependency.
405
563
  await this.configuration.addModule(module);
406
- await this.collectModuleResources();
564
+ this.collectModuleResources();
565
+ await this.populateTemplateCards();
566
+ this.logger.info(`Imported module '${module.name}'`);
407
567
  }
408
568
  /**
409
- * Checks if given path is a project.
569
+ * Checks if a given path is a project.
410
570
  * @param path Path to a project
411
571
  * @returns true, if in the given path there is a project; false otherwise
412
572
  */
413
573
  static isCreated(path) {
414
574
  return pathExists(join(path, 'cardRoot'));
415
575
  }
416
- /**
417
- * Returns whether card is a template card or not
418
- * @param cardKey card to check.
419
- * @todo: This is only used from 'remove'. Could it use the static checker?
420
- * @returns true, if card is template card; false otherwise
421
- */
422
- async isTemplateCard(cardKey) {
423
- const templateCards = await this.allTemplateCards();
424
- return templateCards.find((card) => card.key === cardKey) != null;
425
- }
426
576
  /**
427
577
  * Returns an array of all the link types in the project.
428
578
  * @param from Defines where resources are collected from.
@@ -435,13 +585,15 @@ export class Project extends CardContainer {
435
585
  * Returns an array of cards in the project, in the templates or both.
436
586
  * Cards don't have content and nor metadata.
437
587
  * @param cardsFrom Where to return cards from (project, templates, or both)
438
- * @returns all cards in the project.
588
+ * @returns all cards in the project per container.
439
589
  */
440
590
  async listCards(cardsFrom = CardLocation.all) {
441
591
  const cardListContainer = [];
442
592
  if (cardsFrom === CardLocation.all ||
443
593
  cardsFrom === CardLocation.projectOnly) {
444
- const projectCards = (await super.cards(this.paths.cardRootFolder)).map((item) => item.key);
594
+ const projectCards = super
595
+ .cards(this.paths.cardRootFolder)
596
+ .map((item) => item.key);
445
597
  cardListContainer.push({
446
598
  name: this.projectName,
447
599
  type: 'project',
@@ -455,8 +607,8 @@ export class Project extends CardContainer {
455
607
  const templateObject = new TemplateResource(this, resourceName(template.name)).templateObject();
456
608
  if (templateObject) {
457
609
  // todo: optimization - do all this in parallel
458
- const templateCards = await templateObject.listCards();
459
- if (templateCards) {
610
+ const templateCards = templateObject.listCards();
611
+ if (templateCards.length) {
460
612
  cardListContainer.push({
461
613
  name: template.name,
462
614
  type: 'template',
@@ -475,45 +627,24 @@ export class Project extends CardContainer {
475
627
  * @note that cardIDs are not sorted.
476
628
  */
477
629
  async listCardIds(cardsFrom = CardLocation.all) {
478
- const promises = [];
479
- if (cardsFrom === CardLocation.all ||
480
- cardsFrom === CardLocation.projectOnly) {
481
- promises.push(super
482
- .cards(this.paths.cardRootFolder)
483
- .then((cards) => new Set(cards.map((card) => card.key))));
630
+ const cardContainers = await this.listCards(cardsFrom);
631
+ const allCardIDs = new Set();
632
+ for (const container of cardContainers) {
633
+ const cards = container.cards;
634
+ cards.forEach((card) => allCardIDs.add(card));
484
635
  }
485
- if (cardsFrom === CardLocation.all ||
486
- cardsFrom === CardLocation.templatesOnly) {
487
- promises.push((async () => {
488
- const templates = await this.templates();
489
- const templateResources = templates.map((template) => new TemplateResource(this, resourceName(template.name)));
490
- const templateObjectsResults = await Promise.allSettled(templateResources);
491
- const templateObjects = templateObjectsResults
492
- .filter((result) => result.status === 'fulfilled' && result.value !== null)
493
- .map((result) => result.value);
494
- const listCardsResults = await Promise.allSettled(templateObjects.map((obj) => obj.templateObject().listCards()));
495
- const templateCardIds = new Set();
496
- listCardsResults
497
- .filter((result) => result.status === 'fulfilled')
498
- .forEach((result) => {
499
- result.value.forEach((card) => templateCardIds.add(card.key));
500
- });
501
- return templateCardIds;
502
- })());
503
- }
504
- const allCardIdSets = await Promise.all(promises);
505
- return new Set(allCardIdSets.flatMap((set) => [...set]));
636
+ return allCardIDs;
506
637
  }
507
638
  /**
508
639
  * Returns details of a certain module.
509
640
  * @param moduleName Name of the module.
510
- * @returns module details, or undefined if workflow cannot be found.
641
+ * @returns module details, or undefined if module cannot be found.
511
642
  */
512
643
  async module(moduleName) {
513
644
  const module = await this.findModule(moduleName);
514
645
  if (module && module.path) {
515
646
  const modulePath = join(module.path, module.name);
516
- const moduleConfig = (await readJsonFile(join(modulePath, Project.projectConfigFileName)));
647
+ const moduleConfig = await readJsonFile(join(modulePath, Project.projectConfigFileName));
517
648
  return {
518
649
  name: moduleConfig.name,
519
650
  modules: moduleConfig.modules,
@@ -624,15 +755,12 @@ export class Project extends CardContainer {
624
755
  return this.projectPaths;
625
756
  }
626
757
  /**
627
- * Returns full path to a given card.
628
- * @param cardKey card to check path for.
629
- * @returns path to a given card.
758
+ * Populates the card cache, if it has not been populated.
630
759
  */
631
- pathToCard(cardKey) {
632
- const allFiles = getFilesSync(this.paths.cardRootFolder);
633
- const cardIndexJsonFile = join(cardKey, Project.cardMetadataFile);
634
- const foundFile = allFiles.find((file) => file.includes(cardIndexJsonFile));
635
- return foundFile ? dirname(foundFile) : '';
760
+ async populateCaches() {
761
+ if (!this.cardCache.isPopulated) {
762
+ await this.populateCardsCache();
763
+ }
636
764
  }
637
765
  /**
638
766
  * Returns project name.
@@ -656,6 +784,7 @@ export class Project extends CardContainer {
656
784
  const prefixes = [this.projectPrefix];
657
785
  let files;
658
786
  try {
787
+ // TODO: Could be optimized so that prefixes are stored once fetched.
659
788
  files = await readdir(this.paths.modulesFolder, {
660
789
  withFileTypes: true,
661
790
  recursive: true,
@@ -670,11 +799,39 @@ export class Project extends CardContainer {
670
799
  const configurationPrefixes = await Promise.all(configurationPromises);
671
800
  prefixes.push(...configurationPrefixes);
672
801
  }
673
- catch {
674
- // do nothing if readdir throws // TODO: Log it
802
+ catch (error) {
803
+ this.logger.error({ error }, 'Failed to collect prefixes in use');
675
804
  }
676
805
  return prefixes;
677
806
  }
807
+ /**
808
+ * Removes a module from the project
809
+ * @param module Module (name) to remove.
810
+ */
811
+ async removeModule(moduleName) {
812
+ const toBeRemovedTemplates = this.resources.moduleResources.resourceArray('templates', moduleName);
813
+ // First, remove cards from the cache
814
+ for (const template of toBeRemovedTemplates) {
815
+ this.cardCache.deleteCardsFromTemplate(template.name);
816
+ }
817
+ // Then, remove module from project configuration
818
+ await this.configuration.removeModule(moduleName);
819
+ this.collectModuleResources();
820
+ this.logger.info(`Removed module '${moduleName}'`);
821
+ }
822
+ /**
823
+ * Removes a resource from Project.
824
+ * @param resource Resource to remove.
825
+ */
826
+ removeResource(resource) {
827
+ // Template cards must be removed from the cache when resource is removed.
828
+ if (resource.path.includes('templates')) {
829
+ const templateName = resourceNameToString(resourceName(resource.name));
830
+ this.cardCache.deleteCardsFromTemplate(templateName);
831
+ }
832
+ this.resources.remove(resource);
833
+ this.createdResources.delete(resource.name);
834
+ }
678
835
  /**
679
836
  * Array of reports in the project.
680
837
  * @param from Defines where resources are collected from.
@@ -698,20 +855,12 @@ export class Project extends CardContainer {
698
855
  }
699
856
  return handleBarFiles;
700
857
  }
701
- /**
702
- * Removes a resource from Project.
703
- * @param resource Resource to remove.
704
- */
705
- removeResource(resource) {
706
- this.resources.remove(resource);
707
- this.createdResources.delete(resource.name);
708
- }
709
858
  /**
710
859
  * Returns metadata from a given resource
711
860
  * @param name Name of a resource
712
861
  * @returns Metadata from the resource.
713
862
  */
714
- async resource(name) {
863
+ resource(name) {
715
864
  const resName = resourceName(name);
716
865
  if (this.createdResources.has(resourceNameToString(resName))) {
717
866
  const value = this.createdResources.get(resourceNameToString(resName));
@@ -805,33 +954,22 @@ export class Project extends CardContainer {
805
954
  * Show cards of a project.
806
955
  * @returns an array of all project cards in the project.
807
956
  */
808
- async showProjectCards() {
957
+ showProjectCards() {
809
958
  return this.showCards(this.paths.cardRootFolder);
810
959
  }
811
- /**
812
- * Returns all template cards from the project. This includes all module templates' cards.
813
- * @param cardDetails which details to fetch. Optional.
814
- * @returns all the template cards from the project
815
- */
816
- async allTemplateCards(cardDetails) {
817
- const templates = await this.templates();
818
- const cards = [];
819
- for (const template of templates) {
820
- const templateCards = await this.templateCards(template.name, cardDetails);
821
- if (templateCards)
822
- cards.push(...templateCards);
823
- }
824
- return cards;
825
- }
826
960
  /**
827
961
  * Returns cards from single template.
828
- * @param templateName Name of the template
829
- * @param cardDetails Card information
962
+ * @param templateName Name of the template (supports both full names like 'decision/templates/decision' and short names like 'decision')
830
963
  * @returns List of cards from template.
831
964
  */
832
- async templateCards(templateName, cardDetails) {
833
- const templateObject = new TemplateResource(this, resourceName(templateName)).templateObject();
834
- return await templateObject?.cards('', cardDetails);
965
+ templateCards(templateName) {
966
+ const templateCards = this.cardCache.getAllTemplateCards();
967
+ return templateCards.filter((cachedCard) => {
968
+ if (cachedCard.location === 'project') {
969
+ return false;
970
+ }
971
+ return cachedCard.location === templateName;
972
+ });
835
973
  }
836
974
  /**
837
975
  * Array of templates in the project.
@@ -842,20 +980,19 @@ export class Project extends CardContainer {
842
980
  return this.resources.resources('templates', from);
843
981
  }
844
982
  /**
845
- * Update card content.
846
- * @param cardKey card's ID that is updated.
983
+ * Update a card's content.
984
+ * @param cardKey card key to update.
847
985
  * @param content changed content
848
986
  */
849
987
  async updateCardContent(cardKey, content) {
850
- const card = await this.findCard(this.basePath, cardKey, {
851
- metadata: true,
852
- content: true,
853
- });
854
- if (!card) {
855
- throw new Error(`Card '${cardKey}' does not exist in the project`);
856
- }
988
+ const card = this.findCard(cardKey);
857
989
  card.content = content;
990
+ // Update lastUpdated timestamp in metadata
991
+ if (card.metadata) {
992
+ card.metadata.lastUpdated = new Date().toISOString();
993
+ }
858
994
  await this.saveCard(card);
995
+ await this.handleCardChanged(card);
859
996
  }
860
997
  /**
861
998
  * Updates card metadata's single key.
@@ -864,16 +1001,13 @@ export class Project extends CardContainer {
864
1001
  * @param newValue changed value for the key
865
1002
  */
866
1003
  async updateCardMetadataKey(cardKey, changedKey, newValue) {
867
- const templateCard = await this.isTemplateCard(cardKey);
868
- const card = await this.findCard(templateCard ? this.paths.templatesFolder : this.paths.cardRootFolder, cardKey, {
869
- metadata: true,
870
- });
871
- if (!card) {
872
- throw new Error(`Card '${cardKey}' does not exist in the project`);
873
- }
1004
+ const card = this.findCard(cardKey);
874
1005
  if (!card.metadata || card.metadata[changedKey] === newValue) {
875
1006
  return;
876
1007
  }
1008
+ const isRankChange = changedKey === 'rank';
1009
+ const previousPath = isRankChange ? card.path : undefined;
1010
+ const previousParent = isRankChange ? card.parent : undefined;
877
1011
  const cardAsRecord = card.metadata;
878
1012
  cardAsRecord[changedKey] = newValue;
879
1013
  const invalidCard = isTemplateCard(card)
@@ -882,26 +1016,58 @@ export class Project extends CardContainer {
882
1016
  if (invalidCard.length !== 0) {
883
1017
  throw new Error(invalidCard);
884
1018
  }
885
- await this.saveCardMetadata(card);
1019
+ const updated = await this.saveCardMetadata(card);
1020
+ if (!updated)
1021
+ return;
1022
+ // For rank changes, check if path changed (indicating a move)
1023
+ if (isRankChange) {
1024
+ const updatedCard = this.findCard(cardKey);
1025
+ if (updatedCard.path !== previousPath) {
1026
+ this.changeParent(updatedCard, previousParent);
1027
+ }
1028
+ }
886
1029
  }
887
1030
  /**
888
- * Updates card metadata.
1031
+ * Updates the entire card in the card cache and handles any path/parent changes.
1032
+ * Also persists changes to content and metadata files.
1033
+ * @param card The card with updated information (path, parent, metadata, etc.)
1034
+ */
1035
+ async updateCard(card) {
1036
+ const cachedCard = this.cardCache.getCard(card.key);
1037
+ const pathChange = cachedCard && cachedCard.path !== card.path;
1038
+ if (pathChange) {
1039
+ this.changeParent(card, cachedCard.parent);
1040
+ }
1041
+ const metadataChanged = cachedCard &&
1042
+ JSON.stringify(cachedCard.metadata) !== JSON.stringify(card.metadata);
1043
+ if (metadataChanged) {
1044
+ await this.saveCardMetadata(card);
1045
+ }
1046
+ const contentChanged = cachedCard && cachedCard.content !== card.content;
1047
+ if (contentChanged) {
1048
+ await this.saveCardContent(card);
1049
+ }
1050
+ this.cardCache.updateCard(card.key, card);
1051
+ if (metadataChanged || contentChanged || pathChange) {
1052
+ await this.handleCardChanged(card);
1053
+ }
1054
+ }
1055
+ /**
1056
+ * Updates a card's metadata.
889
1057
  * @param card affected card
890
1058
  * @param changedMetadata changed content for the card
891
1059
  */
892
1060
  async updateCardMetadata(card, changedMetadata) {
893
1061
  card.metadata = changedMetadata;
894
- return this.saveCardMetadata(card);
1062
+ if (await this.saveCardMetadata(card)) {
1063
+ await this.handleCardChanged(card);
1064
+ }
895
1065
  }
896
- /**
897
- * Validates that card's data is valid.
898
- * @param card Card to validate.
899
- * @returns validation errors, if any
900
- */
1066
+ // Validates that card's data is valid.
901
1067
  async validateCard(card) {
902
1068
  const invalidCustomData = await this.validator.validateCustomFields(this, card);
903
- const invalidWorkFlow = await this.validator.validateWorkflowState(this, card);
904
- const invalidLabels = await this.validator.validateCardLabels(card);
1069
+ const invalidWorkFlow = this.validator.validateWorkflowState(this, card);
1070
+ const invalidLabels = this.validator.validateCardLabels(card);
905
1071
  if (invalidCustomData.length === 0 &&
906
1072
  invalidWorkFlow.length === 0 &&
907
1073
  invalidLabels.length === 0) {