@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,25 +13,39 @@
13
13
  /* eslint-disable @typescript-eslint/no-unused-vars */
14
14
 
15
15
  // node
16
- import { readFile, writeFile } from 'node:fs/promises';
16
+ import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
17
17
  import { basename, join } from 'node:path';
18
18
 
19
- import { hasCode } from '../utils/error-utils.js';
20
-
21
19
  import { ArrayHandler } from './array-handler.js';
20
+ import { deleteFile, pathExists } from '../utils/file-utils.js';
21
+ import { getChildLogger } from '../utils/log-utils.js';
22
+ import {
23
+ readJsonFile,
24
+ readJsonFileSync,
25
+ writeJsonFile,
26
+ } from '../utils/json.js';
27
+ import {
28
+ resourceName,
29
+ resourceNameToPath,
30
+ resourceNameToString,
31
+ type ResourceName,
32
+ } from '../utils/resource-utils.js';
33
+ import { ResourcesFrom } from '../containers/project.js';
34
+
22
35
  import type {
23
36
  Card,
24
37
  ResourceFolderType,
25
38
  } from '../interfaces/project-interfaces.js';
26
39
  import type { Logger } from 'pino';
27
- import { type Project, ResourcesFrom } from '../containers/project.js';
28
- import type { ResourceContent } from '../interfaces/resource-interfaces.js';
29
- import type { ResourceName } from '../utils/resource-utils.js';
30
- import { getChildLogger } from '../utils/log-utils.js';
40
+ import type { Project } from '../containers/project.js';
41
+ import type {
42
+ ResourceBaseMetadata,
43
+ UpdateKey,
44
+ } from '../interfaces/resource-interfaces.js';
45
+ import type { Validate } from '../commands/validate.js';
31
46
 
32
47
  // Possible operations to perform when doing "update"
33
48
  export type UpdateOperations = 'add' | 'change' | 'rank' | 'remove';
34
- //| 'elementTypeChange';
35
49
 
36
50
  // Base class for update operations.
37
51
  type BaseOperation<T> = {
@@ -80,18 +94,29 @@ export type OperationMap<T> = {
80
94
  // Given an operation name, get the corresponding operation type
81
95
  export type OperationFor<T, N extends UpdateOperations> = OperationMap<T>[N];
82
96
 
97
+ // T, but U is in the content field
98
+ export type ShowReturnType<T extends ResourceBaseMetadata, U = never> = Omit<
99
+ T,
100
+ 'content'
101
+ > & {
102
+ [K in 'content' as [U] extends [never] ? never : K]: U;
103
+ };
104
+
83
105
  /**
84
106
  * Abstract class for resources.
85
107
  */
86
- export abstract class AbstractResource {
108
+ export abstract class AbstractResource<
109
+ T extends ResourceBaseMetadata,
110
+ U = never, // determines type returned by show()
111
+ > {
87
112
  protected abstract calculate(): Promise<void>; // update resource specific calculations
88
- protected abstract create(content?: ResourceContent): Promise<void>; // create a new with the content (memory)
113
+ protected abstract create(content?: T): Promise<void>; // create a new with the content (memory)
89
114
  protected abstract delete(): Promise<void>; // delete from disk
90
115
  protected abstract read(): Promise<void>; // read content from disk (replaces existing content, if any)
91
116
  protected abstract rename(newName: ResourceName): Promise<void>; // change name of the resource and filename; same as update('name', ...)
92
- protected abstract show(): Promise<ResourceContent>; // return the content as JSON
93
- protected abstract update<Type>(
94
- key: string,
117
+ protected abstract show(): ShowReturnType<T, U>; // return the content as JSON
118
+ protected abstract update<Type, K extends string>(
119
+ updateKey: UpdateKey<K>,
95
120
  operation: Operation<Type>,
96
121
  ): Promise<void>; // change one key of resource
97
122
  protected abstract usage(cards?: Card[]): Promise<string[]>; // list of card keys or resource names where this resource is used in
@@ -102,47 +127,124 @@ export abstract class AbstractResource {
102
127
  protected abstract getLogger(loggerName: string): Logger;
103
128
  }
104
129
 
105
- /**
106
- * Base class for all resources.
107
- */
108
- export class ResourceObject extends AbstractResource {
130
+ type ValidateInstance = InstanceType<typeof Validate>;
131
+
132
+ export abstract class ResourceObject<
133
+ T extends ResourceBaseMetadata,
134
+ U,
135
+ > extends AbstractResource<T, U> {
136
+ private static validateInstancePromise?: Promise<ValidateInstance>;
137
+
138
+ protected content: T;
109
139
  protected moduleResource: boolean;
110
140
  protected contentSchema: JSON = {} as JSON;
111
141
  protected contentSchemaId: string = '';
112
142
  protected type: ResourceFolderType = '' as ResourceFolderType;
113
143
  protected resourceFolder: string = '';
144
+ protected logger: Logger;
114
145
 
146
+ /**
147
+ * Path to the resource metadata file (the .json file).
148
+ */
149
+ public fileName: string = '';
150
+
151
+ /**
152
+ * Constructs a ResourceObject instance
153
+ * @param project Project where this resource is
154
+ * @param resourceName Name for the resource
155
+ * @param type Type of resource
156
+ */
115
157
  constructor(
116
158
  protected project: Project,
117
159
  protected resourceName: ResourceName,
160
+ type: ResourceFolderType,
118
161
  ) {
119
162
  super();
120
163
  this.moduleResource =
121
164
  this.resourceName.prefix !== this.project.projectPrefix;
165
+ this.type = type;
166
+ this.logger = this.getLogger(this.getType);
167
+ this.content = { name: '' } as T; // not found if name is empty
122
168
  }
123
169
 
124
- protected async calculate() {}
125
- protected async create(_content?: ResourceContent) {}
126
- protected async delete() {}
127
- protected async read() {}
128
- protected async rename(_name: ResourceName) {}
129
- protected async show(): Promise<ResourceContent> {
130
- return {} as ResourceContent;
170
+ // Check if resource already exists. First checks the cache, then filesystem.
171
+ private exists(): boolean {
172
+ const name = resourceNameToString(this.resourceName);
173
+ const existsInCache = this.project.resources.exists(name);
174
+ return existsInCache || pathExists(this.fileName);
131
175
  }
132
- protected get getType(): string {
176
+
177
+ // Gets Validate command instance.
178
+ private static async getValidate(): Promise<ValidateInstance> {
179
+ // a bit hacky solution to avoid circular dependencies
180
+ if (!this.validateInstancePromise) {
181
+ this.validateInstancePromise = import('../commands/validate.js').then(
182
+ ({ Validate }) => Validate.getInstance(),
183
+ );
184
+ }
185
+ return this.validateInstancePromise;
186
+ }
187
+
188
+ // Gets handlebar files.
189
+ private async reportHandlerBarFiles(from: ResourcesFrom = ResourcesFrom.all) {
190
+ const reports = this.project.resources.reports(from);
191
+ const handleBarFiles: string[] = [];
192
+ for (const report of reports) {
193
+ handleBarFiles.push(...(await report.handleBarFiles()));
194
+ }
195
+ return handleBarFiles;
196
+ }
197
+
198
+ // Type of resource.
199
+ private resourceType(): ResourceFolderType {
133
200
  return this.type;
134
201
  }
135
- protected getLogger(loggerName: string): Logger {
136
- return getChildLogger({
137
- module: loggerName,
138
- });
202
+
203
+ /**
204
+ * Checks if resource exists.
205
+ * This should only throw, if someone creates resources directly; ie. not through the cache
206
+ * @throws if resource does not exist
207
+ */
208
+ protected assertResourceExists() {
209
+ if (!this.exists()) {
210
+ const resourceType = `${this.type[0].toUpperCase()}${this.type.slice(1, this.type.length - 1)}`;
211
+ const name = resourceNameToString(this.resourceName);
212
+ throw new Error(
213
+ `${resourceType} '${name}' does not exist in the project`,
214
+ );
215
+ }
139
216
  }
140
- protected async update<Type>(_key: string, _op: Operation<Type>) {}
141
- protected async usage(_cards?: Card[]): Promise<string[]> {
142
- return [];
217
+
218
+ /**
219
+ * Calculate; empty implementation.
220
+ */
221
+ protected async calculate() {}
222
+
223
+ /**
224
+ * Calculations that use this resource.
225
+ * @throws if accessing calculations files failed
226
+ */
227
+ protected async calculations(): Promise<string[]> {
228
+ const references: string[] = [];
229
+ const resourceName = resourceNameToString(this.resourceName);
230
+ for (const calculation of this.project.resources.calculations()) {
231
+ const content = calculation.contentData();
232
+ if (content.calculation && content.calculation.includes(resourceName)) {
233
+ references.push(calculation.data!.name);
234
+ }
235
+ }
236
+ return references;
237
+ }
238
+
239
+ /**
240
+ * Cards from project.
241
+ */
242
+ protected cards(): Card[] {
243
+ return [
244
+ ...this.project.cards(undefined),
245
+ ...this.project.allTemplateCards(),
246
+ ];
143
247
  }
144
- protected async validate(_content?: object) {}
145
- protected async write() {}
146
248
 
147
249
  /**
148
250
  * Returns .schema content file.
@@ -158,12 +260,81 @@ export class ResourceObject extends AbstractResource {
158
260
  ] as unknown as JSON;
159
261
  }
160
262
 
263
+ /**
264
+ * Creates resource.
265
+ * @param newContent Content for resource.
266
+ * @throws when resource already exists in the project.
267
+ */
268
+ protected async create(newContent?: T) {
269
+ this.validateResourceIdentifier();
270
+
271
+ if (this.exists()) {
272
+ throw new Error(
273
+ `Resource '${this.resourceName.identifier}' already exists in the project`,
274
+ );
275
+ }
276
+
277
+ if (this.resourceFolder === '') {
278
+ this.resourceName = resourceName(
279
+ `${this.project.projectPrefix}/${this.type}/${this.resourceName.identifier}`,
280
+ );
281
+ this.resourceFolder = this.project.paths.resourcePath(
282
+ this.resourceName.type as ResourceFolderType,
283
+ );
284
+ this.fileName = join(
285
+ this.resourceFolder,
286
+ this.resourceName.identifier + '.json',
287
+ );
288
+ }
289
+
290
+ const validator = await ResourceObject.getValidate();
291
+ const validName = validator.validResourceName(
292
+ this.resourceType(),
293
+ resourceNameToString(this.resourceName),
294
+ this.project.projectPrefixes(),
295
+ );
296
+
297
+ let validContent = {} as T;
298
+ if (newContent) {
299
+ validContent = newContent;
300
+ validContent.name = validName;
301
+ } else {
302
+ validContent.description = '';
303
+ validContent.displayName = '';
304
+ }
305
+
306
+ this.content = validContent;
307
+ await this.write();
308
+
309
+ const resourceString = resourceNameToString(this.resourceName);
310
+ this.project.resources.add(resourceString, this);
311
+ }
312
+
313
+ /**
314
+ * Gets a logger instance.
315
+ * @param loggerName
316
+ * @returns logger instance
317
+ */
318
+ protected getLogger(loggerName: string): Logger {
319
+ return getChildLogger({
320
+ module: loggerName,
321
+ });
322
+ }
323
+
324
+ /**
325
+ * Returns type of this resource.
326
+ */
327
+ protected get getType(): string {
328
+ return this.type;
329
+ }
330
+
161
331
  /**
162
332
  * Handles operation to an array.
163
333
  * @param operation Operation to perform on array.
164
334
  * @param arrayName Name of the array, for error messages.
165
335
  * @param array Array to be updated.
166
336
  * @returns Changed array after the operation.
337
+ * @throws when operation cannot be done.
167
338
  */
168
339
  protected handleArray<Type>(
169
340
  operation: Operation<Type>,
@@ -188,6 +359,7 @@ export class ResourceObject extends AbstractResource {
188
359
  * Updates scalar value. The only accepted operation is 'change'
189
360
  * @param operation Operation to perform on scalar.
190
361
  * @returns What the scalar should be changed to.
362
+ * @throws when operation cannot be done
191
363
  */
192
364
  protected handleScalar<Type>(operation: Operation<Type>): Type {
193
365
  if (
@@ -197,13 +369,185 @@ export class ResourceObject extends AbstractResource {
197
369
  ) {
198
370
  throw new Error(`Cannot do operation ${operation.name} on scalar value`);
199
371
  }
200
- return (operation as ChangeOperation<Type>).to;
372
+ return operation.to;
373
+ }
374
+
375
+ /**
376
+ * Initialize the resource.
377
+ */
378
+ protected initialize() {
379
+ if (this.resourceName.type === '') {
380
+ this.resourceName.type = this.type;
381
+ }
382
+ if (this.resourceName.prefix === '') {
383
+ this.resourceName.prefix = this.project.projectPrefix;
384
+ }
385
+ if (this.type) {
386
+ this.moduleResource =
387
+ this.resourceName.prefix !== this.project.projectPrefix;
388
+ this.resourceFolder = this.moduleResource
389
+ ? join(
390
+ this.project.paths.modulesFolder,
391
+ this.resourceName.prefix,
392
+ this.resourceName.type,
393
+ )
394
+ : this.project.paths.resourcePath(this.type);
395
+ this.fileName = resourceNameToPath(this.project, this.resourceName);
396
+ }
397
+ // Only load content from disk if resource exists in the cache registry
398
+ if (
399
+ this.project.resources.exists(resourceNameToString(this.resourceName))
400
+ ) {
401
+ try {
402
+ this.content = readJsonFileSync(this.fileName);
403
+ } catch {
404
+ this.logger.debug(
405
+ `Initializing resource '${resourceNameToString(this.resourceName)}' failed: failed to read file '${this.fileName}'`,
406
+ );
407
+ }
408
+ }
409
+ }
410
+
411
+ /**
412
+ * Called after inherited class has finished 'update' operation.
413
+ * @param content New content for resource
414
+ * @param updateKey Which property to change
415
+ * @param op What kind of operation is performed to updateKey
416
+ * @throws if validation fails after the update
417
+ */
418
+ protected async postUpdate<Type, K extends string>(
419
+ content: T,
420
+ updateKey: UpdateKey<K>,
421
+ op: Operation<Type>,
422
+ ) {
423
+ function toValue(op: Operation<Type>) {
424
+ if (op.name === 'rank') return op.newIndex;
425
+ if (op.name === 'add') return JSON.stringify(op.target);
426
+ if (op.name === 'remove') return JSON.stringify(op.target);
427
+ if (op.name === 'change') return JSON.stringify(op.to);
428
+ }
429
+
430
+ // Check that new name is valid.
431
+ if (op.name === 'change' && updateKey.key === 'name') {
432
+ const newName = resourceName(
433
+ (op as ChangeOperation<string>).to as string,
434
+ );
435
+ content.name = await this.validName(newName);
436
+ }
437
+
438
+ // Once changes have been made; validate the content.
439
+ try {
440
+ await this.validate(content);
441
+ } catch (error) {
442
+ if (error instanceof Error) {
443
+ const errorValue = typeof op === 'object' ? toValue(op) : op;
444
+ throw new Error(
445
+ `Cannot ${op.name} '${updateKey.key}' --> '${errorValue}: ${error.message}'`,
446
+ );
447
+ }
448
+ }
449
+
450
+ this.content = content;
451
+ await this.write();
452
+ }
453
+
454
+ /**
455
+ * Reads content from file to memory.
456
+ */
457
+ protected async read() {
458
+ this.content = await readJsonFile(this.fileName);
459
+ }
460
+
461
+ /**
462
+ * Renames resource.
463
+ * @param newName New name for the resource.
464
+ * @throws if trying to rename module resource, or
465
+ * if resource does not exist,
466
+ * if trying to rename so that type changes
467
+ */
468
+ protected async rename(newName: ResourceName) {
469
+ if (this.moduleResource) {
470
+ throw new Error(`Cannot rename module resources`);
471
+ }
472
+ if (!this.exists()) {
473
+ throw new Error(
474
+ `Resource '${this.resourceName.identifier}' does not exist`,
475
+ );
476
+ }
477
+ if (newName.prefix !== this.project.projectPrefix) {
478
+ throw new Error('Can only rename project resources');
479
+ }
480
+ if (newName.type !== this.resourceName.type) {
481
+ throw new Error('Cannot change resource type');
482
+ }
483
+ const validator = await ResourceObject.getValidate();
484
+ validator.validResourceName(
485
+ this.resourceType(),
486
+ resourceNameToString(newName),
487
+ this.project.projectPrefixes(),
488
+ );
489
+ const newFilename = join(
490
+ this.project.paths.resourcePath(newName.type as ResourceFolderType),
491
+ newName.identifier + '.json',
492
+ );
493
+
494
+ const oldName = resourceNameToString(this.resourceName);
495
+ await rename(this.fileName, newFilename);
496
+
497
+ this.fileName = newFilename;
498
+ this.content.name = resourceNameToString(newName);
499
+ this.resourceName = newName;
500
+
501
+ this.project.resources.rename(oldName, this.content.name);
502
+ }
503
+
504
+ /**
505
+ * Update resource; the base class makes some checks only.
506
+ * @template type Resource type
507
+ * @template K Resource key
508
+ * @throws if resource does not exist, or
509
+ * if trying to update module content, or
510
+ * if key is empty
511
+ */
512
+ protected async update<Type, K extends string>(
513
+ key: UpdateKey<K>,
514
+ _op: Operation<Type>,
515
+ ): Promise<void> {
516
+ const content = this.data;
517
+ if (!content) {
518
+ throw new Error(
519
+ `Resource '${resourceNameToString(this.resourceName)}' does not exist`,
520
+ );
521
+ }
522
+ if (this.moduleResource) {
523
+ throw new Error(`Cannot update module resources`);
524
+ }
525
+ if (key.key === '' || key === undefined) {
526
+ throw new Error(`Cannot update empty key`);
527
+ }
528
+ }
529
+
530
+ /**
531
+ * Validates resource identifier to prevent filesystem operations with invalid names
532
+ * todo: To Validate?
533
+ */
534
+ protected validateResourceIdentifier() {
535
+ if (!this.moduleResource && this.resourceName.identifier) {
536
+ const identifier = this.resourceName.identifier;
537
+ if (!/^[a-zA-Z0-9._-]+$/.test(identifier)) {
538
+ throw new Error(
539
+ `Resource identifier must follow naming rules. Identifier '${identifier}' is invalid`,
540
+ );
541
+ }
542
+ }
201
543
  }
202
544
 
203
545
  /**
204
546
  * Update calculation files.
205
547
  * @param from Resource name to update
206
548
  * @param to New name for resource
549
+ * @throws if 'from' or 'to' is empty string, or
550
+ * if there was error accessing calculation files.
207
551
  */
208
552
  protected async updateCalculations(from: string, to: string) {
209
553
  if (!from.trim() || !to.trim()) {
@@ -212,41 +556,16 @@ export class ResourceObject extends AbstractResource {
212
556
  );
213
557
  }
214
558
 
215
- const calculations = await this.project.calculations(
559
+ const calculations = this.project.resources.calculations(
216
560
  ResourcesFrom.localOnly,
217
561
  );
218
562
 
219
563
  await Promise.all(
220
564
  calculations.map(async (calculation) => {
221
- if (!calculation.path) {
222
- throw new Error(
223
- `Calculation file's '${calculation.name}' path is not defined`,
224
- );
225
- }
226
-
227
- const base = basename(calculation.name);
228
- const fileNameWithExtension = base.endsWith('.lp')
229
- ? base
230
- : base + '.lp';
231
- const filename = join(calculation.path, fileNameWithExtension);
232
-
233
- try {
234
- const content = await readFile(filename, 'utf-8');
235
- const updatedContent = content.replaceAll(from, to);
236
- await writeFile(filename, updatedContent);
237
- } catch (error) {
238
- if (hasCode(error) && error.code === 'ENOENT') {
239
- // Skip files that don't exist (they may have been renamed or deleted)
240
- this.getLogger(this.getType).warn(
241
- `Skipping non-existent file: ${filename}`,
242
- );
243
- return;
244
- }
245
- if (error instanceof Error) {
246
- throw new Error(
247
- `Failed to process file while updating calculation ${filename}: ${error.message}`,
248
- );
249
- }
565
+ const content = calculation.contentData();
566
+ if (content.calculation) {
567
+ const updatedContent = content.calculation.replaceAll(from, to);
568
+ await calculation.updateFile('calculation.lp', updatedContent);
250
569
  }
251
570
  }),
252
571
  );
@@ -257,6 +576,7 @@ export class ResourceObject extends AbstractResource {
257
576
  * @param from Resource name to update
258
577
  * @param to New name for resource
259
578
  * @param handleBarFiles Optional. List of handlebar files. If omitted, affects all handlebar files in the project.
579
+ * @throws if 'from' or 'to' is empty string
260
580
  */
261
581
  protected async updateHandleBars(
262
582
  from: string,
@@ -270,7 +590,7 @@ export class ResourceObject extends AbstractResource {
270
590
  }
271
591
 
272
592
  if (!handleBarFiles) {
273
- handleBarFiles = await this.project.reportHandlerBarFiles(
593
+ handleBarFiles = await this.reportHandlerBarFiles(
274
594
  ResourcesFrom.localOnly,
275
595
  );
276
596
  }
@@ -284,4 +604,129 @@ export class ResourceObject extends AbstractResource {
284
604
  }),
285
605
  );
286
606
  }
607
+
608
+ /**
609
+ * Check if there are references to the resource in the card content.
610
+ * @note that this needs to be async, since inherited classes need to async operations
611
+ * @param cards cards to check
612
+ * @throws if resource does not exist
613
+ */
614
+ protected async usage(cards?: Card[]): Promise<string[]> {
615
+ if (!this.exists()) {
616
+ throw new Error(
617
+ `Resource '${this.resourceName.identifier}' does not exist in the project`,
618
+ );
619
+ }
620
+ const cardArray = cards?.length ? cards : this.project.cards(undefined);
621
+
622
+ return cardArray
623
+ .filter((card) =>
624
+ card.content?.includes(resourceNameToString(this.resourceName)),
625
+ )
626
+ .map((card) => card.key);
627
+ }
628
+
629
+ /**
630
+ * Checks if resource name is valid.
631
+ * @param newName New name for resource.
632
+ * @returns valid name
633
+ */
634
+ protected async validName(newName: ResourceName) {
635
+ const validator = await ResourceObject.getValidate();
636
+ const validName = validator.validResourceName(
637
+ this.resourceType(),
638
+ resourceNameToString(newName),
639
+ this.project.projectPrefixes(),
640
+ );
641
+ return validName;
642
+ }
643
+
644
+ /**
645
+ * Write the content from memory to disk.
646
+ * @throws if trying to write a module resource.
647
+ */
648
+ protected async write() {
649
+ if (this.moduleResource) {
650
+ throw new Error(`Cannot change module resources`);
651
+ }
652
+
653
+ // Create folder for resources and add correct .schema file.
654
+ await mkdir(this.resourceFolder, { recursive: true });
655
+ await writeJsonFile(
656
+ join(this.resourceFolder, '.schema'),
657
+ this.contentSchema,
658
+ {
659
+ flag: 'wx',
660
+ },
661
+ );
662
+ // Check if "name" has changed. Changing "name" means renaming the file.
663
+ const nameInContent = resourceName(this.content.name).identifier + '.json';
664
+ const currentFileName = basename(this.fileName);
665
+ const resourceString = resourceNameToString(this.resourceName);
666
+
667
+ if (nameInContent !== currentFileName) {
668
+ const newFileName = join(this.resourceFolder, nameInContent);
669
+ await rename(this.fileName, newFileName);
670
+ this.fileName = newFileName;
671
+ this.resourceName = resourceName(this.content.name);
672
+ this.project.resources.rename(resourceString, this.content.name);
673
+ }
674
+ await writeJsonFile(this.fileName, this.content);
675
+ }
676
+
677
+ /**
678
+ * Returns memory resident data as JSON.
679
+ * This is basically same as 'show' but doesn't do any checks; just returns the current content.
680
+ * @returns metadata content or undefined if resource does not exist.
681
+ */
682
+ public get data() {
683
+ return this.content.name !== '' ? this.content : undefined;
684
+ }
685
+
686
+ /**
687
+ * Deletes the file and removes the resource from project.
688
+ * @throws if resource is a module resource, or
689
+ * if resource does not exist, or
690
+ * if resource is used by other resources.
691
+ */
692
+ public async delete() {
693
+ if (this.moduleResource) {
694
+ throw new Error(
695
+ `Cannot delete resource ${resourceNameToString(this.resourceName)}: It is a module resource`,
696
+ );
697
+ }
698
+ if (!this.fileName.endsWith('.json')) {
699
+ this.fileName += '.json';
700
+ }
701
+ if (!this.exists()) {
702
+ throw new Error(
703
+ `Resource '${this.resourceName.identifier}' does not exist in the project`,
704
+ );
705
+ }
706
+ const usedIn = await this.usage();
707
+ if (usedIn.length > 0) {
708
+ throw new Error(
709
+ `Cannot delete resource ${resourceNameToString(this.resourceName)}. It is used by: ${usedIn.join(', ')}`,
710
+ );
711
+ }
712
+ await deleteFile(this.fileName);
713
+ this.project.resources.remove(resourceNameToString(this.resourceName));
714
+ this.fileName = '';
715
+ }
716
+
717
+ /**
718
+ * Validates the content of the resource.
719
+ * @param content Content to be validated.
720
+ * @throws if content is invalid.
721
+ */
722
+ public async validate(content?: object) {
723
+ const validator = await ResourceObject.getValidate();
724
+ const invalidJson = validator.validateJson(
725
+ content ? content : this.content,
726
+ this.contentSchemaId,
727
+ );
728
+ if (invalidJson.length) {
729
+ throw new Error(`Invalid content JSON: ${invalidJson}`);
730
+ }
731
+ }
287
732
  }