@cyberismo/data-handler 0.0.11 → 0.0.12

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 (128) 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 +13 -8
  4. package/dist/command-handler.js +47 -7
  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/index.d.ts +2 -1
  19. package/dist/commands/index.js +2 -1
  20. package/dist/commands/index.js.map +1 -1
  21. package/dist/commands/remove.d.ts +1 -0
  22. package/dist/commands/remove.js +16 -12
  23. package/dist/commands/remove.js.map +1 -1
  24. package/dist/commands/rename.js +4 -6
  25. package/dist/commands/rename.js.map +1 -1
  26. package/dist/commands/show.d.ts +18 -1
  27. package/dist/commands/show.js +52 -0
  28. package/dist/commands/show.js.map +1 -1
  29. package/dist/commands/validate.js +7 -8
  30. package/dist/commands/validate.js.map +1 -1
  31. package/dist/containers/project/calculation-engine.d.ts +8 -0
  32. package/dist/containers/project/calculation-engine.js +20 -9
  33. package/dist/containers/project/calculation-engine.js.map +1 -1
  34. package/dist/containers/project.d.ts +19 -8
  35. package/dist/containers/project.js +52 -34
  36. package/dist/containers/project.js.map +1 -1
  37. package/dist/containers/template.js +1 -1
  38. package/dist/containers/template.js.map +1 -1
  39. package/dist/interfaces/project-interfaces.d.ts +13 -2
  40. package/dist/interfaces/project-interfaces.js.map +1 -1
  41. package/dist/macros/base-macro.d.ts +1 -1
  42. package/dist/macros/base-macro.js +1 -1
  43. package/dist/macros/base-macro.js.map +1 -1
  44. package/dist/macros/createCards/index.d.ts +1 -1
  45. package/dist/macros/createCards/index.js +1 -1
  46. package/dist/macros/createCards/index.js.map +1 -1
  47. package/dist/macros/graph/index.d.ts +1 -1
  48. package/dist/macros/graph/index.js +21 -29
  49. package/dist/macros/graph/index.js.map +1 -1
  50. package/dist/macros/image/index.d.ts +1 -1
  51. package/dist/macros/image/index.js +1 -7
  52. package/dist/macros/image/index.js.map +1 -1
  53. package/dist/macros/include/index.d.ts +1 -1
  54. package/dist/macros/include/index.js +1 -1
  55. package/dist/macros/include/index.js.map +1 -1
  56. package/dist/macros/index.d.ts +12 -5
  57. package/dist/macros/index.js +19 -7
  58. package/dist/macros/index.js.map +1 -1
  59. package/dist/macros/percentage/index.d.ts +1 -1
  60. package/dist/macros/percentage/index.js +1 -1
  61. package/dist/macros/percentage/index.js.map +1 -1
  62. package/dist/macros/report/index.d.ts +1 -1
  63. package/dist/macros/report/index.js +1 -1
  64. package/dist/macros/report/index.js.map +1 -1
  65. package/dist/macros/scoreCard/index.d.ts +1 -1
  66. package/dist/macros/scoreCard/index.js +1 -1
  67. package/dist/macros/scoreCard/index.js.map +1 -1
  68. package/dist/macros/vega/index.d.ts +1 -1
  69. package/dist/macros/vega/index.js +1 -1
  70. package/dist/macros/vega/index.js.map +1 -1
  71. package/dist/macros/vegalite/index.d.ts +1 -1
  72. package/dist/macros/vegalite/index.js +1 -1
  73. package/dist/macros/vegalite/index.js.map +1 -1
  74. package/dist/macros/xref/index.d.ts +1 -1
  75. package/dist/macros/xref/index.js +1 -1
  76. package/dist/macros/xref/index.js.map +1 -1
  77. package/dist/project-settings.d.ts +14 -1
  78. package/dist/project-settings.js +51 -1
  79. package/dist/project-settings.js.map +1 -1
  80. package/dist/resources/field-type-resource.d.ts +5 -0
  81. package/dist/resources/field-type-resource.js +8 -3
  82. package/dist/resources/field-type-resource.js.map +1 -1
  83. package/dist/resources/graph-view-resource.js +3 -5
  84. package/dist/resources/graph-view-resource.js.map +1 -1
  85. package/dist/resources/workflow-resource.js +6 -5
  86. package/dist/resources/workflow-resource.js.map +1 -1
  87. package/dist/utils/log-utils.js +1 -1
  88. package/dist/utils/log-utils.js.map +1 -1
  89. package/dist/utils/report.js +6 -0
  90. package/dist/utils/report.js.map +1 -1
  91. package/dist/utils/resource-utils.d.ts +8 -0
  92. package/dist/utils/resource-utils.js +11 -0
  93. package/dist/utils/resource-utils.js.map +1 -1
  94. package/package.json +7 -9
  95. package/src/card-metadata-updater.ts +1 -1
  96. package/src/command-handler.ts +70 -15
  97. package/src/command-manager.ts +4 -1
  98. package/src/commands/calculate.ts +18 -0
  99. package/src/commands/create.ts +31 -19
  100. package/src/commands/fetch.ts +152 -0
  101. package/src/commands/index.ts +2 -0
  102. package/src/commands/remove.ts +18 -12
  103. package/src/commands/rename.ts +11 -11
  104. package/src/commands/show.ts +68 -0
  105. package/src/commands/validate.ts +7 -8
  106. package/src/containers/project/calculation-engine.ts +26 -8
  107. package/src/containers/project.ts +71 -61
  108. package/src/containers/template.ts +1 -1
  109. package/src/interfaces/project-interfaces.ts +18 -0
  110. package/src/macros/base-macro.ts +5 -2
  111. package/src/macros/createCards/index.ts +1 -1
  112. package/src/macros/graph/index.ts +47 -51
  113. package/src/macros/image/index.ts +1 -7
  114. package/src/macros/include/index.ts +1 -1
  115. package/src/macros/index.ts +19 -7
  116. package/src/macros/percentage/index.ts +1 -1
  117. package/src/macros/report/index.ts +1 -1
  118. package/src/macros/scoreCard/index.ts +1 -1
  119. package/src/macros/vega/index.ts +1 -1
  120. package/src/macros/vegalite/index.ts +1 -1
  121. package/src/macros/xref/index.ts +1 -1
  122. package/src/project-settings.ts +62 -1
  123. package/src/resources/field-type-resource.ts +8 -3
  124. package/src/resources/graph-view-resource.ts +7 -5
  125. package/src/resources/workflow-resource.ts +7 -6
  126. package/src/utils/log-utils.ts +1 -1
  127. package/src/utils/report.ts +6 -0
  128. package/src/utils/resource-utils.ts +16 -0
@@ -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
+ }
@@ -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) => {
@@ -369,6 +381,54 @@ export class Show {
369
381
  }
370
382
  return resource.showFileNames();
371
383
  }
384
+
385
+ /**
386
+ * Shows importable modules.
387
+ * @param showAll - When true, shows all importable modules, even if they have already been imported
388
+ * @param showDetails - When true, shows all properties of modules, not just name.
389
+ * @returns list of modules; the list content depends on the parameters provided
390
+ * by default it is a list of module names that could be imported into the project,
391
+ * with 'showDetails' true, instead of name, the list consists of full details of the modules
392
+ * with 'showAll' true, the list consists of all modules in the hubs, even if they have already been imported
393
+ * Note that the two boolean options can be combined.
394
+ */
395
+ public async showImportableModules(
396
+ showAll?: boolean,
397
+ showDetails?: boolean,
398
+ ): Promise<ModuleSettingFromHub[]> {
399
+ try {
400
+ const moduleList = (
401
+ await readJsonFile(
402
+ resolve(this.project.basePath, MODULE_LIST_FULL_PATH),
403
+ )
404
+ ).modules;
405
+ const currentModules = await this.project.modules();
406
+ const nonImportedModules = moduleList.filter(
407
+ (item: ModuleSettingFromHub) => {
408
+ return !currentModules.some((module) => item.name === module.name);
409
+ },
410
+ );
411
+
412
+ if (showAll && showDetails) {
413
+ return moduleList;
414
+ }
415
+ if (showAll) {
416
+ return moduleList?.map((item: ModuleSettingFromHub) => item?.name);
417
+ }
418
+ if (showDetails) {
419
+ return nonImportedModules;
420
+ }
421
+ // By default return the non-imported modules
422
+ return nonImportedModules.map((item: ModuleSettingFromHub) => item?.name);
423
+ } catch (error) {
424
+ if (error instanceof Error) {
425
+ this.logger.error(error.message);
426
+ }
427
+ // Module list doesn't exist, return empty list
428
+ return [];
429
+ }
430
+ }
431
+
372
432
  /**
373
433
  * Returns all unique labels in a project
374
434
  * @returns labels in a list
@@ -406,6 +466,14 @@ export class Show {
406
466
  return moduleDetails;
407
467
  }
408
468
 
469
+ /**
470
+ * Shows hubs of the project.
471
+ * @returns list of hubs.
472
+ */
473
+ public showHubs(): HubSetting[] {
474
+ return this.project.configuration.hubs;
475
+ }
476
+
409
477
  /**
410
478
  * Returns all project cards in the project. Cards don't have content and nor metadata.
411
479
  * @note AppUi uses this method.
@@ -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 {
@@ -606,17 +605,17 @@ export class Validate {
606
605
  });
607
606
  }
608
607
  }
609
- if (errorMsg.length) {
610
- validationErrors += errorMsg
611
- .filter(this.removeDuplicateEntries)
612
- .join('\n');
613
- }
614
608
  // Validate that there are no duplicate card keys
615
609
  for (const [key, count] of cardIds) {
616
610
  if (count > 1) {
617
- validationErrors += `Duplicate card key '${key}' found ${count} times\n`;
611
+ errorMsg.push(`Duplicate card key '${key}' found ${count} times`);
618
612
  }
619
613
  }
614
+ if (errorMsg.length) {
615
+ validationErrors += errorMsg
616
+ .filter(this.removeDuplicateEntries)
617
+ .join('\n');
618
+ }
620
619
  }
621
620
  } catch (error) {
622
621
  validationErrors += errorFunction(error);
@@ -13,7 +13,7 @@
13
13
 
14
14
  // node
15
15
  import { basename, join, resolve } from 'node:path';
16
- import { readFile } from 'node:fs/promises';
16
+ import { readFile, writeFile } from 'node:fs/promises';
17
17
 
18
18
  import { sanitizeSvgBase64 } from '../../utils/sanitize-svg.js';
19
19
  import { instance } from '@viz-js/viz';
@@ -57,6 +57,7 @@ import {
57
57
  solve,
58
58
  setProgram,
59
59
  removeProgram,
60
+ buildProgram,
60
61
  } from '@cyberismo/node-clingo';
61
62
  import { generateReportContent } from '../../utils/report.js';
62
63
  import { lpFiles, graphvizReport } from '@cyberismo/assets';
@@ -110,6 +111,22 @@ export class CalculationEngine {
110
111
  return createCardFacts(card, this.project);
111
112
  }
112
113
 
114
+ /**
115
+ * Exports logic program to a given file
116
+ * @param destination Destination file path
117
+ * @param programs Programs or categories to export
118
+ * @param query Query to export, if not provided, all programs will be exported
119
+ */
120
+ public async exportLogicProgram(
121
+ destination: string,
122
+ programs: string[],
123
+ query?: QueryName,
124
+ ) {
125
+ let logicProgram = query ? this.queryContent(query) : '';
126
+ logicProgram += await buildProgram('', programs);
127
+ await writeFile(destination, logicProgram);
128
+ }
129
+
113
130
  // // Wrapper to run onCreation query.
114
131
  private async creationQuery(cardKeys: string[], context: Context) {
115
132
  if (!cardKeys) return undefined;
@@ -503,6 +520,13 @@ export class CalculationEngine {
503
520
  return this.parseClingoResult(clingoOutput);
504
521
  }
505
522
 
523
+ private queryContent(queryName: QueryName, options?: unknown) {
524
+ const content = lpFiles.queries[queryName];
525
+ const handlebars = Handlebars.create();
526
+ const compiled = handlebars.compile(content);
527
+ return compiled(options || {});
528
+ }
529
+
506
530
  /**
507
531
  * Runs a pre-defined query.
508
532
  * @param queryName Name of the query file without extension
@@ -514,13 +538,7 @@ export class CalculationEngine {
514
538
  context: Context = 'localApp',
515
539
  options?: unknown,
516
540
  ): Promise<QueryResult<T>[]> {
517
- let content = lpFiles.queries[queryName];
518
- const handlebars = Handlebars.create();
519
- const compiled = handlebars.compile(content);
520
- content = compiled(options || {});
521
- if (!content) {
522
- throw new Error(`Query file ${queryName} not found`);
523
- }
541
+ const content = this.queryContent(queryName, options);
524
542
 
525
543
  this.logger.trace({ queryName }, 'Running query');
526
544
  const clingoOutput = await this.run(content, context);