@cyberismo/data-handler 0.0.14 → 0.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (240) hide show
  1. package/dist/card-metadata-updater.js +1 -3
  2. package/dist/card-metadata-updater.js.map +1 -1
  3. package/dist/command-handler.js +10 -16
  4. package/dist/command-handler.js.map +1 -1
  5. package/dist/command-manager.d.ts +1 -1
  6. package/dist/command-manager.js +4 -3
  7. package/dist/command-manager.js.map +1 -1
  8. package/dist/commands/create.js +13 -59
  9. package/dist/commands/create.js.map +1 -1
  10. package/dist/commands/edit.d.ts +1 -15
  11. package/dist/commands/edit.js +15 -89
  12. package/dist/commands/edit.js.map +1 -1
  13. package/dist/commands/export.js +4 -17
  14. package/dist/commands/export.js.map +1 -1
  15. package/dist/commands/import.js +3 -5
  16. package/dist/commands/import.js.map +1 -1
  17. package/dist/commands/move.d.ts +1 -2
  18. package/dist/commands/move.js +108 -146
  19. package/dist/commands/move.js.map +1 -1
  20. package/dist/commands/remove.js +9 -44
  21. package/dist/commands/remove.js.map +1 -1
  22. package/dist/commands/rename.js +2 -7
  23. package/dist/commands/rename.js.map +1 -1
  24. package/dist/commands/show.d.ts +7 -25
  25. package/dist/commands/show.js +38 -102
  26. package/dist/commands/show.js.map +1 -1
  27. package/dist/commands/transition.js +27 -30
  28. package/dist/commands/transition.js.map +1 -1
  29. package/dist/commands/update.d.ts +5 -3
  30. package/dist/commands/update.js +19 -5
  31. package/dist/commands/update.js.map +1 -1
  32. package/dist/commands/validate.d.ts +3 -3
  33. package/dist/commands/validate.js +19 -26
  34. package/dist/commands/validate.js.map +1 -1
  35. package/dist/containers/card-container.d.ts +87 -24
  36. package/dist/containers/card-container.js +183 -279
  37. package/dist/containers/card-container.js.map +1 -1
  38. package/dist/containers/project/calculation-engine.d.ts +6 -0
  39. package/dist/containers/project/calculation-engine.js +19 -12
  40. package/dist/containers/project/calculation-engine.js.map +1 -1
  41. package/dist/containers/project/card-cache.d.ts +146 -0
  42. package/dist/containers/project/card-cache.js +411 -0
  43. package/dist/containers/project/card-cache.js.map +1 -0
  44. package/dist/containers/project/resource-collector.d.ts +24 -1
  45. package/dist/containers/project/resource-collector.js +8 -1
  46. package/dist/containers/project/resource-collector.js.map +1 -1
  47. package/dist/containers/project.d.ts +117 -83
  48. package/dist/containers/project.js +418 -252
  49. package/dist/containers/project.js.map +1 -1
  50. package/dist/containers/template.d.ts +15 -31
  51. package/dist/containers/template.js +97 -104
  52. package/dist/containers/template.js.map +1 -1
  53. package/dist/index.d.ts +1 -0
  54. package/dist/index.js +1 -0
  55. package/dist/index.js.map +1 -1
  56. package/dist/interfaces/folder-content-interfaces.d.ts +2 -1
  57. package/dist/interfaces/folder-content-interfaces.js.map +1 -1
  58. package/dist/interfaces/macros.d.ts +1 -0
  59. package/dist/interfaces/macros.js +1 -1
  60. package/dist/interfaces/macros.js.map +1 -1
  61. package/dist/interfaces/project-interfaces.d.ts +5 -1
  62. package/dist/interfaces/project-interfaces.js.map +1 -1
  63. package/dist/interfaces/resource-interfaces.d.ts +11 -21
  64. package/dist/interfaces/resource-interfaces.js +3 -0
  65. package/dist/interfaces/resource-interfaces.js.map +1 -1
  66. package/dist/macros/common.d.ts +10 -10
  67. package/dist/macros/createCards/index.d.ts +0 -13
  68. package/dist/macros/createCards/index.js.map +1 -1
  69. package/dist/macros/createCards/types.d.ts +44 -0
  70. package/dist/macros/createCards/types.js +15 -0
  71. package/dist/macros/createCards/types.js.map +1 -0
  72. package/dist/macros/graph/index.d.ts +2 -6
  73. package/dist/macros/graph/index.js +2 -2
  74. package/dist/macros/graph/index.js.map +1 -1
  75. package/dist/macros/graph/types.d.ts +23 -0
  76. package/dist/macros/graph/types.js +15 -0
  77. package/dist/macros/graph/types.js.map +1 -0
  78. package/dist/macros/image/index.d.ts +8 -16
  79. package/dist/macros/image/index.js +36 -33
  80. package/dist/macros/image/index.js.map +1 -1
  81. package/dist/macros/image/types.d.ts +38 -0
  82. package/dist/macros/image/types.js +15 -0
  83. package/dist/macros/image/types.js.map +1 -0
  84. package/dist/macros/include/index.d.ts +1 -6
  85. package/dist/macros/include/index.js +4 -7
  86. package/dist/macros/include/index.js.map +1 -1
  87. package/dist/macros/include/types.d.ts +31 -0
  88. package/dist/macros/include/types.js +15 -0
  89. package/dist/macros/include/types.js.map +1 -0
  90. package/dist/macros/percentage/index.d.ts +0 -6
  91. package/dist/macros/percentage/index.js.map +1 -1
  92. package/dist/macros/percentage/types.d.ts +31 -0
  93. package/dist/macros/percentage/types.js +15 -0
  94. package/dist/macros/percentage/types.js.map +1 -0
  95. package/dist/macros/report/index.d.ts +0 -3
  96. package/dist/macros/report/index.js.map +1 -1
  97. package/dist/macros/report/types.d.ts +19 -0
  98. package/dist/macros/report/types.js +15 -0
  99. package/dist/macros/report/types.js.map +1 -0
  100. package/dist/macros/scoreCard/index.d.ts +0 -6
  101. package/dist/macros/scoreCard/index.js.map +1 -1
  102. package/dist/macros/scoreCard/types.d.ts +31 -0
  103. package/dist/macros/scoreCard/types.js +15 -0
  104. package/dist/macros/scoreCard/types.js.map +1 -0
  105. package/dist/macros/types.d.ts +25 -0
  106. package/dist/macros/types.js +2 -0
  107. package/dist/macros/types.js.map +1 -0
  108. package/dist/macros/vega/index.d.ts +0 -4
  109. package/dist/macros/vega/index.js.map +1 -1
  110. package/dist/macros/vega/types.d.ts +20 -0
  111. package/dist/macros/vega/types.js +2 -0
  112. package/dist/macros/vega/types.js.map +1 -0
  113. package/dist/macros/vegalite/index.d.ts +0 -4
  114. package/dist/macros/vegalite/index.js.map +1 -1
  115. package/dist/macros/vegalite/types.d.ts +20 -0
  116. package/dist/macros/vegalite/types.js +15 -0
  117. package/dist/macros/vegalite/types.js.map +1 -0
  118. package/dist/macros/xref/index.d.ts +0 -3
  119. package/dist/macros/xref/index.js +5 -14
  120. package/dist/macros/xref/index.js.map +1 -1
  121. package/dist/macros/xref/types.d.ts +19 -0
  122. package/dist/macros/xref/types.js +15 -0
  123. package/dist/macros/xref/types.js.map +1 -0
  124. package/dist/module-manager.js +4 -4
  125. package/dist/module-manager.js.map +1 -1
  126. package/dist/project-settings.js.map +1 -1
  127. package/dist/resources/calculation-resource.d.ts +4 -32
  128. package/dist/resources/calculation-resource.js +0 -55
  129. package/dist/resources/calculation-resource.js.map +1 -1
  130. package/dist/resources/card-type-resource.d.ts +4 -21
  131. package/dist/resources/card-type-resource.js +13 -44
  132. package/dist/resources/card-type-resource.js.map +1 -1
  133. package/dist/resources/field-type-resource.d.ts +4 -21
  134. package/dist/resources/field-type-resource.js +14 -38
  135. package/dist/resources/field-type-resource.js.map +1 -1
  136. package/dist/resources/file-resource.d.ts +12 -29
  137. package/dist/resources/file-resource.js +19 -293
  138. package/dist/resources/file-resource.js.map +1 -1
  139. package/dist/resources/folder-resource.d.ts +31 -50
  140. package/dist/resources/folder-resource.js +68 -96
  141. package/dist/resources/folder-resource.js.map +1 -1
  142. package/dist/resources/graph-model-resource.d.ts +5 -33
  143. package/dist/resources/graph-model-resource.js +8 -61
  144. package/dist/resources/graph-model-resource.js.map +1 -1
  145. package/dist/resources/graph-view-resource.d.ts +5 -28
  146. package/dist/resources/graph-view-resource.js +6 -45
  147. package/dist/resources/graph-view-resource.js.map +1 -1
  148. package/dist/resources/link-type-resource.d.ts +4 -21
  149. package/dist/resources/link-type-resource.js +6 -31
  150. package/dist/resources/link-type-resource.js.map +1 -1
  151. package/dist/resources/report-resource.d.ts +5 -17
  152. package/dist/resources/report-resource.js +6 -44
  153. package/dist/resources/report-resource.js.map +1 -1
  154. package/dist/resources/resource-object.d.ts +58 -23
  155. package/dist/resources/resource-object.js +293 -24
  156. package/dist/resources/resource-object.js.map +1 -1
  157. package/dist/resources/template-resource.d.ts +4 -15
  158. package/dist/resources/template-resource.js +10 -25
  159. package/dist/resources/template-resource.js.map +1 -1
  160. package/dist/resources/workflow-resource.d.ts +4 -23
  161. package/dist/resources/workflow-resource.js +12 -38
  162. package/dist/resources/workflow-resource.js.map +1 -1
  163. package/dist/utils/card-utils.d.ts +69 -19
  164. package/dist/utils/card-utils.js +179 -30
  165. package/dist/utils/card-utils.js.map +1 -1
  166. package/dist/utils/clingo-facts.js +11 -3
  167. package/dist/utils/clingo-facts.js.map +1 -1
  168. package/dist/utils/clingo-parser.js +1 -1
  169. package/dist/utils/clingo-parser.js.map +1 -1
  170. package/dist/utils/constants.d.ts +2 -0
  171. package/dist/utils/constants.js +4 -0
  172. package/dist/utils/constants.js.map +1 -1
  173. package/dist/utils/csv.js +1 -1
  174. package/dist/utils/csv.js.map +1 -1
  175. package/package.json +5 -5
  176. package/src/card-metadata-updater.ts +3 -5
  177. package/src/command-handler.ts +11 -18
  178. package/src/command-manager.ts +4 -3
  179. package/src/commands/create.ts +17 -83
  180. package/src/commands/edit.ts +16 -132
  181. package/src/commands/export.ts +8 -29
  182. package/src/commands/import.ts +4 -6
  183. package/src/commands/move.ts +144 -179
  184. package/src/commands/remove.ts +9 -52
  185. package/src/commands/rename.ts +2 -7
  186. package/src/commands/show.ts +50 -143
  187. package/src/commands/transition.ts +30 -33
  188. package/src/commands/update.ts +27 -9
  189. package/src/commands/validate.ts +21 -36
  190. package/src/containers/card-container.ts +200 -360
  191. package/src/containers/project/calculation-engine.ts +21 -13
  192. package/src/containers/project/card-cache.ts +497 -0
  193. package/src/containers/project/resource-collector.ts +9 -1
  194. package/src/containers/project.ts +529 -327
  195. package/src/containers/template.ts +109 -127
  196. package/src/index.ts +1 -0
  197. package/src/interfaces/folder-content-interfaces.ts +7 -1
  198. package/src/interfaces/macros.ts +2 -0
  199. package/src/interfaces/project-interfaces.ts +7 -1
  200. package/src/interfaces/resource-interfaces.ts +12 -24
  201. package/src/macros/createCards/index.ts +1 -12
  202. package/src/macros/createCards/types.ts +46 -0
  203. package/src/macros/graph/index.ts +3 -7
  204. package/src/macros/graph/types.ts +24 -0
  205. package/src/macros/image/index.ts +50 -61
  206. package/src/macros/image/types.ts +39 -0
  207. package/src/macros/include/index.ts +6 -15
  208. package/src/macros/include/types.ts +32 -0
  209. package/src/macros/percentage/index.ts +1 -7
  210. package/src/macros/percentage/types.ts +32 -0
  211. package/src/macros/report/index.ts +1 -4
  212. package/src/macros/report/types.ts +20 -0
  213. package/src/macros/scoreCard/index.ts +1 -7
  214. package/src/macros/scoreCard/types.ts +32 -0
  215. package/src/macros/types.ts +48 -0
  216. package/src/macros/vega/index.ts +1 -4
  217. package/src/macros/vega/types.ts +21 -0
  218. package/src/macros/vegalite/index.ts +1 -4
  219. package/src/macros/vegalite/types.ts +22 -0
  220. package/src/macros/xref/index.ts +6 -20
  221. package/src/macros/xref/types.ts +20 -0
  222. package/src/module-manager.ts +5 -5
  223. package/src/project-settings.ts +1 -1
  224. package/src/resources/calculation-resource.ts +6 -76
  225. package/src/resources/card-type-resource.ts +24 -59
  226. package/src/resources/field-type-resource.ts +22 -51
  227. package/src/resources/file-resource.ts +27 -409
  228. package/src/resources/folder-resource.ts +98 -124
  229. package/src/resources/graph-model-resource.ts +17 -74
  230. package/src/resources/graph-view-resource.ts +14 -54
  231. package/src/resources/link-type-resource.ts +13 -40
  232. package/src/resources/report-resource.ts +17 -57
  233. package/src/resources/resource-object.ts +435 -32
  234. package/src/resources/template-resource.ts +16 -29
  235. package/src/resources/workflow-resource.ts +26 -50
  236. package/src/utils/card-utils.ts +217 -31
  237. package/src/utils/clingo-facts.ts +13 -3
  238. package/src/utils/clingo-parser.ts +1 -1
  239. package/src/utils/constants.ts +6 -0
  240. package/src/utils/csv.ts +1 -1
@@ -13,21 +13,38 @@
13
13
  /* eslint-disable @typescript-eslint/no-unused-vars */
14
14
 
15
15
  // node
16
- import { readFile, writeFile } from 'node:fs/promises';
17
- import { basename, join } from 'node:path';
16
+ import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
17
+ import { basename, join, sep } from 'node:path';
18
18
 
19
19
  import { hasCode } from '../utils/error-utils.js';
20
20
 
21
21
  import { ArrayHandler } from './array-handler.js';
22
22
  import type {
23
23
  Card,
24
+ Resource,
24
25
  ResourceFolderType,
25
26
  } from '../interfaces/project-interfaces.js';
26
27
  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';
28
+ import type { Project } from '../containers/project.js';
29
+ import { ResourcesFrom } from '../containers/project/resource-collector.js';
30
+ import type {
31
+ ResourceBaseMetadata,
32
+ UpdateKey,
33
+ } from '../interfaces/resource-interfaces.js';
34
+ import type { Validate } from '../commands/validate.js';
35
+ import {
36
+ resourceName,
37
+ resourceNameToPath,
38
+ resourceNameToString,
39
+ type ResourceName,
40
+ } from '../utils/resource-utils.js';
30
41
  import { getChildLogger } from '../utils/log-utils.js';
42
+ import { deleteFile, pathExists } from '../utils/file-utils.js';
43
+ import {
44
+ readJsonFile,
45
+ readJsonFileSync,
46
+ writeJsonFile,
47
+ } from '../utils/json.js';
31
48
 
32
49
  // Possible operations to perform when doing "update"
33
50
  export type UpdateOperations = 'add' | 'change' | 'rank' | 'remove';
@@ -80,18 +97,29 @@ export type OperationMap<T> = {
80
97
  // Given an operation name, get the corresponding operation type
81
98
  export type OperationFor<T, N extends UpdateOperations> = OperationMap<T>[N];
82
99
 
100
+ // T, but U is in the content field
101
+ export type ShowReturnType<T extends ResourceBaseMetadata, U = never> = Omit<
102
+ T,
103
+ 'content'
104
+ > & {
105
+ [K in 'content' as [U] extends [never] ? never : K]: U;
106
+ };
107
+
83
108
  /**
84
109
  * Abstract class for resources.
85
110
  */
86
- export abstract class AbstractResource {
111
+ export abstract class AbstractResource<
112
+ T extends ResourceBaseMetadata,
113
+ U = never, // determines type returned by show()
114
+ > {
87
115
  protected abstract calculate(): Promise<void>; // update resource specific calculations
88
- protected abstract create(content?: ResourceContent): Promise<void>; // create a new with the content (memory)
116
+ protected abstract create(content?: T): Promise<void>; // create a new with the content (memory)
89
117
  protected abstract delete(): Promise<void>; // delete from disk
90
118
  protected abstract read(): Promise<void>; // read content from disk (replaces existing content, if any)
91
119
  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,
120
+ protected abstract show(): Promise<ShowReturnType<T, U>>; // return the content as JSON
121
+ protected abstract update<Type, K extends string>(
122
+ updateKey: UpdateKey<K>,
95
123
  operation: Operation<Type>,
96
124
  ): Promise<void>; // change one key of resource
97
125
  protected abstract usage(cards?: Card[]): Promise<string[]>; // list of card keys or resource names where this resource is used in
@@ -102,47 +130,125 @@ export abstract class AbstractResource {
102
130
  protected abstract getLogger(loggerName: string): Logger;
103
131
  }
104
132
 
105
- /**
106
- * Base class for all resources.
107
- */
108
- export class ResourceObject extends AbstractResource {
133
+ type ValidateInstance = InstanceType<typeof Validate>;
134
+
135
+ export abstract class ResourceObject<
136
+ T extends ResourceBaseMetadata,
137
+ U,
138
+ > extends AbstractResource<T, U> {
139
+ // TODO: Remove when INTDEV-1048 is implemented, since caching is done at object level
140
+ private cache: Map<string, JSON>;
141
+ private static validateInstancePromise?: Promise<ValidateInstance>;
142
+
143
+ protected content: T;
109
144
  protected moduleResource: boolean;
110
145
  protected contentSchema: JSON = {} as JSON;
111
146
  protected contentSchemaId: string = '';
112
147
  protected type: ResourceFolderType = '' as ResourceFolderType;
113
148
  protected resourceFolder: string = '';
149
+ protected logger: Logger;
150
+
151
+ /**
152
+ * Path to the resource metadata file (the .json file).
153
+ */
154
+ public fileName: string = '';
114
155
 
115
156
  constructor(
116
157
  protected project: Project,
117
158
  protected resourceName: ResourceName,
159
+ type: ResourceFolderType,
118
160
  ) {
119
161
  super();
120
162
  this.moduleResource =
121
163
  this.resourceName.prefix !== this.project.projectPrefix;
164
+ this.cache = this.project.resourceCache;
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
+ private static async getValidate(): Promise<ValidateInstance> {
171
+ // a bit hacky solution to avoid circular dependencies
172
+ if (!this.validateInstancePromise) {
173
+ this.validateInstancePromise = import('../commands/validate.js').then(
174
+ ({ Validate }) => Validate.getInstance(),
175
+ );
176
+ }
177
+ return this.validateInstancePromise;
131
178
  }
132
- protected get getType(): string {
179
+
180
+ private resourceObjectToResource(): Resource {
181
+ return {
182
+ name: this.data ? this.data.name : '',
183
+ path: this.fileName.substring(0, this.fileName.lastIndexOf(sep)),
184
+ };
185
+ }
186
+
187
+ // Type of resource.
188
+ private resourceType(): ResourceFolderType {
133
189
  return this.type;
134
190
  }
135
- protected getLogger(loggerName: string): Logger {
136
- return getChildLogger({
137
- module: loggerName,
138
- });
191
+
192
+ private toCache() {
193
+ this.cache.set(
194
+ resourceNameToString(this.resourceName),
195
+ this.content as unknown as JSON,
196
+ );
197
+ }
198
+
199
+ /**
200
+ * Checks if resource exists
201
+ * @throws if resource does not exist
202
+ */
203
+ protected assertResourceExists() {
204
+ if (!pathExists(this.fileName)) {
205
+ const resourceType = `${this.type[0].toUpperCase()}${this.type.slice(1, this.type.length - 1)}`;
206
+ const name = resourceNameToString(this.resourceName);
207
+ throw new Error(
208
+ `${resourceType} '${name}' does not exist in the project`,
209
+ );
210
+ }
211
+ }
212
+
213
+ protected async calculate() {}
214
+
215
+ // Calculations that use this resource.
216
+ protected async calculations(): Promise<string[]> {
217
+ const references: string[] = [];
218
+ const resourceName = resourceNameToString(this.resourceName);
219
+ for (const calculation of await this.project.calculations(
220
+ ResourcesFrom.all,
221
+ )) {
222
+ const fileNameWithExtension = calculation.name.endsWith('.lp')
223
+ ? calculation.name
224
+ : calculation.name + '.lp';
225
+ const filename = join(calculation.path, basename(fileNameWithExtension));
226
+ try {
227
+ const content = await readFile(filename, 'utf-8');
228
+ if (content.includes(resourceName)) {
229
+ references.push(calculation.name);
230
+ }
231
+ } catch (error) {
232
+ // Skip files that don't exist (they may have been renamed or deleted)
233
+ if (hasCode(error) && error.code === 'ENOENT') {
234
+ this.logger.warn(`Skipping non-existent file: ${filename}`);
235
+ continue;
236
+ }
237
+ throw new Error(
238
+ `Failed to process file ${filename}: ${(error as Error).message}`,
239
+ );
240
+ }
241
+ }
242
+ return references;
139
243
  }
140
- protected async update<Type>(_key: string, _op: Operation<Type>) {}
141
- protected async usage(_cards?: Card[]): Promise<string[]> {
142
- return [];
244
+
245
+ // Cards from project.
246
+ protected cards(): Card[] {
247
+ return [
248
+ ...this.project.cards(undefined),
249
+ ...this.project.allTemplateCards(),
250
+ ];
143
251
  }
144
- protected async validate(_content?: object) {}
145
- protected async write() {}
146
252
 
147
253
  /**
148
254
  * Returns .schema content file.
@@ -158,6 +264,59 @@ export class ResourceObject extends AbstractResource {
158
264
  ] as unknown as JSON;
159
265
  }
160
266
 
267
+ // Creates resource.
268
+ protected async create(newContent?: T) {
269
+ if (pathExists(this.fileName)) {
270
+ throw new Error(
271
+ `Resource '${this.resourceName.identifier}' already exists in the project`,
272
+ );
273
+ }
274
+
275
+ if (this.resourceFolder === '') {
276
+ this.resourceName = resourceName(
277
+ `${this.project.projectPrefix}/${this.type}/${this.resourceName.identifier}`,
278
+ );
279
+ this.resourceFolder = this.project.paths.resourcePath(
280
+ this.resourceName.type as ResourceFolderType,
281
+ );
282
+ }
283
+
284
+ const validator = await ResourceObject.getValidate();
285
+ const validName = validator.validResourceName(
286
+ this.resourceType(),
287
+ resourceNameToString(this.resourceName),
288
+ await this.project.projectPrefixes(),
289
+ );
290
+
291
+ let validContent = {} as T;
292
+ if (newContent) {
293
+ validContent = newContent;
294
+ validContent.name = validName;
295
+ } else {
296
+ validContent.description = '';
297
+ validContent.displayName = '';
298
+ }
299
+
300
+ this.content = validContent;
301
+ await this.write();
302
+
303
+ // Notify project & collector
304
+ this.project.addResource(
305
+ this.resourceObjectToResource(),
306
+ this.content as unknown as JSON,
307
+ );
308
+ }
309
+
310
+ protected getLogger(loggerName: string): Logger {
311
+ return getChildLogger({
312
+ module: loggerName,
313
+ });
314
+ }
315
+
316
+ protected get getType(): string {
317
+ return this.type;
318
+ }
319
+
161
320
  /**
162
321
  * Handles operation to an array.
163
322
  * @param operation Operation to perform on array.
@@ -197,7 +356,142 @@ export class ResourceObject extends AbstractResource {
197
356
  ) {
198
357
  throw new Error(`Cannot do operation ${operation.name} on scalar value`);
199
358
  }
200
- return (operation as ChangeOperation<Type>).to;
359
+ return operation.to;
360
+ }
361
+
362
+ // Initialize the resource.
363
+ protected initialize() {
364
+ if (this.resourceName.type === '') {
365
+ this.resourceName.type = this.type;
366
+ }
367
+ if (this.resourceName.prefix === '') {
368
+ this.resourceName.prefix = this.project.projectPrefix;
369
+ }
370
+ if (this.type) {
371
+ this.moduleResource =
372
+ this.resourceName.prefix !== this.project.projectPrefix;
373
+ this.resourceFolder = this.moduleResource
374
+ ? join(
375
+ this.project.paths.modulesFolder,
376
+ this.resourceName.prefix,
377
+ this.resourceName.type,
378
+ )
379
+ : this.project.paths.resourcePath(this.type);
380
+ this.fileName = resourceNameToPath(this.project, this.resourceName);
381
+ }
382
+ // Read from cache, if entry exists...
383
+ if (this.cache.has(resourceNameToString(this.resourceName))) {
384
+ this.content = this.cache.get(
385
+ resourceNameToString(this.resourceName),
386
+ ) as unknown as T;
387
+ return;
388
+ }
389
+ //... otherwise read from disk and add to cache
390
+ try {
391
+ this.content = readJsonFileSync(this.fileName);
392
+ this.toCache();
393
+ } catch {
394
+ // do nothing, it is possible that file has not been created yet.
395
+ this.logger.info(
396
+ `Initializing resource '${resourceNameToString(this.resourceName)}' failed: failed to read file '${this.fileName}'`,
397
+ );
398
+ }
399
+ }
400
+
401
+ // Called after inherited class has finished 'update' operation.
402
+ protected async postUpdate<Type, K extends string>(
403
+ content: T,
404
+ updateKey: UpdateKey<K>,
405
+ op: Operation<Type>,
406
+ ) {
407
+ function toValue(op: Operation<Type>) {
408
+ if (op.name === 'rank') return op.newIndex;
409
+ if (op.name === 'add') return JSON.stringify(op.target);
410
+ if (op.name === 'remove') return JSON.stringify(op.target);
411
+ if (op.name === 'change') return JSON.stringify(op.to);
412
+ }
413
+
414
+ // Check that new name is valid.
415
+ if (op.name === 'change' && updateKey.key === 'name') {
416
+ const newName = resourceName(
417
+ (op as ChangeOperation<string>).to as string,
418
+ );
419
+ content.name = await this.validName(newName);
420
+ }
421
+
422
+ // Once changes have been made; validate the content.
423
+ try {
424
+ await this.validate(content);
425
+ } catch (error) {
426
+ if (error instanceof Error) {
427
+ const errorValue = typeof op === 'object' ? toValue(op) : op;
428
+ throw new Error(
429
+ `Cannot ${op.name} '${updateKey.key}' --> '${errorValue}: ${error.message}'`,
430
+ );
431
+ }
432
+ }
433
+
434
+ this.content = content;
435
+ await this.write();
436
+ }
437
+
438
+ // Update resource; the base class makes some checks only.
439
+ protected async update<Type, K extends string>(
440
+ key: UpdateKey<K>,
441
+ _op: Operation<Type>,
442
+ ): Promise<void> {
443
+ const content = this.data;
444
+ if (!content) {
445
+ throw new Error(
446
+ `Resource '${resourceNameToString(this.resourceName)}' does not exist`,
447
+ );
448
+ }
449
+ if (this.moduleResource) {
450
+ throw new Error(`Cannot update module resources`);
451
+ }
452
+ if (key.key === '' || key === undefined) {
453
+ throw new Error(`Cannot update empty key`);
454
+ }
455
+ }
456
+
457
+ // Reads content from file to memory.
458
+ protected async read() {
459
+ this.content = await readJsonFile(this.fileName);
460
+ }
461
+
462
+ // Renames resource.
463
+ protected async rename(newName: ResourceName) {
464
+ if (this.moduleResource) {
465
+ throw new Error(`Cannot rename module resources`);
466
+ }
467
+ if (!pathExists(this.fileName)) {
468
+ throw new Error(
469
+ `Resource '${this.resourceName.identifier}' does not exist`,
470
+ );
471
+ }
472
+ if (newName.prefix !== this.project.projectPrefix) {
473
+ throw new Error('Can only rename project resources');
474
+ }
475
+ if (newName.type !== this.resourceName.type) {
476
+ throw new Error('Cannot change resource type');
477
+ }
478
+ const validator = await ResourceObject.getValidate();
479
+ validator.validResourceName(
480
+ this.resourceType(),
481
+ resourceNameToString(newName),
482
+ await this.project.projectPrefixes(),
483
+ );
484
+ const newFilename = join(
485
+ this.project.paths.resourcePath(newName.type as ResourceFolderType),
486
+ newName.identifier + '.json',
487
+ );
488
+ await rename(this.fileName, newFilename);
489
+
490
+ this.cache.delete(resourceNameToString(this.resourceName));
491
+ this.fileName = newFilename;
492
+ this.content.name = resourceNameToString(newName);
493
+ this.resourceName = newName;
494
+ this.toCache();
201
495
  }
202
496
 
203
497
  /**
@@ -284,4 +578,113 @@ export class ResourceObject extends AbstractResource {
284
578
  }),
285
579
  );
286
580
  }
581
+
582
+ // Check if there are references to the resource in the card content.
583
+ // @note that this needs to be async, since inherited classes need to async operations
584
+ protected async usage(cards?: Card[]): Promise<string[]> {
585
+ if (!pathExists(this.fileName)) {
586
+ throw new Error(
587
+ `Resource '${this.resourceName.identifier}' does not exist in the project`,
588
+ );
589
+ }
590
+ const cardArray = cards?.length ? cards : this.project.cards(undefined);
591
+
592
+ return cardArray
593
+ .filter((card) =>
594
+ card.content?.includes(resourceNameToString(this.resourceName)),
595
+ )
596
+ .map((card) => card.key);
597
+ }
598
+
599
+ protected async validName(newName: ResourceName) {
600
+ const validator = await ResourceObject.getValidate();
601
+ const validName = validator.validResourceName(
602
+ this.resourceType(),
603
+ resourceNameToString(newName),
604
+ await this.project.projectPrefixes(),
605
+ );
606
+ return validName;
607
+ }
608
+
609
+ // Write the content from memory to disk.
610
+ protected async write() {
611
+ if (this.moduleResource) {
612
+ throw new Error(`Cannot change module resources`);
613
+ }
614
+
615
+ // Create folder for resources and add correct .schema file.
616
+ await mkdir(this.resourceFolder, { recursive: true });
617
+ await writeJsonFile(
618
+ join(this.resourceFolder, '.schema'),
619
+ this.contentSchema,
620
+ {
621
+ flag: 'wx',
622
+ },
623
+ );
624
+ // Check if "name" has changed. Changing "name" means renaming the file.
625
+ const nameInContent = resourceName(this.content.name).identifier + '.json';
626
+ const currentFileName = basename(this.fileName);
627
+
628
+ if (nameInContent !== currentFileName) {
629
+ const newFileName = join(this.resourceFolder, nameInContent);
630
+ await rename(this.fileName, newFileName);
631
+ this.fileName = newFileName;
632
+ }
633
+
634
+ await writeJsonFile(this.fileName, this.content);
635
+ this.toCache();
636
+ }
637
+
638
+ /**
639
+ * Returns memory resident data as JSON.
640
+ * This is basically same as 'show' but doesn't do any checks; just returns the current content.
641
+ * @returns metadata content or undefined if resource does not exist.
642
+ */
643
+ public get data() {
644
+ return this.content.name !== '' ? this.content : undefined;
645
+ }
646
+
647
+ /**
648
+ * Deletes the file and removes the resource from project.
649
+ * @throws if resource is a module resource or does not exist or is used by other resources.
650
+ */
651
+ public async delete() {
652
+ if (this.moduleResource) {
653
+ throw new Error(
654
+ `Cannot delete resource ${resourceNameToString(this.resourceName)}: It is a module resource`,
655
+ );
656
+ }
657
+ if (!this.fileName.endsWith('.json')) {
658
+ this.fileName += '.json';
659
+ }
660
+ if (!pathExists(this.fileName)) {
661
+ throw new Error(
662
+ `Resource '${this.resourceName.identifier}' does not exist in the project`,
663
+ );
664
+ }
665
+ const usedIn = await this.usage();
666
+ if (usedIn.length > 0) {
667
+ throw new Error(
668
+ `Cannot delete resource ${resourceNameToString(this.resourceName)}. It is used by: ${usedIn.join(', ')}`,
669
+ );
670
+ }
671
+ await deleteFile(this.fileName);
672
+ this.project.removeResource(this.resourceObjectToResource());
673
+ this.fileName = '';
674
+ }
675
+
676
+ /**
677
+ * Validates the content of the resource.
678
+ * @param content Content to be validated.
679
+ */
680
+ public async validate(content?: object) {
681
+ const validator = await ResourceObject.getValidate();
682
+ const invalidJson = validator.validateJson(
683
+ content ? content : this.content,
684
+ this.contentSchemaId,
685
+ );
686
+ if (invalidJson.length) {
687
+ throw new Error(`Invalid content JSON: ${invalidJson}`);
688
+ }
689
+ }
287
690
  }
@@ -11,7 +11,7 @@
11
11
  License along with this program. If not, see <https://www.gnu.org/licenses/>.
12
12
  */
13
13
 
14
- import { basename, dirname, join } from 'node:path';
14
+ import { dirname, join } from 'node:path';
15
15
  import { mkdir } from 'node:fs/promises';
16
16
 
17
17
  import type {
@@ -29,6 +29,7 @@ import {
29
29
  import type {
30
30
  TemplateConfiguration,
31
31
  TemplateMetadata,
32
+ UpdateKey,
32
33
  } from '../interfaces/resource-interfaces.js';
33
34
  import { Template } from '../containers/template.js';
34
35
  import { writeJsonFile } from '../utils/json.js';
@@ -36,7 +37,7 @@ import { writeJsonFile } from '../utils/json.js';
36
37
  /**
37
38
  * Template resource class.
38
39
  */
39
- export class TemplateResource extends FolderResource {
40
+ export class TemplateResource extends FolderResource<TemplateMetadata, never> {
40
41
  private cardContainer: Template;
41
42
  private cardsFolder = '';
42
43
  private cardsSchema = super.contentSchemaContent('cardBaseSchema');
@@ -47,12 +48,11 @@ export class TemplateResource extends FolderResource {
47
48
  this.contentSchemaId = 'templateSchema';
48
49
  this.contentSchema = super.contentSchemaContent(this.contentSchemaId);
49
50
 
50
- this.initialize();
51
51
  this.cardsFolder = join(this.internalFolder, 'c');
52
52
 
53
53
  // Each template resource contains a template card container (with template cards).
54
54
  this.cardContainer = new Template(this.project, {
55
- name: basename(this.fileName),
55
+ name: resourceNameToString(this.resourceName),
56
56
  path: dirname(this.fileName),
57
57
  });
58
58
  }
@@ -86,13 +86,6 @@ export class TemplateResource extends FolderResource {
86
86
  return super.create(newContent);
87
87
  }
88
88
 
89
- /**
90
- * Returns content data.
91
- */
92
- public get data(): TemplateMetadata {
93
- return super.data as TemplateMetadata;
94
- }
95
-
96
89
  /**
97
90
  * Deletes file and folder that this resource is based on.
98
91
  */
@@ -115,7 +108,7 @@ export class TemplateResource extends FolderResource {
115
108
  * @returns template metadata.
116
109
  */
117
110
  public async show(): Promise<TemplateConfiguration> {
118
- const templateMetadata = (await super.show()) as TemplateMetadata;
111
+ const templateMetadata = await super.show();
119
112
  const container = this.templateObject();
120
113
 
121
114
  return {
@@ -124,7 +117,7 @@ export class TemplateResource extends FolderResource {
124
117
  displayName: templateMetadata.displayName,
125
118
  description: templateMetadata.description,
126
119
  path: this.fileName,
127
- numberOfCards: (await container.listCards()).length,
120
+ numberOfCards: container.listCards().length,
128
121
  };
129
122
  }
130
123
 
@@ -138,20 +131,24 @@ export class TemplateResource extends FolderResource {
138
131
 
139
132
  /**
140
133
  * Updates template resource.
141
- * @param key Key to modify
134
+ * @param updateKey Key to modify
142
135
  * @param op Operation to perform on 'key'
143
136
  * @throws if key is unknown.
144
137
  */
145
- public async update<Type>(key: string, op: Operation<Type>) {
138
+ public async update<Type, K extends string>(
139
+ updateKey: UpdateKey<K>,
140
+ op: Operation<Type>,
141
+ ) {
142
+ const { key } = updateKey;
146
143
  const nameChange = key === 'name';
147
144
  const existingName = this.content.name;
148
145
 
149
146
  // Only call super.update for keys that base class supports
150
147
  if (key === 'name' || key === 'displayName' || key === 'description') {
151
- await super.update(key, op);
148
+ await super.update(updateKey, op);
152
149
  }
153
150
 
154
- const content = structuredClone(this.content) as TemplateMetadata;
151
+ const content = structuredClone(this.content);
155
152
 
156
153
  if (key === 'name') {
157
154
  content.name = super.handleScalar(op) as string;
@@ -165,7 +162,7 @@ export class TemplateResource extends FolderResource {
165
162
  throw new Error(`Unknown property '${key}' for Template`);
166
163
  }
167
164
 
168
- await super.postUpdate(content, key, op);
165
+ await super.postUpdate(content, updateKey, op);
169
166
 
170
167
  // Renaming this template causes that references to its name must be updated.
171
168
  if (nameChange) {
@@ -181,7 +178,7 @@ export class TemplateResource extends FolderResource {
181
178
  * @returns array of card keys, and calculation filenames that refer this resource.
182
179
  */
183
180
  public async usage(cards?: Card[]): Promise<string[]> {
184
- const allCards = cards ?? (await super.cards());
181
+ const allCards = cards ?? super.cards();
185
182
  const [relevantCards, calculations] = await Promise.all([
186
183
  super.usage(allCards),
187
184
  super.calculations(),
@@ -189,16 +186,6 @@ export class TemplateResource extends FolderResource {
189
186
  return [...relevantCards.sort(sortCards), ...calculations];
190
187
  }
191
188
 
192
- /**
193
- * Validates template.
194
- * @throws when there are validation errors.
195
- * @param content Content to be validated.
196
- * @note If content is not provided, base class validation will use resource's current content.
197
- */
198
- public async validate(content?: object) {
199
- return super.validate(content);
200
- }
201
-
202
189
  /**
203
190
  * Create the template's cards folder.
204
191
  */