@cyberismo/data-handler 0.0.13 → 0.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (262) hide show
  1. package/dist/card-metadata-updater.js +1 -3
  2. package/dist/card-metadata-updater.js.map +1 -1
  3. package/dist/command-handler.js +13 -17
  4. package/dist/command-handler.js.map +1 -1
  5. package/dist/command-manager.d.ts +1 -1
  6. package/dist/command-manager.js +4 -3
  7. package/dist/command-manager.js.map +1 -1
  8. package/dist/commands/create.d.ts +3 -3
  9. package/dist/commands/create.js +20 -81
  10. package/dist/commands/create.js.map +1 -1
  11. package/dist/commands/edit.d.ts +12 -25
  12. package/dist/commands/edit.js +25 -74
  13. package/dist/commands/edit.js.map +1 -1
  14. package/dist/commands/export.js +4 -17
  15. package/dist/commands/export.js.map +1 -1
  16. package/dist/commands/fetch.js +2 -1
  17. package/dist/commands/fetch.js.map +1 -1
  18. package/dist/commands/import.js +3 -5
  19. package/dist/commands/import.js.map +1 -1
  20. package/dist/commands/move.d.ts +1 -2
  21. package/dist/commands/move.js +108 -146
  22. package/dist/commands/move.js.map +1 -1
  23. package/dist/commands/remove.js +15 -49
  24. package/dist/commands/remove.js.map +1 -1
  25. package/dist/commands/rename.d.ts +1 -0
  26. package/dist/commands/rename.js +13 -7
  27. package/dist/commands/rename.js.map +1 -1
  28. package/dist/commands/show.d.ts +7 -25
  29. package/dist/commands/show.js +39 -113
  30. package/dist/commands/show.js.map +1 -1
  31. package/dist/commands/transition.js +27 -30
  32. package/dist/commands/transition.js.map +1 -1
  33. package/dist/commands/update.d.ts +5 -3
  34. package/dist/commands/update.js +19 -5
  35. package/dist/commands/update.js.map +1 -1
  36. package/dist/commands/validate.d.ts +3 -3
  37. package/dist/commands/validate.js +20 -27
  38. package/dist/commands/validate.js.map +1 -1
  39. package/dist/containers/card-container.d.ts +87 -24
  40. package/dist/containers/card-container.js +183 -279
  41. package/dist/containers/card-container.js.map +1 -1
  42. package/dist/containers/project/calculation-engine.d.ts +6 -0
  43. package/dist/containers/project/calculation-engine.js +36 -29
  44. package/dist/containers/project/calculation-engine.js.map +1 -1
  45. package/dist/containers/project/card-cache.d.ts +146 -0
  46. package/dist/containers/project/card-cache.js +411 -0
  47. package/dist/containers/project/card-cache.js.map +1 -0
  48. package/dist/containers/project/resource-collector.d.ts +24 -1
  49. package/dist/containers/project/resource-collector.js +8 -1
  50. package/dist/containers/project/resource-collector.js.map +1 -1
  51. package/dist/containers/project.d.ts +119 -84
  52. package/dist/containers/project.js +423 -253
  53. package/dist/containers/project.js.map +1 -1
  54. package/dist/containers/template.d.ts +15 -31
  55. package/dist/containers/template.js +97 -104
  56. package/dist/containers/template.js.map +1 -1
  57. package/dist/index.d.ts +1 -0
  58. package/dist/index.js +1 -0
  59. package/dist/index.js.map +1 -1
  60. package/dist/interfaces/folder-content-interfaces.d.ts +12 -5
  61. package/dist/interfaces/folder-content-interfaces.js +5 -3
  62. package/dist/interfaces/folder-content-interfaces.js.map +1 -1
  63. package/dist/interfaces/macros.d.ts +1 -0
  64. package/dist/interfaces/macros.js +1 -1
  65. package/dist/interfaces/macros.js.map +1 -1
  66. package/dist/interfaces/project-interfaces.d.ts +16 -10
  67. package/dist/interfaces/project-interfaces.js +10 -8
  68. package/dist/interfaces/project-interfaces.js.map +1 -1
  69. package/dist/interfaces/resource-interfaces.d.ts +21 -22
  70. package/dist/interfaces/resource-interfaces.js +3 -0
  71. package/dist/interfaces/resource-interfaces.js.map +1 -1
  72. package/dist/macros/common.d.ts +10 -10
  73. package/dist/macros/createCards/index.d.ts +0 -13
  74. package/dist/macros/createCards/index.js.map +1 -1
  75. package/dist/macros/createCards/types.d.ts +44 -0
  76. package/dist/macros/createCards/types.js +15 -0
  77. package/dist/macros/createCards/types.js.map +1 -0
  78. package/dist/macros/graph/index.d.ts +2 -6
  79. package/dist/macros/graph/index.js +2 -2
  80. package/dist/macros/graph/index.js.map +1 -1
  81. package/dist/macros/graph/types.d.ts +23 -0
  82. package/dist/macros/graph/types.js +15 -0
  83. package/dist/macros/graph/types.js.map +1 -0
  84. package/dist/macros/image/index.d.ts +8 -16
  85. package/dist/macros/image/index.js +36 -33
  86. package/dist/macros/image/index.js.map +1 -1
  87. package/dist/macros/image/types.d.ts +38 -0
  88. package/dist/macros/image/types.js +15 -0
  89. package/dist/macros/image/types.js.map +1 -0
  90. package/dist/macros/include/index.d.ts +1 -6
  91. package/dist/macros/include/index.js +4 -7
  92. package/dist/macros/include/index.js.map +1 -1
  93. package/dist/macros/include/types.d.ts +31 -0
  94. package/dist/macros/include/types.js +15 -0
  95. package/dist/macros/include/types.js.map +1 -0
  96. package/dist/macros/percentage/index.d.ts +0 -6
  97. package/dist/macros/percentage/index.js.map +1 -1
  98. package/dist/macros/percentage/types.d.ts +31 -0
  99. package/dist/macros/percentage/types.js +15 -0
  100. package/dist/macros/percentage/types.js.map +1 -0
  101. package/dist/macros/report/index.d.ts +0 -3
  102. package/dist/macros/report/index.js.map +1 -1
  103. package/dist/macros/report/types.d.ts +19 -0
  104. package/dist/macros/report/types.js +15 -0
  105. package/dist/macros/report/types.js.map +1 -0
  106. package/dist/macros/scoreCard/index.d.ts +0 -6
  107. package/dist/macros/scoreCard/index.js.map +1 -1
  108. package/dist/macros/scoreCard/types.d.ts +31 -0
  109. package/dist/macros/scoreCard/types.js +15 -0
  110. package/dist/macros/scoreCard/types.js.map +1 -0
  111. package/dist/macros/types.d.ts +25 -0
  112. package/dist/macros/types.js +2 -0
  113. package/dist/macros/types.js.map +1 -0
  114. package/dist/macros/vega/index.d.ts +0 -4
  115. package/dist/macros/vega/index.js.map +1 -1
  116. package/dist/macros/vega/types.d.ts +20 -0
  117. package/dist/macros/vega/types.js +2 -0
  118. package/dist/macros/vega/types.js.map +1 -0
  119. package/dist/macros/vegalite/index.d.ts +0 -4
  120. package/dist/macros/vegalite/index.js.map +1 -1
  121. package/dist/macros/vegalite/types.d.ts +20 -0
  122. package/dist/macros/vegalite/types.js +15 -0
  123. package/dist/macros/vegalite/types.js.map +1 -0
  124. package/dist/macros/xref/index.d.ts +0 -3
  125. package/dist/macros/xref/index.js +5 -14
  126. package/dist/macros/xref/index.js.map +1 -1
  127. package/dist/macros/xref/types.d.ts +19 -0
  128. package/dist/macros/xref/types.js +15 -0
  129. package/dist/macros/xref/types.js.map +1 -0
  130. package/dist/module-manager.js +4 -4
  131. package/dist/module-manager.js.map +1 -1
  132. package/dist/project-settings.js.map +1 -1
  133. package/dist/resources/calculation-resource.d.ts +43 -0
  134. package/dist/resources/calculation-resource.js +75 -0
  135. package/dist/resources/calculation-resource.js.map +1 -0
  136. package/dist/resources/card-type-resource.d.ts +4 -21
  137. package/dist/resources/card-type-resource.js +13 -44
  138. package/dist/resources/card-type-resource.js.map +1 -1
  139. package/dist/resources/create-defaults.d.ts +13 -6
  140. package/dist/resources/create-defaults.js +19 -5
  141. package/dist/resources/create-defaults.js.map +1 -1
  142. package/dist/resources/field-type-resource.d.ts +4 -21
  143. package/dist/resources/field-type-resource.js +14 -38
  144. package/dist/resources/field-type-resource.js.map +1 -1
  145. package/dist/resources/file-resource.d.ts +12 -29
  146. package/dist/resources/file-resource.js +19 -287
  147. package/dist/resources/file-resource.js.map +1 -1
  148. package/dist/resources/folder-resource.d.ts +32 -51
  149. package/dist/resources/folder-resource.js +68 -96
  150. package/dist/resources/folder-resource.js.map +1 -1
  151. package/dist/resources/graph-model-resource.d.ts +5 -33
  152. package/dist/resources/graph-model-resource.js +8 -61
  153. package/dist/resources/graph-model-resource.js.map +1 -1
  154. package/dist/resources/graph-view-resource.d.ts +5 -28
  155. package/dist/resources/graph-view-resource.js +6 -45
  156. package/dist/resources/graph-view-resource.js.map +1 -1
  157. package/dist/resources/link-type-resource.d.ts +4 -21
  158. package/dist/resources/link-type-resource.js +6 -31
  159. package/dist/resources/link-type-resource.js.map +1 -1
  160. package/dist/resources/report-resource.d.ts +5 -17
  161. package/dist/resources/report-resource.js +6 -44
  162. package/dist/resources/report-resource.js.map +1 -1
  163. package/dist/resources/resource-object.d.ts +58 -23
  164. package/dist/resources/resource-object.js +307 -26
  165. package/dist/resources/resource-object.js.map +1 -1
  166. package/dist/resources/template-resource.d.ts +4 -15
  167. package/dist/resources/template-resource.js +10 -25
  168. package/dist/resources/template-resource.js.map +1 -1
  169. package/dist/resources/workflow-resource.d.ts +4 -23
  170. package/dist/resources/workflow-resource.js +12 -38
  171. package/dist/resources/workflow-resource.js.map +1 -1
  172. package/dist/utils/card-utils.d.ts +69 -19
  173. package/dist/utils/card-utils.js +179 -30
  174. package/dist/utils/card-utils.js.map +1 -1
  175. package/dist/utils/clingo-facts.js +11 -3
  176. package/dist/utils/clingo-facts.js.map +1 -1
  177. package/dist/utils/clingo-parser.js +1 -1
  178. package/dist/utils/clingo-parser.js.map +1 -1
  179. package/dist/utils/constants.d.ts +2 -0
  180. package/dist/utils/constants.js +5 -0
  181. package/dist/utils/constants.js.map +1 -1
  182. package/dist/utils/csv.js +1 -1
  183. package/dist/utils/csv.js.map +1 -1
  184. package/dist/utils/error-utils.d.ts +34 -0
  185. package/dist/utils/error-utils.js +56 -0
  186. package/dist/utils/error-utils.js.map +1 -0
  187. package/dist/utils/log-utils.d.ts +0 -27
  188. package/dist/utils/log-utils.js +0 -58
  189. package/dist/utils/log-utils.js.map +1 -1
  190. package/dist/utils/user-preferences.js +6 -3
  191. package/dist/utils/user-preferences.js.map +1 -1
  192. package/package.json +5 -5
  193. package/src/card-metadata-updater.ts +3 -5
  194. package/src/command-handler.ts +14 -19
  195. package/src/command-manager.ts +4 -3
  196. package/src/commands/create.ts +28 -112
  197. package/src/commands/edit.ts +27 -118
  198. package/src/commands/export.ts +8 -29
  199. package/src/commands/fetch.ts +2 -1
  200. package/src/commands/import.ts +4 -6
  201. package/src/commands/move.ts +144 -179
  202. package/src/commands/remove.ts +12 -54
  203. package/src/commands/rename.ts +22 -7
  204. package/src/commands/show.ts +51 -156
  205. package/src/commands/transition.ts +30 -33
  206. package/src/commands/update.ts +27 -9
  207. package/src/commands/validate.ts +22 -37
  208. package/src/containers/card-container.ts +200 -360
  209. package/src/containers/project/calculation-engine.ts +43 -33
  210. package/src/containers/project/card-cache.ts +497 -0
  211. package/src/containers/project/resource-collector.ts +9 -1
  212. package/src/containers/project.ts +533 -328
  213. package/src/containers/template.ts +109 -127
  214. package/src/index.ts +1 -0
  215. package/src/interfaces/folder-content-interfaces.ts +23 -5
  216. package/src/interfaces/macros.ts +2 -0
  217. package/src/interfaces/project-interfaces.ts +19 -10
  218. package/src/interfaces/resource-interfaces.ts +22 -24
  219. package/src/macros/createCards/index.ts +1 -12
  220. package/src/macros/createCards/types.ts +46 -0
  221. package/src/macros/graph/index.ts +3 -7
  222. package/src/macros/graph/types.ts +24 -0
  223. package/src/macros/image/index.ts +50 -61
  224. package/src/macros/image/types.ts +39 -0
  225. package/src/macros/include/index.ts +6 -15
  226. package/src/macros/include/types.ts +32 -0
  227. package/src/macros/percentage/index.ts +1 -7
  228. package/src/macros/percentage/types.ts +32 -0
  229. package/src/macros/report/index.ts +1 -4
  230. package/src/macros/report/types.ts +20 -0
  231. package/src/macros/scoreCard/index.ts +1 -7
  232. package/src/macros/scoreCard/types.ts +32 -0
  233. package/src/macros/types.ts +48 -0
  234. package/src/macros/vega/index.ts +1 -4
  235. package/src/macros/vega/types.ts +21 -0
  236. package/src/macros/vegalite/index.ts +1 -4
  237. package/src/macros/vegalite/types.ts +22 -0
  238. package/src/macros/xref/index.ts +6 -20
  239. package/src/macros/xref/types.ts +20 -0
  240. package/src/module-manager.ts +5 -5
  241. package/src/project-settings.ts +1 -1
  242. package/src/resources/calculation-resource.ts +101 -0
  243. package/src/resources/card-type-resource.ts +24 -59
  244. package/src/resources/create-defaults.ts +21 -5
  245. package/src/resources/field-type-resource.ts +22 -51
  246. package/src/resources/file-resource.ts +27 -403
  247. package/src/resources/folder-resource.ts +99 -125
  248. package/src/resources/graph-model-resource.ts +17 -74
  249. package/src/resources/graph-view-resource.ts +14 -54
  250. package/src/resources/link-type-resource.ts +13 -40
  251. package/src/resources/report-resource.ts +17 -57
  252. package/src/resources/resource-object.ts +454 -39
  253. package/src/resources/template-resource.ts +16 -29
  254. package/src/resources/workflow-resource.ts +26 -50
  255. package/src/utils/card-utils.ts +217 -31
  256. package/src/utils/clingo-facts.ts +13 -3
  257. package/src/utils/clingo-parser.ts +1 -1
  258. package/src/utils/constants.ts +7 -0
  259. package/src/utils/csv.ts +1 -1
  260. package/src/utils/error-utils.ts +62 -0
  261. package/src/utils/log-utils.ts +0 -68
  262. package/src/utils/user-preferences.ts +7 -3
@@ -13,19 +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
+
19
+ import { hasCode } from '../utils/error-utils.js';
18
20
 
19
21
  import { ArrayHandler } from './array-handler.js';
20
22
  import type {
21
23
  Card,
24
+ Resource,
22
25
  ResourceFolderType,
23
26
  } from '../interfaces/project-interfaces.js';
24
27
  import type { Logger } from 'pino';
25
- import { type Project, ResourcesFrom } from '../containers/project.js';
26
- import type { ResourceContent } from '../interfaces/resource-interfaces.js';
27
- 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';
28
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';
29
48
 
30
49
  // Possible operations to perform when doing "update"
31
50
  export type UpdateOperations = 'add' | 'change' | 'rank' | 'remove';
@@ -78,18 +97,29 @@ export type OperationMap<T> = {
78
97
  // Given an operation name, get the corresponding operation type
79
98
  export type OperationFor<T, N extends UpdateOperations> = OperationMap<T>[N];
80
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
+
81
108
  /**
82
109
  * Abstract class for resources.
83
110
  */
84
- export abstract class AbstractResource {
111
+ export abstract class AbstractResource<
112
+ T extends ResourceBaseMetadata,
113
+ U = never, // determines type returned by show()
114
+ > {
85
115
  protected abstract calculate(): Promise<void>; // update resource specific calculations
86
- 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)
87
117
  protected abstract delete(): Promise<void>; // delete from disk
88
118
  protected abstract read(): Promise<void>; // read content from disk (replaces existing content, if any)
89
119
  protected abstract rename(newName: ResourceName): Promise<void>; // change name of the resource and filename; same as update('name', ...)
90
- protected abstract show(): Promise<ResourceContent>; // return the content as JSON
91
- protected abstract update<Type>(
92
- 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>,
93
123
  operation: Operation<Type>,
94
124
  ): Promise<void>; // change one key of resource
95
125
  protected abstract usage(cards?: Card[]): Promise<string[]>; // list of card keys or resource names where this resource is used in
@@ -100,47 +130,125 @@ export abstract class AbstractResource {
100
130
  protected abstract getLogger(loggerName: string): Logger;
101
131
  }
102
132
 
103
- /**
104
- * Base class for all resources.
105
- */
106
- 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;
107
144
  protected moduleResource: boolean;
108
145
  protected contentSchema: JSON = {} as JSON;
109
146
  protected contentSchemaId: string = '';
110
147
  protected type: ResourceFolderType = '' as ResourceFolderType;
111
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 = '';
112
155
 
113
156
  constructor(
114
157
  protected project: Project,
115
158
  protected resourceName: ResourceName,
159
+ type: ResourceFolderType,
116
160
  ) {
117
161
  super();
118
162
  this.moduleResource =
119
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
120
168
  }
121
169
 
122
- protected async calculate() {}
123
- protected async create(_content?: ResourceContent) {}
124
- protected async delete() {}
125
- protected async read() {}
126
- protected async rename(_name: ResourceName) {}
127
- protected async show(): Promise<ResourceContent> {
128
- 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;
129
178
  }
130
- 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 {
131
189
  return this.type;
132
190
  }
133
- protected getLogger(loggerName: string): Logger {
134
- return getChildLogger({
135
- module: loggerName,
136
- });
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;
137
243
  }
138
- protected async update<Type>(_key: string, _op: Operation<Type>) {}
139
- protected async usage(_cards?: Card[]): Promise<string[]> {
140
- return [];
244
+
245
+ // Cards from project.
246
+ protected cards(): Card[] {
247
+ return [
248
+ ...this.project.cards(undefined),
249
+ ...this.project.allTemplateCards(),
250
+ ];
141
251
  }
142
- protected async validate(_content?: object) {}
143
- protected async write() {}
144
252
 
145
253
  /**
146
254
  * Returns .schema content file.
@@ -156,6 +264,59 @@ export class ResourceObject extends AbstractResource {
156
264
  ] as unknown as JSON;
157
265
  }
158
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
+
159
320
  /**
160
321
  * Handles operation to an array.
161
322
  * @param operation Operation to perform on array.
@@ -195,7 +356,142 @@ export class ResourceObject extends AbstractResource {
195
356
  ) {
196
357
  throw new Error(`Cannot do operation ${operation.name} on scalar value`);
197
358
  }
198
- 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();
199
495
  }
200
496
 
201
497
  /**
@@ -222,19 +518,29 @@ export class ResourceObject extends AbstractResource {
222
518
  );
223
519
  }
224
520
 
225
- const filename = join(
226
- calculation.path,
227
- basename(calculation.name) + '.lp',
228
- );
521
+ const base = basename(calculation.name);
522
+ const fileNameWithExtension = base.endsWith('.lp')
523
+ ? base
524
+ : base + '.lp';
525
+ const filename = join(calculation.path, fileNameWithExtension);
229
526
 
230
527
  try {
231
528
  const content = await readFile(filename, 'utf-8');
232
529
  const updatedContent = content.replaceAll(from, to);
233
530
  await writeFile(filename, updatedContent);
234
531
  } catch (error) {
235
- throw new Error(
236
- `Failed to process file ${filename}: ${(error as Error).message}`,
237
- );
532
+ if (hasCode(error) && error.code === 'ENOENT') {
533
+ // Skip files that don't exist (they may have been renamed or deleted)
534
+ this.getLogger(this.getType).warn(
535
+ `Skipping non-existent file: ${filename}`,
536
+ );
537
+ return;
538
+ }
539
+ if (error instanceof Error) {
540
+ throw new Error(
541
+ `Failed to process file while updating calculation ${filename}: ${error.message}`,
542
+ );
543
+ }
238
544
  }
239
545
  }),
240
546
  );
@@ -272,4 +578,113 @@ export class ResourceObject extends AbstractResource {
272
578
  }),
273
579
  );
274
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
+ }
275
690
  }