@cyberismo/data-handler 0.0.20 → 0.0.22

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 (177) hide show
  1. package/dist/command-handler.js +13 -24
  2. package/dist/command-handler.js.map +1 -1
  3. package/dist/command-manager.d.ts +21 -6
  4. package/dist/command-manager.js +34 -32
  5. package/dist/command-manager.js.map +1 -1
  6. package/dist/commands/calculate.js +101 -46
  7. package/dist/commands/calculate.js.map +1 -1
  8. package/dist/commands/create.js +420 -320
  9. package/dist/commands/create.js.map +1 -1
  10. package/dist/commands/edit.js +117 -68
  11. package/dist/commands/edit.js.map +1 -1
  12. package/dist/commands/export.js +301 -252
  13. package/dist/commands/export.js.map +1 -1
  14. package/dist/commands/fetch.js +205 -156
  15. package/dist/commands/fetch.js.map +1 -1
  16. package/dist/commands/import.js +189 -134
  17. package/dist/commands/import.js.map +1 -1
  18. package/dist/commands/migrate.js +91 -45
  19. package/dist/commands/migrate.js.map +1 -1
  20. package/dist/commands/move.js +347 -267
  21. package/dist/commands/move.js.map +1 -1
  22. package/dist/commands/remove.d.ts +1 -0
  23. package/dist/commands/remove.js +202 -135
  24. package/dist/commands/remove.js.map +1 -1
  25. package/dist/commands/rename.js +233 -187
  26. package/dist/commands/rename.js.map +1 -1
  27. package/dist/commands/show.d.ts +8 -8
  28. package/dist/commands/show.js +477 -372
  29. package/dist/commands/show.js.map +1 -1
  30. package/dist/commands/transition.js +119 -73
  31. package/dist/commands/transition.js.map +1 -1
  32. package/dist/commands/update.js +8 -3
  33. package/dist/commands/update.js.map +1 -1
  34. package/dist/commands/validate.js +1 -1
  35. package/dist/commands/validate.js.map +1 -1
  36. package/dist/containers/project/calculation-engine.js +0 -1
  37. package/dist/containers/project/calculation-engine.js.map +1 -1
  38. package/dist/containers/project/card-cache.js +1 -1
  39. package/dist/containers/project/card-cache.js.map +1 -1
  40. package/dist/containers/project.d.ts +16 -0
  41. package/dist/containers/project.js +59 -1
  42. package/dist/containers/project.js.map +1 -1
  43. package/dist/containers/template.js +1 -1
  44. package/dist/containers/template.js.map +1 -1
  45. package/dist/interfaces/command-options.d.ts +1 -0
  46. package/dist/interfaces/resource-interfaces.d.ts +5 -12
  47. package/dist/interfaces/resource-interfaces.js.map +1 -1
  48. package/dist/macros/base-macro.js +1 -1
  49. package/dist/macros/base-macro.js.map +1 -1
  50. package/dist/macros/graph/index.js +3 -1
  51. package/dist/macros/graph/index.js.map +1 -1
  52. package/dist/macros/include/index.js +16 -1
  53. package/dist/macros/include/index.js.map +1 -1
  54. package/dist/macros/include/types.d.ts +15 -12
  55. package/dist/macros/index.js +4 -1
  56. package/dist/macros/index.js.map +1 -1
  57. package/dist/macros/report/index.js +1 -1
  58. package/dist/macros/report/index.js.map +1 -1
  59. package/dist/module-manager.js +5 -3
  60. package/dist/module-manager.js.map +1 -1
  61. package/dist/project-settings.js +2 -2
  62. package/dist/project-settings.js.map +1 -1
  63. package/dist/resources/card-type-resource.js +1 -1
  64. package/dist/resources/card-type-resource.js.map +1 -1
  65. package/dist/resources/create-defaults.js +0 -1
  66. package/dist/resources/create-defaults.js.map +1 -1
  67. package/dist/resources/field-type-resource.js +2 -5
  68. package/dist/resources/field-type-resource.js.map +1 -1
  69. package/dist/resources/file-resource.js +4 -1
  70. package/dist/resources/file-resource.js.map +1 -1
  71. package/dist/resources/folder-resource.d.ts +1 -1
  72. package/dist/resources/folder-resource.js +4 -1
  73. package/dist/resources/folder-resource.js.map +1 -1
  74. package/dist/resources/graph-model-resource.d.ts +1 -8
  75. package/dist/resources/graph-model-resource.js +0 -14
  76. package/dist/resources/graph-model-resource.js.map +1 -1
  77. package/dist/resources/graph-view-resource.d.ts +1 -8
  78. package/dist/resources/graph-view-resource.js +0 -14
  79. package/dist/resources/graph-view-resource.js.map +1 -1
  80. package/dist/resources/link-type-resource.js +1 -1
  81. package/dist/resources/link-type-resource.js.map +1 -1
  82. package/dist/resources/report-resource.d.ts +1 -8
  83. package/dist/resources/report-resource.js +0 -14
  84. package/dist/resources/report-resource.js.map +1 -1
  85. package/dist/resources/resource-object.d.ts +11 -1
  86. package/dist/resources/resource-object.js +19 -2
  87. package/dist/resources/resource-object.js.map +1 -1
  88. package/dist/resources/template-resource.d.ts +1 -9
  89. package/dist/resources/template-resource.js +0 -15
  90. package/dist/resources/template-resource.js.map +1 -1
  91. package/dist/resources/workflow-resource.d.ts +6 -0
  92. package/dist/resources/workflow-resource.js +29 -13
  93. package/dist/resources/workflow-resource.js.map +1 -1
  94. package/dist/utils/card-utils.js +1 -1
  95. package/dist/utils/card-utils.js.map +1 -1
  96. package/dist/utils/commit-context.d.ts +23 -0
  97. package/dist/utils/commit-context.js +30 -0
  98. package/dist/utils/commit-context.js.map +1 -0
  99. package/dist/utils/csv.d.ts +8 -0
  100. package/dist/utils/csv.js +11 -0
  101. package/dist/utils/csv.js.map +1 -1
  102. package/dist/utils/file-utils.js +3 -1
  103. package/dist/utils/file-utils.js.map +1 -1
  104. package/dist/utils/git-manager.d.ts +29 -0
  105. package/dist/utils/git-manager.js +76 -0
  106. package/dist/utils/git-manager.js.map +1 -0
  107. package/dist/utils/handlebars-helpers.d.ts +22 -0
  108. package/dist/utils/handlebars-helpers.js +78 -0
  109. package/dist/utils/handlebars-helpers.js.map +1 -0
  110. package/dist/utils/json.d.ts +17 -10
  111. package/dist/utils/json.js +27 -14
  112. package/dist/utils/json.js.map +1 -1
  113. package/dist/utils/log-utils.d.ts +7 -2
  114. package/dist/utils/log-utils.js +28 -3
  115. package/dist/utils/log-utils.js.map +1 -1
  116. package/dist/utils/report.d.ts +0 -19
  117. package/dist/utils/report.js +4 -63
  118. package/dist/utils/report.js.map +1 -1
  119. package/dist/utils/rw-lock.d.ts +71 -0
  120. package/dist/utils/rw-lock.js +220 -0
  121. package/dist/utils/rw-lock.js.map +1 -0
  122. package/dist/utils/user-preferences.js +3 -3
  123. package/dist/utils/user-preferences.js.map +1 -1
  124. package/package.json +10 -10
  125. package/src/command-handler.ts +14 -22
  126. package/src/command-manager.ts +43 -37
  127. package/src/commands/calculate.ts +8 -1
  128. package/src/commands/create.ts +39 -6
  129. package/src/commands/edit.ts +3 -0
  130. package/src/commands/export.ts +8 -2
  131. package/src/commands/fetch.ts +3 -0
  132. package/src/commands/import.ts +5 -0
  133. package/src/commands/migrate.ts +2 -0
  134. package/src/commands/move.ts +34 -0
  135. package/src/commands/remove.ts +24 -2
  136. package/src/commands/rename.ts +2 -0
  137. package/src/commands/show.ts +63 -34
  138. package/src/commands/transition.ts +2 -0
  139. package/src/commands/update.ts +9 -3
  140. package/src/commands/validate.ts +1 -1
  141. package/src/containers/project/calculation-engine.ts +0 -1
  142. package/src/containers/project/card-cache.ts +1 -0
  143. package/src/containers/project.ts +75 -1
  144. package/src/containers/template.ts +1 -1
  145. package/src/interfaces/command-options.ts +1 -0
  146. package/src/interfaces/resource-interfaces.ts +5 -12
  147. package/src/macros/base-macro.ts +1 -1
  148. package/src/macros/graph/index.ts +3 -0
  149. package/src/macros/include/index.ts +19 -1
  150. package/src/macros/include/types.ts +15 -12
  151. package/src/macros/index.ts +4 -1
  152. package/src/macros/report/index.ts +1 -0
  153. package/src/module-manager.ts +5 -2
  154. package/src/project-settings.ts +2 -1
  155. package/src/resources/card-type-resource.ts +1 -1
  156. package/src/resources/create-defaults.ts +0 -1
  157. package/src/resources/field-type-resource.ts +2 -4
  158. package/src/resources/file-resource.ts +3 -1
  159. package/src/resources/folder-resource.ts +7 -2
  160. package/src/resources/graph-model-resource.ts +1 -25
  161. package/src/resources/graph-view-resource.ts +1 -25
  162. package/src/resources/link-type-resource.ts +1 -1
  163. package/src/resources/report-resource.ts +1 -25
  164. package/src/resources/resource-object.ts +22 -1
  165. package/src/resources/template-resource.ts +0 -23
  166. package/src/resources/workflow-resource.ts +45 -16
  167. package/src/utils/card-utils.ts +1 -1
  168. package/src/utils/commit-context.ts +45 -0
  169. package/src/utils/csv.ts +12 -0
  170. package/src/utils/file-utils.ts +3 -1
  171. package/src/utils/git-manager.ts +87 -0
  172. package/src/utils/handlebars-helpers.ts +95 -0
  173. package/src/utils/json.ts +29 -15
  174. package/src/utils/log-utils.ts +33 -4
  175. package/src/utils/report.ts +8 -74
  176. package/src/utils/rw-lock.ts +279 -0
  177. package/src/utils/user-preferences.ts +3 -0
@@ -48,6 +48,7 @@ import type { ResourceName } from '../utils/resource-utils.js';
48
48
  import type { ResourceMap } from '../containers/project/resource-cache.js';
49
49
 
50
50
  import { UserPreferences } from '../utils/user-preferences.js';
51
+ import { read, write } from '../utils/rw-lock.js';
51
52
  import ReportMacro from '../macros/report/index.js';
52
53
  import TaskQueue from '../macros/task-queue.js';
53
54
  import { evaluateMacros } from '../macros/index.js';
@@ -146,10 +147,13 @@ export class Show {
146
147
  * Shows all template cards in a project.
147
148
  * @returns all template cards in a project.
148
149
  */
149
- public showAllTemplateCards(): {
150
- name: string;
151
- cards: CardWithChildrenCards[];
152
- }[] {
150
+ @read
151
+ public async showAllTemplateCards(): Promise<
152
+ {
153
+ name: string;
154
+ cards: CardWithChildrenCards[];
155
+ }[]
156
+ > {
153
157
  return this.project.resources.templates().map((template) => {
154
158
  const cards = template.templateObject().listCards();
155
159
  const buildCards = buildCardHierarchy(cards);
@@ -165,6 +169,7 @@ export class Show {
165
169
  * Shows all attachments (either template or project attachments) from a project.
166
170
  * @returns array of card attachments
167
171
  */
172
+ @read
168
173
  public async showAttachments(): Promise<CardAttachment[]> {
169
174
  const attachments = this.project.attachments();
170
175
  const templateAttachments = await this.attachmentsFromTemplates();
@@ -178,7 +183,11 @@ export class Show {
178
183
  * @param filename attachment filename
179
184
  * @returns attachment details
180
185
  */
181
- public showAttachment(cardKey: string, filename: string): attachmentPayload {
186
+ @read
187
+ public async showAttachment(
188
+ cardKey: string,
189
+ filename: string,
190
+ ): Promise<attachmentPayload> {
182
191
  if (!cardKey) {
183
192
  throw new Error(`Mandatory parameter 'cardKey' missing`);
184
193
  }
@@ -203,6 +212,7 @@ export class Show {
203
212
  * @param waitDelay amount of time to wait for the application to open the attachment
204
213
  * @todo: Move away from Show.
205
214
  */
215
+ @read
206
216
  public async openAttachment(
207
217
  cardKey: string,
208
218
  filename: string,
@@ -259,10 +269,11 @@ export class Show {
259
269
  * @param contentType Content format in which content is to be shown
260
270
  * @returns card details
261
271
  */
262
- public showCardDetails(
272
+ @read
273
+ public async showCardDetails(
263
274
  cardKey?: string,
264
275
  contentType?: FileContentType,
265
- ): Card {
276
+ ): Promise<Card> {
266
277
  if (!cardKey) {
267
278
  throw new Error(`Mandatory parameter 'cardKey' missing`);
268
279
  }
@@ -285,6 +296,7 @@ export class Show {
285
296
  * @param cardsFrom - The location from which to look for cards. Either from the project, templates or both.
286
297
  * @returns cards list array
287
298
  */
299
+ @read
288
300
  public async showCards(
289
301
  cardsFrom?: CardLocation,
290
302
  ): Promise<CardListContainer[]> {
@@ -296,6 +308,7 @@ export class Show {
296
308
  * @param cardKey The key of the card.
297
309
  * @returns the content of the logic program.
298
310
  */
311
+ @read
299
312
  public async showCardLogicProgram(cardKey: string) {
300
313
  return this.project.calculationEngine.cardLogicProgram(cardKey);
301
314
  }
@@ -304,6 +317,7 @@ export class Show {
304
317
  * Shows all card types in a project.
305
318
  * @returns array of card type details
306
319
  */
320
+ @read
307
321
  public async showCardTypesWithDetails(): Promise<(CardType | undefined)[]> {
308
322
  const container = [];
309
323
  for (const cardType of this.project.resources.cardTypes()) {
@@ -324,6 +338,7 @@ export class Show {
324
338
  * with 'showAll' true, the list consists of all modules in the hubs, even if they have already been imported
325
339
  * Note that the two boolean options can be combined.
326
340
  */
341
+ @write()
327
342
  public async showImportableModules(
328
343
  showAll?: boolean,
329
344
  showDetails?: boolean,
@@ -368,7 +383,8 @@ export class Show {
368
383
  * Returns all unique labels in a project
369
384
  * @returns labels in a list
370
385
  */
371
- public showLabels(): string[] {
386
+ @read
387
+ public async showLabels(): Promise<string[]> {
372
388
  const cards = flattenCardArray(
373
389
  this.project.showProjectCards(),
374
390
  this.project,
@@ -384,6 +400,7 @@ export class Show {
384
400
  * @param resource Name of the resource.
385
401
  * @returns the content of the logic program.
386
402
  */
403
+ @read
387
404
  public async showLogicProgram(resource: ResourceName) {
388
405
  return this.project.calculationEngine.resourceLogicProgram(resource);
389
406
  }
@@ -393,6 +410,7 @@ export class Show {
393
410
  * @param moduleName name of a module
394
411
  * @returns details of a module.
395
412
  */
413
+ @read
396
414
  public async showModule(moduleName: string): Promise<ModuleContent> {
397
415
  const moduleDetails = await this.project.module(moduleName);
398
416
  if (!moduleDetails) {
@@ -405,6 +423,7 @@ export class Show {
405
423
  * Shows hubs of the project.
406
424
  * @returns list of hubs.
407
425
  */
426
+ @write()
408
427
  public async showHubs(): Promise<HubSetting[]> {
409
428
  // Ensure module list is up to date before showing
410
429
  await this.fetchCmd.ensureModuleListUpToDate();
@@ -415,7 +434,8 @@ export class Show {
415
434
  * Returns all project cards in the project. Cards don't have content and nor metadata.
416
435
  * @returns array of cards
417
436
  */
418
- public showProjectCards(): Card[] {
437
+ @read
438
+ public async showProjectCards(): Promise<Card[]> {
419
439
  return this.project.showProjectCards();
420
440
  }
421
441
 
@@ -423,7 +443,8 @@ export class Show {
423
443
  * Shows all modules (if any) in a project.
424
444
  * @returns all modules in a project.
425
445
  */
426
- public showModules(): string[] {
446
+ @read
447
+ public async showModules(): Promise<string[]> {
427
448
  return this.project.resources.moduleNames().sort();
428
449
  }
429
450
 
@@ -431,6 +452,7 @@ export class Show {
431
452
  * Shows details of a particular project.
432
453
  * @returns project information
433
454
  */
455
+ @read
434
456
  public async showProject(): Promise<ProjectMetadata> {
435
457
  return this.project.show();
436
458
  }
@@ -445,6 +467,7 @@ export class Show {
445
467
  * @returns Report results as a string
446
468
  * @throws Error if the report does not exist
447
469
  */
470
+ @read
448
471
  public async showReportResults(
449
472
  reportName: string,
450
473
  cardKey: string,
@@ -487,6 +510,7 @@ export class Show {
487
510
  if (error instanceof Error) {
488
511
  throw new Error(
489
512
  `Failed to write report to ${outputPath}: ${error.message}`,
513
+ { cause: error },
490
514
  );
491
515
  }
492
516
  }
@@ -515,29 +539,31 @@ export class Show {
515
539
  arg2?: boolean | ResourceType,
516
540
  arg3?: boolean,
517
541
  ): Promise<AnyResourceContent> {
518
- const hasResourceType = typeof arg2 === 'string';
519
- const resourceType = hasResourceType ? arg2 : null;
520
- const showUse = hasResourceType ? arg3 : arg2;
521
-
522
- const type = this.project.resources.extractType(name);
523
- if (resourceType !== null && resourceType !== type) {
524
- throw new Error(
525
- `While fetching '${name}': Expected type '${resourceType}', but got '${type}' instead`,
526
- );
527
- }
528
- const resource = this.project.resources.byType(name, type);
529
- const [details, usage] = await Promise.all([
530
- resource?.show(),
531
- showUse ? resource?.usage() : [],
532
- ]);
533
- if (showUse) {
534
- return {
535
- ...details,
536
- usedIn: [...usage],
537
- };
538
- } else {
539
- return details;
540
- }
542
+ return this.project.lock.read(async () => {
543
+ const hasResourceType = typeof arg2 === 'string';
544
+ const resourceType = hasResourceType ? arg2 : null;
545
+ const showUse = hasResourceType ? arg3 : arg2;
546
+
547
+ const type = this.project.resources.extractType(name);
548
+ if (resourceType !== null && resourceType !== type) {
549
+ throw new Error(
550
+ `While fetching '${name}': Expected type '${resourceType}', but got '${type}' instead`,
551
+ );
552
+ }
553
+ const resource = this.project.resources.byType(name, type);
554
+ const [details, usage] = await Promise.all([
555
+ resource?.show(),
556
+ showUse ? resource?.usage() : [],
557
+ ]);
558
+ if (showUse) {
559
+ return {
560
+ ...details,
561
+ usedIn: [...usage],
562
+ };
563
+ } else {
564
+ return details;
565
+ }
566
+ });
541
567
  }
542
568
 
543
569
  /**
@@ -545,6 +571,7 @@ export class Show {
545
571
  * @param type Name of resources to return (in plural form, e.g. 'templates')
546
572
  * @returns sorted array of resources
547
573
  */
574
+ @read
548
575
  public async showResources(type: string): Promise<string[]> {
549
576
  const func = this.resourceFunctions[type];
550
577
  if (!func) return [];
@@ -555,6 +582,7 @@ export class Show {
555
582
  * Shows all templates with full details in a project.
556
583
  * @returns all templates in a project.
557
584
  */
585
+ @read
558
586
  public async showTemplatesWithDetails(): Promise<TemplateConfiguration[]> {
559
587
  const templates = [];
560
588
  for (const template of this.project.resources.templates()) {
@@ -567,7 +595,8 @@ export class Show {
567
595
  * Shows all workflows with full details in a project.
568
596
  * @returns workflows with full details
569
597
  */
570
- public showWorkflowsWithDetails(): (Workflow | undefined)[] {
598
+ @read
599
+ public async showWorkflowsWithDetails(): Promise<(Workflow | undefined)[]> {
571
600
  const workflows = [];
572
601
  for (const workflow of this.project.resources.workflows()) {
573
602
  workflows.push(workflow.data);
@@ -15,6 +15,7 @@ import { ActionGuard } from '../permissions/action-guard.js';
15
15
  import { CardMetadataUpdater } from '../card-metadata-updater.js';
16
16
  import type { Project } from '../containers/project.js';
17
17
  import type { WorkflowState } from '../interfaces/resource-interfaces.js';
18
+ import { write } from '../utils/rw-lock.js';
18
19
 
19
20
  /**
20
21
  * Handles transitions.
@@ -40,6 +41,7 @@ export class Transition {
40
41
  * @param cardKey card key
41
42
  * @param transition which transition to do
42
43
  */
44
+ @write((cardKey) => `Transition card ${cardKey}`)
43
45
  public async cardTransition(cardKey: string, transition: WorkflowState) {
44
46
  const card = this.project.findCard(cardKey);
45
47
 
@@ -21,6 +21,7 @@ import type {
21
21
  } from '../resources/resource-object.js';
22
22
  import type { Project } from '../containers/project.js';
23
23
  import type { UpdateKey } from '../interfaces/resource-interfaces.js';
24
+ import { runWithDefaultCommitMessage } from '../utils/commit-context.js';
24
25
 
25
26
  /**
26
27
  * Class that handles 'update' commands.
@@ -47,9 +48,13 @@ export class Update {
47
48
  T extends UpdateOperations,
48
49
  K extends string,
49
50
  >(name: string, updateKey: UpdateKey<K>, operation: OperationFor<Type, T>) {
50
- const type = this.project.resources.extractType(name);
51
- const resource = this.project.resources.byType(name, type);
52
- await resource?.update(updateKey, operation);
51
+ const run = () =>
52
+ this.project.lock.write(async () => {
53
+ const type = this.project.resources.extractType(name);
54
+ const resource = this.project.resources.byType(name, type);
55
+ await resource?.update(updateKey, operation);
56
+ });
57
+ return runWithDefaultCommitMessage('Apply resource operation', run);
53
58
  }
54
59
 
55
60
  /**
@@ -69,6 +74,7 @@ export class Update {
69
74
  optionalDetail?: Type, // todo: for 'rank' it might be reasonable to accept also 'number'
70
75
  mappingTable?: { stateMapping: Record<string, string> },
71
76
  ) {
77
+ // Safe to not have lock here, this is just a wrapper to applyResourceOperation
72
78
  const op: Operation<Type> = {
73
79
  name: operation,
74
80
  target: '' as Type,
@@ -240,7 +240,7 @@ export class Validate {
240
240
  const result = await Promise.all(promises);
241
241
  message.push(...result.flat(1));
242
242
  } catch (error) {
243
- throw new Error(errorFunction(error));
243
+ throw new Error(errorFunction(error), { cause: error });
244
244
  }
245
245
  return message;
246
246
  }
@@ -476,7 +476,6 @@ export class CalculationEngine {
476
476
  model: model,
477
477
  view: view,
478
478
  },
479
- graph: true,
480
479
  context,
481
480
  });
482
481
  let graph = (await instance()).renderString(result, {
@@ -174,6 +174,7 @@ export class CardCache {
174
174
  if (error instanceof Error) {
175
175
  throw new Error(
176
176
  `Invalid JSON in file '${metadataPath}': ${error.message}`,
177
+ { cause: error },
177
178
  );
178
179
  }
179
180
  throw error;
@@ -56,6 +56,9 @@ import { Validate } from '../commands/validate.js';
56
56
  import { ContentWatcher } from './project/project-content-watcher.js';
57
57
  import { getChildLogger } from '../utils/log-utils.js';
58
58
  import { MigrationExecutor } from '../migrations/migration-executor.js';
59
+ import { RWLock } from '../utils/rw-lock.js';
60
+ import { GitManager } from '../utils/git-manager.js';
61
+ import { getCommitContext } from '../utils/commit-context.js';
59
62
 
60
63
  import type { MigrationResult } from '@cyberismo/migrations';
61
64
  import type { Template } from './template.js';
@@ -78,13 +81,16 @@ export { ResourcesFrom };
78
81
  export interface ProjectOptions {
79
82
  autoSave?: boolean;
80
83
  watchResourceChanges?: boolean;
84
+ autocommit?: boolean;
81
85
  }
82
86
 
83
87
  /**
84
88
  * Represents project folder.
85
89
  */
86
90
  export class Project extends CardContainer {
91
+ public readonly lock = new RWLock();
87
92
  public calculationEngine: CalculationEngine;
93
+ private gitManager?: GitManager;
88
94
  private logger = getChildLogger({ module: 'Project' });
89
95
  private projectPaths: ProjectPaths;
90
96
  private resourceHandler: ResourceHandler;
@@ -140,6 +146,28 @@ export class Project extends CardContainer {
140
146
  },
141
147
  );
142
148
  }
149
+
150
+ if (this.options.autocommit) {
151
+ this.gitManager = new GitManager(path);
152
+
153
+ // Commit after successful writes
154
+ this.lock.onAfterWrite(async () => {
155
+ const context = getCommitContext();
156
+ await this.gitManager!.commit(
157
+ context.message ?? 'Autocommit',
158
+ context.author,
159
+ );
160
+ });
161
+
162
+ // Rollback on failed writes
163
+ this.lock.onWriteError(async () => {
164
+ await this.gitManager!.rollback();
165
+ // Invalidate caches after rollback since filesystem state changed
166
+ this.cardCache.clear();
167
+ await this.populateCardsCache();
168
+ this.resources.changed();
169
+ });
170
+ }
143
171
  }
144
172
 
145
173
  // Changes a card's parent in the cache and updates all relationships.
@@ -868,6 +896,15 @@ export class Project extends CardContainer {
868
896
  return this.projectPaths;
869
897
  }
870
898
 
899
+ /**
900
+ * Initialize git repo for autocommit mode. No-op if autocommit is disabled.
901
+ */
902
+ public async initializeGit(): Promise<void> {
903
+ if (this.gitManager) {
904
+ await this.gitManager.initialize(getCommitContext().author);
905
+ }
906
+ }
907
+
871
908
  /**
872
909
  * Populates the card cache, if it has not been populated.
873
910
  */
@@ -939,7 +976,7 @@ export class Project extends CardContainer {
939
976
  await unlink(attachmentPath);
940
977
  } catch (error) {
941
978
  this.logger.error({ error }, 'Removing card attachment');
942
- throw new Error(`Attachment not found: ${fileName}`);
979
+ throw new Error(`Attachment not found: ${fileName}`, { cause: error });
943
980
  }
944
981
  await this.handleAttachmentChange(cardKey, 'removed', fileName);
945
982
  }
@@ -1134,6 +1171,43 @@ export class Project extends CardContainer {
1134
1171
  }
1135
1172
  }
1136
1173
 
1174
+ /**
1175
+ * Updates descendant card paths in the cache after a parent card has been moved.
1176
+ * This ensures cached paths reflect the actual filesystem locations.
1177
+ * @param cardKey The card whose descendants need path updates
1178
+ * @param oldBasePath The old base path before the move
1179
+ * @param newBasePath The new base path after the move
1180
+ */
1181
+ public updateDescendantPathsAfterMove(
1182
+ cardKey: string,
1183
+ oldBasePath: string,
1184
+ newBasePath: string,
1185
+ ): void {
1186
+ const card = this.cardCache.getCard(cardKey);
1187
+ if (!card) return;
1188
+
1189
+ if (card.path.startsWith(oldBasePath)) {
1190
+ card.path = card.path.replace(oldBasePath, newBasePath);
1191
+
1192
+ if (card.attachments && card.attachments.length > 0) {
1193
+ for (const attachment of card.attachments) {
1194
+ if (attachment.path.startsWith(oldBasePath)) {
1195
+ attachment.path = attachment.path.replace(oldBasePath, newBasePath);
1196
+ }
1197
+ }
1198
+ }
1199
+
1200
+ this.cardCache.updateCard(card.key, card);
1201
+ }
1202
+
1203
+ // Recursively update children
1204
+ if (card.children && card.children.length > 0) {
1205
+ for (const childKey of card.children) {
1206
+ this.updateDescendantPathsAfterMove(childKey, oldBasePath, newBasePath);
1207
+ }
1208
+ }
1209
+ }
1210
+
1137
1211
  /**
1138
1212
  * Updates the entire card in the card cache and handles any path/parent changes.
1139
1213
  * Also persists changes to content and metadata files.
@@ -399,7 +399,7 @@ export class Template extends CardContainer {
399
399
  const destinationCardPath = parentCard
400
400
  ? join(this.cardFolder(parentCard.key), 'c')
401
401
  : this.templateCardsPath;
402
- let newCardKey = '';
402
+ let newCardKey: string;
403
403
 
404
404
  try {
405
405
  // todo: to use cache instead of file access
@@ -92,6 +92,7 @@ export interface ShowCommandOptions extends BaseCommandOptions {
92
92
  export interface StartCommandOptions extends BaseCommandOptions {
93
93
  forceStart?: boolean;
94
94
  watchResourceChanges?: boolean;
95
+ autocommit?: boolean;
95
96
  }
96
97
 
97
98
  // Options for 'transition' command
@@ -83,18 +83,14 @@ export interface FieldType extends ResourceBaseMetadata {
83
83
  }
84
84
 
85
85
  // Graph model content.
86
- export interface GraphModelMetadata extends ResourceBaseMetadata {
87
- category?: string;
88
- }
86
+ export type GraphModelMetadata = ResourceBaseMetadata;
89
87
  export interface GraphModel extends GraphModelMetadata {
90
88
  content: GraphModelContent;
91
89
  }
92
90
  export type GraphModelContentPropertyName = 'model';
93
91
 
94
92
  // Graph view content.
95
- export interface GraphViewMetadata extends ResourceBaseMetadata {
96
- category?: string;
97
- }
93
+ export type GraphViewMetadata = ResourceBaseMetadata;
98
94
  export type GraphViewContentPropertyName = 'viewTemplate';
99
95
  export interface GraphView extends GraphViewMetadata {
100
96
  content: GraphViewContent;
@@ -128,13 +124,12 @@ export type ReportContentPropertyName =
128
124
  | 'schema';
129
125
 
130
126
  // Metadata for report
131
- export interface ReportMetadata extends ResourceBaseMetadata {
132
- category: string;
133
- }
127
+ export type ReportMetadata = ResourceBaseMetadata;
134
128
 
135
129
  // Base interface for all resources.
136
130
  export interface ResourceBaseMetadata {
137
131
  name: string;
132
+ category?: string;
138
133
  description?: string;
139
134
  displayName: string;
140
135
  usedIn?: string[];
@@ -173,9 +168,7 @@ export interface TemplateConfiguration extends TemplateMetadata {
173
168
  }
174
169
 
175
170
  // Template configuration content details.
176
- export interface TemplateMetadata extends ResourceBaseMetadata {
177
- category?: string;
178
- }
171
+ export type TemplateMetadata = ResourceBaseMetadata;
179
172
  type ContentUpdateKey = { key: 'content'; subKey: string };
180
173
  type PropertyUpdateKey<K extends string = string> = {
181
174
  key: Exclude<K, 'content'>;
@@ -134,7 +134,7 @@ abstract class BaseMacro {
134
134
  } catch (error) {
135
135
  if (error instanceof Error) {
136
136
  const errorMessage = `From card '${context.cardKey}' a macro validation error:\n\n${error.message}.\n\nCard content:\n ${input}`;
137
- throw new Error(errorMessage);
137
+ throw new Error(errorMessage, { cause: error });
138
138
  }
139
139
  throw error;
140
140
  }
@@ -13,6 +13,7 @@
13
13
 
14
14
  import BaseMacro from '../base-macro.js';
15
15
  import { createImage, validateMacroContent } from '../index.js';
16
+ import { registerComparisonHelpers } from '../../utils/handlebars-helpers.js';
16
17
  import Handlebars from 'handlebars';
17
18
  import macroMetadata from './metadata.js';
18
19
  import { ClingoError } from '@cyberismo/node-clingo';
@@ -54,6 +55,7 @@ class GraphMacro extends BaseMacro {
54
55
  };
55
56
 
56
57
  const handlebars = Handlebars.create();
58
+ registerComparisonHelpers(handlebars);
57
59
  const view = handlebars.compile(viewContent.viewTemplate)(
58
60
  handlebarsContext,
59
61
  );
@@ -69,6 +71,7 @@ class GraphMacro extends BaseMacro {
69
71
  if (error instanceof ClingoError) {
70
72
  throw new Error(
71
73
  `Error running graph in view '${options.view}' in model '${options.model}': ${error.details.errors.join('\n')}`,
74
+ { cause: error },
72
75
  );
73
76
  }
74
77
  throw error;
@@ -19,6 +19,8 @@ import macroMetadata from './metadata.js';
19
19
  import BaseMacro from '../base-macro.js';
20
20
  import type TaskQueue from '../task-queue.js';
21
21
  import { MAX_LEVEL_OFFSET } from '../../utils/constants.js';
22
+ import { escapeCsvField } from '../../utils/csv.js';
23
+ import { escapeJsonString } from '../../utils/json.js';
22
24
 
23
25
  export default class IncludeMacro extends BaseMacro {
24
26
  constructor(tasksQueue: TaskQueue) {
@@ -40,6 +42,14 @@ export default class IncludeMacro extends BaseMacro {
40
42
  if (!options.pageTitles) {
41
43
  options.pageTitles = 'normal';
42
44
  }
45
+
46
+ // Validate incompatible option combinations
47
+ if (options.escape && options.title !== 'exclude') {
48
+ throw new Error(
49
+ 'The "escape" option can only be used with "title": "exclude". ' +
50
+ 'Escaping is meant for embedding raw content in JSON/CSV documents, not for generating AsciiDoc output.',
51
+ );
52
+ }
43
53
  const newContext = {
44
54
  ...context,
45
55
  cardKey: options.cardKey,
@@ -52,6 +62,8 @@ export default class IncludeMacro extends BaseMacro {
52
62
  card.content,
53
63
  newContext,
54
64
  );
65
+
66
+ // Skip the leading newlines if trim is enabled
55
67
  const content = `\n\n${anchor}${title}${cardContent}`;
56
68
 
57
69
  let levelOffset = 0;
@@ -102,7 +114,13 @@ export default class IncludeMacro extends BaseMacro {
102
114
  context: MacroGenerationContext,
103
115
  ): Promise<string> {
104
116
  if (options.title !== 'only') {
105
- return await evaluateMacros(cardContent ?? '', context, true);
117
+ let content = await evaluateMacros(cardContent ?? '', context, true);
118
+ if (options.escape === 'json') {
119
+ content = escapeJsonString(content);
120
+ } else if (options.escape === 'csv') {
121
+ content = escapeCsvField(content);
122
+ }
123
+ return content;
106
124
  }
107
125
  return '';
108
126
  }
@@ -12,21 +12,23 @@
12
12
  License along with this program. If not, see <https://www.gnu.org/licenses/>.
13
13
  */
14
14
 
15
+ /**
16
+ * Include macro options.
17
+ * @param cardKey Card key of the card being included
18
+ * @param levelOffset A positive number will increase the level of headings.
19
+ * A negative value will decrease the level of headings in the included content.
20
+ * @param title Determines behaviour with the title that is in card metadata
21
+ * include --> includes the title
22
+ * exclude --> excludes the title
23
+ * only --> includes title but does not import content
24
+ * @param whitespace Whether to trim leading and trailing whitespace
25
+ * @param escape Type of escaping to apply to the included content
26
+ * json --> escapes for JSON strings
27
+ * csv --> escapes for CSV fields
28
+ */
15
29
  export interface IncludeMacroOptions {
16
- /**
17
- * Card key of the card being included
18
- */
19
30
  cardKey: string;
20
- /**
21
- * A positive number wil increase the level of headings and a negative alue will the level of headings in the included content
22
- */
23
31
  levelOffset?: string;
24
- /**
25
- * Determines behaviour with the title that is in card metadata
26
- * include --> includes the title
27
- * exclude --> excludes the title
28
- * only --> includes title but does not import content
29
- */
30
32
  title?: 'include' | 'exclude' | 'only';
31
33
  pageTitles?: 'normal' | 'discrete';
32
34
  /**
@@ -35,4 +37,5 @@ export interface IncludeMacroOptions {
35
37
  * Default is 'keep'.
36
38
  */
37
39
  whitespace?: 'keep' | 'trim';
40
+ escape?: 'json' | 'csv';
38
41
  }
@@ -175,7 +175,9 @@ export function validateMacroContent<T>(
175
175
  if (error instanceof DHValidationError) {
176
176
  message = `${error.errors?.map((e) => e.message).join(', ')}`;
177
177
  }
178
- throw new Error(`${macro.name} macro JSON validation error: ${message}`);
178
+ throw new Error(`${macro.name} macro JSON validation error: ${message}`, {
179
+ cause: error,
180
+ });
179
181
  }
180
182
  }
181
183
 
@@ -249,6 +251,7 @@ export async function evaluateMacros(
249
251
  try {
250
252
  const compiled = handlebars.compile(preprocessRawBlocks(result), {
251
253
  strict: true,
254
+ ignoreStandalone: true,
252
255
  });
253
256
  result = compiled({ cardKey: context.cardKey });
254
257
 
@@ -56,6 +56,7 @@ class ReportMacro extends BaseMacro {
56
56
  if (error instanceof ClingoError) {
57
57
  throw new Error(
58
58
  `Error running logic program in report '${options.name}':${error.details.errors.join('\n')}`,
59
+ { cause: error },
59
60
  );
60
61
  }
61
62
  throw error;