@cyberismo/data-handler 0.0.7 → 0.0.8

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 (52) hide show
  1. package/dist/commands/create.d.ts +1 -1
  2. package/dist/commands/create.js +15 -10
  3. package/dist/commands/create.js.map +1 -1
  4. package/dist/commands/import.js +8 -2
  5. package/dist/commands/import.js.map +1 -1
  6. package/dist/commands/remove.d.ts +1 -1
  7. package/dist/commands/remove.js +4 -10
  8. package/dist/commands/remove.js.map +1 -1
  9. package/dist/commands/validate.d.ts +0 -8
  10. package/dist/commands/validate.js +0 -32
  11. package/dist/commands/validate.js.map +1 -1
  12. package/dist/containers/project/resource-collector.d.ts +2 -1
  13. package/dist/containers/project/resource-collector.js +33 -23
  14. package/dist/containers/project/resource-collector.js.map +1 -1
  15. package/dist/containers/project.d.ts +0 -5
  16. package/dist/containers/project.js +9 -17
  17. package/dist/containers/project.js.map +1 -1
  18. package/dist/exceptions/index.d.ts +20 -0
  19. package/dist/exceptions/index.js +16 -0
  20. package/dist/exceptions/index.js.map +1 -1
  21. package/dist/interfaces/macros.d.ts +3 -0
  22. package/dist/macros/base-macro.d.ts +2 -0
  23. package/dist/macros/base-macro.js +66 -19
  24. package/dist/macros/base-macro.js.map +1 -1
  25. package/dist/macros/graph/index.d.ts +0 -1
  26. package/dist/macros/graph/index.js +11 -7
  27. package/dist/macros/graph/index.js.map +1 -1
  28. package/dist/macros/index.d.ts +6 -0
  29. package/dist/macros/index.js +25 -2
  30. package/dist/macros/index.js.map +1 -1
  31. package/dist/macros/report/index.js +18 -9
  32. package/dist/macros/report/index.js.map +1 -1
  33. package/dist/module-manager.d.ts +16 -4
  34. package/dist/module-manager.js +179 -55
  35. package/dist/module-manager.js.map +1 -1
  36. package/dist/project-settings.js +2 -8
  37. package/dist/project-settings.js.map +1 -1
  38. package/package.json +3 -4
  39. package/src/commands/create.ts +18 -10
  40. package/src/commands/import.ts +15 -2
  41. package/src/commands/remove.ts +7 -12
  42. package/src/commands/validate.ts +0 -35
  43. package/src/containers/project/resource-collector.ts +33 -14
  44. package/src/containers/project.ts +36 -18
  45. package/src/exceptions/index.ts +36 -0
  46. package/src/interfaces/macros.ts +3 -0
  47. package/src/macros/base-macro.ts +89 -25
  48. package/src/macros/graph/index.ts +12 -7
  49. package/src/macros/index.ts +29 -2
  50. package/src/macros/report/index.ts +19 -10
  51. package/src/module-manager.ts +228 -66
  52. package/src/project-settings.ts +2 -11
@@ -23,6 +23,7 @@ import type TaskQueue from '../task-queue.js';
23
23
  import { ReportResource } from '../../resources/report-resource.js';
24
24
  import { resourceName } from '../../utils/resource-utils.js';
25
25
  import { generateReportContent } from '../../utils/report.js';
26
+ import { ClingoError } from '@cyberismo/node-clingo';
26
27
 
27
28
  export interface ReportOptions extends MacroOptions {
28
29
  name: string;
@@ -58,16 +59,24 @@ class ReportMacro extends BaseMacro {
58
59
  schema: report.schema,
59
60
  });
60
61
  }
61
-
62
- return generateReportContent({
63
- calculate: this.calculate,
64
- contentTemplate: report.contentTemplate,
65
- queryTemplate: report.queryTemplate,
66
- options: {
67
- cardKey: context.cardKey,
68
- ...options,
69
- },
70
- });
62
+ try {
63
+ return await generateReportContent({
64
+ calculate: this.calculate,
65
+ contentTemplate: report.contentTemplate,
66
+ queryTemplate: report.queryTemplate,
67
+ options: {
68
+ cardKey: context.cardKey,
69
+ ...options,
70
+ },
71
+ });
72
+ } catch (error) {
73
+ if (error instanceof ClingoError) {
74
+ throw new Error(
75
+ `Error running logic program in report '${options.name}':${error.details.errors.join('\n')}`,
76
+ );
77
+ }
78
+ throw error;
79
+ }
71
80
  };
72
81
 
73
82
  private validate(data: unknown): ReportOptions {
@@ -29,17 +29,24 @@ import { readJsonFile } from './utils/json.js';
29
29
  import { Validate } from './commands/index.js';
30
30
 
31
31
  const FILE_PROTOCOL = 'file:';
32
- // todo: add support for git's default branch.
33
- const MAIN_BRANCH = 'main';
32
+ const HTTPS_PROTOCOL = 'https:';
33
+
34
34
  // timeout in milliseconds for git client (no stdout / stderr activity)
35
35
  const DEFAULT_TIMEOUT = 10000;
36
36
 
37
+ // When dependencies are built to a map, use map that has
38
+ // key: module name,
39
+ // value: list of unique prefixes
40
+ type DependencyGraph = Map<string, Set<string>>;
41
+
37
42
  /**
38
43
  * Class that handles module updates and imports.
39
44
  */
40
45
  export class ModuleManager {
41
46
  private modules: ModuleSetting[] = [];
42
47
  private tempModulesDir: string = '';
48
+ private defaultBranchCache: Map<string, string> = new Map();
49
+
43
50
  constructor(private project: Project) {
44
51
  this.tempModulesDir = join(this.project.paths.tempFolder, 'modules');
45
52
  }
@@ -53,6 +60,35 @@ export class ModuleManager {
53
60
  await this.project.collectModuleResources();
54
61
  }
55
62
 
63
+ // Creates a map of what dependencies each module depend from.
64
+ private async buildDependencyGraph(
65
+ dependencies: ModuleSetting[],
66
+ ): Promise<DependencyGraph> {
67
+ const dependencyNames = dependencies.map((item) => item.name);
68
+ const dependencyGraph = new Map<string, Set<string>>() as DependencyGraph;
69
+ for (const dependency of dependencyNames) {
70
+ dependencyGraph.set(
71
+ dependency,
72
+ await this.transientDependencies(dependency),
73
+ );
74
+ }
75
+ return dependencyGraph;
76
+ }
77
+
78
+ // Returns 'true' if 'moduleName' can be removed.
79
+ private canBeRemoved(
80
+ dependencies: DependencyGraph,
81
+ moduleName: string,
82
+ ): boolean {
83
+ let unused = true;
84
+ dependencies.forEach((transientDependencies, key) => {
85
+ if (key !== moduleName && transientDependencies.has(moduleName)) {
86
+ unused = false;
87
+ }
88
+ });
89
+ return unused;
90
+ }
91
+
56
92
  // Handles cloning of a repository.
57
93
  private async clone(
58
94
  module: ModuleSetting,
@@ -67,10 +103,15 @@ export class ModuleManager {
67
103
 
68
104
  let remote = module.location;
69
105
  if (module.private) {
70
- if (credentials && credentials?.username && credentials?.token) {
106
+ if (
107
+ credentials &&
108
+ credentials?.username &&
109
+ credentials?.token &&
110
+ module.location.startsWith(HTTPS_PROTOCOL)
111
+ ) {
71
112
  if (verbose) {
72
113
  console.log(
73
- `... Using credentials '${credentials?.username}' for cloning '${module.name}'`,
114
+ `... Using HTTPS with credentials '${credentials?.username}' for cloning '${module.name}'`,
74
115
  );
75
116
  }
76
117
  try {
@@ -83,16 +124,22 @@ export class ModuleManager {
83
124
  } catch {
84
125
  throw new Error(`Invalid repository URL: ${module.location}`);
85
126
  }
86
- } else {
127
+ } else if (module.location.startsWith('git@')) {
87
128
  if (verbose) {
88
- console.log(`... Not using credentials for cloning '${module.name}'`);
129
+ console.log(`... Using SSH for cloning '${module.name}'`);
89
130
  }
90
131
  }
132
+ } else {
133
+ if (verbose) {
134
+ console.log(
135
+ `... Using HTTPS without credentials for cloning '${module.name}'`,
136
+ );
137
+ }
91
138
  }
92
139
 
93
140
  try {
94
141
  await mkdir(this.tempModulesDir, { recursive: true });
95
- const cloneOptions = this.setCloneOptions(module);
142
+ const cloneOptions = await this.setCloneOptions(module);
96
143
  await rm(destinationPath, { recursive: true, force: true });
97
144
 
98
145
  const git: SimpleGit = simpleGit({
@@ -110,7 +157,6 @@ export class ModuleManager {
110
157
  console.log(`... Cloned '${module.name}' to a temporary folder`);
111
158
  }
112
159
  } catch (error) {
113
- console.error(error);
114
160
  if (error instanceof Error)
115
161
  throw new Error(
116
162
  `Failed to clone module '${module.name}': ${error.message}`,
@@ -143,6 +189,48 @@ export class ModuleManager {
143
189
  }
144
190
  }
145
191
 
192
+ // Gets the default branch for a repository from remote or cache
193
+ private async defaultBranch(module: ModuleSetting): Promise<string> {
194
+ if (this.defaultBranchCache.has(module.location)) {
195
+ return this.defaultBranchCache.get(module.location)!;
196
+ }
197
+ // Set the default branch if branch was not specified
198
+ if (!module.branch) {
199
+ const destinationPath = join(this.tempModulesDir, module.name);
200
+ // Only return path after cloning
201
+ if (pathExists(destinationPath)) {
202
+ const git: SimpleGit = simpleGit({
203
+ timeout: {
204
+ block: DEFAULT_TIMEOUT,
205
+ },
206
+ });
207
+ const options = ['--abbrev-ref', 'HEAD'];
208
+ const defaultBranch = await git.cwd(destinationPath).revparse(options);
209
+ this.defaultBranchCache.set(module.location, defaultBranch);
210
+ return defaultBranch;
211
+ }
212
+ }
213
+ // The actual default branch will be updated later (after cloning).
214
+ return '';
215
+ }
216
+
217
+ // Fetches direct dependencies of a module.
218
+ private async dependencies(moduleName: string): Promise<Set<string>> {
219
+ const allModules = await this.project.modules();
220
+ if (!allModules) return new Set();
221
+ const module = allModules.find((m) => m.name === moduleName);
222
+ if (!module) {
223
+ throw new Error(`Module '${moduleName}' not found`);
224
+ }
225
+ const modulePath = join(module.path, module.name, 'cardsConfig.json');
226
+ const moduleConfiguration = (await readJsonFile(
227
+ modulePath,
228
+ )) as ProjectConfiguration;
229
+ return moduleConfiguration.modules
230
+ ? new Set(moduleConfiguration.modules.map((m) => m.name))
231
+ : new Set();
232
+ }
233
+
146
234
  // Collects one module's dependency prefixes to 'this.modules'.
147
235
  // Note that there can be duplicate entries.
148
236
  private async doCollectModulePrefix(
@@ -167,16 +255,17 @@ export class ModuleManager {
167
255
 
168
256
  // Updates one module that is read from local file system.
169
257
  private async handleFileModule(module: ModuleSetting) {
170
- this.removeProtocolFromLocation(module);
258
+ this.stripProtocolFromLocation(module);
171
259
  await this.remove(module);
172
- await this.importFromFolder(module);
260
+ await this.importFromFolder(module.location, module.name);
173
261
  }
174
262
 
175
263
  // Updates one module that is received from Git.
176
264
  private async handleGitModule(module: ModuleSetting) {
177
265
  await this.clone(module);
266
+ const tempLocation = join(this.tempModulesDir, module.name);
178
267
  await this.remove(module);
179
- await this.importFromTemp(module);
268
+ await this.importFromFolder(tempLocation, module.name);
180
269
  }
181
270
 
182
271
  // Updates one module.
@@ -186,19 +275,11 @@ export class ModuleManager {
186
275
  : this.handleFileModule(module);
187
276
  }
188
277
 
189
- // Handles importing a module from module settings 'location'
190
- private async importFromFolder(module: ModuleSetting) {
191
- await this.importFileModule(module.location);
192
- console.log(
193
- `... Imported module '${module.name}' to '${this.project.configuration.name}'`,
194
- );
195
- }
196
-
197
- // Handles importing a module from '.temp' folder
198
- private async importFromTemp(module: ModuleSetting) {
199
- await this.importFileModule(join(this.tempModulesDir, module.name));
278
+ // Imports from a given folder. Is used both for .temp/<module name> and file locations.
279
+ private async importFromFolder(path: string, name: string) {
280
+ await this.importFileModule(path);
200
281
  console.log(
201
- `... Imported module '${module.name}' to '${this.project.configuration.name}'`,
282
+ `... Imported module '${name}' to '${this.project.configuration.name}'`,
202
283
  );
203
284
  }
204
285
 
@@ -211,7 +292,41 @@ export class ModuleManager {
211
292
  // Returns true if module is imported from git.
212
293
  private isGitModule(module: ModuleSetting): boolean {
213
294
  if (!module.location) return false;
214
- return module.location.startsWith('https:');
295
+ return (
296
+ module.location.startsWith('https:') || module.location.startsWith('git@')
297
+ );
298
+ }
299
+
300
+ // Collect modules that could be removed from .cards/modules when
301
+ // 'moduleName' is removed.
302
+ private async orphanedModules(
303
+ dependencies: DependencyGraph,
304
+ moduleName: string,
305
+ ): Promise<string[]> {
306
+ const projectModules = this.project.configuration.modules;
307
+ const removableTransientModules: string[] = [];
308
+ if (dependencies.has(moduleName)) {
309
+ const deps = dependencies.get(moduleName);
310
+ for (const dependency of deps!) {
311
+ const projectDependency = projectModules.some(
312
+ (item) => item.name === dependency,
313
+ );
314
+ if (projectDependency) continue;
315
+
316
+ let orphanModule = true;
317
+ dependencies.forEach((transientDependencies, key) => {
318
+ if (key === moduleName) return;
319
+ if (transientDependencies.has(dependency)) {
320
+ orphanModule = false;
321
+ }
322
+ });
323
+
324
+ if (orphanModule) {
325
+ removableTransientModules.push(dependency);
326
+ }
327
+ }
328
+ }
329
+ return removableTransientModules;
215
330
  }
216
331
 
217
332
  // Prepares '.temp/modules' for cloning
@@ -231,11 +346,6 @@ export class ModuleManager {
231
346
  }
232
347
  }
233
348
 
234
- // Returns whether to use git or file system for handling the module.
235
- private protocol(module: ModuleSetting) {
236
- return this.isFileModule(module) ? 'file' : 'git';
237
- }
238
-
239
349
  // Handles removing an imported module.
240
350
  private async remove(module: ModuleSetting) {
241
351
  try {
@@ -249,37 +359,19 @@ export class ModuleManager {
249
359
  }
250
360
  }
251
361
 
252
- // Remove module files.
253
- private async removeModuleFiles(moduleName: string) {
254
- const module = await this.project.module(moduleName);
255
- if (!module) {
256
- throw new Error(`Module '${moduleName}' not found`);
257
- }
258
- await deleteDir(module.path);
259
- }
260
-
261
- // Updates module's 'location' not to have 'protocol:' in the beginning (only for "file:" needed).
262
- private removeProtocolFromLocation(module: ModuleSetting) {
263
- const protocol = this.protocol(module);
264
- module.location = module.location.substring(
265
- protocol.length + 1,
266
- module.location.length,
267
- );
268
- }
269
-
270
362
  // Checks for duplicate ModuleSetting entries and throws an error if modules
271
363
  // with the same name have different branches or locations.
272
- // Treats undefined branch, empty string branch, and "main" branch as equivalent.
364
+ // Treats undefined branch, empty string branch, and default branch as equivalent.
273
365
  // Returns an array with duplicate entries removed
274
- private removeDuplicates(modules: ModuleSetting[]): ModuleSetting[] {
366
+ private async removeDuplicates(
367
+ modules: ModuleSetting[],
368
+ ): Promise<ModuleSetting[]> {
275
369
  const moduleMap = new Map<string, ModuleSetting>();
276
370
 
277
- // Assume that empty, or missing branch means 'main'
278
- const normalizeBranch = (branch: string | undefined): string => {
279
- if (!branch || branch === '' || branch === MAIN_BRANCH) {
280
- return MAIN_BRANCH;
281
- }
282
- return branch;
371
+ // Normalize branch names by checking against the default branch for each module
372
+
373
+ const normalizeBranch = async (module: ModuleSetting) => {
374
+ return module.branch ? module.branch! : await this.defaultBranch(module);
283
375
  };
284
376
 
285
377
  for (const module of modules) {
@@ -291,8 +383,8 @@ export class ModuleManager {
291
383
  ) {
292
384
  throw new Error(
293
385
  `Module conflict: '${module.name}' has different access:\n` +
294
- ` - ${existingModule.private === true ? 'true' : 'false'}\n` +
295
- ` - ${module.private === true ? 'true' : 'false'}`,
386
+ ` - ${Boolean(existingModule.private)}\n` +
387
+ ` - ${Boolean(module.private)}`,
296
388
  );
297
389
  }
298
390
  if (existingModule.location !== module.location) {
@@ -302,8 +394,8 @@ export class ModuleManager {
302
394
  ` - ${module.location}`,
303
395
  );
304
396
  }
305
- const existingBranch = normalizeBranch(existingModule.branch);
306
- const newBranch = normalizeBranch(module.branch);
397
+ const existingBranch = await normalizeBranch(existingModule);
398
+ const newBranch = await normalizeBranch(module);
307
399
 
308
400
  if (existingBranch !== newBranch) {
309
401
  throw new Error(
@@ -319,6 +411,15 @@ export class ModuleManager {
319
411
  return Array.from(moduleMap.values());
320
412
  }
321
413
 
414
+ // Remove module files.
415
+ private async removeModuleFiles(moduleName: string) {
416
+ const module = await this.project.module(moduleName);
417
+ if (!module) {
418
+ throw new Error(`Module '${moduleName}' not found`);
419
+ }
420
+ await deleteDir(module.path);
421
+ }
422
+
322
423
  // Gets repository name from gitUrl
323
424
  private repositoryName(gitUrl: string): string {
324
425
  const last = gitUrl.lastIndexOf('/');
@@ -326,19 +427,47 @@ export class ModuleManager {
326
427
  return repoName;
327
428
  }
328
429
 
329
- // Sets cloning options.
330
- private setCloneOptions(module: ModuleSetting) {
430
+ // Sets cloning options with support for default branch.
431
+ private async setCloneOptions(module: ModuleSetting): Promise<string[]> {
331
432
  const cloneOptions = ['--depth', '1'];
433
+ const defaultBranch = await this.defaultBranch(module);
434
+ // Only specify branch if it's different from the default branch
332
435
  if (
333
436
  module.branch &&
334
437
  module.branch !== '' &&
335
- module.branch !== MAIN_BRANCH
438
+ module.branch !== defaultBranch
336
439
  ) {
337
440
  cloneOptions.push('--branch', module.branch);
338
441
  }
339
442
  return cloneOptions;
340
443
  }
341
444
 
445
+ // Updates module's 'location' not to have 'protocol:' in the beginning (only for "file:" needed).
446
+ private stripProtocolFromLocation(module: ModuleSetting) {
447
+ const protocol = this.isFileModule(module) ? 'file' : 'git';
448
+ module.location = module.location.substring(
449
+ protocol.length + 1,
450
+ module.location.length,
451
+ );
452
+ }
453
+
454
+ // Fetches all dependencies for a module.
455
+ private async transientDependencies(
456
+ moduleName: string,
457
+ ): Promise<Set<string>> {
458
+ const dependencies = await this.dependencies(moduleName);
459
+ let transientDependencies: Set<string> = new Set(dependencies);
460
+ for (const dependency of dependencies) {
461
+ const depTransients = await this.transientDependencies(dependency);
462
+ transientDependencies = new Set([
463
+ ...transientDependencies,
464
+ ...depTransients,
465
+ ]);
466
+ }
467
+
468
+ return transientDependencies;
469
+ }
470
+
342
471
  // Updates modules in the project.
343
472
  private async update(module?: ModuleSetting, credentials?: Credentials) {
344
473
  // Prints dots every half second so that user knows that something is ongoing
@@ -350,7 +479,9 @@ export class ModuleManager {
350
479
  // Stops the above, and shows results
351
480
  function finished(interval: NodeJS.Timeout, modules: string[]) {
352
481
  clearInterval(interval);
353
- console.log(`\n... Found modules: ${modules.join(', ')}`);
482
+ if (modules.length > 0) {
483
+ console.log(`\n... Found modules: ${modules.join(', ')}`);
484
+ }
354
485
  }
355
486
 
356
487
  await this.prepare();
@@ -366,8 +497,7 @@ export class ModuleManager {
366
497
  let uniqueModules: ModuleSetting[] = [];
367
498
  try {
368
499
  await this.collectModulePrefixes(modules, credentials);
369
-
370
- uniqueModules = this.removeDuplicates(this.modules);
500
+ uniqueModules = await this.removeDuplicates(this.modules);
371
501
  } finally {
372
502
  finished(
373
503
  dotInterval,
@@ -380,7 +510,6 @@ export class ModuleManager {
380
510
  promises.push(this.handleModule(module)),
381
511
  );
382
512
  await Promise.all(promises);
383
-
384
513
  await deleteDir(this.tempModulesDir);
385
514
  await this.project.collectModuleResources();
386
515
  }
@@ -467,6 +596,39 @@ export class ModuleManager {
467
596
  return modulePrefix;
468
597
  }
469
598
 
599
+ /**
600
+ * Removed module from project.
601
+ * If module is not used by any other modules, then will remove the module from disk as well.
602
+ * Otherwise, only updates project configuration.
603
+ * @param moduleName Name of the module to remove
604
+ */
605
+ public async removeModule(moduleName: string) {
606
+ const projectModules = this.project.configuration.modules;
607
+ const dependencies = await this.buildDependencyGraph(projectModules);
608
+ const module = await this.project.module(moduleName);
609
+
610
+ if (!module) {
611
+ throw new Error(`Module '${moduleName}' not found`);
612
+ }
613
+
614
+ // Project module can always be removed from project configuration,
615
+ // but modules under .cards/modules must be checked not to be used by
616
+ // other modules.
617
+ if (this.canBeRemoved(dependencies, moduleName)) {
618
+ const orphans = await this.orphanedModules(dependencies, moduleName);
619
+ await deleteDir(module.path);
620
+ for (const moduleToDelete of orphans) {
621
+ const modulePath = join(
622
+ this.project.paths.modulesFolder,
623
+ moduleToDelete,
624
+ );
625
+ await deleteDir(modulePath);
626
+ }
627
+ await this.project.collectModuleResources();
628
+ }
629
+ await this.project.configuration.removeModule(moduleName);
630
+ }
631
+
470
632
  /**
471
633
  * Imports module from a local file path or a git URL.
472
634
  * @param module Module to update. If not provided, updates all modules.
@@ -57,18 +57,9 @@ export class ProjectConfiguration implements ProjectSettings {
57
57
 
58
58
  // Sets configuration values from file.
59
59
  private readSettings() {
60
- let settings;
61
- try {
62
- settings = readJsonFileSync(this.settingPath) as ProjectConfiguration;
63
- } catch {
64
- throw new Error(
65
- `Invalid path '${this.settingPath}' to configuration file`,
66
- );
67
- }
60
+ const settings = readJsonFileSync(this.settingPath) as ProjectConfiguration;
68
61
  if (!settings) {
69
- throw new Error(
70
- `Invalid path '${this.settingPath}' to configuration file`,
71
- );
62
+ throw new Error(`File at '${this.settingPath}' is not a valid JSON file`);
72
63
  }
73
64
 
74
65
  const valid =