@cyberismo/data-handler 0.0.15 → 0.0.17

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 (191) hide show
  1. package/dist/card-metadata-updater.js +7 -1
  2. package/dist/card-metadata-updater.js.map +1 -1
  3. package/dist/command-handler.d.ts +4 -0
  4. package/dist/command-handler.js +22 -8
  5. package/dist/command-handler.js.map +1 -1
  6. package/dist/command-manager.d.ts +24 -1
  7. package/dist/command-manager.js +31 -7
  8. package/dist/command-manager.js.map +1 -1
  9. package/dist/commands/create.d.ts +1 -1
  10. package/dist/commands/create.js +34 -36
  11. package/dist/commands/create.js.map +1 -1
  12. package/dist/commands/export.d.ts +11 -2
  13. package/dist/commands/export.js +54 -41
  14. package/dist/commands/export.js.map +1 -1
  15. package/dist/commands/fetch.d.ts +8 -0
  16. package/dist/commands/fetch.js +101 -23
  17. package/dist/commands/fetch.js.map +1 -1
  18. package/dist/commands/import.d.ts +14 -3
  19. package/dist/commands/import.js +27 -10
  20. package/dist/commands/import.js.map +1 -1
  21. package/dist/commands/move.js +0 -1
  22. package/dist/commands/move.js.map +1 -1
  23. package/dist/commands/remove.d.ts +11 -2
  24. package/dist/commands/remove.js +15 -5
  25. package/dist/commands/remove.js.map +1 -1
  26. package/dist/commands/rename.d.ts +4 -9
  27. package/dist/commands/rename.js +37 -101
  28. package/dist/commands/rename.js.map +1 -1
  29. package/dist/commands/show.d.ts +20 -12
  30. package/dist/commands/show.js +79 -57
  31. package/dist/commands/show.js.map +1 -1
  32. package/dist/commands/transition.d.ts +9 -2
  33. package/dist/commands/transition.js +25 -17
  34. package/dist/commands/transition.js.map +1 -1
  35. package/dist/commands/update.d.ts +16 -12
  36. package/dist/commands/update.js +19 -17
  37. package/dist/commands/update.js.map +1 -1
  38. package/dist/commands/validate.d.ts +17 -9
  39. package/dist/commands/validate.js +94 -35
  40. package/dist/commands/validate.js.map +1 -1
  41. package/dist/containers/card-container.d.ts +7 -5
  42. package/dist/containers/card-container.js +30 -5
  43. package/dist/containers/card-container.js.map +1 -1
  44. package/dist/containers/project/calculation-engine.d.ts +7 -4
  45. package/dist/containers/project/calculation-engine.js +61 -66
  46. package/dist/containers/project/calculation-engine.js.map +1 -1
  47. package/dist/containers/project/project-paths.d.ts +7 -4
  48. package/dist/containers/project/project-paths.js +22 -12
  49. package/dist/containers/project/project-paths.js.map +1 -1
  50. package/dist/containers/project/resource-cache.d.ts +169 -0
  51. package/dist/containers/project/resource-cache.js +509 -0
  52. package/dist/containers/project/resource-cache.js.map +1 -0
  53. package/dist/containers/project/resource-handler.d.ts +129 -0
  54. package/dist/containers/project/resource-handler.js +206 -0
  55. package/dist/containers/project/resource-handler.js.map +1 -0
  56. package/dist/containers/project.d.ts +46 -152
  57. package/dist/containers/project.js +179 -409
  58. package/dist/containers/project.js.map +1 -1
  59. package/dist/containers/template.d.ts +8 -2
  60. package/dist/containers/template.js +24 -19
  61. package/dist/containers/template.js.map +1 -1
  62. package/dist/interfaces/command-options.d.ts +3 -1
  63. package/dist/interfaces/folder-content-interfaces.d.ts +5 -3
  64. package/dist/interfaces/folder-content-interfaces.js +3 -3
  65. package/dist/interfaces/folder-content-interfaces.js.map +1 -1
  66. package/dist/interfaces/project-interfaces.d.ts +7 -9
  67. package/dist/interfaces/project-interfaces.js.map +1 -1
  68. package/dist/interfaces/resource-interfaces.d.ts +14 -1
  69. package/dist/interfaces/resource-interfaces.js.map +1 -1
  70. package/dist/macros/graph/index.js +12 -26
  71. package/dist/macros/graph/index.js.map +1 -1
  72. package/dist/macros/index.d.ts +1 -1
  73. package/dist/macros/index.js +2 -2
  74. package/dist/macros/index.js.map +1 -1
  75. package/dist/macros/report/index.js +3 -6
  76. package/dist/macros/report/index.js.map +1 -1
  77. package/dist/module-manager.d.ts +16 -3
  78. package/dist/module-manager.js +51 -19
  79. package/dist/module-manager.js.map +1 -1
  80. package/dist/project-settings.d.ts +21 -3
  81. package/dist/project-settings.js +91 -14
  82. package/dist/project-settings.js.map +1 -1
  83. package/dist/resources/calculation-resource.d.ts +4 -3
  84. package/dist/resources/calculation-resource.js +11 -5
  85. package/dist/resources/calculation-resource.js.map +1 -1
  86. package/dist/resources/card-type-resource.d.ts +6 -1
  87. package/dist/resources/card-type-resource.js +34 -23
  88. package/dist/resources/card-type-resource.js.map +1 -1
  89. package/dist/resources/create-defaults.d.ts +3 -2
  90. package/dist/resources/create-defaults.js +3 -2
  91. package/dist/resources/create-defaults.js.map +1 -1
  92. package/dist/resources/field-type-resource.d.ts +4 -1
  93. package/dist/resources/field-type-resource.js +22 -23
  94. package/dist/resources/field-type-resource.js.map +1 -1
  95. package/dist/resources/file-resource.d.ts +5 -9
  96. package/dist/resources/file-resource.js +6 -11
  97. package/dist/resources/file-resource.js.map +1 -1
  98. package/dist/resources/folder-resource.d.ts +29 -32
  99. package/dist/resources/folder-resource.js +59 -78
  100. package/dist/resources/folder-resource.js.map +1 -1
  101. package/dist/resources/graph-model-resource.d.ts +4 -1
  102. package/dist/resources/graph-model-resource.js +11 -4
  103. package/dist/resources/graph-model-resource.js.map +1 -1
  104. package/dist/resources/graph-view-resource.d.ts +5 -2
  105. package/dist/resources/graph-view-resource.js +7 -3
  106. package/dist/resources/graph-view-resource.js.map +1 -1
  107. package/dist/resources/link-type-resource.d.ts +5 -2
  108. package/dist/resources/link-type-resource.js +5 -2
  109. package/dist/resources/link-type-resource.js.map +1 -1
  110. package/dist/resources/report-resource.d.ts +6 -7
  111. package/dist/resources/report-resource.js +14 -23
  112. package/dist/resources/report-resource.js.map +1 -1
  113. package/dist/resources/resource-object.d.ts +94 -8
  114. package/dist/resources/resource-object.js +212 -109
  115. package/dist/resources/resource-object.js.map +1 -1
  116. package/dist/resources/template-resource.d.ts +7 -3
  117. package/dist/resources/template-resource.js +10 -3
  118. package/dist/resources/template-resource.js.map +1 -1
  119. package/dist/resources/workflow-resource.d.ts +5 -2
  120. package/dist/resources/workflow-resource.js +18 -22
  121. package/dist/resources/workflow-resource.js.map +1 -1
  122. package/dist/utils/card-utils.d.ts +2 -2
  123. package/dist/utils/card-utils.js +1 -1
  124. package/dist/utils/clingo-fact-builder.d.ts +25 -14
  125. package/dist/utils/clingo-fact-builder.js +27 -5
  126. package/dist/utils/clingo-fact-builder.js.map +1 -1
  127. package/dist/utils/clingo-facts.js +3 -4
  128. package/dist/utils/clingo-facts.js.map +1 -1
  129. package/dist/utils/configuration-logger.d.ts +91 -0
  130. package/dist/utils/configuration-logger.js +151 -0
  131. package/dist/utils/configuration-logger.js.map +1 -0
  132. package/dist/utils/constants.d.ts +1 -1
  133. package/dist/utils/constants.js +5 -3
  134. package/dist/utils/constants.js.map +1 -1
  135. package/dist/utils/resource-utils.d.ts +1 -0
  136. package/dist/utils/resource-utils.js +2 -1
  137. package/dist/utils/resource-utils.js.map +1 -1
  138. package/package.json +9 -9
  139. package/src/card-metadata-updater.ts +6 -2
  140. package/src/command-handler.ts +39 -12
  141. package/src/command-manager.ts +33 -21
  142. package/src/commands/create.ts +43 -78
  143. package/src/commands/export.ts +63 -52
  144. package/src/commands/fetch.ts +143 -34
  145. package/src/commands/import.ts +37 -15
  146. package/src/commands/move.ts +0 -1
  147. package/src/commands/remove.ts +20 -7
  148. package/src/commands/rename.ts +58 -149
  149. package/src/commands/show.ts +123 -80
  150. package/src/commands/transition.ts +26 -28
  151. package/src/commands/update.ts +25 -22
  152. package/src/commands/validate.ts +104 -67
  153. package/src/containers/card-container.ts +37 -5
  154. package/src/containers/project/calculation-engine.ts +61 -93
  155. package/src/containers/project/project-paths.ts +29 -13
  156. package/src/containers/project/resource-cache.ts +651 -0
  157. package/src/containers/project/resource-handler.ts +265 -0
  158. package/src/containers/project.ts +250 -527
  159. package/src/containers/template.ts +28 -23
  160. package/src/interfaces/command-options.ts +3 -1
  161. package/src/interfaces/folder-content-interfaces.ts +7 -6
  162. package/src/interfaces/project-interfaces.ts +12 -11
  163. package/src/interfaces/resource-interfaces.ts +18 -3
  164. package/src/macros/graph/index.ts +26 -47
  165. package/src/macros/index.ts +2 -2
  166. package/src/macros/report/index.ts +3 -9
  167. package/src/module-manager.ts +74 -17
  168. package/src/project-settings.ts +96 -14
  169. package/src/resources/calculation-resource.ts +18 -18
  170. package/src/resources/card-type-resource.ts +50 -50
  171. package/src/resources/create-defaults.ts +3 -2
  172. package/src/resources/field-type-resource.ts +41 -55
  173. package/src/resources/file-resource.ts +10 -36
  174. package/src/resources/folder-resource.ts +69 -120
  175. package/src/resources/graph-model-resource.ts +20 -22
  176. package/src/resources/graph-view-resource.ts +15 -17
  177. package/src/resources/link-type-resource.ts +10 -13
  178. package/src/resources/report-resource.ts +21 -43
  179. package/src/resources/resource-object.ts +263 -149
  180. package/src/resources/template-resource.ts +17 -16
  181. package/src/resources/workflow-resource.ts +25 -44
  182. package/src/utils/card-utils.ts +2 -2
  183. package/src/utils/clingo-fact-builder.ts +28 -16
  184. package/src/utils/clingo-facts.ts +3 -4
  185. package/src/utils/configuration-logger.ts +206 -0
  186. package/src/utils/constants.ts +5 -3
  187. package/src/utils/resource-utils.ts +2 -1
  188. package/dist/containers/project/resource-collector.d.ts +0 -110
  189. package/dist/containers/project/resource-collector.js +0 -344
  190. package/dist/containers/project/resource-collector.js.map +0 -1
  191. package/src/containers/project/resource-collector.ts +0 -404
@@ -15,31 +15,18 @@
15
15
  import { join, resolve } from 'node:path';
16
16
  import { mkdir, writeFile } from 'node:fs/promises';
17
17
 
18
+ import { SCHEMA_VERSION } from '@cyberismo/assets';
18
19
  import { errorFunction } from '../utils/error-utils.js';
19
20
  import { Project } from '../containers/project.js';
20
- import { Validate } from './index.js';
21
+ import { Validate } from './validate.js';
21
22
 
22
23
  import { EMPTY_RANK, sortItems } from '../utils/lexorank.js';
23
24
  import { isModulePath } from '../utils/card-utils.js';
24
- import type {
25
- DataType,
26
- Link,
27
- LinkType,
28
- } from '../interfaces/resource-interfaces.js';
25
+ import type { DataType } from '../interfaces/resource-interfaces.js';
29
26
  import type { Card, ProjectFile } from '../interfaces/project-interfaces.js';
30
27
  import { resourceName, resourceNameToString } from '../utils/resource-utils.js';
31
28
  import { writeJsonFile } from '../utils/json.js';
32
29
 
33
- import { CalculationResource } from '../resources/calculation-resource.js';
34
- import { CardTypeResource } from '../resources/card-type-resource.js';
35
- import { FieldTypeResource } from '../resources/field-type-resource.js';
36
- import { GraphModelResource } from '../resources/graph-model-resource.js';
37
- import { GraphViewResource } from '../resources/graph-view-resource.js';
38
- import { LinkTypeResource } from '../resources/link-type-resource.js';
39
- import { ReportResource } from '../resources/report-resource.js';
40
- import { TemplateResource } from '../resources/template-resource.js';
41
- import { WorkflowResource } from '../resources/workflow-resource.js';
42
-
43
30
  // todo: Is there a easy to way to make JSON schema into a TypeScript interface/type?
44
31
  // Check this out: https://www.npmjs.com/package/json-schema-to-ts
45
32
 
@@ -58,6 +45,7 @@ export class Create {
58
45
  {
59
46
  path: '.cards/local',
60
47
  content: {
48
+ schemaVersion: SCHEMA_VERSION,
61
49
  cardKeyPrefix: '$PROJECT-PREFIX',
62
50
  name: '$PROJECT-NAME',
63
51
  modules: [],
@@ -107,9 +95,9 @@ export class Create {
107
95
  if (cardTypeName === undefined) {
108
96
  throw new Error(`Input validation error: card type cannot be empty`);
109
97
  }
110
- const templateResource = new TemplateResource(
111
- this.project,
112
- resourceName(templateName),
98
+ const templateResource = this.project.resources.byType(
99
+ templateName,
100
+ 'templates',
113
101
  );
114
102
  const templateObject = templateResource.templateObject();
115
103
  const specificCard = card ? templateObject.findCard(card) : undefined;
@@ -181,11 +169,9 @@ export class Create {
181
169
  * @param calculationName name for the calculation resource
182
170
  */
183
171
  public async createCalculation(calculationName: string) {
184
- const calculation = new CalculationResource(
185
- this.project,
186
- resourceName(calculationName),
187
- );
188
- await calculation.create();
172
+ return this.project.resources
173
+ .byType(calculationName, 'calculations')
174
+ .create();
189
175
  }
190
176
 
191
177
  /**
@@ -198,15 +184,15 @@ export class Create {
198
184
  templateName: string,
199
185
  parentCardKey?: string,
200
186
  ): Promise<Card[]> {
201
- const templateResource = new TemplateResource(
202
- this.project,
203
- resourceName(templateName),
187
+ const templateResource = this.project.resources.byType(
188
+ templateName,
189
+ 'templates',
204
190
  );
205
191
 
206
192
  Validate.getInstance().validResourceName(
207
193
  'templates',
208
194
  resourceNameToString(resourceName(templateName)),
209
- await this.project.projectPrefixes(),
195
+ this.project.allModulePrefixes(),
210
196
  );
211
197
 
212
198
  await templateResource.validate();
@@ -236,12 +222,9 @@ export class Create {
236
222
  * @param workflowName workflow name to use in the card type.
237
223
  */
238
224
  public async createCardType(cardTypeName: string, workflowName: string) {
239
- const cardType = new CardTypeResource(
240
- this.project,
241
- resourceName(cardTypeName),
242
- );
243
-
244
- await cardType.createCardType(workflowName);
225
+ return this.project.resources
226
+ .byType(cardTypeName, 'cardTypes')
227
+ .createCardType(workflowName);
245
228
  }
246
229
 
247
230
  /**
@@ -250,11 +233,9 @@ export class Create {
250
233
  * @param dataType data type for the field type
251
234
  */
252
235
  public async createFieldType(fieldTypeName: string, dataType: DataType) {
253
- const fieldType = new FieldTypeResource(
254
- this.project,
255
- resourceName(fieldTypeName),
256
- );
257
- await fieldType.createFieldType(dataType);
236
+ return this.project.resources
237
+ .byType(fieldTypeName, 'fieldTypes')
238
+ .createFieldType(dataType);
258
239
  }
259
240
 
260
241
  /**
@@ -262,23 +243,17 @@ export class Create {
262
243
  * @param graphModelName name for the graph model.
263
244
  */
264
245
  public async createGraphModel(graphModelName: string) {
265
- const graphModel = new GraphModelResource(
266
- this.project,
267
- resourceName(graphModelName),
268
- );
269
- await graphModel.create();
246
+ return this.project.resources
247
+ .byType(graphModelName, 'graphModels')
248
+ .create();
270
249
  }
271
250
 
272
251
  /**
273
252
  * Creates a new graph view.
274
- * @param graphModelName name for the graph view.
253
+ * @param graphViewName name for the graph view.
275
254
  */
276
255
  public async createGraphView(graphViewName: string) {
277
- const graphView = new GraphViewResource(
278
- this.project,
279
- resourceName(graphViewName),
280
- );
281
- await graphView.create();
256
+ return this.project.resources.byType(graphViewName, 'graphViews').create();
282
257
  }
283
258
 
284
259
  /**
@@ -307,11 +282,7 @@ export class Create {
307
282
  * @param linkTypeName name for the link type.
308
283
  */
309
284
  public async createLinkType(linkTypeName: string) {
310
- const linkType = new LinkTypeResource(
311
- this.project,
312
- resourceName(linkTypeName),
313
- );
314
- await linkType.create();
285
+ return this.project.resources.byType(linkTypeName, 'linkTypes').create();
315
286
  }
316
287
 
317
288
  /**
@@ -335,10 +306,9 @@ export class Create {
335
306
  const card = this.project.findCard(cardKey);
336
307
  const destinationCard = this.project.findCard(destinationCardKey);
337
308
  // make sure the link type exists
338
- const linkTypeObject = this.project.resource<LinkType>(linkType);
339
- if (!linkTypeObject) {
340
- throw new Error(`Link type '${linkType}' does not exist in the project`);
341
- }
309
+ const linkTypeObject = this.project.resources
310
+ .byType(linkType, 'linkTypes')
311
+ .show();
342
312
 
343
313
  // make sure that if linkDescription is not enabled, linkDescription is not provided
344
314
  if (
@@ -387,7 +357,7 @@ export class Create {
387
357
  );
388
358
  }
389
359
 
390
- const links: Link[] = card.metadata?.links || [];
360
+ const links = card.metadata?.links || [];
391
361
  links.push({
392
362
  linkType,
393
363
  cardKey: destinationCardKey,
@@ -416,6 +386,12 @@ export class Create {
416
386
 
417
387
  const projectFolders: string[] = ['.cards/local', 'cardRoot'];
418
388
 
389
+ if (!Validate.validateFolder(projectPath)) {
390
+ throw new Error(
391
+ `Input validation error: folder name '${projectPath}' is invalid`,
392
+ );
393
+ }
394
+
419
395
  if (
420
396
  projectPrefix === undefined ||
421
397
  projectPrefix.length < 3 ||
@@ -486,8 +462,7 @@ export class Create {
486
462
  * @param name name of the report
487
463
  */
488
464
  public async createReport(name: string) {
489
- const report = new ReportResource(this.project, resourceName(name));
490
- await report.createReport();
465
+ return this.project.resources.byType(name, 'reports').createReport();
491
466
  }
492
467
 
493
468
  /**
@@ -496,14 +471,9 @@ export class Create {
496
471
  * @param templateContent JSON content for the template file.
497
472
  */
498
473
  public async createTemplate(templateName: string, templateContent: string) {
499
- const template = new TemplateResource(
500
- this.project,
501
- resourceName(templateName),
502
- );
503
-
504
- await template.create(
505
- templateContent ? JSON.parse(templateContent) : undefined,
506
- );
474
+ return this.project.resources
475
+ .byType(templateName, 'templates')
476
+ .create(templateContent ? JSON.parse(templateContent) : undefined);
507
477
  }
508
478
 
509
479
  /**
@@ -512,13 +482,8 @@ export class Create {
512
482
  * @param workflowContent workflow content JSON
513
483
  */
514
484
  public async createWorkflow(workflowName: string, workflowContent: string) {
515
- const workflow = new WorkflowResource(
516
- this.project,
517
- resourceName(workflowName),
518
- );
519
-
520
- await workflow.create(
521
- workflowContent ? JSON.parse(workflowContent) : undefined,
522
- );
485
+ return this.project.resources
486
+ .byType(workflowName, 'workflows')
487
+ .create(workflowContent ? JSON.parse(workflowContent) : undefined);
523
488
  }
524
489
  }
@@ -31,12 +31,20 @@ import { generateReportContent } from '../utils/report.js';
31
31
  import { getStaticDirectoryPath, pdfReport } from '@cyberismo/assets';
32
32
  import { Project } from '../containers/project.js';
33
33
  import type { QueryResult } from '../types/queries.js';
34
- import type { Show } from './index.js';
34
+ import type { Show } from './show.js';
35
35
  import { sortItems } from '../utils/lexorank.js';
36
36
 
37
37
  const attachmentFolder: string = 'a';
38
38
 
39
+ /**
40
+ * Handles all export commands.
41
+ */
39
42
  export class Export {
43
+ /**
44
+ * Creates an instance of export.
45
+ * @param project Project to use
46
+ * @param showCmd Instance of Export command to use.
47
+ */
40
48
  constructor(
41
49
  protected project: Project,
42
50
  protected showCmd: Show,
@@ -97,6 +105,56 @@ export class Export {
97
105
  return content;
98
106
  }
99
107
 
108
+ // Runs Ascii Doctor converter --> to PDF
109
+ private async runAsciidoctorPdf(content: string): Promise<Buffer> {
110
+ const staticRootDir = await getStaticDirectoryPath();
111
+ const proc = spawn(
112
+ 'asciidoctor-pdf',
113
+ [
114
+ '-a',
115
+ 'pdf-theme=cyberismo',
116
+ '-a',
117
+ `pdf-themesdir=${join(staticRootDir, 'pdf-themes')}`,
118
+ '-a',
119
+ `pdf-fontsdir=${join(staticRootDir, 'pdf-themes', 'fonts')};GEM_FONTS_DIR`,
120
+ '-',
121
+ ],
122
+ {
123
+ timeout: 100000,
124
+ shell: process.platform === 'win32',
125
+ },
126
+ );
127
+ proc.stdin.end(content);
128
+ const result = await new Promise<Buffer>((resolve, reject) => {
129
+ const chunks: Buffer[] = [];
130
+ proc.stdout.on('data', (chunk) => {
131
+ chunks.push(chunk);
132
+ });
133
+ proc.stderr.on('data', (chunk) => {
134
+ process.stderr.write(chunk);
135
+ });
136
+ proc.on('error', (error) => {
137
+ if ('code' in error && error.code === 'ENOENT') {
138
+ reject(
139
+ new Error(
140
+ 'Asciidoctor-pdf not found. Please install asciidoctor-pdf to use this feature.',
141
+ ),
142
+ );
143
+ }
144
+ reject(error);
145
+ });
146
+ proc.on('close', (code) => {
147
+ if (code === 0) {
148
+ resolve(Buffer.concat(chunks));
149
+ } else {
150
+ reject(new Error(`Asciidoctor-pdf failed with code ${code}`));
151
+ }
152
+ });
153
+ });
154
+ return result;
155
+ }
156
+
157
+ // Adds cards to an ADOC file as additional content.
100
158
  private async toAdocFileAsContent(path: string, cards: Card[]) {
101
159
  for (const card of cards) {
102
160
  let fileContent = '';
@@ -108,9 +166,9 @@ export class Export {
108
166
  }
109
167
 
110
168
  if (card.metadata) {
111
- const cardTypeForCard = await this.project.resource<CardType>(
112
- card.metadata?.cardType,
113
- );
169
+ const cardTypeForCard = this.project.resources
170
+ .byType(card.metadata?.cardType, 'cardTypes')
171
+ .show();
114
172
  const metaDataContent = this.metaToAdoc(card, cardTypeForCard);
115
173
  fileContent += metaDataContent;
116
174
  }
@@ -177,6 +235,7 @@ export class Export {
177
235
  * Convert treeQueryResult object into a Card object and add content, metadata & attachments
178
236
  * Handles card children recursively
179
237
  * @param treeQueryResult tree query result object
238
+ * @returns Tree query result as a Card.
180
239
  */
181
240
  protected async treeQueryResultToCard(
182
241
  treeQueryResult: QueryResult<'tree'>,
@@ -218,54 +277,6 @@ export class Export {
218
277
  return card;
219
278
  }
220
279
 
221
- private async runAsciidoctorPdf(content: string): Promise<Buffer> {
222
- const staticRootDir = await getStaticDirectoryPath();
223
- const proc = spawn(
224
- 'asciidoctor-pdf',
225
- [
226
- '-a',
227
- 'pdf-theme=cyberismo',
228
- '-a',
229
- `pdf-themesdir=${join(staticRootDir, 'pdf-themes')}`,
230
- '-a',
231
- `pdf-fontsdir=${join(staticRootDir, 'pdf-themes', 'fonts')};GEM_FONTS_DIR`,
232
- '-',
233
- ],
234
- {
235
- timeout: 100000,
236
- shell: process.platform === 'win32',
237
- },
238
- );
239
- proc.stdin.end(content);
240
- const result = await new Promise<Buffer>((resolve, reject) => {
241
- const chunks: Buffer[] = [];
242
- proc.stdout.on('data', (chunk) => {
243
- chunks.push(chunk);
244
- });
245
- proc.stderr.on('data', (chunk) => {
246
- process.stderr.write(chunk);
247
- });
248
- proc.on('error', (error) => {
249
- if ('code' in error && error.code === 'ENOENT') {
250
- reject(
251
- new Error(
252
- 'Asciidoctor-pdf not found. Please install asciidoctor-pdf to use this feature.',
253
- ),
254
- );
255
- }
256
- reject(error);
257
- });
258
- proc.on('close', (code) => {
259
- if (code === 0) {
260
- resolve(Buffer.concat(chunks));
261
- } else {
262
- reject(new Error(`Asciidoctor-pdf failed with code ${code}`));
263
- }
264
- });
265
- });
266
- return result;
267
- }
268
-
269
280
  /**
270
281
  * Exports the card(s) to pdf.
271
282
  * @param destination Path to where the resulting file(s) will be created.
@@ -13,16 +13,28 @@
13
13
 
14
14
  import { mkdir } from 'node:fs/promises';
15
15
  import { resolve, sep } from 'node:path';
16
- import type { Project } from '../containers/project.js';
17
16
 
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 } from '../utils/error-utils.js';
22
17
  import { getChildLogger } from '../utils/log-utils.js';
18
+ import { readJsonFile, writeJsonFile } from '../utils/json.js';
19
+ import { validateJson } from '../utils/validate.js';
20
+
21
+ import type { ModuleSetting } from '../interfaces/project-interfaces.js';
22
+ import type { Project } from '../containers/project.js';
23
23
 
24
- const FETCH_TIMEOUT = 30000; // 30s timeout for fetching a hub file.
25
- const MAX_RESPONSE_SIZE = 1024 * 1024; // 1MB limit for safety
24
+ // Hub structure
25
+ interface HubVersionInfo {
26
+ location: string;
27
+ version: number;
28
+ }
29
+
30
+ // Structure of .temp/moduleList.json file.
31
+ interface ModuleListFile {
32
+ modules: ModuleSetting[];
33
+ hubs: HubVersionInfo[];
34
+ }
35
+
36
+ const FETCH_TIMEOUT_MS = 30 * 1000; // 30s timeout for fetching a hub file.
37
+ const MAX_RESPONSE_SIZE_MB = 1024 * 1024; // 1MB limit for safety
26
38
  const HUB_SCHEMA = 'hubSchema';
27
39
  const MODULE_LIST_FILE = 'moduleList.json';
28
40
  const TEMP_FOLDER = `.temp`;
@@ -30,7 +42,10 @@ const TEMP_FOLDER = `.temp`;
30
42
  export const MODULE_LIST_FULL_PATH = `${TEMP_FOLDER}/${MODULE_LIST_FILE}`;
31
43
 
32
44
  export class Fetch {
33
- constructor(private project: Project) {}
45
+ private moduleListPath;
46
+ constructor(private project: Project) {
47
+ this.moduleListPath = resolve(this.project.basePath, MODULE_LIST_FULL_PATH);
48
+ }
34
49
 
35
50
  private get logger() {
36
51
  return getChildLogger({
@@ -38,6 +53,38 @@ export class Fetch {
38
53
  });
39
54
  }
40
55
 
56
+ // Checks the version of the remote moduleList.json.
57
+ private async checkRemoteVersion(
58
+ location: string,
59
+ ): Promise<number | undefined> {
60
+ try {
61
+ const url = new URL(`${location}/${MODULE_LIST_FILE}`);
62
+ if (!['http:', 'https:'].includes(url.protocol)) {
63
+ return undefined;
64
+ }
65
+
66
+ const response = await fetch(url.toString(), {
67
+ method: 'GET',
68
+ headers: {
69
+ Accept: 'application/json',
70
+ 'User-Agent': 'Cyberismo/1.0',
71
+ },
72
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
73
+ });
74
+
75
+ if (!response.ok) {
76
+ return undefined;
77
+ }
78
+
79
+ const json = await response.json();
80
+ return json.version;
81
+ } catch (error) {
82
+ this.logger.error(error, `Could not check hub version for ${location} }`);
83
+ return undefined;
84
+ }
85
+ }
86
+
87
+ // Fetches one hub's data as JSON.
41
88
  private async fetchJSON(location: string, schemaId: string) {
42
89
  try {
43
90
  const url = new URL(`${location}/${MODULE_LIST_FILE}`);
@@ -47,14 +94,14 @@ export class Fetch {
47
94
  );
48
95
  }
49
96
 
50
- this.logger.info(`Fetching module list from: ${url.toString()}`);
97
+ this.logger.info(`Fetching module list from hub: ${url.toString()}`);
51
98
  const response = await fetch(url.toString(), {
52
99
  method: 'GET',
53
100
  headers: {
54
101
  Accept: 'application/json',
55
102
  'User-Agent': 'Cyberismo/1.0',
56
103
  },
57
- signal: AbortSignal.timeout(FETCH_TIMEOUT),
104
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
58
105
  });
59
106
 
60
107
  if (!response.ok) {
@@ -65,9 +112,9 @@ export class Fetch {
65
112
 
66
113
  // Check content length before downloading
67
114
  const contentLength = response.headers.get('content-length');
68
- if (contentLength && parseInt(contentLength) > MAX_RESPONSE_SIZE) {
115
+ if (contentLength && parseInt(contentLength) > MAX_RESPONSE_SIZE_MB) {
69
116
  throw new Error(
70
- `Response too large: ${contentLength} bytes (max: ${MAX_RESPONSE_SIZE})`,
117
+ `Response too large: ${contentLength} bytes (max: ${MAX_RESPONSE_SIZE_MB})`,
71
118
  );
72
119
  }
73
120
 
@@ -79,14 +126,10 @@ export class Fetch {
79
126
  const json = await response.json();
80
127
  // Validate the incoming JSON before saving it into a file.
81
128
  await validateJson(json, { schemaId: schemaId });
82
-
83
- // Validate JSON structure and prevent prototype pollution
84
129
  if (typeof json !== 'object' || json === null || Array.isArray(json)) {
85
130
  throw new Error('Response must be a JSON object');
86
131
  }
87
-
88
- // Additional size check after JSON parsing
89
- if (JSON.stringify(json).length > MAX_RESPONSE_SIZE) {
132
+ if (JSON.stringify(json).length > MAX_RESPONSE_SIZE_MB) {
90
133
  throw new Error('JSON content too large after parsing');
91
134
  }
92
135
 
@@ -94,41 +137,109 @@ export class Fetch {
94
137
  } catch (error) {
95
138
  this.logger.error(
96
139
  error,
97
- `Failed to fetch module list from ${location}: ${errorFunction(error)}`,
140
+ `Failed to fetch module list from hub ${location}`,
98
141
  );
99
142
  throw error;
100
143
  }
101
144
  }
102
145
 
146
+ // Checks if the local moduleList.json needs to be updated by comparing
147
+ // each hub's version with the stored version.
148
+ private async fetchModuleList(): Promise<boolean> {
149
+ try {
150
+ const configuredHubs = this.project.configuration.hubs;
151
+ if (configuredHubs.length === 0) {
152
+ return false;
153
+ }
154
+
155
+ const localData = (await readJsonFile(
156
+ this.moduleListPath,
157
+ )) as ModuleListFile;
158
+ const localHubs = localData.hubs || [];
159
+ if (localHubs.length !== configuredHubs.length) {
160
+ this.logger.info('Hub configuration changed, fetching module list');
161
+ return true;
162
+ }
163
+
164
+ // Check each hub's version
165
+ for (const configHub of configuredHubs) {
166
+ const localHub = localHubs.find(
167
+ (hub) => hub.location === configHub.location,
168
+ );
169
+
170
+ if (!localHub) {
171
+ this.logger.info(
172
+ `New hub detected: ${configHub.location}, fetching module list`,
173
+ );
174
+ return true;
175
+ }
176
+
177
+ const remoteVersion = await this.checkRemoteVersion(configHub.location);
178
+ if (remoteVersion === undefined) {
179
+ const hubName = configHub.displayName || configHub.location;
180
+ this.logger.info(`Hub ${hubName} has no version data, skipped.`);
181
+ continue;
182
+ }
183
+
184
+ if (remoteVersion > localHub.version) {
185
+ this.logger.info(
186
+ `Hub ${configHub.location} has newer version (remote: ${remoteVersion}, local: ${localHub.version}), fetching module list`,
187
+ );
188
+ return true;
189
+ }
190
+ }
191
+
192
+ this.logger.info('Module list is up to date');
193
+ return false;
194
+ } catch (error) {
195
+ this.logger.error(
196
+ error,
197
+ `Error when checking versions for hub module list`,
198
+ );
199
+ return true;
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Ensures the module list is up to date by fetching if needed.
205
+ */
206
+ public async ensureModuleListUpToDate() {
207
+ await this.fetchHubs();
208
+ }
209
+
103
210
  /**
104
211
  * Fetches modules from modules hub(s) and writes them to a file.
212
+ * Only fetches if the remote version is newer than the local version.
105
213
  */
106
214
  public async fetchHubs() {
107
- const hubs = this.project.configuration.hubs;
215
+ const needsFetch = await this.fetchModuleList();
216
+ if (!needsFetch) {
217
+ return;
218
+ }
108
219
 
220
+ const hubs = this.project.configuration.hubs;
109
221
  const moduleMap: Map<string, ModuleSetting> = new Map([]);
222
+ const hubVersions: HubVersionInfo[] = [];
110
223
 
111
224
  for (const hub of hubs) {
112
225
  const json = await this.fetchJSON(hub.location, HUB_SCHEMA);
113
226
  json.modules.forEach((module: ModuleSetting) => {
114
227
  if (!moduleMap.has(module.name)) {
115
228
  moduleMap.set(module.name, module);
116
- } else {
117
- this.logger.info(
118
- `Skipping module '${module.name}' since it was already listed.`,
119
- );
120
229
  }
121
230
  });
231
+
232
+ hubVersions.push({
233
+ location: hub.location,
234
+ version: json.version || 1,
235
+ });
122
236
  }
123
237
 
124
238
  try {
125
- const fullPath = resolve(this.project.basePath, MODULE_LIST_FULL_PATH);
126
239
  const normalizedBasePath = resolve(this.project.basePath);
127
-
128
- // Ensure the file is written within the project directory (prevent path traversal)
129
240
  if (
130
- !fullPath.startsWith(normalizedBasePath + sep) &&
131
- fullPath !== normalizedBasePath
241
+ !this.moduleListPath.startsWith(normalizedBasePath + sep) &&
242
+ this.moduleListPath !== normalizedBasePath
132
243
  ) {
133
244
  throw new Error(
134
245
  'Invalid file path: attempting to write outside project directory',
@@ -138,15 +249,13 @@ export class Fetch {
138
249
  await mkdir(resolve(this.project.basePath, TEMP_FOLDER), {
139
250
  recursive: true,
140
251
  });
141
- await writeJsonFile(fullPath, {
252
+ await writeJsonFile(this.moduleListPath, {
142
253
  modules: Array.from(moduleMap.values()),
254
+ hubs: hubVersions,
143
255
  });
144
- this.logger.info(`Module list written to: ${fullPath}`);
256
+ this.logger.info(`Module list written to: ${this.moduleListPath}`);
145
257
  } catch (error) {
146
- this.logger.error(
147
- error,
148
- `Failed to write module list to local file: ${errorFunction(error)}`,
149
- );
258
+ this.logger.error(error, `Failed to write module list to local file`);
150
259
  throw error;
151
260
  }
152
261
  }