@cyberismo/data-handler 0.0.16 → 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.
- package/dist/command-handler.js +5 -7
- package/dist/command-handler.js.map +1 -1
- package/dist/command-manager.js +4 -4
- package/dist/command-manager.js.map +1 -1
- package/dist/commands/create.js +1 -1
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/fetch.d.ts +8 -0
- package/dist/commands/fetch.js +101 -23
- package/dist/commands/fetch.js.map +1 -1
- package/dist/commands/import.d.ts +5 -2
- package/dist/commands/import.js +12 -3
- package/dist/commands/import.js.map +1 -1
- package/dist/commands/remove.d.ts +3 -1
- package/dist/commands/remove.js +7 -1
- package/dist/commands/remove.js.map +1 -1
- package/dist/commands/rename.js +5 -0
- package/dist/commands/rename.js.map +1 -1
- package/dist/commands/show.d.ts +4 -2
- package/dist/commands/show.js +8 -2
- package/dist/commands/show.js.map +1 -1
- package/dist/commands/validate.js +3 -5
- package/dist/commands/validate.js.map +1 -1
- package/dist/containers/card-container.d.ts +7 -5
- package/dist/containers/card-container.js +30 -5
- package/dist/containers/card-container.js.map +1 -1
- package/dist/containers/project/project-paths.d.ts +2 -0
- package/dist/containers/project/project-paths.js +6 -0
- package/dist/containers/project/project-paths.js.map +1 -1
- package/dist/containers/project/resource-cache.js +9 -7
- package/dist/containers/project/resource-cache.js.map +1 -1
- package/dist/containers/project.d.ts +11 -2
- package/dist/containers/project.js +54 -8
- package/dist/containers/project.js.map +1 -1
- package/dist/containers/template.js +4 -4
- package/dist/containers/template.js.map +1 -1
- package/dist/interfaces/command-options.d.ts +3 -1
- package/dist/interfaces/project-interfaces.d.ts +5 -5
- package/dist/interfaces/project-interfaces.js.map +1 -1
- package/dist/project-settings.d.ts +5 -0
- package/dist/project-settings.js +12 -0
- package/dist/project-settings.js.map +1 -1
- package/dist/resources/resource-object.d.ts +1 -0
- package/dist/resources/resource-object.js +52 -1
- package/dist/resources/resource-object.js.map +1 -1
- package/dist/utils/configuration-logger.d.ts +91 -0
- package/dist/utils/configuration-logger.js +151 -0
- package/dist/utils/configuration-logger.js.map +1 -0
- package/dist/utils/constants.d.ts +1 -1
- package/dist/utils/constants.js +5 -3
- package/dist/utils/constants.js.map +1 -1
- package/package.json +4 -4
- package/src/command-handler.ts +17 -9
- package/src/command-manager.ts +4 -4
- package/src/commands/create.ts +1 -1
- package/src/commands/fetch.ts +143 -34
- package/src/commands/import.ts +13 -1
- package/src/commands/remove.ts +10 -1
- package/src/commands/rename.ts +15 -0
- package/src/commands/show.ts +11 -3
- package/src/commands/validate.ts +3 -7
- package/src/containers/card-container.ts +37 -5
- package/src/containers/project/project-paths.ts +8 -0
- package/src/containers/project/resource-cache.ts +12 -9
- package/src/containers/project.ts +76 -9
- package/src/containers/template.ts +4 -4
- package/src/interfaces/command-options.ts +3 -1
- package/src/interfaces/project-interfaces.ts +5 -5
- package/src/project-settings.ts +13 -0
- package/src/resources/resource-object.ts +73 -1
- package/src/utils/configuration-logger.ts +206 -0
- package/src/utils/constants.ts +5 -3
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
unlink,
|
|
22
22
|
writeFile,
|
|
23
23
|
} from 'node:fs/promises';
|
|
24
|
+
import { readdirSync } from 'node:fs';
|
|
24
25
|
|
|
25
26
|
// base class
|
|
26
27
|
import { CardContainer } from './card-container.js';
|
|
@@ -59,6 +60,11 @@ import type { Template } from './template.js';
|
|
|
59
60
|
|
|
60
61
|
import { ROOT } from '../utils/constants.js';
|
|
61
62
|
|
|
63
|
+
import {
|
|
64
|
+
ConfigurationLogger,
|
|
65
|
+
ConfigurationOperation,
|
|
66
|
+
} from '../utils/configuration-logger.js';
|
|
67
|
+
|
|
62
68
|
// Re-export this, so that classes that use Project do not need to have separate import.
|
|
63
69
|
export { ResourcesFrom };
|
|
64
70
|
|
|
@@ -83,6 +89,7 @@ export class Project extends CardContainer {
|
|
|
83
89
|
private resourceWatcher: ContentWatcher | undefined;
|
|
84
90
|
private settings: ProjectConfiguration;
|
|
85
91
|
private validator: Validate;
|
|
92
|
+
private cachedAllModulePrefixes: string[] = [];
|
|
86
93
|
|
|
87
94
|
constructor(
|
|
88
95
|
path: string,
|
|
@@ -95,7 +102,7 @@ export class Project extends CardContainer {
|
|
|
95
102
|
join(path, '.cards', 'local', Project.projectConfigFileName),
|
|
96
103
|
options.autoSave ?? true,
|
|
97
104
|
);
|
|
98
|
-
super(path, settings.cardKeyPrefix
|
|
105
|
+
super(path, settings.cardKeyPrefix);
|
|
99
106
|
this.settings = settings;
|
|
100
107
|
|
|
101
108
|
this.logger.info({ path }, 'Initializing project');
|
|
@@ -103,16 +110,16 @@ export class Project extends CardContainer {
|
|
|
103
110
|
this.calculationEngine = new CalculationEngine(this);
|
|
104
111
|
this.projectPaths = new ProjectPaths(path);
|
|
105
112
|
this.resourceHandler = new ResourceHandler(this);
|
|
106
|
-
|
|
107
|
-
this.containerName = this.settings.name;
|
|
108
113
|
// todo: implement project validation
|
|
109
114
|
this.validator = Validate.getInstance();
|
|
110
115
|
|
|
111
116
|
this.logger.info(
|
|
112
|
-
{ name: this.
|
|
117
|
+
{ name: this.settings.name },
|
|
113
118
|
'Project initialization complete',
|
|
114
119
|
);
|
|
115
120
|
|
|
121
|
+
this.refreshAllModulePrefixes();
|
|
122
|
+
|
|
116
123
|
const ignoreRenameFileChanges = true;
|
|
117
124
|
|
|
118
125
|
// Watch changes in .cards if there are multiple instances of Project being
|
|
@@ -217,12 +224,34 @@ export class Project extends CardContainer {
|
|
|
217
224
|
}
|
|
218
225
|
}
|
|
219
226
|
|
|
227
|
+
// Refreshes the cached list of all module prefixes.
|
|
228
|
+
// This includes both direct and transient module dependencies.
|
|
229
|
+
private refreshAllModulePrefixes(): void {
|
|
230
|
+
const prefixes: string[] = [this.projectPrefix];
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
const modules = readdirSync(this.paths.modulesFolder, {
|
|
234
|
+
withFileTypes: true,
|
|
235
|
+
})
|
|
236
|
+
.filter((item) => item.isDirectory())
|
|
237
|
+
.map((item) => item.name);
|
|
238
|
+
|
|
239
|
+
prefixes.push(...modules);
|
|
240
|
+
} catch {
|
|
241
|
+
// If modules folder doesn't exist, fall back to configuration modules only
|
|
242
|
+
const moduleNames = this.configuration.modules.map((item) => item.name);
|
|
243
|
+
prefixes.push(...moduleNames);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
this.cachedAllModulePrefixes = prefixes;
|
|
247
|
+
}
|
|
248
|
+
|
|
220
249
|
// Validates that card's data is valid.
|
|
221
250
|
private async validateCard(card: Card): Promise<string> {
|
|
222
251
|
const invalidCustomData = await this.validator.validateCustomFields(
|
|
223
252
|
this,
|
|
224
253
|
card,
|
|
225
|
-
this.
|
|
254
|
+
this.allModulePrefixes(),
|
|
226
255
|
);
|
|
227
256
|
const invalidWorkFlow = await this.validator.validateWorkflowState(
|
|
228
257
|
this,
|
|
@@ -256,7 +285,7 @@ export class Project extends CardContainer {
|
|
|
256
285
|
protected async populateTemplateCards(): Promise<void> {
|
|
257
286
|
try {
|
|
258
287
|
const templateResources = this.resources.templates();
|
|
259
|
-
const prefixes = this.
|
|
288
|
+
const prefixes = this.allModulePrefixes();
|
|
260
289
|
const loadPromises = templateResources.map(async (template) => {
|
|
261
290
|
try {
|
|
262
291
|
this.validator.validResourceName(
|
|
@@ -609,12 +638,30 @@ export class Project extends CardContainer {
|
|
|
609
638
|
/**
|
|
610
639
|
* Adds a module from project.
|
|
611
640
|
* @param module Module to add
|
|
641
|
+
* @param skipMigrationLog If true, skip logging to migration log. Used during project creation.
|
|
612
642
|
*/
|
|
613
|
-
public async importModule(module: ModuleSetting) {
|
|
643
|
+
public async importModule(module: ModuleSetting, skipMigrationLog = false) {
|
|
614
644
|
// Add module as a dependency.
|
|
615
645
|
await this.configuration.addModule(module);
|
|
616
646
|
this.resources.changedModules();
|
|
647
|
+
this.refreshAllModulePrefixes();
|
|
617
648
|
await this.populateTemplateCards();
|
|
649
|
+
|
|
650
|
+
// Log configuration change
|
|
651
|
+
if (!skipMigrationLog) {
|
|
652
|
+
await ConfigurationLogger.log(
|
|
653
|
+
this.basePath,
|
|
654
|
+
ConfigurationOperation.MODULE_ADD,
|
|
655
|
+
module.name,
|
|
656
|
+
{
|
|
657
|
+
parameters: {
|
|
658
|
+
location: module.location,
|
|
659
|
+
branch: module.branch,
|
|
660
|
+
private: module.private,
|
|
661
|
+
},
|
|
662
|
+
},
|
|
663
|
+
);
|
|
664
|
+
}
|
|
618
665
|
this.logger.info(`Imported module '${module.name}'`);
|
|
619
666
|
}
|
|
620
667
|
|
|
@@ -831,9 +878,18 @@ export class Project extends CardContainer {
|
|
|
831
878
|
}
|
|
832
879
|
|
|
833
880
|
/**
|
|
834
|
-
* Returns all prefixes used in the project
|
|
881
|
+
* Returns all prefixes used in the project.
|
|
882
|
+
* This includes both direct dependencies and transient dependencies.
|
|
835
883
|
* @returns all prefixes used in the project.
|
|
836
884
|
*/
|
|
885
|
+
public allModulePrefixes(): string[] {
|
|
886
|
+
return this.cachedAllModulePrefixes;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* Returns prefixes for direct module dependencies only (from cardsConfig.json).
|
|
891
|
+
* @returns prefixes for direct module dependencies.
|
|
892
|
+
*/
|
|
837
893
|
public projectPrefixes(): string[] {
|
|
838
894
|
const prefixes: string[] = [this.projectPrefix];
|
|
839
895
|
const moduleNames = this.configuration.modules.map((item) => item.name);
|
|
@@ -892,6 +948,17 @@ export class Project extends CardContainer {
|
|
|
892
948
|
// Finally, remove module from project configuration
|
|
893
949
|
await this.configuration.removeModule(moduleName);
|
|
894
950
|
|
|
951
|
+
// Refresh cached module prefixes after removal
|
|
952
|
+
this.refreshAllModulePrefixes();
|
|
953
|
+
|
|
954
|
+
// Log configuration change
|
|
955
|
+
await ConfigurationLogger.log(
|
|
956
|
+
this.basePath,
|
|
957
|
+
ConfigurationOperation.MODULE_REMOVE,
|
|
958
|
+
moduleName,
|
|
959
|
+
{},
|
|
960
|
+
);
|
|
961
|
+
|
|
895
962
|
this.logger.info(`Removed module '${moduleName}'`);
|
|
896
963
|
}
|
|
897
964
|
|
|
@@ -909,7 +976,7 @@ export class Project extends CardContainer {
|
|
|
909
976
|
*/
|
|
910
977
|
public async show(): Promise<ProjectMetadata> {
|
|
911
978
|
return {
|
|
912
|
-
name: this.
|
|
979
|
+
name: this.settings.name,
|
|
913
980
|
path: this.basePath,
|
|
914
981
|
prefix: this.projectPrefix,
|
|
915
982
|
hubs: this.configuration.hubs,
|
|
@@ -65,7 +65,7 @@ export class Template extends CardContainer {
|
|
|
65
65
|
constructor(project: Project, template: Resource) {
|
|
66
66
|
// Templates might come from modules. Remove module name from template name.
|
|
67
67
|
const templateName = stripExtension(basename(template.name));
|
|
68
|
-
super(template.path, project.projectPrefix
|
|
68
|
+
super(template.path, project.projectPrefix);
|
|
69
69
|
this.templateName = templateName;
|
|
70
70
|
this.fullTemplateName = template.name;
|
|
71
71
|
|
|
@@ -399,7 +399,7 @@ export class Template extends CardContainer {
|
|
|
399
399
|
try {
|
|
400
400
|
// todo: to use cache instead of file access
|
|
401
401
|
if (!pathExists(this.templateFolder())) {
|
|
402
|
-
throw new Error(`Template '${this.
|
|
402
|
+
throw new Error(`Template '${this.templateName}' does not exist`);
|
|
403
403
|
}
|
|
404
404
|
const cardType = this.project.resources
|
|
405
405
|
.byType(cardTypeName, 'cardTypes')
|
|
@@ -407,7 +407,7 @@ export class Template extends CardContainer {
|
|
|
407
407
|
|
|
408
408
|
if (parentCard && !this.hasTemplateCard(parentCard.key)) {
|
|
409
409
|
throw new Error(
|
|
410
|
-
`Card '${parentCard.key}' does not exist in template '${this.
|
|
410
|
+
`Card '${parentCard.key}' does not exist in template '${this.templateName}'`,
|
|
411
411
|
);
|
|
412
412
|
}
|
|
413
413
|
|
|
@@ -496,7 +496,7 @@ export class Template extends CardContainer {
|
|
|
496
496
|
const cards = this.cards();
|
|
497
497
|
if (cards.length === 0) {
|
|
498
498
|
throw new Error(
|
|
499
|
-
`No cards in template '${this.
|
|
499
|
+
`No cards in template '${this.templateName}'. Please add template cards with 'add' command first.`,
|
|
500
500
|
);
|
|
501
501
|
}
|
|
502
502
|
return this.doCreateCards(cards, parentCard);
|
|
@@ -56,7 +56,9 @@ export interface ExportCommandOptions extends BaseCommandOptions {
|
|
|
56
56
|
export type FetchCommandOptions = BaseCommandOptions;
|
|
57
57
|
|
|
58
58
|
// Options for 'import' command
|
|
59
|
-
export
|
|
59
|
+
export interface ImportCommandOptions extends BaseCommandOptions {
|
|
60
|
+
skipMigrationLog?: boolean;
|
|
61
|
+
}
|
|
60
62
|
|
|
61
63
|
// Options for 'move' command
|
|
62
64
|
export type MoveCommandOptions = BaseCommandOptions;
|
|
@@ -47,13 +47,15 @@ export interface CardListContainer {
|
|
|
47
47
|
}
|
|
48
48
|
// Remember to add all these keys to utils/constants.ts
|
|
49
49
|
export interface PredefinedCardMetadata {
|
|
50
|
-
title: string;
|
|
51
50
|
cardType: string;
|
|
52
|
-
|
|
53
|
-
rank: string;
|
|
51
|
+
labels?: string[];
|
|
54
52
|
lastTransitioned?: string;
|
|
55
53
|
lastUpdated?: string;
|
|
54
|
+
links: Link[];
|
|
55
|
+
rank: string;
|
|
56
56
|
templateCardKey?: string;
|
|
57
|
+
title: string;
|
|
58
|
+
workflowState: string;
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
// todo: do we need in the future separation between module-template-cards and local template-cards
|
|
@@ -65,8 +67,6 @@ export enum CardLocation {
|
|
|
65
67
|
|
|
66
68
|
// Card's index.json file content.
|
|
67
69
|
export interface CardMetadata extends PredefinedCardMetadata {
|
|
68
|
-
labels?: string[];
|
|
69
|
-
links: Link[];
|
|
70
70
|
[key: string]: MetadataContent;
|
|
71
71
|
}
|
|
72
72
|
|
package/src/project-settings.ts
CHANGED
|
@@ -265,4 +265,17 @@ export class ProjectConfiguration implements ProjectSettings {
|
|
|
265
265
|
`Prefix '${newPrefix}' is not valid prefix. Prefix should be in lowercase and contain letters from a to z (max length 10).`,
|
|
266
266
|
);
|
|
267
267
|
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Changes project name.
|
|
271
|
+
* @param newName New project name
|
|
272
|
+
*/
|
|
273
|
+
public async setProjectName(newName: string) {
|
|
274
|
+
const isValid = Validate.isValidProjectName(newName);
|
|
275
|
+
if (isValid) {
|
|
276
|
+
this.name = newName;
|
|
277
|
+
return this.save();
|
|
278
|
+
}
|
|
279
|
+
throw new Error(`Project name '${newName}' is not valid.`);
|
|
280
|
+
}
|
|
268
281
|
}
|
|
@@ -44,6 +44,11 @@ import type {
|
|
|
44
44
|
} from '../interfaces/resource-interfaces.js';
|
|
45
45
|
import type { Validate } from '../commands/validate.js';
|
|
46
46
|
|
|
47
|
+
import {
|
|
48
|
+
ConfigurationLogger,
|
|
49
|
+
ConfigurationOperation,
|
|
50
|
+
} from '../utils/configuration-logger.js';
|
|
51
|
+
|
|
47
52
|
// Possible operations to perform when doing "update"
|
|
48
53
|
export type UpdateOperations = 'add' | 'change' | 'rank' | 'remove';
|
|
49
54
|
|
|
@@ -308,6 +313,9 @@ export abstract class ResourceObject<
|
|
|
308
313
|
|
|
309
314
|
const resourceString = resourceNameToString(this.resourceName);
|
|
310
315
|
this.project.resources.add(resourceString, this);
|
|
316
|
+
|
|
317
|
+
// Log resource creation to migration log
|
|
318
|
+
await this.logResourceOperation('create');
|
|
311
319
|
}
|
|
312
320
|
|
|
313
321
|
/**
|
|
@@ -408,6 +416,56 @@ export abstract class ResourceObject<
|
|
|
408
416
|
}
|
|
409
417
|
}
|
|
410
418
|
|
|
419
|
+
// Log details
|
|
420
|
+
protected async logResourceOperation<Type>(
|
|
421
|
+
operationType: 'create' | 'delete' | 'update' | 'rename',
|
|
422
|
+
op?: Operation<Type>,
|
|
423
|
+
key?: string,
|
|
424
|
+
): Promise<void> {
|
|
425
|
+
let configOperation: ConfigurationOperation;
|
|
426
|
+
const target = resourceNameToString(this.resourceName);
|
|
427
|
+
const parameters: Record<string, unknown> = { type: this.type };
|
|
428
|
+
|
|
429
|
+
switch (operationType) {
|
|
430
|
+
case 'create':
|
|
431
|
+
configOperation = ConfigurationOperation.RESOURCE_CREATE;
|
|
432
|
+
break;
|
|
433
|
+
case 'delete':
|
|
434
|
+
configOperation = ConfigurationOperation.RESOURCE_DELETE;
|
|
435
|
+
break;
|
|
436
|
+
case 'update':
|
|
437
|
+
configOperation = ConfigurationOperation.RESOURCE_UPDATE;
|
|
438
|
+
if (op) {
|
|
439
|
+
parameters.operation = op.name;
|
|
440
|
+
}
|
|
441
|
+
if (key) {
|
|
442
|
+
parameters.key = key;
|
|
443
|
+
}
|
|
444
|
+
break;
|
|
445
|
+
case 'rename':
|
|
446
|
+
configOperation = ConfigurationOperation.RESOURCE_RENAME;
|
|
447
|
+
if (op && op.name === 'change') {
|
|
448
|
+
const changeOp = op as ChangeOperation<string>;
|
|
449
|
+
parameters.oldName = changeOp.target;
|
|
450
|
+
parameters.newName = changeOp.to;
|
|
451
|
+
}
|
|
452
|
+
break;
|
|
453
|
+
default:
|
|
454
|
+
throw new Error(`Unknown operation type: ${operationType}`);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
await ConfigurationLogger.log(
|
|
458
|
+
this.project.basePath,
|
|
459
|
+
configOperation,
|
|
460
|
+
target,
|
|
461
|
+
{
|
|
462
|
+
parameters,
|
|
463
|
+
},
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
this.logger.info(`Configuration: ${configOperation} - ${target}`);
|
|
467
|
+
}
|
|
468
|
+
|
|
411
469
|
/**
|
|
412
470
|
* Called after inherited class has finished 'update' operation.
|
|
413
471
|
* @param content New content for resource
|
|
@@ -449,6 +507,9 @@ export abstract class ResourceObject<
|
|
|
449
507
|
|
|
450
508
|
this.content = content;
|
|
451
509
|
await this.write();
|
|
510
|
+
|
|
511
|
+
// Log resource update to migration log
|
|
512
|
+
await this.logResourceOperation('update', op, updateKey.key);
|
|
452
513
|
}
|
|
453
514
|
|
|
454
515
|
/**
|
|
@@ -496,9 +557,17 @@ export abstract class ResourceObject<
|
|
|
496
557
|
|
|
497
558
|
this.fileName = newFilename;
|
|
498
559
|
this.content.name = resourceNameToString(newName);
|
|
560
|
+
const newNameString = this.content.name;
|
|
499
561
|
this.resourceName = newName;
|
|
500
562
|
|
|
501
|
-
this.project.resources.rename(oldName,
|
|
563
|
+
this.project.resources.rename(oldName, newNameString);
|
|
564
|
+
|
|
565
|
+
// Log resource rename to migration log
|
|
566
|
+
await this.logResourceOperation('rename', {
|
|
567
|
+
name: 'change',
|
|
568
|
+
target: oldName,
|
|
569
|
+
to: newNameString,
|
|
570
|
+
} as ChangeOperation<string>);
|
|
502
571
|
}
|
|
503
572
|
|
|
504
573
|
/**
|
|
@@ -712,6 +781,9 @@ export abstract class ResourceObject<
|
|
|
712
781
|
await deleteFile(this.fileName);
|
|
713
782
|
this.project.resources.remove(resourceNameToString(this.resourceName));
|
|
714
783
|
this.fileName = '';
|
|
784
|
+
|
|
785
|
+
// Log resource deletion to migration log
|
|
786
|
+
await this.logResourceOperation('delete');
|
|
715
787
|
}
|
|
716
788
|
|
|
717
789
|
/**
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
Cyberismo
|
|
3
|
+
Copyright © Cyberismo Ltd and contributors 2025
|
|
4
|
+
|
|
5
|
+
This program is free software: you can redistribute it and/or modify it under
|
|
6
|
+
the terms of the GNU Affero General Public License version 3 as published by
|
|
7
|
+
the Free Software Foundation. This program is distributed in the hope that it
|
|
8
|
+
will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
|
|
9
|
+
of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
10
|
+
See the GNU Affero General Public License for more details.
|
|
11
|
+
You should have received a copy of the GNU Affero General Public
|
|
12
|
+
License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { readFile, rename } from 'node:fs/promises';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
|
|
18
|
+
import { getChildLogger } from './log-utils.js';
|
|
19
|
+
import { ProjectPaths } from '../containers/project/project-paths.js';
|
|
20
|
+
import { writeFileSafe, pathExists } from './file-utils.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Enum for configuration change operation types.
|
|
24
|
+
*/
|
|
25
|
+
export enum ConfigurationOperation {
|
|
26
|
+
MODULE_ADD = 'module_add',
|
|
27
|
+
MODULE_REMOVE = 'module_remove',
|
|
28
|
+
PROJECT_RENAME = 'project_rename',
|
|
29
|
+
RESOURCE_CREATE = 'resource_create',
|
|
30
|
+
RESOURCE_DELETE = 'resource_delete',
|
|
31
|
+
RESOURCE_RENAME = 'resource_rename',
|
|
32
|
+
RESOURCE_UPDATE = 'resource_update',
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Individual log entry representing a single configuration change.
|
|
37
|
+
* @param timestamp Timestamp when the operation occurred (ISO string)
|
|
38
|
+
* @param operation Type of operation performed
|
|
39
|
+
* @param target Target of the operation
|
|
40
|
+
* @param parameters Additional parameters specific to the operation
|
|
41
|
+
*/
|
|
42
|
+
export interface ConfigurationLogEntry {
|
|
43
|
+
timestamp: string;
|
|
44
|
+
operation: ConfigurationOperation;
|
|
45
|
+
target: string;
|
|
46
|
+
parameters?: Record<string, unknown>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Options for logging configuration changes.
|
|
51
|
+
* @param parameters Additional parameters to store with the operation
|
|
52
|
+
*/
|
|
53
|
+
export interface ConfigurationLogOptions {
|
|
54
|
+
parameters?: Record<string, unknown>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Logger for tracking configuration changes that affect project structure.
|
|
59
|
+
*/
|
|
60
|
+
export class ConfigurationLogger {
|
|
61
|
+
/**
|
|
62
|
+
* Path to the configuration log file.
|
|
63
|
+
* @param projectPath Path to the project root
|
|
64
|
+
* @returns Path to the log file
|
|
65
|
+
*/
|
|
66
|
+
public static logFile(projectPath: string): string {
|
|
67
|
+
const paths = new ProjectPaths(projectPath);
|
|
68
|
+
return paths.configurationChangesLog;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Clears all log entries.
|
|
73
|
+
* @param projectPath Path to the project root
|
|
74
|
+
* @note Use with caution.
|
|
75
|
+
*/
|
|
76
|
+
public static async clearLog(projectPath: string): Promise<void> {
|
|
77
|
+
const logFile = ConfigurationLogger.logFile(projectPath);
|
|
78
|
+
await writeFileSafe(logFile, '', 'utf-8');
|
|
79
|
+
const logger = getChildLogger({ module: 'ConfigurationLogger' });
|
|
80
|
+
logger.info('Configuration log cleared');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Create a versioned snapshot of the current migration log.
|
|
85
|
+
* Renames current migrationLog.jsonl to migrationLog_<version>.jsonl
|
|
86
|
+
* @param projectPath Path to the project root
|
|
87
|
+
* @param version Version identifier (e.g., "1.0.0", "v2")
|
|
88
|
+
* @returns Path to the versioned log file
|
|
89
|
+
*/
|
|
90
|
+
public static async createVersion(
|
|
91
|
+
projectPath: string,
|
|
92
|
+
version: string,
|
|
93
|
+
): Promise<string> {
|
|
94
|
+
const paths = new ProjectPaths(projectPath);
|
|
95
|
+
const currentLogPath = paths.configurationChangesLog;
|
|
96
|
+
const versionedLogPath = join(
|
|
97
|
+
paths.migrationLogFolder,
|
|
98
|
+
`migrationLog_${version}.jsonl`,
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
// Only create version if current log exists and has content
|
|
102
|
+
if (!pathExists(currentLogPath)) {
|
|
103
|
+
throw new Error('No current migration log exists to version');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const content = await readFile(currentLogPath, 'utf-8');
|
|
107
|
+
if (!content.trim()) {
|
|
108
|
+
throw new Error('Current migration log is empty');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Rename current to versioned
|
|
112
|
+
await rename(currentLogPath, versionedLogPath);
|
|
113
|
+
|
|
114
|
+
const logger = getChildLogger({ module: 'ConfigurationLogger' });
|
|
115
|
+
logger.info(
|
|
116
|
+
`Created migration to version: ${version} at ${versionedLogPath}`,
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
return versionedLogPath;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Reads all configuration log entries using JSON Lines format.
|
|
124
|
+
* @param projectPath Path to the project root
|
|
125
|
+
* @returns Array of log entries
|
|
126
|
+
*/
|
|
127
|
+
public static async entries(
|
|
128
|
+
projectPath: string,
|
|
129
|
+
): Promise<ConfigurationLogEntry[]> {
|
|
130
|
+
const logFile = ConfigurationLogger.logFile(projectPath);
|
|
131
|
+
const logger = getChildLogger({ module: 'ConfigurationLogger' });
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const content = await readFile(logFile, 'utf-8');
|
|
135
|
+
const lines = content
|
|
136
|
+
.trim()
|
|
137
|
+
.split('\n')
|
|
138
|
+
.filter((line) => line.trim());
|
|
139
|
+
|
|
140
|
+
const entries: ConfigurationLogEntry[] = [];
|
|
141
|
+
for (const line of lines) {
|
|
142
|
+
try {
|
|
143
|
+
const entry = JSON.parse(line) as ConfigurationLogEntry;
|
|
144
|
+
if (entry.timestamp && entry.operation && entry.target) {
|
|
145
|
+
entries.push(entry);
|
|
146
|
+
}
|
|
147
|
+
} catch {
|
|
148
|
+
logger.error(`Invalid configuration line: ${line}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return entries;
|
|
153
|
+
} catch (error) {
|
|
154
|
+
logger.error({ error }, `Failed to read configuration log`);
|
|
155
|
+
return [];
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Check if a configuration log exists for the given project path.
|
|
161
|
+
* @param projectPath Path to the project root
|
|
162
|
+
* @returns True if log file exists
|
|
163
|
+
*/
|
|
164
|
+
public static hasLog(projectPath: string): boolean {
|
|
165
|
+
const logPath = new ProjectPaths(projectPath).configurationChangesLog;
|
|
166
|
+
return pathExists(logPath);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Log a configuration change operation.
|
|
171
|
+
* @note This is designed to be called AFTER the operation succeeds.
|
|
172
|
+
* @param projectPath Path to the project root
|
|
173
|
+
* @param operation The type of operation
|
|
174
|
+
* @param target The target of the operation
|
|
175
|
+
* @param options Additional options for the log entry
|
|
176
|
+
*/
|
|
177
|
+
public static async log(
|
|
178
|
+
projectPath: string,
|
|
179
|
+
operation: ConfigurationOperation,
|
|
180
|
+
target: string,
|
|
181
|
+
options?: ConfigurationLogOptions,
|
|
182
|
+
): Promise<void> {
|
|
183
|
+
const logFile = ConfigurationLogger.logFile(projectPath);
|
|
184
|
+
const logger = getChildLogger({ module: 'ConfigurationLogger' });
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
const entry: ConfigurationLogEntry = {
|
|
188
|
+
timestamp: new Date().toISOString(),
|
|
189
|
+
operation,
|
|
190
|
+
target,
|
|
191
|
+
parameters: options?.parameters,
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
await writeFileSafe(logFile, JSON.stringify(entry) + '\n', {
|
|
195
|
+
flag: 'a',
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
logger.debug(`Logged ${operation} operation for target: ${target}`);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
logger.error(
|
|
201
|
+
{ error, operation, target },
|
|
202
|
+
`Configuration logging failed`,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
package/src/utils/constants.ts
CHANGED
|
@@ -42,13 +42,15 @@ export const VALID_FOLDER_RESOURCE_FILES = [
|
|
|
42
42
|
* These are field names that are non-custom fields that present in metadata
|
|
43
43
|
*/
|
|
44
44
|
export const PREDEFINED_FIELDS = [
|
|
45
|
-
'rank',
|
|
46
45
|
'cardType',
|
|
47
|
-
'
|
|
48
|
-
'workflowState',
|
|
46
|
+
'labels',
|
|
49
47
|
'lastUpdated',
|
|
50
48
|
'lastTransitioned',
|
|
49
|
+
'links',
|
|
50
|
+
'rank',
|
|
51
51
|
'templateCardKey',
|
|
52
|
+
'title',
|
|
53
|
+
'workflowState',
|
|
52
54
|
] satisfies (keyof PredefinedCardMetadata)[];
|
|
53
55
|
|
|
54
56
|
/**
|