@cyberismo/data-handler 0.0.14 → 0.0.16

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 (280) hide show
  1. package/dist/card-metadata-updater.js +8 -4
  2. package/dist/card-metadata-updater.js.map +1 -1
  3. package/dist/command-handler.d.ts +4 -0
  4. package/dist/command-handler.js +29 -19
  5. package/dist/command-handler.js.map +1 -1
  6. package/dist/command-manager.d.ts +25 -2
  7. package/dist/command-manager.js +30 -5
  8. package/dist/command-manager.js.map +1 -1
  9. package/dist/commands/create.d.ts +1 -1
  10. package/dist/commands/create.js +45 -93
  11. package/dist/commands/create.js.map +1 -1
  12. package/dist/commands/edit.d.ts +1 -15
  13. package/dist/commands/edit.js +15 -89
  14. package/dist/commands/edit.js.map +1 -1
  15. package/dist/commands/export.d.ts +11 -2
  16. package/dist/commands/export.js +58 -58
  17. package/dist/commands/export.js.map +1 -1
  18. package/dist/commands/import.d.ts +9 -1
  19. package/dist/commands/import.js +17 -11
  20. package/dist/commands/import.js.map +1 -1
  21. package/dist/commands/move.d.ts +1 -2
  22. package/dist/commands/move.js +107 -146
  23. package/dist/commands/move.js.map +1 -1
  24. package/dist/commands/remove.d.ts +8 -1
  25. package/dist/commands/remove.js +17 -48
  26. package/dist/commands/remove.js.map +1 -1
  27. package/dist/commands/rename.d.ts +4 -9
  28. package/dist/commands/rename.js +34 -108
  29. package/dist/commands/rename.js.map +1 -1
  30. package/dist/commands/show.d.ts +22 -34
  31. package/dist/commands/show.js +103 -151
  32. package/dist/commands/show.js.map +1 -1
  33. package/dist/commands/transition.d.ts +9 -2
  34. package/dist/commands/transition.js +49 -44
  35. package/dist/commands/transition.js.map +1 -1
  36. package/dist/commands/update.d.ts +18 -12
  37. package/dist/commands/update.js +34 -18
  38. package/dist/commands/update.js.map +1 -1
  39. package/dist/commands/validate.d.ts +18 -10
  40. package/dist/commands/validate.js +101 -47
  41. package/dist/commands/validate.js.map +1 -1
  42. package/dist/containers/card-container.d.ts +87 -24
  43. package/dist/containers/card-container.js +183 -279
  44. package/dist/containers/card-container.js.map +1 -1
  45. package/dist/containers/project/calculation-engine.d.ts +13 -4
  46. package/dist/containers/project/calculation-engine.js +79 -77
  47. package/dist/containers/project/calculation-engine.js.map +1 -1
  48. package/dist/containers/project/card-cache.d.ts +146 -0
  49. package/dist/containers/project/card-cache.js +411 -0
  50. package/dist/containers/project/card-cache.js.map +1 -0
  51. package/dist/containers/project/project-paths.d.ts +5 -4
  52. package/dist/containers/project/project-paths.js +16 -12
  53. package/dist/containers/project/project-paths.js.map +1 -1
  54. package/dist/containers/project/resource-cache.d.ts +169 -0
  55. package/dist/containers/project/resource-cache.js +507 -0
  56. package/dist/containers/project/resource-cache.js.map +1 -0
  57. package/dist/containers/project/resource-handler.d.ts +129 -0
  58. package/dist/containers/project/resource-handler.js +206 -0
  59. package/dist/containers/project/resource-handler.js.map +1 -0
  60. package/dist/containers/project.d.ts +114 -195
  61. package/dist/containers/project.js +425 -535
  62. package/dist/containers/project.js.map +1 -1
  63. package/dist/containers/template.d.ts +22 -32
  64. package/dist/containers/template.js +113 -115
  65. package/dist/containers/template.js.map +1 -1
  66. package/dist/index.d.ts +1 -0
  67. package/dist/index.js +1 -0
  68. package/dist/index.js.map +1 -1
  69. package/dist/interfaces/folder-content-interfaces.d.ts +7 -4
  70. package/dist/interfaces/folder-content-interfaces.js +3 -3
  71. package/dist/interfaces/folder-content-interfaces.js.map +1 -1
  72. package/dist/interfaces/macros.d.ts +1 -0
  73. package/dist/interfaces/macros.js +1 -1
  74. package/dist/interfaces/macros.js.map +1 -1
  75. package/dist/interfaces/project-interfaces.d.ts +7 -5
  76. package/dist/interfaces/project-interfaces.js.map +1 -1
  77. package/dist/interfaces/resource-interfaces.d.ts +25 -22
  78. package/dist/interfaces/resource-interfaces.js +3 -0
  79. package/dist/interfaces/resource-interfaces.js.map +1 -1
  80. package/dist/macros/common.d.ts +10 -10
  81. package/dist/macros/createCards/index.d.ts +0 -13
  82. package/dist/macros/createCards/index.js.map +1 -1
  83. package/dist/macros/createCards/types.d.ts +44 -0
  84. package/dist/macros/createCards/types.js +15 -0
  85. package/dist/macros/createCards/types.js.map +1 -0
  86. package/dist/macros/graph/index.d.ts +2 -6
  87. package/dist/macros/graph/index.js +14 -28
  88. package/dist/macros/graph/index.js.map +1 -1
  89. package/dist/macros/graph/types.d.ts +23 -0
  90. package/dist/macros/graph/types.js +15 -0
  91. package/dist/macros/graph/types.js.map +1 -0
  92. package/dist/macros/image/index.d.ts +8 -16
  93. package/dist/macros/image/index.js +36 -33
  94. package/dist/macros/image/index.js.map +1 -1
  95. package/dist/macros/image/types.d.ts +38 -0
  96. package/dist/macros/image/types.js +15 -0
  97. package/dist/macros/image/types.js.map +1 -0
  98. package/dist/macros/include/index.d.ts +1 -6
  99. package/dist/macros/include/index.js +4 -7
  100. package/dist/macros/include/index.js.map +1 -1
  101. package/dist/macros/include/types.d.ts +31 -0
  102. package/dist/macros/include/types.js +15 -0
  103. package/dist/macros/include/types.js.map +1 -0
  104. package/dist/macros/index.d.ts +1 -1
  105. package/dist/macros/index.js +2 -2
  106. package/dist/macros/index.js.map +1 -1
  107. package/dist/macros/percentage/index.d.ts +0 -6
  108. package/dist/macros/percentage/index.js.map +1 -1
  109. package/dist/macros/percentage/types.d.ts +31 -0
  110. package/dist/macros/percentage/types.js +15 -0
  111. package/dist/macros/percentage/types.js.map +1 -0
  112. package/dist/macros/report/index.d.ts +0 -3
  113. package/dist/macros/report/index.js +3 -6
  114. package/dist/macros/report/index.js.map +1 -1
  115. package/dist/macros/report/types.d.ts +19 -0
  116. package/dist/macros/report/types.js +15 -0
  117. package/dist/macros/report/types.js.map +1 -0
  118. package/dist/macros/scoreCard/index.d.ts +0 -6
  119. package/dist/macros/scoreCard/index.js.map +1 -1
  120. package/dist/macros/scoreCard/types.d.ts +31 -0
  121. package/dist/macros/scoreCard/types.js +15 -0
  122. package/dist/macros/scoreCard/types.js.map +1 -0
  123. package/dist/macros/types.d.ts +25 -0
  124. package/dist/macros/types.js +2 -0
  125. package/dist/macros/types.js.map +1 -0
  126. package/dist/macros/vega/index.d.ts +0 -4
  127. package/dist/macros/vega/index.js.map +1 -1
  128. package/dist/macros/vega/types.d.ts +20 -0
  129. package/dist/macros/vega/types.js +2 -0
  130. package/dist/macros/vega/types.js.map +1 -0
  131. package/dist/macros/vegalite/index.d.ts +0 -4
  132. package/dist/macros/vegalite/index.js.map +1 -1
  133. package/dist/macros/vegalite/types.d.ts +20 -0
  134. package/dist/macros/vegalite/types.js +15 -0
  135. package/dist/macros/vegalite/types.js.map +1 -0
  136. package/dist/macros/xref/index.d.ts +0 -3
  137. package/dist/macros/xref/index.js +5 -14
  138. package/dist/macros/xref/index.js.map +1 -1
  139. package/dist/macros/xref/types.d.ts +19 -0
  140. package/dist/macros/xref/types.js +15 -0
  141. package/dist/macros/xref/types.js.map +1 -0
  142. package/dist/module-manager.d.ts +16 -3
  143. package/dist/module-manager.js +55 -23
  144. package/dist/module-manager.js.map +1 -1
  145. package/dist/project-settings.d.ts +16 -3
  146. package/dist/project-settings.js +79 -14
  147. package/dist/project-settings.js.map +1 -1
  148. package/dist/resources/calculation-resource.d.ts +6 -33
  149. package/dist/resources/calculation-resource.js +11 -60
  150. package/dist/resources/calculation-resource.js.map +1 -1
  151. package/dist/resources/card-type-resource.d.ts +10 -22
  152. package/dist/resources/card-type-resource.js +46 -66
  153. package/dist/resources/card-type-resource.js.map +1 -1
  154. package/dist/resources/create-defaults.d.ts +3 -2
  155. package/dist/resources/create-defaults.js +3 -2
  156. package/dist/resources/create-defaults.js.map +1 -1
  157. package/dist/resources/field-type-resource.d.ts +8 -22
  158. package/dist/resources/field-type-resource.js +35 -60
  159. package/dist/resources/field-type-resource.js.map +1 -1
  160. package/dist/resources/file-resource.d.ts +14 -35
  161. package/dist/resources/file-resource.js +22 -301
  162. package/dist/resources/file-resource.js.map +1 -1
  163. package/dist/resources/folder-resource.d.ts +44 -66
  164. package/dist/resources/folder-resource.js +102 -149
  165. package/dist/resources/folder-resource.js.map +1 -1
  166. package/dist/resources/graph-model-resource.d.ts +9 -34
  167. package/dist/resources/graph-model-resource.js +18 -64
  168. package/dist/resources/graph-model-resource.js.map +1 -1
  169. package/dist/resources/graph-view-resource.d.ts +9 -29
  170. package/dist/resources/graph-view-resource.js +13 -48
  171. package/dist/resources/graph-view-resource.js.map +1 -1
  172. package/dist/resources/link-type-resource.d.ts +9 -23
  173. package/dist/resources/link-type-resource.js +11 -33
  174. package/dist/resources/link-type-resource.js.map +1 -1
  175. package/dist/resources/report-resource.d.ts +10 -23
  176. package/dist/resources/report-resource.js +20 -67
  177. package/dist/resources/report-resource.js.map +1 -1
  178. package/dist/resources/resource-object.d.ts +143 -23
  179. package/dist/resources/resource-object.js +369 -48
  180. package/dist/resources/resource-object.js.map +1 -1
  181. package/dist/resources/template-resource.d.ts +10 -17
  182. package/dist/resources/template-resource.js +19 -27
  183. package/dist/resources/template-resource.js.map +1 -1
  184. package/dist/resources/workflow-resource.d.ts +9 -25
  185. package/dist/resources/workflow-resource.js +25 -55
  186. package/dist/resources/workflow-resource.js.map +1 -1
  187. package/dist/utils/card-utils.d.ts +69 -19
  188. package/dist/utils/card-utils.js +179 -30
  189. package/dist/utils/card-utils.js.map +1 -1
  190. package/dist/utils/clingo-fact-builder.d.ts +25 -14
  191. package/dist/utils/clingo-fact-builder.js +27 -5
  192. package/dist/utils/clingo-fact-builder.js.map +1 -1
  193. package/dist/utils/clingo-facts.js +14 -7
  194. package/dist/utils/clingo-facts.js.map +1 -1
  195. package/dist/utils/clingo-parser.js +1 -1
  196. package/dist/utils/clingo-parser.js.map +1 -1
  197. package/dist/utils/constants.d.ts +2 -0
  198. package/dist/utils/constants.js +4 -0
  199. package/dist/utils/constants.js.map +1 -1
  200. package/dist/utils/csv.js +1 -1
  201. package/dist/utils/csv.js.map +1 -1
  202. package/dist/utils/resource-utils.d.ts +1 -0
  203. package/dist/utils/resource-utils.js +2 -1
  204. package/dist/utils/resource-utils.js.map +1 -1
  205. package/package.json +11 -11
  206. package/src/card-metadata-updater.ts +9 -7
  207. package/src/command-handler.ts +35 -23
  208. package/src/command-manager.ts +32 -19
  209. package/src/commands/create.ts +59 -160
  210. package/src/commands/edit.ts +16 -132
  211. package/src/commands/export.ts +71 -81
  212. package/src/commands/import.ts +26 -18
  213. package/src/commands/move.ts +143 -179
  214. package/src/commands/remove.ts +20 -59
  215. package/src/commands/rename.ts +45 -156
  216. package/src/commands/show.ts +153 -211
  217. package/src/commands/transition.ts +53 -58
  218. package/src/commands/update.ts +44 -23
  219. package/src/commands/validate.ts +108 -82
  220. package/src/containers/card-container.ts +200 -360
  221. package/src/containers/project/calculation-engine.ts +81 -105
  222. package/src/containers/project/card-cache.ts +497 -0
  223. package/src/containers/project/project-paths.ts +21 -13
  224. package/src/containers/project/resource-cache.ts +648 -0
  225. package/src/containers/project/resource-handler.ts +265 -0
  226. package/src/containers/project.ts +551 -693
  227. package/src/containers/template.ts +129 -142
  228. package/src/index.ts +1 -0
  229. package/src/interfaces/folder-content-interfaces.ts +14 -7
  230. package/src/interfaces/macros.ts +2 -0
  231. package/src/interfaces/project-interfaces.ts +14 -7
  232. package/src/interfaces/resource-interfaces.ts +30 -27
  233. package/src/macros/createCards/index.ts +1 -12
  234. package/src/macros/createCards/types.ts +46 -0
  235. package/src/macros/graph/index.ts +27 -52
  236. package/src/macros/graph/types.ts +24 -0
  237. package/src/macros/image/index.ts +50 -61
  238. package/src/macros/image/types.ts +39 -0
  239. package/src/macros/include/index.ts +6 -15
  240. package/src/macros/include/types.ts +32 -0
  241. package/src/macros/index.ts +2 -2
  242. package/src/macros/percentage/index.ts +1 -7
  243. package/src/macros/percentage/types.ts +32 -0
  244. package/src/macros/report/index.ts +4 -13
  245. package/src/macros/report/types.ts +20 -0
  246. package/src/macros/scoreCard/index.ts +1 -7
  247. package/src/macros/scoreCard/types.ts +32 -0
  248. package/src/macros/types.ts +48 -0
  249. package/src/macros/vega/index.ts +1 -4
  250. package/src/macros/vega/types.ts +21 -0
  251. package/src/macros/vegalite/index.ts +1 -4
  252. package/src/macros/vegalite/types.ts +22 -0
  253. package/src/macros/xref/index.ts +6 -20
  254. package/src/macros/xref/types.ts +20 -0
  255. package/src/module-manager.ts +79 -22
  256. package/src/project-settings.ts +84 -15
  257. package/src/resources/calculation-resource.ts +21 -91
  258. package/src/resources/card-type-resource.ts +74 -109
  259. package/src/resources/create-defaults.ts +3 -2
  260. package/src/resources/field-type-resource.ts +61 -104
  261. package/src/resources/file-resource.ts +33 -441
  262. package/src/resources/folder-resource.ts +130 -207
  263. package/src/resources/graph-model-resource.ts +36 -95
  264. package/src/resources/graph-view-resource.ts +28 -70
  265. package/src/resources/link-type-resource.ts +23 -53
  266. package/src/resources/report-resource.ts +34 -96
  267. package/src/resources/resource-object.ts +511 -66
  268. package/src/resources/template-resource.ts +32 -44
  269. package/src/resources/workflow-resource.ts +42 -85
  270. package/src/utils/card-utils.ts +217 -31
  271. package/src/utils/clingo-fact-builder.ts +28 -16
  272. package/src/utils/clingo-facts.ts +16 -7
  273. package/src/utils/clingo-parser.ts +1 -1
  274. package/src/utils/constants.ts +6 -0
  275. package/src/utils/csv.ts +1 -1
  276. package/src/utils/resource-utils.ts +2 -1
  277. package/dist/containers/project/resource-collector.d.ts +0 -87
  278. package/dist/containers/project/resource-collector.js +0 -337
  279. package/dist/containers/project/resource-collector.js.map +0 -1
  280. package/src/containers/project/resource-collector.ts +0 -396
@@ -13,10 +13,17 @@
13
13
  */
14
14
 
15
15
  // node
16
- import { dirname, join, resolve, sep } from 'node:path';
17
- import { readdir } from 'node:fs/promises';
16
+ import { basename, join, resolve } from 'node:path';
17
+ import {
18
+ constants as fsConstants,
19
+ copyFile,
20
+ mkdir,
21
+ unlink,
22
+ writeFile,
23
+ } from 'node:fs/promises';
18
24
 
19
- import { CardContainer } from './card-container.js'; // base class
25
+ // base class
26
+ import { CardContainer } from './card-container.js';
20
27
 
21
28
  import { CalculationEngine } from './project/calculation-engine.js';
22
29
  import {
@@ -31,222 +38,373 @@ import {
31
38
  type ModuleSetting,
32
39
  type ProjectFetchCardDetails,
33
40
  type ProjectMetadata,
34
- type ProjectSettings,
35
- type Resource,
36
- type ResourceFolderType,
37
41
  } from '../interfaces/project-interfaces.js';
38
- import { getFilesSync, pathExists } from '../utils/file-utils.js';
42
+ import { pathExists } from '../utils/file-utils.js';
39
43
  import { generateRandomString } from '../utils/random.js';
40
- import { isTemplateCard } from '../utils/card-utils.js';
44
+ import {
45
+ cardPathParts,
46
+ isModulePath,
47
+ isTemplateCard,
48
+ } from '../utils/card-utils.js';
41
49
  import { ProjectConfiguration } from '../project-settings.js';
42
50
  import { ProjectPaths } from './project/project-paths.js';
43
51
  import { readJsonFile } from '../utils/json.js';
44
- import {
45
- resourceName,
46
- type ResourceName,
47
- resourceNameToString,
48
- } from '../utils/resource-utils.js';
49
- import {
50
- ResourcesFrom,
51
- ResourceCollector,
52
- } from './project/resource-collector.js';
53
- import type { Template } from './template.js';
52
+ import { ResourcesFrom } from './project/resource-cache.js';
53
+ import { ResourceHandler } from './project/resource-handler.js';
54
54
  import { Validate } from '../commands/validate.js';
55
+ import { ContentWatcher } from './project/project-content-watcher.js';
56
+ import { getChildLogger } from '../utils/log-utils.js';
55
57
 
56
- import { CalculationResource } from '../resources/calculation-resource.js';
57
- import { CardTypeResource } from '../resources/card-type-resource.js';
58
- import { FieldTypeResource } from '../resources/field-type-resource.js';
59
- import { GraphModelResource } from '../resources/graph-model-resource.js';
60
- import { GraphViewResource } from '../resources/graph-view-resource.js';
61
- import { LinkTypeResource } from '../resources/link-type-resource.js';
62
- import { ReportResource } from '../resources/report-resource.js';
63
- import { TemplateResource } from '../resources/template-resource.js';
64
- import { WorkflowResource } from '../resources/workflow-resource.js';
58
+ import type { Template } from './template.js';
65
59
 
66
- import { ContentWatcher } from './project/project-content-watcher.js';
67
- import { pathToResourceName } from '../utils/resource-utils.js';
60
+ import { ROOT } from '../utils/constants.js';
68
61
 
69
62
  // Re-export this, so that classes that use Project do not need to have separate import.
70
63
  export { ResourcesFrom };
71
64
 
65
+ /**
66
+ * Options for Project initialization.
67
+ * autoSave - If project configuration changes are saved automatically. Default true.
68
+ * watchResourceChanges - If project refresh automatically to filesystem changes. Default false.
69
+ */
70
+ export interface ProjectOptions {
71
+ autoSave?: boolean;
72
+ watchResourceChanges?: boolean;
73
+ }
74
+
72
75
  /**
73
76
  * Represents project folder.
74
77
  */
75
78
  export class Project extends CardContainer {
76
79
  public calculationEngine: CalculationEngine;
77
- private resources: ResourceCollector;
80
+ private logger = getChildLogger({ module: 'Project' });
78
81
  private projectPaths: ProjectPaths;
82
+ private resourceHandler: ResourceHandler;
83
+ private resourceWatcher: ContentWatcher | undefined;
79
84
  private settings: ProjectConfiguration;
80
85
  private validator: Validate;
81
- private resourceWatcher: ContentWatcher | undefined;
82
-
83
- // Created resources are held in a cache.
84
- // In the cache, key is resource name, and data is resource metadata (as JSON).
85
- private createdResources = new Map<string, JSON>();
86
86
 
87
87
  constructor(
88
88
  path: string,
89
- private watchResourceChanges?: boolean,
89
+ private options: ProjectOptions = {
90
+ autoSave: true,
91
+ watchResourceChanges: false,
92
+ },
90
93
  ) {
91
- super(path, '');
92
-
93
- this.calculationEngine = new CalculationEngine(this);
94
-
95
- this.settings = new ProjectConfiguration(
94
+ const settings = new ProjectConfiguration(
96
95
  join(path, '.cards', 'local', Project.projectConfigFileName),
96
+ options.autoSave ?? true,
97
97
  );
98
+ super(path, settings.cardKeyPrefix, '');
99
+ this.settings = settings;
100
+
101
+ this.logger.info({ path }, 'Initializing project');
102
+
103
+ this.calculationEngine = new CalculationEngine(this);
98
104
  this.projectPaths = new ProjectPaths(path);
99
- this.resources = new ResourceCollector(this);
105
+ this.resourceHandler = new ResourceHandler(this);
100
106
 
101
107
  this.containerName = this.settings.name;
102
108
  // todo: implement project validation
103
109
  this.validator = Validate.getInstance();
104
- this.resources.collectLocalResources();
110
+
111
+ this.logger.info(
112
+ { name: this.containerName },
113
+ 'Project initialization complete',
114
+ );
105
115
 
106
116
  const ignoreRenameFileChanges = true;
107
117
 
108
118
  // Watch changes in .cards if there are multiple instances of Project being
109
119
  // run concurrently.
110
- if (this.watchResourceChanges) {
120
+ if (this.options.watchResourceChanges) {
111
121
  this.resourceWatcher = new ContentWatcher(
112
122
  ignoreRenameFileChanges,
113
123
  this.paths.resourcesFolder,
114
124
  (fileName: string) => {
115
125
  void (async () => {
116
- let resource;
117
- try {
118
- resource = pathToResourceName(
119
- this,
120
- join(this.paths.resourcesFolder, fileName),
121
- );
122
- if (!resource) {
123
- return;
124
- }
125
- } catch {
126
- // it wasn't a resource that changed, so ignore the change
127
- return;
128
- }
129
- const resourceName = `${resource.prefix}/${resource.type}/${resource.identifier}`;
130
- await this.replaceCacheValue(resourceName);
131
- this.resources.collectLocalResources();
126
+ this.resources.handleFileSystemChange(
127
+ join(this.paths.resourcesFolder, fileName),
128
+ );
129
+ this.resources.changed();
132
130
  })();
133
131
  },
134
132
  );
135
133
  }
136
134
  }
137
135
 
138
- // Removes current version of resource from cache.
139
- // Then re-creates the resource with current data and caches the value again.
140
- // If the value wasn't in the cache before, it will be added.
141
- private async replaceCacheValue(resourceName: string) {
142
- if (this.createdResources.has(resourceName)) {
143
- // First, remove the old version from cache
144
- this.createdResources.delete(resourceName);
136
+ // Changes a card's parent in the cache and updates all relationships.
137
+ private changeParent(updatedCard: Card, previousParent?: string) {
138
+ if (previousParent && previousParent !== ROOT) {
139
+ this.removeCachedChildren(previousParent, updatedCard.key);
145
140
  }
146
- const resourceData = await this.resource(resourceName);
147
- if (!resourceData) return;
148
- this.createdResources.set(resourceName, resourceData as JSON);
141
+ if (updatedCard.parent && updatedCard.parent !== ROOT) {
142
+ this.updateCachedChildren(updatedCard.parent, updatedCard);
143
+ }
144
+ this.cardCache.updateCard(updatedCard.key, updatedCard);
149
145
  }
150
146
 
151
147
  // Finds specific module.
152
- private async findModule(moduleName: string): Promise<Resource | undefined> {
153
- return (await this.resources.resources('modules')).find(
154
- (item) => item.name === moduleName && item.path,
148
+ private async findModule(
149
+ moduleName: string,
150
+ ): Promise<{ name: string; path: string } | undefined> {
151
+ const moduleExists = this.resources.moduleNames().includes(moduleName);
152
+ if (!moduleExists) {
153
+ return undefined;
154
+ }
155
+
156
+ // For modules, we need to construct the local path where the module is stored
157
+ const moduleConfig = this.configuration.modules?.find(
158
+ (module) => module.name === moduleName,
155
159
  );
160
+ if (!moduleConfig) {
161
+ return undefined;
162
+ }
163
+
164
+ return {
165
+ name: moduleName,
166
+ path: join(this.paths.modulesFolder, moduleConfig.name),
167
+ };
168
+ }
169
+
170
+ // Handles attachment changes after filesystem operations.
171
+ private async handleAttachmentChange(
172
+ cardKey: string,
173
+ operation: 'added' | 'removed' | 'refresh',
174
+ fileName: string,
175
+ ): Promise<void> {
176
+ if (operation === 'added') {
177
+ this.cardCache.addAttachment(cardKey, fileName);
178
+ } else if (operation === 'removed') {
179
+ this.cardCache.deleteAttachment(cardKey, fileName);
180
+ } else if (operation === 'refresh') {
181
+ const newAttachments = this.cardCache.getCardAttachments(cardKey);
182
+ if (newAttachments) {
183
+ this.cardCache.updateCardAttachments(cardKey, newAttachments);
184
+ }
185
+ }
186
+ }
187
+
188
+ // Determines the parent card key from a card's filesystem path.
189
+ // todo: could be moved to card-utils
190
+ private parentFromPath(cardPath: string): string {
191
+ return cardPathParts(this.projectPrefix, cardPath).parents.at(-1) || 'root';
192
+ }
193
+
194
+ // Remove children from a card in the card cache
195
+ private removeCachedChildren(parentKey: string, childKey: string) {
196
+ const parentCard = this.cardCache.getCard(parentKey);
197
+ if (parentCard && parentCard.children) {
198
+ parentCard.children = parentCard.children.filter(
199
+ (child) => child !== childKey,
200
+ );
201
+ this.cardCache.updateCard(parentCard.key, parentCard);
202
+ }
203
+ }
204
+
205
+ // Updates children in the card cache
206
+ private updateCachedChildren(parentKey: string, newChild: Card) {
207
+ const parentCard = this.cardCache.getCard(parentKey);
208
+ if (parentCard) {
209
+ // Add or update the child in the parent's children array
210
+ const existingChildIndex = parentCard.children?.findIndex(
211
+ (child) => child === newChild.key,
212
+ );
213
+ if (existingChildIndex === -1) {
214
+ parentCard.children.push(newChild.key);
215
+ }
216
+ this.cardCache.updateCard(parentCard.key, parentCard);
217
+ }
218
+ }
219
+
220
+ // Validates that card's data is valid.
221
+ private async validateCard(card: Card): Promise<string> {
222
+ const invalidCustomData = await this.validator.validateCustomFields(
223
+ this,
224
+ card,
225
+ this.projectPrefixes(),
226
+ );
227
+ const invalidWorkFlow = await this.validator.validateWorkflowState(
228
+ this,
229
+ card,
230
+ );
231
+
232
+ const invalidLabels = this.validator.validateCardLabels(card);
233
+ if (
234
+ invalidCustomData.length === 0 &&
235
+ invalidWorkFlow.length === 0 &&
236
+ invalidLabels.length === 0
237
+ ) {
238
+ return '';
239
+ }
240
+ const errors: string[] = [];
241
+ if (invalidCustomData.length > 0) {
242
+ errors.push(invalidCustomData);
243
+ }
244
+ if (invalidWorkFlow.length > 0) {
245
+ errors.push(invalidWorkFlow);
246
+ }
247
+ if (invalidLabels.length > 0) {
248
+ errors.push(invalidLabels);
249
+ }
250
+ return errors.join('\n');
251
+ }
252
+
253
+ /**
254
+ * Populate template cards into the card cache.
255
+ */
256
+ protected async populateTemplateCards(): Promise<void> {
257
+ try {
258
+ const templateResources = this.resources.templates();
259
+ const prefixes = this.projectPrefixes();
260
+ const loadPromises = templateResources.map(async (template) => {
261
+ try {
262
+ this.validator.validResourceName(
263
+ 'templates',
264
+ template.data?.name || '',
265
+ prefixes,
266
+ );
267
+ } catch (error) {
268
+ this.logger.warn(
269
+ { templateName: template, error },
270
+ `Template name '${template}' does not follow required format, skipping`,
271
+ );
272
+ return;
273
+ }
274
+
275
+ const templateObject = template.templateObject();
276
+ const isCreated = templateObject && templateObject.isCreated();
277
+ if (!templateObject || !isCreated) {
278
+ return;
279
+ }
280
+
281
+ await this.cardCache.populateFromPath(
282
+ templateObject.templateCardsFolder(),
283
+ false,
284
+ );
285
+ });
286
+
287
+ await Promise.all(loadPromises);
288
+
289
+ // Once all templates have been fetched, build child-parent relationships.
290
+ this.cardCache.populateChildrenRelationships();
291
+ } catch (error) {
292
+ this.logger.error(
293
+ { error },
294
+ 'Failed to populate template cards into the card cache',
295
+ );
296
+ }
156
297
  }
157
298
 
158
- // Returns (local or all) resources of a given type.
159
- // @todo: if this would be public, we could remove cardTypes(), fieldTypes(), ... and similar APIs
160
- private async resourcesOfType(
161
- type: ResourceFolderType,
162
- from: ResourcesFrom = ResourcesFrom.localOnly,
163
- ): Promise<Resource[]> {
164
- return this.resources.resources(type, from);
299
+ /**
300
+ * Populate both the project cards, and all template cards into card cache.
301
+ */
302
+ protected async populateCardsCache(): Promise<void> {
303
+ await this.cardCache.populateFromPath(this.paths.cardRootFolder);
304
+ await this.populateTemplateCards();
165
305
  }
166
306
 
167
307
  /**
168
- * Add a given 'resource' to the local resource arrays.
169
- * @param resource Resource to add.
170
- * @param data JSON data for the resource.
308
+ * Returns all template cards from the project. This includes all module templates' cards.
309
+ * @returns all the template cards from the project
171
310
  */
172
- public addResource(resource: Resource, data: JSON) {
173
- this.resources.add(resource);
174
- this.createdResources.set(resource.name, data);
311
+ public allTemplateCards(): Card[] {
312
+ return this.cardCache.getAllTemplateCards();
175
313
  }
176
314
 
177
315
  /**
178
316
  * Returns an array of all the attachments in the project card's (excluding ones in templates).
179
317
  * @returns all attachments in the project.
180
318
  */
181
- public async attachments(): Promise<CardAttachment[]> {
319
+ public attachments(): CardAttachment[] {
182
320
  return super.attachments(this.paths.cardRootFolder);
183
321
  }
184
322
 
185
323
  /**
186
- * Returns an array of all the calculation files (*.lp) in the project.
187
- * @param from Defines where resources are collected from.
188
- * @returns array of all calculation files in the project.
324
+ * Returns attachments from cards at a specific path using the card cache.
325
+ * This method allows templates to access attachments from the shared cache.
326
+ * @param path The path to get attachments from
327
+ * @returns Array of attachments from cards at the specified path
189
328
  */
190
- public async calculations(
191
- from: ResourcesFrom = ResourcesFrom.all,
192
- ): Promise<Resource[]> {
193
- return this.resources.resources('calculations', from);
329
+ public attachmentsByPath(path: string): CardAttachment[] {
330
+ return super.attachments(path);
194
331
  }
195
332
 
196
333
  /**
197
- * Returns path to card's attachment folder.
334
+ * Returns path to a card's attachment folder.
198
335
  * @param cardKey card key
199
- * @returns path to card's attachment folder.
200
- * @throws if card path cannot be found
336
+ * @returns path to a card's attachment folder.
201
337
  */
202
- public async cardAttachmentFolder(cardKey: string): Promise<string> {
203
- // Check if it is a template card.
204
- if (await this.isTemplateCard(cardKey)) {
205
- const cardPath = await this.cardFolder(cardKey);
206
- return join(cardPath, 'a');
207
- }
208
-
209
- const pathToProjectCard = this.pathToCard(cardKey);
210
- if (!pathToProjectCard) {
211
- throw new Error(`Card '${cardKey}' not found`);
212
- }
213
- return join(this.paths.cardRootFolder, pathToProjectCard, 'a');
338
+ public cardAttachmentFolder(cardKey: string): string {
339
+ const pathToCard = this.findCard(cardKey).path;
340
+ return join(pathToCard, 'a');
214
341
  }
215
342
 
216
343
  /**
217
- * Returns details (as defined by cardDetails) of a card.
218
- * @param cardKey card key (project prefix and a number, e.g. test_1)
219
- * @param cardDetails which card details are returned.
220
- * @returns Card details, or undefined if the card cannot be found.
344
+ * Creates an attachment for a card.
345
+ * @param cardKey The card to add attachment to
346
+ * @param attachmentName The name for the attachment file
347
+ * @param attachmentData The attachment data (file path or buffer)
348
+ * @throws If trying to add attachment to module card, or if attachment is not found
221
349
  */
222
- public async cardDetailsById(
350
+ public async createCardAttachment(
223
351
  cardKey: string,
224
- cardDetails: ProjectFetchCardDetails,
225
- ): Promise<Card | undefined> {
226
- return this.findSpecificCard(cardKey, cardDetails);
352
+ attachmentName: string,
353
+ attachmentData: string | Buffer,
354
+ ): Promise<void> {
355
+ const attachmentFolder = this.cardAttachmentFolder(cardKey);
356
+
357
+ // Check if this is a module template
358
+ if (isModulePath(attachmentFolder)) {
359
+ throw new Error(`Cannot modify imported module`);
360
+ }
361
+
362
+ // Create the attachment folder if it doesn't exist
363
+ await mkdir(attachmentFolder, { recursive: true });
364
+
365
+ const attachmentPath = join(attachmentFolder, basename(attachmentName));
366
+
367
+ if (Buffer.isBuffer(attachmentData)) {
368
+ await writeFile(attachmentPath, attachmentData, { flag: 'wx' });
369
+ } else {
370
+ try {
371
+ await copyFile(
372
+ attachmentData,
373
+ attachmentPath,
374
+ fsConstants.COPYFILE_EXCL,
375
+ );
376
+ } catch {
377
+ throw new Error(`Attachment file not found: ${attachmentData}`);
378
+ }
379
+ }
380
+
381
+ // Update cache
382
+ await this.handleAttachmentChange(
383
+ cardKey,
384
+ 'added',
385
+ basename(attachmentName),
386
+ );
227
387
  }
228
388
 
229
389
  /**
230
- * Returns path to card's folder.
390
+ * Returns path to a card's folder.
231
391
  * @param cardKey card key
232
- * @returns path to card's folder.
392
+ * @returns path to a card's folder.
233
393
  */
234
394
  public async cardFolder(cardKey: string): Promise<string> {
235
- const found = await super.findCard(this.paths.cardRootFolder, cardKey);
395
+ const found = super.findCard(cardKey);
236
396
  if (found) {
237
397
  return found.path;
238
398
  }
239
399
 
240
- const templates = await this.templates();
241
- const templatePromises = templates.map(async (template) => {
242
- const templateObject = new TemplateResource(
243
- this,
244
- resourceName(template.name),
245
- ).templateObject();
400
+ const templates = this.resources.templates();
401
+ const templatePromises = templates.map((template) => {
402
+ const templateObject = template.templateObject();
246
403
  const templateCard = templateObject
247
- ? await templateObject.findSpecificCard(cardKey)
404
+ ? templateObject.findCard(cardKey)
248
405
  : undefined;
249
- return templateCard ? templateCard.path : '';
406
+ const path = templateCard ? templateCard.path : '';
407
+ return path;
250
408
  });
251
409
 
252
410
  const templatePaths = await Promise.all(templatePromises);
@@ -254,104 +412,58 @@ export class Project extends CardContainer {
254
412
  }
255
413
 
256
414
  /**
257
- * Splits card path to parts. Returns the parts.
258
- * Returned parts are: prefix, card key, array of parents and template name. Template name is returned only for template cards.
259
- * @param cardPath path to a card
260
- * @returns card path logical parts
261
- * @throws when called with wrong path, or wrong card owner
262
- * todo: if prefix would be parameter; this could be static, or util method
415
+ * Fetches full Card data for given card keys
416
+ * @param cardIds array of card keys to fetch
417
+ * @returns Card data to the given card keys
263
418
  */
264
- public cardPathParts(cardPath: string) {
265
- const pathParts = cardPath.split(sep);
266
- const cardKey = pathParts.at(pathParts.length - 1);
267
- const parents = [];
268
- let prefix = this.projectPrefix;
269
- let template = '';
270
- let startIndex = -1;
271
- let templatesNameIndex = -1;
272
-
273
- const cardRootIndex = pathParts.indexOf('cardRoot');
274
- const projectInternalsIndex = pathParts.indexOf('.cards');
275
-
276
- if (projectInternalsIndex === -1 && cardRootIndex >= 0) {
277
- startIndex = projectInternalsIndex;
278
- } else if (projectInternalsIndex >= 0 && cardRootIndex === -1) {
279
- const templatesIndex = pathParts.indexOf('templates');
280
- startIndex = templatesIndex;
281
- if (templatesIndex === -1) {
282
- throw new Error(
283
- `Invalid card path. Template card must have 'templates' in path`,
284
- );
285
- }
286
- const modulesIndex = pathParts.indexOf('modules');
287
- if (modulesIndex !== -1) {
288
- prefix = pathParts.at(modulesIndex + 1) || '';
289
- }
290
- templatesNameIndex = templatesIndex + 1;
291
- template = `${prefix}/templates/${pathParts.at(templatesNameIndex)}`;
292
- } else {
293
- throw new Error(`Card must be either project card, or template card`);
294
- }
295
-
296
- // Look for parents in the path.
297
- let previousWasParent = false;
298
- for (let index = startIndex; index <= pathParts.length; index++) {
299
- if (previousWasParent) {
300
- previousWasParent = false;
301
- parents.push(pathParts.at(index - 2));
302
- }
303
- const cardsSubFolder = pathParts.at(index) === 'c';
304
- const ignoreOrNotTemplatesParent =
305
- index - 1 !== templatesNameIndex || templatesNameIndex === -1;
306
- if (cardsSubFolder && ignoreOrNotTemplatesParent) {
307
- previousWasParent = true;
419
+ public cardKeysToCards(cardIds: string[]): Card[] {
420
+ const cards: Card[] = [];
421
+ for (const cardId of cardIds) {
422
+ const card = this.cardCache.getCard(cardId);
423
+ if (card) {
424
+ cards.push(card);
308
425
  }
309
426
  }
310
-
311
- return {
312
- cardKey: cardKey,
313
- parents: parents,
314
- prefix: prefix,
315
- template: template,
316
- };
427
+ return cards;
317
428
  }
318
429
 
319
430
  /**
320
- * Returns an array of all the cards in the project. Cards have content and metadata
321
- * @param path Optional path from which to fetch the cards. Generally it is best to fetch from Project root, e.g. Project.cardRootFolder
322
- * @param details Which details to include in the cards; by default only "content" and "metadata" are included.
431
+ * Returns an array of all the cards in the project.
432
+ * @note These are project cards only, by default (unless path dictates otherwise).
433
+ * @param path Path from which to fetch the cards. Generally it is best to fetch from Project root, e.g. Project.cardRootFolder
434
+ * @param details Which details to include in the cards; by default all details are included.
323
435
  * @returns all cards from the given path in the project.
324
436
  */
325
- public async cards(
437
+ public cards(
326
438
  path: string = this.paths.cardRootFolder,
327
- details: FetchCardDetails = { content: true, metadata: true },
328
- ): Promise<Card[]> {
439
+ details?: FetchCardDetails,
440
+ ): Card[] {
329
441
  return super.cards(path, details);
330
442
  }
331
443
 
332
444
  /**
333
- * Returns an array of all the card types in the project.
334
- * @param from Defines where resources are collected from.
335
- * @returns array of all card types in the project.
336
- */
337
- public async cardTypes(
338
- from: ResourcesFrom = ResourcesFrom.all,
339
- ): Promise<Resource[]> {
340
- return this.resources.resources('cardTypes', from);
341
- }
342
-
343
- /**
344
- * Updates all local resources.
445
+ * Accessor for cards cache.
446
+ * Used by template container (it needs to access project's cache, not their own instance).
447
+ * @note Should not be used directly (other than Template).
345
448
  */
346
- public collectLocalResources() {
347
- this.resources.changed();
449
+ public get cardsCache() {
450
+ return this.cardCache;
348
451
  }
349
452
 
350
453
  /**
351
- * Updates all imported module resources.
454
+ * Returns children of a given card; as Card array
455
+ * @param card Parent card to fetch children from
456
+ * @returns children of a given card; as Card array
352
457
  */
353
- public async collectModuleResources() {
354
- await this.resources.moduleImported();
458
+ public childrenCards(card: Card): Card[] {
459
+ const cards: Card[] = [];
460
+ for (const child of card.children) {
461
+ const card = this.cardCache.getCard(child);
462
+ if (card) {
463
+ cards.push(card);
464
+ }
465
+ }
466
+ return cards;
355
467
  }
356
468
 
357
469
  /**
@@ -371,8 +483,9 @@ export class Project extends CardContainer {
371
483
  if (!card || !card.path || !isTemplateCard(card)) {
372
484
  return undefined;
373
485
  }
374
- const { template } = this.cardPathParts(card.path);
375
- return new TemplateResource(this, resourceName(template)).templateObject();
486
+ const { template } = cardPathParts(this.projectPrefix, card.path);
487
+ const templateResource = this.resources.byType(template, 'templates');
488
+ return templateResource.templateObject();
376
489
  }
377
490
 
378
491
  /**
@@ -386,14 +499,13 @@ export class Project extends CardContainer {
386
499
  }
387
500
 
388
501
  /**
389
- * Returns an array of all the field types in the project.
390
- * @param from Defines where resources are collected from.
391
- * @returns array of all field types in the project.
502
+ * Returns specific card.
503
+ * @param cardToFind Card key to find
504
+ * @param details Defines which card details are included in the return values.
505
+ * @returns specific card details, or undefined if card is not part of the project.
392
506
  */
393
- public async fieldTypes(
394
- from: ResourcesFrom = ResourcesFrom.all,
395
- ): Promise<Resource[]> {
396
- return this.resources.resources('fieldTypes', from);
507
+ public findCard(cardToFind: string, details?: ProjectFetchCardDetails): Card {
508
+ return super.findCard(cardToFind, details);
397
509
  }
398
510
 
399
511
  /**
@@ -415,81 +527,12 @@ export class Project extends CardContainer {
415
527
  return Project.findProjectRoot(parentPath);
416
528
  }
417
529
 
418
- /**
419
- * Returns specific card.
420
- * @param cardToFind Card key to find
421
- * @param details Defines which card details are included in the return values.
422
- * @returns specific card details, or undefined if card is not part of the project.
423
- */
424
- public async findSpecificCard(
425
- cardToFind: string,
426
- details: ProjectFetchCardDetails = {},
427
- ): Promise<Card | undefined> {
428
- let card;
429
-
430
- if (
431
- details.location === CardLocation.projectOnly ||
432
- details.location === CardLocation.all ||
433
- !details.location
434
- ) {
435
- card = await super.findCard(
436
- this.paths.cardRootFolder,
437
- cardToFind,
438
- details,
439
- );
440
- }
441
-
442
- if (
443
- !card &&
444
- (details.location === CardLocation.templatesOnly ||
445
- details.location === CardLocation.all ||
446
- !details.location)
447
- ) {
448
- const templates = await this.templates();
449
- for (const template of templates) {
450
- const templateObject = new TemplateResource(
451
- this,
452
- resourceName(template.name),
453
- ).templateObject();
454
- if (!templateObject) continue;
455
-
456
- // optimize: execute each find in template parallel
457
- card = await templateObject.findSpecificCard(cardToFind, details);
458
- if (card) {
459
- break;
460
- }
461
- }
462
- }
463
- return card;
464
- }
465
-
466
- /**
467
- * Returns an array of all the graph models in the project.
468
- * @param from Defines where resources are collected from.
469
- * @returns array of all the graph models in the project.
470
- */
471
- public async graphModels(
472
- from: ResourcesFrom = ResourcesFrom.all,
473
- ): Promise<Resource[]> {
474
- return this.resources.resources('graphModels', from);
475
- }
476
-
477
- /**
478
- * Returns an array of all the graph views in the project.
479
- * @param from Defines where resources are collected from.
480
- * @returns array of all the graph views in the project.
481
- */
482
- public async graphViews(
483
- from: ResourcesFrom = ResourcesFrom.all,
484
- ): Promise<Resource[]> {
485
- return this.resources.resources('graphViews', from);
486
- }
487
-
488
530
  /**
489
531
  * When card changes.
490
532
  * @param changedCard Card that was changed.
491
533
  */
492
534
  public async handleCardChanged(changedCard: Card) {
535
+ // Notify the calculation engine about the change
493
536
  return this.calculationEngine.handleCardChanged(changedCard);
494
537
  }
495
538
 
@@ -497,39 +540,86 @@ export class Project extends CardContainer {
497
540
  * When cards are removed.
498
541
  * @param deletedCard Card that is to be removed.
499
542
  */
500
- public async handleDeleteCard(deletedCard: Card) {
543
+ public async handleCardDeleted(deletedCard: Card) {
544
+ // Delete children from the cache first
545
+ if (deletedCard.children && deletedCard.children.length > 0) {
546
+ for (const child of deletedCard.children) {
547
+ try {
548
+ const childCard = this.findCard(child);
549
+ await this.handleCardDeleted(childCard);
550
+ } catch {
551
+ this.logger.warn(
552
+ `Accessing child '${child}' of '${deletedCard.key}' when deleting cards caused an exception`,
553
+ );
554
+ continue;
555
+ }
556
+ }
557
+ }
558
+ await super.removeCard(deletedCard.key);
501
559
  return this.calculationEngine.handleDeleteCard(deletedCard);
502
560
  }
503
561
 
504
562
  /**
505
- * When new cards are added.
506
- * @param cards Added cards.
563
+ * When card is moved.
564
+ * @param movedCard Card that moved
565
+ * @param newParentCard New parent for the 'movedCard'
566
+ * @param oldParentCard Previous parent of the 'movedCard'
507
567
  */
508
- public async handleNewCards(cards: Card[]) {
509
- return this.calculationEngine.handleNewCards(cards);
568
+ public async handleCardMoved(
569
+ movedCard: Card,
570
+ newParentCard?: Card,
571
+ oldParentCard?: Card,
572
+ ) {
573
+ if (newParentCard) {
574
+ this.cardCache.updateCard(newParentCard.key, newParentCard);
575
+ }
576
+ if (oldParentCard) {
577
+ this.cardCache.updateCard(oldParentCard.key, oldParentCard);
578
+ }
579
+ this.cardCache.updateCard(movedCard.key, movedCard);
580
+
581
+ // todo: it would be enough to just update parent, previous parent and changed card
582
+ this.cardCache.populateChildrenRelationships();
583
+ await this.handleCardChanged(movedCard);
584
+ await this.calculationEngine.handleCardMoved();
510
585
  }
511
586
 
512
587
  /**
513
- * Checks if a given card is part of this project.
514
- * @param cardKey card to check.
515
- * @returns true if a given card is found from project, false otherwise.
588
+ * When new cards are added.
589
+ * @param cards Added cards.
516
590
  */
517
- public hasCard(cardKey: string): boolean {
518
- return super.hasCard(cardKey, this.paths.cardRootFolder);
591
+ public async handleNewCards(cards: Card[]) {
592
+ // Add new cards to the card cache
593
+ cards.forEach((card) => {
594
+ const cardWithParent = {
595
+ ...card,
596
+ parent: card.parent || this.parentFromPath(card.path),
597
+ };
598
+
599
+ this.cardCache.updateCard(cardWithParent.key, cardWithParent);
600
+
601
+ // Update the parent's children list in the cache
602
+ if (cardWithParent.parent && cardWithParent.parent !== ROOT) {
603
+ this.updateCachedChildren(cardWithParent.parent, cardWithParent);
604
+ }
605
+ });
606
+ return this.calculationEngine.handleNewCards(cards);
519
607
  }
520
608
 
521
609
  /**
522
610
  * Adds a module from project.
523
- * @param module Name of the module
611
+ * @param module Module to add
524
612
  */
525
613
  public async importModule(module: ModuleSetting) {
526
614
  // Add module as a dependency.
527
615
  await this.configuration.addModule(module);
528
- await this.collectModuleResources();
616
+ this.resources.changedModules();
617
+ await this.populateTemplateCards();
618
+ this.logger.info(`Imported module '${module.name}'`);
529
619
  }
530
620
 
531
621
  /**
532
- * Checks if given path is a project.
622
+ * Checks if a given path is a project.
533
623
  * @param path Path to a project
534
624
  * @returns true, if in the given path there is a project; false otherwise
535
625
  */
@@ -537,33 +627,11 @@ export class Project extends CardContainer {
537
627
  return pathExists(join(path, 'cardRoot'));
538
628
  }
539
629
 
540
- /**
541
- * Returns whether card is a template card or not
542
- * @param cardKey card to check.
543
- * @todo: This is only used from 'remove'. Could it use the static checker?
544
- * @returns true, if card is template card; false otherwise
545
- */
546
- public async isTemplateCard(cardKey: string): Promise<boolean> {
547
- const templateCards = await this.allTemplateCards();
548
- return templateCards.find((card) => card.key === cardKey) != null;
549
- }
550
-
551
- /**
552
- * Returns an array of all the link types in the project.
553
- * @param from Defines where resources are collected from.
554
- * @returns array of all link types in the project.
555
- */
556
- public async linkTypes(
557
- from: ResourcesFrom = ResourcesFrom.all,
558
- ): Promise<Resource[]> {
559
- return this.resources.resources('linkTypes', from);
560
- }
561
-
562
630
  /**
563
631
  * Returns an array of cards in the project, in the templates or both.
564
632
  * Cards don't have content and nor metadata.
565
633
  * @param cardsFrom Where to return cards from (project, templates, or both)
566
- * @returns all cards in the project.
634
+ * @returns all cards in the project per container.
567
635
  */
568
636
  public async listCards(
569
637
  cardsFrom: CardLocation = CardLocation.all,
@@ -573,9 +641,9 @@ export class Project extends CardContainer {
573
641
  cardsFrom === CardLocation.all ||
574
642
  cardsFrom === CardLocation.projectOnly
575
643
  ) {
576
- const projectCards = (await super.cards(this.paths.cardRootFolder)).map(
577
- (item) => item.key,
578
- );
644
+ const projectCards = super
645
+ .cards(this.paths.cardRootFolder)
646
+ .map((item) => item.key);
579
647
  cardListContainer.push({
580
648
  name: this.projectName,
581
649
  type: 'project',
@@ -587,18 +655,15 @@ export class Project extends CardContainer {
587
655
  cardsFrom === CardLocation.all ||
588
656
  cardsFrom === CardLocation.templatesOnly
589
657
  ) {
590
- const templates = await this.templates();
658
+ const templates = this.resources.templates();
591
659
  for (const template of templates) {
592
- const templateObject = new TemplateResource(
593
- this,
594
- resourceName(template.name),
595
- ).templateObject();
660
+ const templateObject = template.templateObject();
596
661
  if (templateObject) {
597
662
  // todo: optimization - do all this in parallel
598
- const templateCards = await templateObject.listCards();
599
- if (templateCards) {
663
+ const templateCards = templateObject.listCards();
664
+ if (templateCards.length) {
600
665
  cardListContainer.push({
601
- name: template.name,
666
+ name: template.data?.name || '',
602
667
  type: 'template',
603
668
  cards: templateCards.map((item) => item.key),
604
669
  });
@@ -618,142 +683,59 @@ export class Project extends CardContainer {
618
683
  public async listCardIds(
619
684
  cardsFrom: CardLocation = CardLocation.all,
620
685
  ): Promise<Set<string>> {
621
- const promises: Promise<Set<string>>[] = [];
622
- if (
623
- cardsFrom === CardLocation.all ||
624
- cardsFrom === CardLocation.projectOnly
625
- ) {
626
- promises.push(
627
- super
628
- .cards(this.paths.cardRootFolder)
629
- .then((cards) => new Set(cards.map((card) => card.key))),
630
- );
631
- }
632
- if (
633
- cardsFrom === CardLocation.all ||
634
- cardsFrom === CardLocation.templatesOnly
635
- ) {
636
- promises.push(
637
- (async () => {
638
- const templates = await this.templates();
639
- const templateResources = templates.map(
640
- (template) =>
641
- new TemplateResource(this, resourceName(template.name)),
642
- );
643
- const templateObjectsResults =
644
- await Promise.allSettled(templateResources);
645
- const templateObjects = templateObjectsResults
646
- .filter(
647
- (result): result is PromiseFulfilledResult<TemplateResource> =>
648
- result.status === 'fulfilled' && result.value !== null,
649
- )
650
- .map((result) => result.value);
651
-
652
- const listCardsResults = await Promise.allSettled(
653
- templateObjects.map((obj) => obj.templateObject().listCards()),
654
- );
655
- const templateCardIds = new Set<string>();
656
- listCardsResults
657
- .filter(
658
- (result): result is PromiseFulfilledResult<Card[]> =>
659
- result.status === 'fulfilled',
660
- )
661
- .forEach((result) => {
662
- result.value.forEach((card) => templateCardIds.add(card.key));
663
- });
664
- return templateCardIds;
665
- })(),
666
- );
686
+ const cardContainers = await this.listCards(cardsFrom);
687
+ const allCardIDs = new Set<string>();
688
+ for (const container of cardContainers) {
689
+ const cards = container.cards;
690
+ cards.forEach((card) => allCardIDs.add(card));
667
691
  }
668
- const allCardIdSets = await Promise.all(promises);
669
- return new Set(allCardIdSets.flatMap((set) => [...set]));
692
+ return allCardIDs;
670
693
  }
671
694
 
672
695
  /**
673
696
  * Returns details of a certain module.
674
697
  * @param moduleName Name of the module.
675
- * @returns module details, or undefined if workflow cannot be found.
698
+ * @returns module details, or undefined if module cannot be found.
676
699
  */
677
700
  public async module(moduleName: string): Promise<ModuleContent | undefined> {
678
701
  const module = await this.findModule(moduleName);
679
702
  if (module && module.path) {
680
- const modulePath = join(module.path, module.name);
681
- const moduleConfig = (await readJsonFile(
703
+ const modulePath = module.path;
704
+ const moduleConfig = await readJsonFile(
682
705
  join(modulePath, Project.projectConfigFileName),
683
- )) as ModuleContent;
706
+ );
684
707
  return {
685
708
  name: moduleConfig.name,
686
709
  modules: moduleConfig.modules,
687
710
  hubs: moduleConfig.hubs,
688
711
  path: modulePath,
689
712
  cardKeyPrefix: moduleConfig.cardKeyPrefix,
690
- calculations: [
691
- ...(await this.resources.collectResourcesFromModules(
692
- 'calculations',
693
- moduleName,
694
- )),
695
- ],
696
- cardTypes: [
697
- ...(await this.resources.collectResourcesFromModules(
698
- 'cardTypes',
699
- moduleName,
700
- )),
701
- ],
702
- fieldTypes: [
703
- ...(await this.resources.collectResourcesFromModules(
704
- 'fieldTypes',
705
- moduleName,
706
- )),
707
- ],
708
- graphModels: [
709
- ...(await this.resources.collectResourcesFromModules(
710
- 'graphModels',
711
- moduleName,
712
- )),
713
- ],
714
- graphViews: [
715
- ...(await this.resources.collectResourcesFromModules(
716
- 'graphViews',
717
- moduleName,
718
- )),
719
- ],
720
- linkTypes: [
721
- ...(await this.resources.collectResourcesFromModules(
722
- 'linkTypes',
723
- moduleName,
724
- )),
725
- ],
726
- reports: [
727
- ...(await this.resources.collectResourcesFromModules(
728
- 'reports',
729
- moduleName,
730
- )),
731
- ],
732
- templates: [
733
- ...(await this.resources.collectResourcesFromModules(
734
- 'templates',
735
- moduleName,
736
- )),
737
- ],
738
- workflows: [
739
- ...(await this.resources.collectResourcesFromModules(
740
- 'workflows',
741
- moduleName,
742
- )),
743
- ],
713
+ calculations: this.resources.moduleResourceNames(
714
+ 'calculations',
715
+ moduleName,
716
+ ),
717
+ cardTypes: this.resources.moduleResourceNames('cardTypes', moduleName),
718
+ fieldTypes: this.resources.moduleResourceNames(
719
+ 'fieldTypes',
720
+ moduleName,
721
+ ),
722
+ graphModels: this.resources.moduleResourceNames(
723
+ 'graphModels',
724
+ moduleName,
725
+ ),
726
+ graphViews: this.resources.moduleResourceNames(
727
+ 'graphViews',
728
+ moduleName,
729
+ ),
730
+ linkTypes: this.resources.moduleResourceNames('linkTypes', moduleName),
731
+ reports: this.resources.moduleResourceNames('reports', moduleName),
732
+ templates: this.resources.moduleResourceNames('templates', moduleName),
733
+ workflows: this.resources.moduleResourceNames('workflows', moduleName),
744
734
  };
745
735
  }
746
736
  return undefined;
747
737
  }
748
738
 
749
- /**
750
- * Returns list of modules in the project.
751
- * @returns list of modules in the project.
752
- */
753
- public async modules(): Promise<Resource[]> {
754
- return this.resources.resources('modules');
755
- }
756
-
757
739
  /**
758
740
  * Returns a new unique card key with project prefix (e.g. test_x649it4x).
759
741
  * Random part of string will be always 8 characters in base-36 (0-9a-z)
@@ -822,15 +804,16 @@ export class Project extends CardContainer {
822
804
  }
823
805
 
824
806
  /**
825
- * Returns full path to a given card.
826
- * @param cardKey card to check path for.
827
- * @returns path to a given card.
807
+ * Populates the card cache, if it has not been populated.
828
808
  */
829
- public pathToCard(cardKey: string): string {
830
- const allFiles = getFilesSync(this.paths.cardRootFolder);
831
- const cardIndexJsonFile = join(cardKey, Project.cardMetadataFile);
832
- const foundFile = allFiles.find((file) => file.includes(cardIndexJsonFile));
833
- return foundFile ? dirname(foundFile) : '';
809
+ public async populateCaches() {
810
+ if (!this.cardCache.isPopulated) {
811
+ // Only collect modules that are registered in the project configuration
812
+ if (this.configuration.modules && this.configuration.modules.length > 0) {
813
+ this.resources.changedModules();
814
+ }
815
+ await this.populateCardsCache();
816
+ }
834
817
  }
835
818
 
836
819
  /**
@@ -848,158 +831,76 @@ export class Project extends CardContainer {
848
831
  }
849
832
 
850
833
  /**
851
- * Collects all prefixes used in the project (project's own plus all from modules).
834
+ * Returns all prefixes used in the project (project's own plus all from imported modules).
852
835
  * @returns all prefixes used in the project.
853
- * @todo - move the module prefix fetch to resource-collector.
854
- * Make it use cached value that is only changed when module is removed/imported.
855
836
  */
856
- public async projectPrefixes(): Promise<string[]> {
837
+ public projectPrefixes(): string[] {
857
838
  const prefixes: string[] = [this.projectPrefix];
858
- let files;
859
- try {
860
- files = await readdir(this.paths.modulesFolder, {
861
- withFileTypes: true,
862
- recursive: true,
863
- });
864
- const configurationFiles = files
865
- .filter((dirent) => dirent.isFile())
866
- .filter((dirent) => dirent.name === Project.projectConfigFileName);
867
-
868
- const configurationPromises = configurationFiles.map(async (file) => {
869
- const configuration = (await readJsonFile(
870
- join(file.parentPath, file.name),
871
- )) as ProjectSettings;
872
- return configuration.cardKeyPrefix;
873
- });
874
-
875
- const configurationPrefixes = await Promise.all(configurationPromises);
876
- prefixes.push(...configurationPrefixes);
877
- } catch {
878
- // do nothing if readdir throws // TODO: Log it
879
- }
839
+ const moduleNames = this.configuration.modules.map((item) => item.name);
840
+ prefixes.push(...moduleNames);
880
841
 
881
842
  return prefixes;
882
843
  }
883
844
 
884
845
  /**
885
- * Array of reports in the project.
886
- * @param from Defines where resources are collected from.
887
- * @returns array of all reports in the project.
846
+ * Removes an attachment from a card.
847
+ * @param cardKey The card to remove attachment from
848
+ * @param fileName The name of the attachment file to remove
849
+ * @throws if trying to remove module card attachment, or the attachment was not found.
888
850
  */
889
- public async reports(
890
- from: ResourcesFrom = ResourcesFrom.all,
891
- ): Promise<Resource[]> {
892
- return this.resources.resources('reports', from);
893
- }
851
+ public async removeCardAttachment(
852
+ cardKey: string,
853
+ fileName: string,
854
+ ): Promise<void> {
855
+ const attachmentFolder = this.cardAttachmentFolder(cardKey);
894
856
 
895
- /**
896
- * Returns handlebar files from reports.
897
- * @param from Defines where report handlebar files are collected from.
898
- * @returns handlebar files from reports.
899
- */
900
- public async reportHandlerBarFiles(from: ResourcesFrom = ResourcesFrom.all) {
901
- const reports = await this.reports(from);
902
- const handleBarFiles: string[] = [];
903
- for (const reportResourceName of reports) {
904
- const name = resourceName(reportResourceName.name);
905
- const report = new ReportResource(this, name);
906
- handleBarFiles.push(...(await report.handleBarFiles()));
857
+ // Modules cannot be modified.
858
+ if (isModulePath(attachmentFolder)) {
859
+ throw new Error(`Cannot modify imported module`);
907
860
  }
908
- return handleBarFiles;
909
- }
910
861
 
911
- /**
912
- * Removes a resource from Project.
913
- * @param resource Resource to remove.
914
- */
915
- public removeResource(resource: Resource) {
916
- this.resources.remove(resource);
917
- this.createdResources.delete(resource.name);
918
- }
862
+ const attachmentPath = join(attachmentFolder, fileName);
919
863
 
920
- /**
921
- * Returns metadata from a given resource
922
- * @param name Name of a resource
923
- * @returns Metadata from the resource.
924
- */
925
- public async resource<Type>(name: string): Promise<Type | undefined> {
926
- const resName = resourceName(name);
927
- if (this.createdResources.has(resourceNameToString(resName))) {
928
- const value = this.createdResources.get(
929
- resourceNameToString(resName),
930
- ) as unknown as Type;
931
- return value;
932
- }
933
- let resource = undefined;
934
864
  try {
935
- resource = Project.resourceObject(this, resName);
936
- } catch {
937
- return undefined;
938
- }
939
- const data = resource?.data as Type;
940
- if (!data) {
941
- return undefined;
865
+ await unlink(attachmentPath);
866
+ } catch (error) {
867
+ this.logger.error({ error }, 'Removing card attachment');
868
+ throw new Error(`Attachment not found: ${fileName}`);
942
869
  }
943
- return data;
870
+ await this.handleAttachmentChange(cardKey, 'removed', fileName);
944
871
  }
945
872
 
946
873
  /**
947
- * Returns resource cache.
874
+ * Removes a module from the project cache and configuration.
875
+ * @note that ModuleManager removes the actual files.
876
+ * @param moduleName Module name to remove.
948
877
  */
949
- public get resourceCache(): Map<string, JSON> {
950
- return this.createdResources;
951
- }
952
-
953
- /**
954
- * Checks if a given resource exists in the project already.
955
- * @param resourceType Type of resource as a string.
956
- * @param name Valid name of resource.
957
- * @returns boolean, true if resource exists; false otherwise.
958
- */
959
- public async resourceExists(
960
- resourceType: ResourceFolderType,
961
- name: string,
962
- ): Promise<boolean> {
963
- const resources = await this.resourcesOfType(
964
- resourceType,
965
- ResourcesFrom.all,
878
+ public async removeModule(moduleName: string) {
879
+ const toBeRemovedTemplates = this.resources.moduleResourceNames(
880
+ 'templates',
881
+ moduleName,
966
882
  );
967
- const resource = resources.find((item) => item.name === name);
968
- return resource !== undefined;
883
+
884
+ // First, remove template cards from the cache that are part of removed templates.
885
+ for (const templateName of toBeRemovedTemplates) {
886
+ this.cardCache.deleteCardsFromTemplate(templateName);
887
+ }
888
+
889
+ // Then, remove all module resources from cache
890
+ this.resources.removeModule(moduleName);
891
+
892
+ // Finally, remove module from project configuration
893
+ await this.configuration.removeModule(moduleName);
894
+
895
+ this.logger.info(`Removed module '${moduleName}'`);
969
896
  }
970
897
 
971
898
  /**
972
- * Instantiates resource object from project with a resource name.
973
- * @note that this is memory based object only.
974
- * To manipulate the resource (create files, delete files etc), use the resource object's API.
975
- * @param project Project from which resources are created from.
976
- * @param name Resource name
977
- * @throws if called with unsupported resource type.
978
- * @returns Created resource.
899
+ * Accessor for resource handler.
900
+ * @returns Resource handler instance.
979
901
  */
980
- public static resourceObject(project: Project, name: ResourceName) {
981
- if (name.type === 'calculations') {
982
- return new CalculationResource(project, name);
983
- } else if (name.type === 'cardTypes') {
984
- return new CardTypeResource(project, name);
985
- } else if (name.type === 'fieldTypes') {
986
- return new FieldTypeResource(project, name);
987
- } else if (name.type === 'graphModels') {
988
- return new GraphModelResource(project, name);
989
- } else if (name.type === 'graphViews') {
990
- return new GraphViewResource(project, name);
991
- } else if (name.type === 'linkTypes') {
992
- return new LinkTypeResource(project, name);
993
- } else if (name.type === 'reports') {
994
- return new ReportResource(project, name);
995
- } else if (name.type === 'templates') {
996
- return new TemplateResource(project, name);
997
- } else if (name.type === 'workflows') {
998
- return new WorkflowResource(project, name);
999
- }
1000
- throw new Error(
1001
- `Unsupported resource type '${resourceNameToString(name)}'`,
1002
- );
902
+ public get resources(): ResourceHandler {
903
+ return this.resourceHandler;
1003
904
  }
1004
905
 
1005
906
  /**
@@ -1012,7 +913,7 @@ export class Project extends CardContainer {
1012
913
  path: this.basePath,
1013
914
  prefix: this.projectPrefix,
1014
915
  hubs: this.configuration.hubs,
1015
- modules: (await this.modules()).map((item) => item.name),
916
+ modules: this.resources.moduleNames(),
1016
917
  numberOfCards: (await this.listCards(CardLocation.projectOnly))[0].cards
1017
918
  .length,
1018
919
  };
@@ -1022,73 +923,41 @@ export class Project extends CardContainer {
1022
923
  * Show cards of a project.
1023
924
  * @returns an array of all project cards in the project.
1024
925
  */
1025
- public async showProjectCards(): Promise<Card[]> {
926
+ public showProjectCards(): Card[] {
1026
927
  return this.showCards(this.paths.cardRootFolder);
1027
928
  }
1028
929
 
1029
- /**
1030
- * Returns all template cards from the project. This includes all module templates' cards.
1031
- * @param cardDetails which details to fetch. Optional.
1032
- * @returns all the template cards from the project
1033
- */
1034
- public async allTemplateCards(
1035
- cardDetails?: FetchCardDetails,
1036
- ): Promise<Card[]> {
1037
- const templates = await this.templates();
1038
- const cards: Card[] = [];
1039
- for (const template of templates) {
1040
- const templateCards = await this.templateCards(
1041
- template.name,
1042
- cardDetails,
1043
- );
1044
- if (templateCards) cards.push(...templateCards);
1045
- }
1046
- return cards;
1047
- }
1048
-
1049
930
  /**
1050
931
  * Returns cards from single template.
1051
- * @param templateName Name of the template
1052
- * @param cardDetails Card information
932
+ * @param templateName Name of the template (supports both full names like 'decision/templates/decision' and short names like 'decision')
1053
933
  * @returns List of cards from template.
1054
934
  */
1055
- public async templateCards(
1056
- templateName: string,
1057
- cardDetails?: FetchCardDetails,
1058
- ): Promise<Card[]> {
1059
- const templateObject = new TemplateResource(
1060
- this,
1061
- resourceName(templateName),
1062
- ).templateObject();
1063
- return await templateObject?.cards('', cardDetails);
1064
- }
1065
-
1066
- /**
1067
- * Array of templates in the project.
1068
- * @param from Defines where resources are collected from.
1069
- * @returns array of all templates in the project.
1070
- */
1071
- public async templates(
1072
- from: ResourcesFrom = ResourcesFrom.all,
1073
- ): Promise<Resource[]> {
1074
- return this.resources.resources('templates', from);
935
+ public templateCards(templateName: string): Card[] {
936
+ const templateCards = this.cardCache.getAllTemplateCards();
937
+ return templateCards.filter((cachedCard) => {
938
+ if (cachedCard.location === 'project') {
939
+ return false;
940
+ }
941
+ return cachedCard.location === templateName;
942
+ });
1075
943
  }
1076
944
 
1077
945
  /**
1078
- * Update card content.
1079
- * @param cardKey card's ID that is updated.
946
+ * Update a card's content.
947
+ * @param cardKey card key to update.
1080
948
  * @param content changed content
1081
949
  */
1082
950
  public async updateCardContent(cardKey: string, content: string) {
1083
- const card = await this.findCard(this.basePath, cardKey, {
1084
- metadata: true,
1085
- content: true,
1086
- });
1087
- if (!card) {
1088
- throw new Error(`Card '${cardKey}' does not exist in the project`);
1089
- }
951
+ const card = this.findCard(cardKey);
1090
952
  card.content = content;
953
+
954
+ // Update lastUpdated timestamp in metadata
955
+ if (card.metadata) {
956
+ card.metadata.lastUpdated = new Date().toISOString();
957
+ }
958
+
1091
959
  await this.saveCard(card);
960
+ await this.handleCardChanged(card);
1092
961
  }
1093
962
 
1094
963
  /**
@@ -1102,21 +971,15 @@ export class Project extends CardContainer {
1102
971
  changedKey: string,
1103
972
  newValue: MetadataContent,
1104
973
  ) {
1105
- const templateCard = await this.isTemplateCard(cardKey);
1106
- const card = await this.findCard(
1107
- templateCard ? this.paths.templatesFolder : this.paths.cardRootFolder,
1108
- cardKey,
1109
- {
1110
- metadata: true,
1111
- },
1112
- );
1113
- if (!card) {
1114
- throw new Error(`Card '${cardKey}' does not exist in the project`);
1115
- }
1116
-
974
+ const card = this.findCard(cardKey);
1117
975
  if (!card.metadata || card.metadata[changedKey] === newValue) {
1118
976
  return;
1119
977
  }
978
+
979
+ const isRankChange = changedKey === 'rank';
980
+ const previousPath = isRankChange ? card.path : undefined;
981
+ const previousParent = isRankChange ? card.parent : undefined;
982
+
1120
983
  const cardAsRecord: Record<string, MetadataContent> = card.metadata;
1121
984
  cardAsRecord[changedKey] = newValue;
1122
985
 
@@ -1127,63 +990,58 @@ export class Project extends CardContainer {
1127
990
  throw new Error(invalidCard);
1128
991
  }
1129
992
 
1130
- await this.saveCardMetadata(card);
1131
- }
993
+ const updated = await this.saveCardMetadata(card);
994
+ if (!updated) return;
1132
995
 
1133
- /**
1134
- * Updates card metadata.
1135
- * @param card affected card
1136
- * @param changedMetadata changed content for the card
1137
- */
1138
- public async updateCardMetadata(card: Card, changedMetadata: CardMetadata) {
1139
- card.metadata = changedMetadata;
1140
- return this.saveCardMetadata(card);
996
+ // For rank changes, check if path changed (indicating a move)
997
+ if (isRankChange) {
998
+ const updatedCard = this.findCard(cardKey);
999
+ if (updatedCard.path !== previousPath) {
1000
+ this.changeParent(updatedCard, previousParent);
1001
+ }
1002
+ }
1141
1003
  }
1142
1004
 
1143
1005
  /**
1144
- * Validates that card's data is valid.
1145
- * @param card Card to validate.
1146
- * @returns validation errors, if any
1006
+ * Updates the entire card in the card cache and handles any path/parent changes.
1007
+ * Also persists changes to content and metadata files.
1008
+ * @param card The card with updated information (path, parent, metadata, etc.)
1147
1009
  */
1148
- public async validateCard(card: Card): Promise<string> {
1149
- const invalidCustomData = await this.validator.validateCustomFields(
1150
- this,
1151
- card,
1152
- );
1153
- const invalidWorkFlow = await this.validator.validateWorkflowState(
1154
- this,
1155
- card,
1156
- );
1010
+ public async updateCard(card: Card) {
1011
+ const cachedCard = this.cardCache.getCard(card.key);
1012
+ const pathChange = cachedCard && cachedCard.path !== card.path;
1157
1013
 
1158
- const invalidLabels = await this.validator.validateCardLabels(card);
1159
- if (
1160
- invalidCustomData.length === 0 &&
1161
- invalidWorkFlow.length === 0 &&
1162
- invalidLabels.length === 0
1163
- ) {
1164
- return '';
1014
+ if (pathChange) {
1015
+ this.changeParent(card, cachedCard.parent);
1165
1016
  }
1166
- const errors: string[] = [];
1167
- if (invalidCustomData.length > 0) {
1168
- errors.push(invalidCustomData);
1017
+
1018
+ const metadataChanged =
1019
+ cachedCard &&
1020
+ JSON.stringify(cachedCard.metadata) !== JSON.stringify(card.metadata);
1021
+ if (metadataChanged) {
1022
+ await this.saveCardMetadata(card);
1169
1023
  }
1170
- if (invalidWorkFlow.length > 0) {
1171
- errors.push(invalidWorkFlow);
1024
+
1025
+ const contentChanged = cachedCard && cachedCard.content !== card.content;
1026
+ if (contentChanged) {
1027
+ await this.saveCardContent(card);
1172
1028
  }
1173
- if (invalidLabels.length > 0) {
1174
- errors.push(invalidLabels);
1029
+
1030
+ this.cardCache.updateCard(card.key, card);
1031
+ if (metadataChanged || contentChanged || pathChange) {
1032
+ await this.handleCardChanged(card);
1175
1033
  }
1176
- return errors.join('\n');
1177
1034
  }
1178
1035
 
1179
1036
  /**
1180
- * Array of workflows in the project.
1181
- * @param from Defines where resources are collected from.
1182
- * @returns array of all workflows in the project.
1037
+ * Updates a card's metadata.
1038
+ * @param card affected card
1039
+ * @param changedMetadata changed content for the card
1183
1040
  */
1184
- public async workflows(
1185
- from: ResourcesFrom = ResourcesFrom.all,
1186
- ): Promise<Resource[]> {
1187
- return this.resources.resources('workflows', from);
1041
+ public async updateCardMetadata(card: Card, changedMetadata: CardMetadata) {
1042
+ card.metadata = changedMetadata;
1043
+ if (await this.saveCardMetadata(card)) {
1044
+ await this.handleCardChanged(card);
1045
+ }
1188
1046
  }
1189
1047
  }