@cyberismo/data-handler 0.0.11 → 0.0.13

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 (179) hide show
  1. package/dist/card-metadata-updater.js +1 -1
  2. package/dist/card-metadata-updater.js.map +1 -1
  3. package/dist/command-handler.d.ts +26 -39
  4. package/dist/command-handler.js +76 -31
  5. package/dist/command-handler.js.map +1 -1
  6. package/dist/command-manager.d.ts +2 -1
  7. package/dist/command-manager.js +4 -2
  8. package/dist/command-manager.js.map +1 -1
  9. package/dist/commands/calculate.d.ts +7 -0
  10. package/dist/commands/calculate.js +9 -0
  11. package/dist/commands/calculate.js.map +1 -1
  12. package/dist/commands/create.d.ts +5 -0
  13. package/dist/commands/create.js +15 -8
  14. package/dist/commands/create.js.map +1 -1
  15. package/dist/commands/fetch.d.ts +24 -0
  16. package/dist/commands/fetch.js +118 -0
  17. package/dist/commands/fetch.js.map +1 -0
  18. package/dist/commands/import.js +2 -2
  19. package/dist/commands/import.js.map +1 -1
  20. package/dist/commands/index.d.ts +2 -1
  21. package/dist/commands/index.js +2 -1
  22. package/dist/commands/index.js.map +1 -1
  23. package/dist/commands/remove.d.ts +1 -0
  24. package/dist/commands/remove.js +16 -12
  25. package/dist/commands/remove.js.map +1 -1
  26. package/dist/commands/rename.js +4 -6
  27. package/dist/commands/rename.js.map +1 -1
  28. package/dist/commands/show.d.ts +22 -1
  29. package/dist/commands/show.js +56 -0
  30. package/dist/commands/show.js.map +1 -1
  31. package/dist/commands/update.d.ts +11 -1
  32. package/dist/commands/update.js +14 -2
  33. package/dist/commands/update.js.map +1 -1
  34. package/dist/commands/validate.d.ts +2 -1
  35. package/dist/commands/validate.js +10 -10
  36. package/dist/commands/validate.js.map +1 -1
  37. package/dist/containers/card-container.js +1 -1
  38. package/dist/containers/card-container.js.map +1 -1
  39. package/dist/containers/project/calculation-engine.d.ts +8 -0
  40. package/dist/containers/project/calculation-engine.js +21 -10
  41. package/dist/containers/project/calculation-engine.js.map +1 -1
  42. package/dist/containers/project.d.ts +19 -8
  43. package/dist/containers/project.js +52 -34
  44. package/dist/containers/project.js.map +1 -1
  45. package/dist/containers/template.js +1 -1
  46. package/dist/containers/template.js.map +1 -1
  47. package/dist/index.d.ts +4 -2
  48. package/dist/index.js.map +1 -1
  49. package/dist/interfaces/command-options.d.ts +81 -0
  50. package/dist/interfaces/command-options.js +14 -0
  51. package/dist/interfaces/command-options.js.map +1 -0
  52. package/dist/interfaces/folder-content-interfaces.d.ts +50 -0
  53. package/dist/interfaces/folder-content-interfaces.js +45 -0
  54. package/dist/interfaces/folder-content-interfaces.js.map +1 -0
  55. package/dist/interfaces/project-interfaces.d.ts +13 -2
  56. package/dist/interfaces/project-interfaces.js.map +1 -1
  57. package/dist/interfaces/resource-interfaces.d.ts +28 -10
  58. package/dist/interfaces/resource-interfaces.js.map +1 -1
  59. package/dist/macros/base-macro.d.ts +1 -1
  60. package/dist/macros/base-macro.js +1 -1
  61. package/dist/macros/base-macro.js.map +1 -1
  62. package/dist/macros/createCards/index.d.ts +1 -1
  63. package/dist/macros/createCards/index.js +1 -1
  64. package/dist/macros/createCards/index.js.map +1 -1
  65. package/dist/macros/graph/index.d.ts +1 -1
  66. package/dist/macros/graph/index.js +21 -29
  67. package/dist/macros/graph/index.js.map +1 -1
  68. package/dist/macros/image/index.d.ts +1 -1
  69. package/dist/macros/image/index.js +1 -7
  70. package/dist/macros/image/index.js.map +1 -1
  71. package/dist/macros/include/index.d.ts +1 -1
  72. package/dist/macros/include/index.js +1 -1
  73. package/dist/macros/include/index.js.map +1 -1
  74. package/dist/macros/index.d.ts +12 -5
  75. package/dist/macros/index.js +19 -7
  76. package/dist/macros/index.js.map +1 -1
  77. package/dist/macros/percentage/index.d.ts +1 -1
  78. package/dist/macros/percentage/index.js +1 -1
  79. package/dist/macros/percentage/index.js.map +1 -1
  80. package/dist/macros/report/index.d.ts +1 -1
  81. package/dist/macros/report/index.js +5 -5
  82. package/dist/macros/report/index.js.map +1 -1
  83. package/dist/macros/scoreCard/index.d.ts +1 -1
  84. package/dist/macros/scoreCard/index.js +1 -1
  85. package/dist/macros/scoreCard/index.js.map +1 -1
  86. package/dist/macros/vega/index.d.ts +1 -1
  87. package/dist/macros/vega/index.js +1 -1
  88. package/dist/macros/vega/index.js.map +1 -1
  89. package/dist/macros/vegalite/index.d.ts +1 -1
  90. package/dist/macros/vegalite/index.js +1 -1
  91. package/dist/macros/vegalite/index.js.map +1 -1
  92. package/dist/macros/xref/index.d.ts +1 -1
  93. package/dist/macros/xref/index.js +1 -1
  94. package/dist/macros/xref/index.js.map +1 -1
  95. package/dist/project-settings.d.ts +14 -1
  96. package/dist/project-settings.js +51 -1
  97. package/dist/project-settings.js.map +1 -1
  98. package/dist/resources/card-type-resource.js +11 -5
  99. package/dist/resources/card-type-resource.js.map +1 -1
  100. package/dist/resources/field-type-resource.d.ts +5 -0
  101. package/dist/resources/field-type-resource.js +9 -4
  102. package/dist/resources/field-type-resource.js.map +1 -1
  103. package/dist/resources/folder-resource.d.ts +37 -9
  104. package/dist/resources/folder-resource.js +108 -12
  105. package/dist/resources/folder-resource.js.map +1 -1
  106. package/dist/resources/graph-model-resource.d.ts +7 -4
  107. package/dist/resources/graph-model-resource.js +12 -25
  108. package/dist/resources/graph-model-resource.js.map +1 -1
  109. package/dist/resources/graph-view-resource.d.ts +7 -4
  110. package/dist/resources/graph-view-resource.js +15 -31
  111. package/dist/resources/graph-view-resource.js.map +1 -1
  112. package/dist/resources/link-type-resource.js +1 -1
  113. package/dist/resources/link-type-resource.js.map +1 -1
  114. package/dist/resources/report-resource.d.ts +14 -10
  115. package/dist/resources/report-resource.js +41 -45
  116. package/dist/resources/report-resource.js.map +1 -1
  117. package/dist/resources/resource-object.d.ts +7 -0
  118. package/dist/resources/resource-object.js.map +1 -1
  119. package/dist/resources/template-resource.d.ts +5 -1
  120. package/dist/resources/template-resource.js +12 -7
  121. package/dist/resources/template-resource.js.map +1 -1
  122. package/dist/resources/workflow-resource.js +12 -5
  123. package/dist/resources/workflow-resource.js.map +1 -1
  124. package/dist/utils/log-utils.js +1 -1
  125. package/dist/utils/log-utils.js.map +1 -1
  126. package/dist/utils/report.js +6 -0
  127. package/dist/utils/report.js.map +1 -1
  128. package/dist/utils/resource-utils.d.ts +8 -0
  129. package/dist/utils/resource-utils.js +11 -0
  130. package/dist/utils/resource-utils.js.map +1 -1
  131. package/package.json +11 -11
  132. package/src/card-metadata-updater.ts +1 -1
  133. package/src/command-handler.ts +129 -61
  134. package/src/command-manager.ts +4 -1
  135. package/src/commands/calculate.ts +18 -0
  136. package/src/commands/create.ts +31 -19
  137. package/src/commands/fetch.ts +152 -0
  138. package/src/commands/import.ts +2 -0
  139. package/src/commands/index.ts +2 -0
  140. package/src/commands/remove.ts +18 -12
  141. package/src/commands/rename.ts +11 -11
  142. package/src/commands/show.ts +72 -0
  143. package/src/commands/update.ts +20 -2
  144. package/src/commands/validate.ts +13 -10
  145. package/src/containers/card-container.ts +1 -1
  146. package/src/containers/project/calculation-engine.ts +27 -11
  147. package/src/containers/project.ts +71 -61
  148. package/src/containers/template.ts +1 -1
  149. package/src/index.ts +36 -2
  150. package/src/interfaces/command-options.ts +144 -0
  151. package/src/interfaces/folder-content-interfaces.ts +69 -0
  152. package/src/interfaces/project-interfaces.ts +18 -0
  153. package/src/interfaces/resource-interfaces.ts +41 -12
  154. package/src/macros/base-macro.ts +5 -2
  155. package/src/macros/createCards/index.ts +1 -1
  156. package/src/macros/graph/index.ts +47 -51
  157. package/src/macros/image/index.ts +1 -7
  158. package/src/macros/include/index.ts +1 -1
  159. package/src/macros/index.ts +19 -7
  160. package/src/macros/percentage/index.ts +1 -1
  161. package/src/macros/report/index.ts +5 -5
  162. package/src/macros/scoreCard/index.ts +1 -1
  163. package/src/macros/vega/index.ts +1 -1
  164. package/src/macros/vegalite/index.ts +1 -1
  165. package/src/macros/xref/index.ts +1 -1
  166. package/src/project-settings.ts +62 -1
  167. package/src/resources/card-type-resource.ts +12 -6
  168. package/src/resources/field-type-resource.ts +9 -4
  169. package/src/resources/folder-resource.ts +149 -19
  170. package/src/resources/graph-model-resource.ts +16 -27
  171. package/src/resources/graph-view-resource.ts +23 -33
  172. package/src/resources/link-type-resource.ts +1 -1
  173. package/src/resources/report-resource.ts +60 -62
  174. package/src/resources/resource-object.ts +11 -0
  175. package/src/resources/template-resource.ts +12 -7
  176. package/src/resources/workflow-resource.ts +11 -6
  177. package/src/utils/log-utils.ts +1 -1
  178. package/src/utils/report.ts +6 -0
  179. package/src/utils/resource-utils.ts +16 -0
@@ -44,6 +44,8 @@ import { ReportResource } from '../resources/report-resource.js';
44
44
  import { TemplateResource } from '../resources/template-resource.js';
45
45
  import { WorkflowResource } from '../resources/workflow-resource.js';
46
46
 
47
+ const MODULES_PATH = `${sep}modules${sep}`;
48
+
47
49
  // todo: Is there a easy to way to make JSON schema into a TypeScript interface/type?
48
50
  // Check this out: https://www.npmjs.com/package/json-schema-to-ts
49
51
 
@@ -65,6 +67,7 @@ export class Create {
65
67
  cardKeyPrefix: '$PROJECT-PREFIX',
66
68
  name: '$PROJECT-NAME',
67
69
  modules: [],
70
+ hubs: [],
68
71
  },
69
72
  name: Project.projectConfigFileName,
70
73
  },
@@ -122,13 +125,13 @@ export class Create {
122
125
  ? await templateObject.findSpecificCard(card)
123
126
  : undefined;
124
127
  if (card && !specificCard) {
125
- throw Error(
128
+ throw new Error(
126
129
  `Card '${card}' was not found from template '${origTemplateName}'`,
127
130
  );
128
131
  }
129
132
 
130
133
  if (templateObject.templateFolder().includes(`${sep}modules${sep}`)) {
131
- throw Error(`Cannot add cards to imported module templates`);
134
+ throw new Error(`Cannot add cards to imported module templates`);
132
135
  }
133
136
 
134
137
  // Collect all add-card promises and settle them in parallel.
@@ -159,6 +162,14 @@ export class Create {
159
162
  }
160
163
  }
161
164
 
165
+ /**
166
+ * Adds a new hub location.
167
+ * @param hubUrl URL of the hub
168
+ */
169
+ public async addHubLocation(hubUrl: string) {
170
+ return this.project.configuration.addHub(hubUrl);
171
+ }
172
+
162
173
  /**
163
174
  * Adds an attachment to a card.
164
175
  * @param cardKey card ID
@@ -176,12 +187,10 @@ export class Create {
176
187
  );
177
188
  }
178
189
  const attachmentFolder = await this.project.cardAttachmentFolder(cardKey);
179
- if (!attachmentFolder) {
180
- throw new Error(`Attachment folder for '${cardKey}' not found`);
181
- }
182
190
 
183
191
  // Imported templates cannot be modified.
184
- if (attachmentFolder.includes(`${sep}modules${sep}`)) {
192
+ // @todo: make MODULES_PATH project level constant
193
+ if (attachmentFolder.includes(MODULES_PATH)) {
185
194
  throw new Error(`Cannot modify imported module`);
186
195
  }
187
196
 
@@ -528,20 +537,23 @@ export class Create {
528
537
  ),
529
538
  );
530
539
  });
531
- Create.JSONFileContent.forEach(async (entry) => {
532
- if ('cardKeyPrefix' in entry.content) {
533
- if (entry.content.cardKeyPrefix.includes('$PROJECT-PREFIX')) {
534
- entry.content.cardKeyPrefix = projectPrefix.toLowerCase();
535
- }
536
- if (entry.content.name.includes('$PROJECT-NAME')) {
537
- entry.content.name = projectName;
540
+
541
+ await Promise.all(
542
+ Create.JSONFileContent.map(async (entry) => {
543
+ if ('cardKeyPrefix' in entry.content) {
544
+ if (entry.content.cardKeyPrefix.includes('$PROJECT-PREFIX')) {
545
+ entry.content.cardKeyPrefix = projectPrefix.toLowerCase();
546
+ }
547
+ if (entry.content.name.includes('$PROJECT-NAME')) {
548
+ entry.content.name = projectName;
549
+ }
538
550
  }
539
- }
540
- await writeJsonFile(
541
- join(projectPath, entry.path, entry.name),
542
- entry.content,
543
- );
544
- });
551
+ await writeJsonFile(
552
+ join(projectPath, entry.path, entry.name),
553
+ entry.content,
554
+ );
555
+ }),
556
+ );
545
557
 
546
558
  try {
547
559
  await writeFile(
@@ -0,0 +1,152 @@
1
+ /**
2
+ Cyberismo
3
+ Copyright © Cyberismo Ltd and contributors 2025
4
+ This program is free software: you can redistribute it and/or modify it under
5
+ the terms of the GNU Affero General Public License version 3 as published by
6
+ the Free Software Foundation.
7
+ This program is distributed in the hope that it will be useful, but WITHOUT
8
+ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
9
+ FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
10
+ details. You should have received a copy of the GNU Affero General Public
11
+ License along with this program. If not, see <https://www.gnu.org/licenses/>.
12
+ */
13
+
14
+ import { mkdir } from 'node:fs/promises';
15
+ import { resolve, sep } from 'node:path';
16
+ import type { Project } from '../containers/project.js';
17
+
18
+ import { writeJsonFile } from '../utils/json.js';
19
+ import { validateJson } from '../utils/validate.js';
20
+ import { type ModuleSetting } from '../interfaces/project-interfaces.js';
21
+ import { errorFunction, getChildLogger } from '../utils/log-utils.js';
22
+
23
+ const FETCH_TIMEOUT = 30000; // 30s timeout for fetching a hub file.
24
+ const MAX_RESPONSE_SIZE = 1024 * 1024; // 1MB limit for safety
25
+ const HUB_SCHEMA = 'hubSchema';
26
+ const MODULE_LIST_FILE = 'moduleList.json';
27
+ const TEMP_FOLDER = `.temp`;
28
+
29
+ export const MODULE_LIST_FULL_PATH = `${TEMP_FOLDER}/${MODULE_LIST_FILE}`;
30
+
31
+ export class Fetch {
32
+ constructor(private project: Project) {}
33
+
34
+ private get logger() {
35
+ return getChildLogger({
36
+ module: 'fetch',
37
+ });
38
+ }
39
+
40
+ private async fetchJSON(location: string, schemaId: string) {
41
+ try {
42
+ const url = new URL(`${location}/${MODULE_LIST_FILE}`);
43
+ if (!['http:', 'https:'].includes(url.protocol)) {
44
+ throw new Error(
45
+ `Invalid protocol: ${url.protocol}. Only HTTP and HTTPS are supported.`,
46
+ );
47
+ }
48
+
49
+ this.logger.info(`Fetching module list from: ${url.toString()}`);
50
+ const response = await fetch(url.toString(), {
51
+ method: 'GET',
52
+ headers: {
53
+ Accept: 'application/json',
54
+ 'User-Agent': 'Cyberismo/1.0',
55
+ },
56
+ signal: AbortSignal.timeout(FETCH_TIMEOUT),
57
+ });
58
+
59
+ if (!response.ok) {
60
+ throw new Error(
61
+ `HTTP ${response.status}: ${response.statusText} when fetching from ${url.toString()}`,
62
+ );
63
+ }
64
+
65
+ // Check content length before downloading
66
+ const contentLength = response.headers.get('content-length');
67
+ if (contentLength && parseInt(contentLength) > MAX_RESPONSE_SIZE) {
68
+ throw new Error(
69
+ `Response too large: ${contentLength} bytes (max: ${MAX_RESPONSE_SIZE})`,
70
+ );
71
+ }
72
+
73
+ const contentType = response.headers.get('content-type');
74
+ if (!contentType?.includes('application/json')) {
75
+ this.logger.warn(`Expected JSON response, got: ${contentType}`);
76
+ }
77
+
78
+ const json = await response.json();
79
+ // Validate the incoming JSON before saving it into a file.
80
+ await validateJson(json, { schemaId: schemaId });
81
+
82
+ // Validate JSON structure and prevent prototype pollution
83
+ if (typeof json !== 'object' || json === null || Array.isArray(json)) {
84
+ throw new Error('Response must be a JSON object');
85
+ }
86
+
87
+ // Additional size check after JSON parsing
88
+ if (JSON.stringify(json).length > MAX_RESPONSE_SIZE) {
89
+ throw new Error('JSON content too large after parsing');
90
+ }
91
+
92
+ return json;
93
+ } catch (error) {
94
+ this.logger.error(
95
+ error,
96
+ `Failed to fetch module list from ${location}: ${errorFunction(error)}`,
97
+ );
98
+ throw error;
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Fetches modules from modules hub(s) and writes them to a file.
104
+ */
105
+ public async fetchHubs() {
106
+ const hubs = this.project.configuration.hubs;
107
+
108
+ const moduleMap: Map<string, ModuleSetting> = new Map([]);
109
+
110
+ for (const hub of hubs) {
111
+ const json = await this.fetchJSON(hub.location, HUB_SCHEMA);
112
+ json.modules.forEach((module: ModuleSetting) => {
113
+ if (!moduleMap.has(module.name)) {
114
+ moduleMap.set(module.name, module);
115
+ } else {
116
+ this.logger.info(
117
+ `Skipping module '${module.name}' since it was already listed.`,
118
+ );
119
+ }
120
+ });
121
+ }
122
+
123
+ try {
124
+ const fullPath = resolve(this.project.basePath, MODULE_LIST_FULL_PATH);
125
+ const normalizedBasePath = resolve(this.project.basePath);
126
+
127
+ // Ensure the file is written within the project directory (prevent path traversal)
128
+ if (
129
+ !fullPath.startsWith(normalizedBasePath + sep) &&
130
+ fullPath !== normalizedBasePath
131
+ ) {
132
+ throw new Error(
133
+ 'Invalid file path: attempting to write outside project directory',
134
+ );
135
+ }
136
+
137
+ await mkdir(resolve(this.project.basePath, TEMP_FOLDER), {
138
+ recursive: true,
139
+ });
140
+ await writeJsonFile(fullPath, {
141
+ modules: Array.from(moduleMap.values()),
142
+ });
143
+ this.logger.info(`Module list written to: ${fullPath}`);
144
+ } catch (error) {
145
+ this.logger.error(
146
+ error,
147
+ `Failed to write module list to local file: ${errorFunction(error)}`,
148
+ );
149
+ throw error;
150
+ }
151
+ }
152
+ }
@@ -136,6 +136,7 @@ export class Import {
136
136
  ) {
137
137
  const beforeImportValidateErrors = await Validate.getInstance().validate(
138
138
  this.project.basePath,
139
+ () => this.project,
139
140
  );
140
141
  const gitModule = source.startsWith('https') || source.startsWith('git@');
141
142
  const modulePrefix = gitModule
@@ -164,6 +165,7 @@ export class Import {
164
165
  // Validate the project after module has been imported
165
166
  const afterImportValidateErrors = await Validate.getInstance().validate(
166
167
  this.project.basePath,
168
+ () => this.project,
167
169
  );
168
170
  if (afterImportValidateErrors.length > beforeImportValidateErrors.length) {
169
171
  console.error(
@@ -15,6 +15,7 @@ import { Calculate } from './calculate.js';
15
15
  import { Create } from './create.js';
16
16
  import { Edit } from './edit.js';
17
17
  import { Export } from './export.js';
18
+ import { Fetch } from './fetch.js';
18
19
  import { Import } from './import.js';
19
20
  import { Move } from './move.js';
20
21
  import { Remove } from './remove.js';
@@ -29,6 +30,7 @@ export {
29
30
  Create,
30
31
  Edit,
31
32
  Export,
33
+ Fetch,
32
34
  Import,
33
35
  Move,
34
36
  Remove,
@@ -53,9 +53,6 @@ export class Remove {
53
53
  }
54
54
 
55
55
  const attachmentFolder = await this.project.cardAttachmentFolder(cardKey);
56
- if (!attachmentFolder) {
57
- throw new Error(`Card '${cardKey}' not found`);
58
- }
59
56
 
60
57
  // Imported templates cannot be modified.
61
58
  if (attachmentFolder.includes(MODULES_PATH)) {
@@ -95,13 +92,16 @@ export class Remove {
95
92
  },
96
93
  );
97
94
  const promiseContainer: Promise<void>[] = [];
98
- allCards.filter((item) => {
99
- item.metadata?.links.forEach(async (link) => {
95
+
96
+ for (const item of allCards) {
97
+ const links = item.metadata?.links ?? [];
98
+ for (const link of links) {
100
99
  if (link.cardKey === cardKey) {
101
100
  promiseContainer.push(this.removeLink(item.key, link.cardKey));
102
101
  }
103
- });
104
- });
102
+ }
103
+ }
104
+
105
105
  await Promise.all(promiseContainer);
106
106
 
107
107
  // Calculations need to be updated before card is removed.
@@ -182,6 +182,11 @@ export class Remove {
182
182
  await this.project.updateCardMetadataKey(sourceCardKey, 'links', newLinks);
183
183
  }
184
184
 
185
+ // Remove a hub from project.
186
+ private async removeHubLocation(name: string) {
187
+ await this.project.configuration.removeHub(name);
188
+ }
189
+
185
190
  /**
186
191
  * Removes either attachment, card, imported module, link or resource from project.
187
192
  * @param type Type of resource
@@ -221,14 +226,15 @@ export class Remove {
221
226
  return resource?.delete();
222
227
  } else {
223
228
  // Something else than resources...
224
- if (type == 'attachment')
229
+ if (type === 'attachment')
225
230
  return this.removeAttachment(targetName, rest[0]);
226
- else if (type == 'card') return this.removeCard(targetName);
227
- else if (type == 'link')
231
+ else if (type === 'card') return this.removeCard(targetName);
232
+ else if (type === 'link')
228
233
  return this.removeLink(targetName, rest[0], rest[1], rest.at(2));
229
- else if (type == 'module')
234
+ else if (type === 'module')
230
235
  return this.moduleManager.removeModule(targetName);
231
- else if (type == 'label') return this.removeLabel(targetName, rest[0]);
236
+ else if (type === 'label') return this.removeLabel(targetName, rest[0]);
237
+ else if (type === 'hub') return this.removeHubLocation(targetName);
232
238
  }
233
239
  throw new Error(`Unknown resource type '${type}'`);
234
240
  }
@@ -160,17 +160,17 @@ export class Rename {
160
160
  );
161
161
 
162
162
  // Then replace all values that match in the conversion map.
163
- files.forEach(async (item) => {
164
- const target = join(item.parentPath, item.name);
165
- let fileContent = await readFile(target, {
166
- encoding: 'utf-8',
167
- });
168
- for (const [key, value] of conversionMap) {
169
- const re = new RegExp(key, 'g');
170
- fileContent = fileContent.replaceAll(re, value);
171
- }
172
- await writeFile(target, fileContent);
173
- });
163
+ await Promise.all(
164
+ files.map(async (item) => {
165
+ const target = join(item.parentPath, item.name);
166
+ let fileContent = await readFile(target, { encoding: 'utf-8' });
167
+ for (const [key, value] of conversionMap) {
168
+ const re = new RegExp(key, 'g');
169
+ fileContent = fileContent.replace(re, value);
170
+ }
171
+ await writeFile(target, fileContent);
172
+ }),
173
+ );
174
174
  }
175
175
 
176
176
  // Changes the name of a resource to match the new prefix.
@@ -18,6 +18,8 @@ import { join, resolve } from 'node:path';
18
18
  import { spawn } from 'node:child_process';
19
19
  import { readFile, writeFile } from 'node:fs/promises';
20
20
 
21
+ import { MODULE_LIST_FULL_PATH } from './fetch.js';
22
+
21
23
  import mime from 'mime-types';
22
24
 
23
25
  import type { attachmentPayload } from '../interfaces/request-status-interfaces.js';
@@ -26,6 +28,8 @@ import type {
26
28
  Card,
27
29
  CardListContainer,
28
30
  ModuleContent,
31
+ HubSetting,
32
+ ModuleSettingFromHub,
29
33
  ProjectFetchCardDetails,
30
34
  ProjectMetadata,
31
35
  Resource,
@@ -53,6 +57,8 @@ import ReportMacro from '../macros/report/index.js';
53
57
  import TaskQueue from '../macros/task-queue.js';
54
58
  import { evaluateMacros } from '../macros/index.js';
55
59
  import { FolderResource } from '../resources/folder-resource.js';
60
+ import { readJsonFile } from '../utils/json.js';
61
+ import { getChildLogger } from '../utils/log-utils.js';
56
62
 
57
63
  /**
58
64
  * Show command.
@@ -76,6 +82,12 @@ export class Show {
76
82
  ]);
77
83
  }
78
84
 
85
+ private get logger() {
86
+ return getChildLogger({
87
+ module: 'show',
88
+ });
89
+ }
90
+
79
91
  // Collect all labels from cards.
80
92
  private collectLabels = (cards: Card[]): string[] => {
81
93
  return cards.reduce<string[]>((labels, card) => {
@@ -320,6 +332,8 @@ export class Show {
320
332
 
321
333
  /**
322
334
  * Shows the content of a file in a resource.
335
+ * TODO: To be removed
336
+ * @deprecated
323
337
  * @param resourceName Name of the resource.
324
338
  * @param fileName Name of the file to show.
325
339
  * @returns the content of the file.
@@ -348,6 +362,8 @@ export class Show {
348
362
 
349
363
  /**
350
364
  * Shows all file names in a folder resource.
365
+ * TODO: To be removed
366
+ * @deprecated
351
367
  * @param resourceName Name of the resource.
352
368
  * @returns all file names in the resource.
353
369
  */
@@ -369,6 +385,54 @@ export class Show {
369
385
  }
370
386
  return resource.showFileNames();
371
387
  }
388
+
389
+ /**
390
+ * Shows importable modules.
391
+ * @param showAll - When true, shows all importable modules, even if they have already been imported
392
+ * @param showDetails - When true, shows all properties of modules, not just name.
393
+ * @returns list of modules; the list content depends on the parameters provided
394
+ * by default it is a list of module names that could be imported into the project,
395
+ * with 'showDetails' true, instead of name, the list consists of full details of the modules
396
+ * with 'showAll' true, the list consists of all modules in the hubs, even if they have already been imported
397
+ * Note that the two boolean options can be combined.
398
+ */
399
+ public async showImportableModules(
400
+ showAll?: boolean,
401
+ showDetails?: boolean,
402
+ ): Promise<ModuleSettingFromHub[]> {
403
+ try {
404
+ const moduleList = (
405
+ await readJsonFile(
406
+ resolve(this.project.basePath, MODULE_LIST_FULL_PATH),
407
+ )
408
+ ).modules;
409
+ const currentModules = await this.project.modules();
410
+ const nonImportedModules = moduleList.filter(
411
+ (item: ModuleSettingFromHub) => {
412
+ return !currentModules.some((module) => item.name === module.name);
413
+ },
414
+ );
415
+
416
+ if (showAll && showDetails) {
417
+ return moduleList;
418
+ }
419
+ if (showAll) {
420
+ return moduleList?.map((item: ModuleSettingFromHub) => item?.name);
421
+ }
422
+ if (showDetails) {
423
+ return nonImportedModules;
424
+ }
425
+ // By default return the non-imported modules
426
+ return nonImportedModules.map((item: ModuleSettingFromHub) => item?.name);
427
+ } catch (error) {
428
+ if (error instanceof Error) {
429
+ this.logger.error(error.message);
430
+ }
431
+ // Module list doesn't exist, return empty list
432
+ return [];
433
+ }
434
+ }
435
+
372
436
  /**
373
437
  * Returns all unique labels in a project
374
438
  * @returns labels in a list
@@ -406,6 +470,14 @@ export class Show {
406
470
  return moduleDetails;
407
471
  }
408
472
 
473
+ /**
474
+ * Shows hubs of the project.
475
+ * @returns list of hubs.
476
+ */
477
+ public showHubs(): HubSetting[] {
478
+ return this.project.configuration.hubs;
479
+ }
480
+
409
481
  /**
410
482
  * Returns all project cards in the project. Cards don't have content and nor metadata.
411
483
  * @note AppUi uses this method.
@@ -14,6 +14,7 @@ import type {
14
14
  AddOperation,
15
15
  ChangeOperation,
16
16
  Operation,
17
+ OperationFor,
17
18
  RankOperation,
18
19
  RemoveOperation,
19
20
  UpdateOperations,
@@ -44,7 +45,6 @@ export class Update {
44
45
  optionalDetail?: Type, // todo: for 'rank' it might be reasonable to accept also 'number'
45
46
  mappingTable?: { stateMapping: Record<string, string> },
46
47
  ) {
47
- const resource = Project.resourceObject(this.project, resourceName(name));
48
48
  const op: Operation<Type> = {
49
49
  name: operation,
50
50
  target: '' as Type,
@@ -76,7 +76,25 @@ export class Update {
76
76
  : undefined;
77
77
  }
78
78
 
79
- await resource?.update(key, op);
79
+ await this.applyResourceOperation(name, key, op);
80
+ }
81
+
82
+ /**
83
+ * Update single resource property
84
+ * This is similar to updateValue, but allows the operation to be fully specified
85
+ * @param name Name of the resource to operate on.
86
+ * @param key Property to change in resource JSON
87
+ * @param operation The full operation object
88
+ * @template Type Type of the target of the operation
89
+ * @template T Type of operation ('add', 'remove', 'change', 'rank')
90
+ */
91
+ public async applyResourceOperation<Type, T extends UpdateOperations>(
92
+ name: string,
93
+ key: string,
94
+ operation: OperationFor<Type, T>,
95
+ ) {
96
+ const resource = Project.resourceObject(this.project, resourceName(name));
97
+ await resource?.update(key, operation);
80
98
  this.project.collectLocalResources();
81
99
  }
82
100
  }
@@ -14,7 +14,6 @@
14
14
  // node
15
15
  import { type Dirent } from 'node:fs';
16
16
  import { basename, dirname, extname, join, parse, resolve } from 'node:path';
17
- import { fileURLToPath } from 'node:url';
18
17
  import { readdir } from 'node:fs/promises';
19
18
 
20
19
  // dependencies
@@ -52,7 +51,7 @@ const SHORT_TEXT_MAX_LENGTH = 80;
52
51
 
53
52
  import * as EmailValidator from 'email-validator';
54
53
  import { evaluateMacros } from '../macros/index.js';
55
- const baseDir = dirname(fileURLToPath(import.meta.url));
54
+ const baseDir = import.meta.dirname;
56
55
  const subFoldersToValidate = ['.cards', 'cardRoot'];
57
56
 
58
57
  export interface LengthProvider {
@@ -522,9 +521,13 @@ export class Validate {
522
521
  * Validates that a given directory path (and its children) conform to a JSON schema.
523
522
  * @note Validates also content in the directory tree, if .schema file is found.
524
523
  * @param projectPath path to validate.
524
+ * @param projectFn function that returns a Project instance.
525
525
  * @returns string containing all validation errors
526
526
  */
527
- public async validate(projectPath: string): Promise<string> {
527
+ public async validate(
528
+ projectPath: string,
529
+ projectFn: () => Project,
530
+ ): Promise<string> {
528
531
  let validationErrors = '';
529
532
  this.validatedFieldTypes.clear();
530
533
  this.validatedWorkflows.clear();
@@ -546,7 +549,7 @@ export class Validate {
546
549
  return validationErrors;
547
550
  } else {
548
551
  const errorMsg: string[] = [];
549
- const project = new Project(projectPath);
552
+ const project = projectFn();
550
553
 
551
554
  // Then, validate that each 'contentSchema' children as well.
552
555
  const result = await this.readAndValidateContentFiles(
@@ -606,17 +609,17 @@ export class Validate {
606
609
  });
607
610
  }
608
611
  }
609
- if (errorMsg.length) {
610
- validationErrors += errorMsg
611
- .filter(this.removeDuplicateEntries)
612
- .join('\n');
613
- }
614
612
  // Validate that there are no duplicate card keys
615
613
  for (const [key, count] of cardIds) {
616
614
  if (count > 1) {
617
- validationErrors += `Duplicate card key '${key}' found ${count} times\n`;
615
+ errorMsg.push(`Duplicate card key '${key}' found ${count} times`);
618
616
  }
619
617
  }
618
+ if (errorMsg.length) {
619
+ validationErrors += errorMsg
620
+ .filter(this.removeDuplicateEntries)
621
+ .join('\n');
622
+ }
620
623
  }
621
624
  } catch (error) {
622
625
  validationErrors += errorFunction(error);
@@ -18,9 +18,9 @@ import type { Dirent } from 'node:fs';
18
18
  import { readdir, readFile, writeFile } from 'node:fs/promises';
19
19
 
20
20
  import { findParentPath } from '../utils/card-utils.js';
21
+ import { getFilesSync } from '../utils/file-utils.js';
21
22
  import { readJsonFile } from '../utils/json.js';
22
23
  import { writeJsonFile } from '../utils/json.js';
23
- import { getFilesSync } from '../utils/file-utils.js';
24
24
 
25
25
  // interfaces
26
26
  import {