@cyberismo/data-handler 0.0.18 → 0.0.19
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.d.ts +2 -0
- package/dist/command-handler.js +26 -2
- package/dist/command-handler.js.map +1 -1
- package/dist/command-manager.d.ts +2 -0
- package/dist/command-manager.js +3 -0
- package/dist/command-manager.js.map +1 -1
- package/dist/commands/create.d.ts +3 -1
- package/dist/commands/create.js +10 -1
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/migrate.d.ts +33 -0
- package/dist/commands/migrate.js +66 -0
- package/dist/commands/migrate.js.map +1 -0
- package/dist/containers/project/card-cache.js +13 -1
- package/dist/containers/project/card-cache.js.map +1 -1
- package/dist/containers/project/project-paths.d.ts +1 -0
- package/dist/containers/project/project-paths.js +5 -2
- package/dist/containers/project/project-paths.js.map +1 -1
- package/dist/containers/project.d.ts +10 -0
- package/dist/containers/project.js +39 -2
- package/dist/containers/project.js.map +1 -1
- package/dist/containers/template.js +2 -0
- package/dist/containers/template.js.map +1 -1
- package/dist/interfaces/command-options.d.ts +6 -1
- package/dist/interfaces/project-interfaces.d.ts +4 -0
- package/dist/interfaces/project-interfaces.js.map +1 -1
- package/dist/migrations/index.d.ts +14 -0
- package/dist/migrations/index.js +14 -0
- package/dist/migrations/index.js.map +1 -0
- package/dist/migrations/migration-executor.d.ts +79 -0
- package/dist/migrations/migration-executor.js +312 -0
- package/dist/migrations/migration-executor.js.map +1 -0
- package/dist/migrations/migration-worker.d.ts +13 -0
- package/dist/migrations/migration-worker.js +156 -0
- package/dist/migrations/migration-worker.js.map +1 -0
- package/dist/migrations/worker-executor.d.ts +24 -0
- package/dist/migrations/worker-executor.js +157 -0
- package/dist/migrations/worker-executor.js.map +1 -0
- package/dist/project-settings.d.ts +2 -0
- package/dist/project-settings.js +7 -0
- package/dist/project-settings.js.map +1 -1
- package/dist/resources/calculation-resource.d.ts +9 -0
- package/dist/resources/calculation-resource.js +13 -2
- package/dist/resources/calculation-resource.js.map +1 -1
- package/dist/resources/card-type-resource.d.ts +7 -2
- package/dist/resources/card-type-resource.js +13 -13
- package/dist/resources/card-type-resource.js.map +1 -1
- package/dist/resources/field-type-resource.d.ts +5 -0
- package/dist/resources/field-type-resource.js +13 -7
- package/dist/resources/field-type-resource.js.map +1 -1
- package/dist/resources/file-resource.d.ts +13 -1
- package/dist/resources/file-resource.js +17 -8
- package/dist/resources/file-resource.js.map +1 -1
- package/dist/resources/graph-model-resource.d.ts +5 -0
- package/dist/resources/graph-model-resource.js +6 -0
- package/dist/resources/graph-model-resource.js.map +1 -1
- package/dist/resources/graph-view-resource.d.ts +5 -0
- package/dist/resources/graph-view-resource.js +6 -0
- package/dist/resources/graph-view-resource.js.map +1 -1
- package/dist/resources/link-type-resource.d.ts +6 -0
- package/dist/resources/link-type-resource.js +26 -0
- package/dist/resources/link-type-resource.js.map +1 -1
- package/dist/resources/report-resource.d.ts +6 -1
- package/dist/resources/report-resource.js +7 -1
- package/dist/resources/report-resource.js.map +1 -1
- package/dist/resources/resource-object.d.ts +22 -7
- package/dist/resources/resource-object.js +44 -15
- package/dist/resources/resource-object.js.map +1 -1
- package/dist/resources/template-resource.d.ts +5 -1
- package/dist/resources/template-resource.js +6 -1
- package/dist/resources/template-resource.js.map +1 -1
- package/dist/resources/workflow-resource.d.ts +6 -2
- package/dist/resources/workflow-resource.js +11 -6
- package/dist/resources/workflow-resource.js.map +1 -1
- package/dist/utils/card-utils.d.ts +1 -1
- package/dist/utils/common-utils.d.ts +8 -0
- package/dist/utils/common-utils.js +14 -0
- package/dist/utils/common-utils.js.map +1 -1
- package/dist/utils/file-utils.d.ts +15 -3
- package/dist/utils/file-utils.js +48 -9
- package/dist/utils/file-utils.js.map +1 -1
- package/dist/utils/json.js +2 -2
- package/dist/utils/json.js.map +1 -1
- package/package.json +5 -3
- package/src/command-handler.ts +38 -1
- package/src/command-manager.ts +3 -0
- package/src/commands/create.ts +11 -0
- package/src/commands/migrate.ts +88 -0
- package/src/containers/project/card-cache.ts +18 -1
- package/src/containers/project/project-paths.ts +6 -2
- package/src/containers/project.ts +66 -1
- package/src/containers/template.ts +5 -0
- package/src/interfaces/command-options.ts +8 -0
- package/src/interfaces/project-interfaces.ts +4 -0
- package/src/migrations/index.ts +20 -0
- package/src/migrations/migration-executor.ts +478 -0
- package/src/migrations/migration-worker.ts +190 -0
- package/src/migrations/worker-executor.ts +185 -0
- package/src/project-settings.ts +7 -0
- package/src/resources/calculation-resource.ts +13 -2
- package/src/resources/card-type-resource.ts +19 -14
- package/src/resources/field-type-resource.ts +18 -7
- package/src/resources/file-resource.ts +25 -8
- package/src/resources/graph-model-resource.ts +6 -0
- package/src/resources/graph-view-resource.ts +6 -0
- package/src/resources/link-type-resource.ts +34 -0
- package/src/resources/report-resource.ts +7 -1
- package/src/resources/resource-object.ts +57 -18
- package/src/resources/template-resource.ts +6 -1
- package/src/resources/workflow-resource.ts +17 -7
- package/src/utils/common-utils.ts +15 -0
- package/src/utils/file-utils.ts +56 -12
- package/src/utils/json.ts +2 -6
|
@@ -80,6 +80,10 @@ export class ProjectPaths {
|
|
|
80
80
|
return join(this.resourcesFolder, 'graphViews');
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
public get internalRootFolder(): string {
|
|
84
|
+
return join(this.path, '.cards');
|
|
85
|
+
}
|
|
86
|
+
|
|
83
87
|
public get linkTypesFolder(): string {
|
|
84
88
|
return join(this.resourcesFolder, 'linkTypes');
|
|
85
89
|
}
|
|
@@ -93,7 +97,7 @@ export class ProjectPaths {
|
|
|
93
97
|
}
|
|
94
98
|
|
|
95
99
|
public get modulesFolder(): string {
|
|
96
|
-
return join(this.
|
|
100
|
+
return join(this.internalRootFolder, 'modules');
|
|
97
101
|
}
|
|
98
102
|
|
|
99
103
|
public moduleResourcePath(
|
|
@@ -105,7 +109,7 @@ export class ProjectPaths {
|
|
|
105
109
|
}
|
|
106
110
|
|
|
107
111
|
public get resourcesFolder(): string {
|
|
108
|
-
return join(this.
|
|
112
|
+
return join(this.internalRootFolder, 'local');
|
|
109
113
|
}
|
|
110
114
|
|
|
111
115
|
public get reportsFolder(): string {
|
|
@@ -55,7 +55,9 @@ import { ResourceHandler } from './project/resource-handler.js';
|
|
|
55
55
|
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
|
+
import { MigrationExecutor } from '../migrations/migration-executor.js';
|
|
58
59
|
|
|
60
|
+
import type { MigrationResult } from '@cyberismo/migrations';
|
|
59
61
|
import type { Template } from './template.js';
|
|
60
62
|
|
|
61
63
|
import { ROOT } from '../utils/constants.js';
|
|
@@ -572,12 +574,27 @@ export class Project extends CardContainer {
|
|
|
572
574
|
public async handleCardDeleted(deletedCard: Card) {
|
|
573
575
|
// Delete children from the cache first
|
|
574
576
|
if (deletedCard.children && deletedCard.children.length > 0) {
|
|
577
|
+
const parentCachedCard = this.cardCache.getCard(deletedCard.key);
|
|
578
|
+
const parentLocation = parentCachedCard?.location || 'project';
|
|
579
|
+
|
|
575
580
|
for (const child of deletedCard.children) {
|
|
576
581
|
try {
|
|
577
582
|
const childCard = this.findCard(child);
|
|
583
|
+
const childCachedCard = this.cardCache.getCard(child);
|
|
584
|
+
|
|
585
|
+
// Safety check: only delete children from the same location (project or template)
|
|
586
|
+
if (childCachedCard && childCachedCard.location !== parentLocation) {
|
|
587
|
+
const errorMessage =
|
|
588
|
+
`Cannot delete child card '${child}' from different location '${childCachedCard.location}' ` +
|
|
589
|
+
`than parent card '${deletedCard.key}' from '${parentLocation}'`;
|
|
590
|
+
this.logger.error(errorMessage);
|
|
591
|
+
throw new Error(errorMessage);
|
|
592
|
+
}
|
|
593
|
+
|
|
578
594
|
await this.handleCardDeleted(childCard);
|
|
579
|
-
} catch {
|
|
595
|
+
} catch (error) {
|
|
580
596
|
this.logger.warn(
|
|
597
|
+
{ error },
|
|
581
598
|
`Accessing child '${child}' of '${deletedCard.key}' when deleting cards caused an exception`,
|
|
582
599
|
);
|
|
583
600
|
continue;
|
|
@@ -753,6 +770,7 @@ export class Project extends CardContainer {
|
|
|
753
770
|
);
|
|
754
771
|
return {
|
|
755
772
|
name: moduleConfig.name,
|
|
773
|
+
description: moduleConfig.description || '',
|
|
756
774
|
modules: moduleConfig.modules,
|
|
757
775
|
hubs: moduleConfig.hubs,
|
|
758
776
|
path: modulePath,
|
|
@@ -970,6 +988,51 @@ export class Project extends CardContainer {
|
|
|
970
988
|
return this.resourceHandler;
|
|
971
989
|
}
|
|
972
990
|
|
|
991
|
+
/**
|
|
992
|
+
* Run migrations to bring project schema to target version.
|
|
993
|
+
* @param fromVersion Current schema version
|
|
994
|
+
* @param toVersion Target schema version
|
|
995
|
+
* @param backupDir Optional directory for backups. If undefined, no backup is created.
|
|
996
|
+
* @param timeoutMilliSeconds Optional timeout in milliseconds. If undefined, uses default (2 minutes).
|
|
997
|
+
* @returns Migration result
|
|
998
|
+
*/
|
|
999
|
+
public async runMigrations(
|
|
1000
|
+
fromVersion: number,
|
|
1001
|
+
toVersion: number,
|
|
1002
|
+
backupDir?: string,
|
|
1003
|
+
timeoutMilliSeconds?: number,
|
|
1004
|
+
): Promise<MigrationResult> {
|
|
1005
|
+
this.logger.info({ fromVersion, toVersion }, 'Starting schema migration');
|
|
1006
|
+
|
|
1007
|
+
const executor = new MigrationExecutor(
|
|
1008
|
+
this,
|
|
1009
|
+
backupDir,
|
|
1010
|
+
timeoutMilliSeconds,
|
|
1011
|
+
);
|
|
1012
|
+
const result = await executor.migrate(
|
|
1013
|
+
fromVersion,
|
|
1014
|
+
toVersion,
|
|
1015
|
+
async (version: number) => {
|
|
1016
|
+
this.settings.schemaVersion = version;
|
|
1017
|
+
await this.settings.save();
|
|
1018
|
+
},
|
|
1019
|
+
);
|
|
1020
|
+
|
|
1021
|
+
if (result.success) {
|
|
1022
|
+
this.logger.info(
|
|
1023
|
+
{ fromVersion, toVersion },
|
|
1024
|
+
'Migration completed successfully',
|
|
1025
|
+
);
|
|
1026
|
+
} else {
|
|
1027
|
+
this.logger.error(
|
|
1028
|
+
{ error: result.error, message: result.message },
|
|
1029
|
+
'Migration failed',
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
return result;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
973
1036
|
/**
|
|
974
1037
|
* Shows details of a project.
|
|
975
1038
|
* @returns details of a project.
|
|
@@ -979,6 +1042,8 @@ export class Project extends CardContainer {
|
|
|
979
1042
|
name: this.settings.name,
|
|
980
1043
|
path: this.basePath,
|
|
981
1044
|
prefix: this.projectPrefix,
|
|
1045
|
+
category: this.configuration.category,
|
|
1046
|
+
description: this.configuration.description,
|
|
982
1047
|
hubs: this.configuration.hubs,
|
|
983
1048
|
modules: this.resources.moduleNames(),
|
|
984
1049
|
numberOfCards: (await this.listCards(CardLocation.projectOnly))[0].cards
|
|
@@ -152,6 +152,11 @@ export class Template extends CardContainer {
|
|
|
152
152
|
|
|
153
153
|
card.key = templateIDMap.get(card.key) || card.key;
|
|
154
154
|
|
|
155
|
+
// Remap children keys from template keys to new project card keys
|
|
156
|
+
card.children = card.children.map(
|
|
157
|
+
(childKey) => templateIDMap.get(childKey) || childKey,
|
|
158
|
+
);
|
|
159
|
+
|
|
155
160
|
// Set parent field based on template hierarchy and creation location
|
|
156
161
|
// Store the original template parent before key remapping
|
|
157
162
|
const originalParentKey = card.parent;
|
|
@@ -60,6 +60,12 @@ export interface ImportCommandOptions extends BaseCommandOptions {
|
|
|
60
60
|
skipMigrationLog?: boolean;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
// Options for 'migrate' command
|
|
64
|
+
export interface MigrateCommandOptions extends BaseCommandOptions {
|
|
65
|
+
backup?: string;
|
|
66
|
+
timeout?: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
63
69
|
// Options for 'move' command
|
|
64
70
|
export type MoveCommandOptions = BaseCommandOptions;
|
|
65
71
|
|
|
@@ -111,6 +117,7 @@ export type AllCommandOptions =
|
|
|
111
117
|
| ExportCommandOptions
|
|
112
118
|
| FetchCommandOptions
|
|
113
119
|
| ImportCommandOptions
|
|
120
|
+
| MigrateCommandOptions
|
|
114
121
|
| MoveCommandOptions
|
|
115
122
|
| RankCommandOptions
|
|
116
123
|
| RemoveCommandOptions
|
|
@@ -132,6 +139,7 @@ export type CommandOptions<T extends CmdKey> = {
|
|
|
132
139
|
export: ExportCommandOptions;
|
|
133
140
|
fetch: FetchCommandOptions;
|
|
134
141
|
import: ImportCommandOptions;
|
|
142
|
+
migrate: MigrateCommandOptions;
|
|
135
143
|
move: MoveCommandOptions;
|
|
136
144
|
rank: RankCommandOptions;
|
|
137
145
|
remove: RemoveCommandOptions;
|
|
@@ -171,6 +171,8 @@ export interface ProjectMetadata {
|
|
|
171
171
|
name: string;
|
|
172
172
|
path: string;
|
|
173
173
|
prefix: string;
|
|
174
|
+
category?: string;
|
|
175
|
+
description?: string;
|
|
174
176
|
modules: string[];
|
|
175
177
|
hubs: HubSetting[];
|
|
176
178
|
numberOfCards: number;
|
|
@@ -181,6 +183,8 @@ export interface ProjectSettings {
|
|
|
181
183
|
schemaVersion?: number;
|
|
182
184
|
cardKeyPrefix: string;
|
|
183
185
|
name: string;
|
|
186
|
+
category?: string;
|
|
187
|
+
description: string;
|
|
184
188
|
modules: ModuleSetting[];
|
|
185
189
|
hubs: HubSetting[];
|
|
186
190
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
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
|
+
export { MigrationExecutor } from './migration-executor.js';
|
|
15
|
+
export type {
|
|
16
|
+
Migration,
|
|
17
|
+
MigrationContext,
|
|
18
|
+
MigrationResult,
|
|
19
|
+
MigrationStepResult,
|
|
20
|
+
} from '@cyberismo/migrations';
|
|
@@ -0,0 +1,478 @@
|
|
|
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 { dirname, join } from 'node:path';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
|
|
17
|
+
import { availableMigrations, migration } from '@cyberismo/migrations';
|
|
18
|
+
import { availableSpace, folderSize } from '../utils/file-utils.js';
|
|
19
|
+
import { executeStep } from './worker-executor.js';
|
|
20
|
+
import { getChildLogger } from '../utils/log-utils.js';
|
|
21
|
+
import { Validate } from '../commands/validate.js';
|
|
22
|
+
|
|
23
|
+
import type {
|
|
24
|
+
Migration,
|
|
25
|
+
MigrationContext,
|
|
26
|
+
MigrationResult,
|
|
27
|
+
MigrationStepResult,
|
|
28
|
+
} from '@cyberismo/migrations';
|
|
29
|
+
import type { Project } from '../containers/project.js';
|
|
30
|
+
|
|
31
|
+
const DEFAULT_MIGRATION_TIMEOUT_MS = 2 * 60 * 1000; // 2 minutes
|
|
32
|
+
const MEGABYTES = 1024 * 1024; // 1 MB
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Messages sent from the main thread to worker threads.
|
|
36
|
+
*/
|
|
37
|
+
export interface WorkerMessage {
|
|
38
|
+
type: 'execute' | 'cancel';
|
|
39
|
+
migrationPath?: string;
|
|
40
|
+
stepName?: string;
|
|
41
|
+
context?: MigrationContext;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Response messages sent from worker threads back to the main thread.
|
|
46
|
+
*/
|
|
47
|
+
export interface WorkerResponse {
|
|
48
|
+
type: 'result' | 'error';
|
|
49
|
+
result?: MigrationStepResult;
|
|
50
|
+
error?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Internal state for tracking migrations.
|
|
55
|
+
*/
|
|
56
|
+
interface ExecutionState {
|
|
57
|
+
context: MigrationContext;
|
|
58
|
+
stepsExecuted: string[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Executes schema migrations for a project.
|
|
63
|
+
*/
|
|
64
|
+
export class MigrationExecutor {
|
|
65
|
+
private logger = getChildLogger({ module: 'MigrationExecutor' });
|
|
66
|
+
private timeoutMilliSeconds: number;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Constructs instance of MigrationExecutor
|
|
70
|
+
* @param project Project instance to use
|
|
71
|
+
* @param backupDir Backup directory, if any.
|
|
72
|
+
* @param timeoutMilliSeconds Timeout in milliseconds (defaults to 2 minutes)
|
|
73
|
+
*/
|
|
74
|
+
constructor(
|
|
75
|
+
private project: Project,
|
|
76
|
+
private backupDir?: string,
|
|
77
|
+
timeoutMilliSeconds?: number,
|
|
78
|
+
) {
|
|
79
|
+
this.timeoutMilliSeconds =
|
|
80
|
+
timeoutMilliSeconds ?? DEFAULT_MIGRATION_TIMEOUT_MS;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Helper to create failure result from ExecutionState
|
|
84
|
+
private createFailureResult(
|
|
85
|
+
state: ExecutionState,
|
|
86
|
+
message: string,
|
|
87
|
+
error?: Error,
|
|
88
|
+
): MigrationResult {
|
|
89
|
+
return {
|
|
90
|
+
success: false,
|
|
91
|
+
message,
|
|
92
|
+
error,
|
|
93
|
+
stepsExecuted: state.stepsExecuted,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Execute a single migration step and handle failure
|
|
98
|
+
private async executeStep(
|
|
99
|
+
stepName: string,
|
|
100
|
+
migrationPath: string,
|
|
101
|
+
state: ExecutionState,
|
|
102
|
+
): Promise<{ success: true } | MigrationResult> {
|
|
103
|
+
const result = await executeStep(
|
|
104
|
+
migrationPath,
|
|
105
|
+
stepName,
|
|
106
|
+
state.context,
|
|
107
|
+
this.timeoutMilliSeconds,
|
|
108
|
+
);
|
|
109
|
+
state.stepsExecuted.push(stepName);
|
|
110
|
+
if (!result.success) {
|
|
111
|
+
const messagePrefix =
|
|
112
|
+
{
|
|
113
|
+
before: 'Pre-migration check failed',
|
|
114
|
+
backup: 'Backup failed',
|
|
115
|
+
migrate: 'Migration failed',
|
|
116
|
+
after: 'Post-migration step failed',
|
|
117
|
+
}[stepName] || `Step '${stepName}' failed`;
|
|
118
|
+
|
|
119
|
+
return this.createFailureResult(
|
|
120
|
+
state,
|
|
121
|
+
`${messagePrefix}: ${result.message || 'Unknown error'}`,
|
|
122
|
+
result.error,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
return { success: true };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Execute a single migration.
|
|
129
|
+
private async executeMigration(
|
|
130
|
+
migrationPath: string,
|
|
131
|
+
fromVersion: number,
|
|
132
|
+
toVersion: number,
|
|
133
|
+
updateVersionCallback: (version: number) => Promise<void>,
|
|
134
|
+
): Promise<MigrationResult> {
|
|
135
|
+
this.logger.info(
|
|
136
|
+
{ fromVersion, toVersion },
|
|
137
|
+
`Executing migration from version ${fromVersion} to ${toVersion}`,
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const state: ExecutionState = {
|
|
141
|
+
context: {
|
|
142
|
+
cardRootPath: this.project.paths.cardRootFolder,
|
|
143
|
+
cardsConfigPath: this.project.paths.internalRootFolder,
|
|
144
|
+
fromVersion,
|
|
145
|
+
toVersion,
|
|
146
|
+
},
|
|
147
|
+
stepsExecuted: [],
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
// Load migration to check which steps exist
|
|
152
|
+
const migration = this.loadMigration(toVersion);
|
|
153
|
+
if (!migration) {
|
|
154
|
+
return this.createFailureResult(
|
|
155
|
+
state,
|
|
156
|
+
`Failed to load migration for version ${toVersion}`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (migration.before) {
|
|
161
|
+
const result = await this.executeStep('before', migrationPath, state);
|
|
162
|
+
if (!result.success) return result;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (migration.backup && this.backupDir !== undefined) {
|
|
166
|
+
state.context.backupDir = this.backupDir;
|
|
167
|
+
const result = await this.executeStep('backup', migrationPath, state);
|
|
168
|
+
if (!result.success) return result;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const migrateResult = await this.executeStep(
|
|
172
|
+
'migrate',
|
|
173
|
+
migrationPath,
|
|
174
|
+
state,
|
|
175
|
+
);
|
|
176
|
+
if (!migrateResult.success) return migrateResult;
|
|
177
|
+
|
|
178
|
+
// Update schema version in project after successful migration
|
|
179
|
+
await updateVersionCallback(toVersion);
|
|
180
|
+
state.stepsExecuted.push('update-version');
|
|
181
|
+
|
|
182
|
+
if (migration.after) {
|
|
183
|
+
const result = await this.executeStep('after', migrationPath, state);
|
|
184
|
+
if (!result.success) return result;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Run validation after migration
|
|
188
|
+
state.stepsExecuted.push('validate');
|
|
189
|
+
const validationErrors = await this.validate();
|
|
190
|
+
if (validationErrors) {
|
|
191
|
+
return this.createFailureResult(
|
|
192
|
+
state,
|
|
193
|
+
`Post-migration validation failed: ${validationErrors}`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
this.logger.info('Post-migration validation passed');
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
success: true,
|
|
200
|
+
message: `Successfully migrated from version ${fromVersion} to ${toVersion}`,
|
|
201
|
+
stepsExecuted: state.stepsExecuted,
|
|
202
|
+
};
|
|
203
|
+
} catch (error) {
|
|
204
|
+
return this.createFailureResult(
|
|
205
|
+
state,
|
|
206
|
+
`Migration threw an exception: ${error}`,
|
|
207
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Checks if pre-migration validation succeeds.
|
|
213
|
+
private async preMigrateValidation(
|
|
214
|
+
fromVersion: number,
|
|
215
|
+
toVersion: number,
|
|
216
|
+
stepsExecuted: string[],
|
|
217
|
+
): Promise<MigrationResult> {
|
|
218
|
+
const validationErrors = await this.validate();
|
|
219
|
+
if (validationErrors) {
|
|
220
|
+
this.logger.error(
|
|
221
|
+
{ errors: validationErrors },
|
|
222
|
+
'Pre-migration validation failed',
|
|
223
|
+
);
|
|
224
|
+
return {
|
|
225
|
+
success: false,
|
|
226
|
+
message: `Pre-migration validation failed. Please fix the following errors before migrating:\n${validationErrors}`,
|
|
227
|
+
stepsExecuted,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
this.logger.info('Pre-migration validation passed');
|
|
231
|
+
return {
|
|
232
|
+
success: true,
|
|
233
|
+
stepsExecuted,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Validate the project.
|
|
238
|
+
private async validate() {
|
|
239
|
+
const validator = Validate.getInstance();
|
|
240
|
+
return await validator.validate(this.project.basePath, () => this.project);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Validates that migration versions are valid.
|
|
244
|
+
private validateMigrationVersions(
|
|
245
|
+
fromVersion: number,
|
|
246
|
+
toVersion: number,
|
|
247
|
+
stepsExecuted: string[],
|
|
248
|
+
): MigrationResult {
|
|
249
|
+
const valid = fromVersion < toVersion;
|
|
250
|
+
return {
|
|
251
|
+
success: valid,
|
|
252
|
+
message: valid
|
|
253
|
+
? undefined
|
|
254
|
+
: `Current version (${fromVersion}) is not lower than target version (${toVersion})`,
|
|
255
|
+
stepsExecuted,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Checks if there is enough disk space.
|
|
260
|
+
protected async checkDiskSpace(
|
|
261
|
+
fromVersion: number,
|
|
262
|
+
toVersion: number,
|
|
263
|
+
stepsExecuted: string[],
|
|
264
|
+
): Promise<MigrationResult> {
|
|
265
|
+
try {
|
|
266
|
+
const internalSize = await folderSize(
|
|
267
|
+
this.project.paths.internalRootFolder,
|
|
268
|
+
);
|
|
269
|
+
const cardRootSize = await folderSize(this.project.paths.cardRootFolder);
|
|
270
|
+
const projectSize = internalSize + cardRootSize;
|
|
271
|
+
const requiredSpace = projectSize * 2;
|
|
272
|
+
const spaceAvailable = await availableSpace(this.project.basePath);
|
|
273
|
+
const projectSizeMB = (projectSize / MEGABYTES).toFixed(2);
|
|
274
|
+
const requiredSpaceMB = (requiredSpace / MEGABYTES).toFixed(2);
|
|
275
|
+
const availableSpaceMB = (spaceAvailable / MEGABYTES).toFixed(2);
|
|
276
|
+
|
|
277
|
+
if (spaceAvailable < requiredSpace) {
|
|
278
|
+
return {
|
|
279
|
+
success: false,
|
|
280
|
+
message: `Insufficient disk space. Required: ${requiredSpaceMB} MB, Available: ${availableSpaceMB} MB. Migration needs at least 2x the project size (${projectSizeMB} MB).`,
|
|
281
|
+
stepsExecuted,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
this.logger.info('Disk space check passed');
|
|
286
|
+
return {
|
|
287
|
+
success: true,
|
|
288
|
+
stepsExecuted,
|
|
289
|
+
};
|
|
290
|
+
} catch (error) {
|
|
291
|
+
this.logger.error({ error }, 'Failed to check disk space');
|
|
292
|
+
return {
|
|
293
|
+
success: false,
|
|
294
|
+
message: `Failed to check disk space: ${error instanceof Error ? error.message : String(error)}`,
|
|
295
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
296
|
+
stepsExecuted,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Discover available migrations between two versions.
|
|
303
|
+
* @param fromVersion Starting version (exclusive)
|
|
304
|
+
* @param toVersion Target version (inclusive)
|
|
305
|
+
* @returns Sorted list of migration version numbers
|
|
306
|
+
*/
|
|
307
|
+
protected migrationsAvailable(
|
|
308
|
+
fromVersion: number,
|
|
309
|
+
toVersion: number,
|
|
310
|
+
): number[] {
|
|
311
|
+
const allVersions = availableMigrations();
|
|
312
|
+
return allVersions.filter(
|
|
313
|
+
(version) => version > fromVersion && version <= toVersion,
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Validates and discovers available migrations
|
|
318
|
+
protected availableMigrations(
|
|
319
|
+
fromVersion: number,
|
|
320
|
+
toVersion: number,
|
|
321
|
+
stepsExecuted: string[],
|
|
322
|
+
): MigrationResult & { migrationVersions: number[] } {
|
|
323
|
+
const migrationVersions = this.migrationsAvailable(fromVersion, toVersion);
|
|
324
|
+
const found = migrationVersions.length > 0;
|
|
325
|
+
if (found) {
|
|
326
|
+
this.logger.info(
|
|
327
|
+
{ versions: migrationVersions },
|
|
328
|
+
`Found ${migrationVersions.length} migration(s)`,
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
success: found,
|
|
333
|
+
message: found
|
|
334
|
+
? undefined
|
|
335
|
+
: `No migrations found between version ${fromVersion} and ${toVersion}`,
|
|
336
|
+
stepsExecuted,
|
|
337
|
+
migrationVersions,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Load a migration module for a specific version.
|
|
342
|
+
protected loadMigration(version: number): Migration | undefined {
|
|
343
|
+
this.logger.debug({ version }, 'Loading migration');
|
|
344
|
+
|
|
345
|
+
try {
|
|
346
|
+
const migrationObject = migration(version);
|
|
347
|
+
|
|
348
|
+
if (!migrationObject) {
|
|
349
|
+
this.logger.error({ version }, `Migration not found`);
|
|
350
|
+
return undefined;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (typeof migrationObject.migrate !== 'function') {
|
|
354
|
+
throw new Error(
|
|
355
|
+
`Migration ${version} does not implement migrate() function`,
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return migrationObject;
|
|
360
|
+
} catch (error) {
|
|
361
|
+
this.logger.error({ error, version }, `Failed to load migration`);
|
|
362
|
+
return undefined;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Get the file path for a migration worker.
|
|
368
|
+
* @param version Migration version number
|
|
369
|
+
* @returns Path to migration file
|
|
370
|
+
*/
|
|
371
|
+
protected migrationWorkerPath(version: number): string {
|
|
372
|
+
const migrationsPackageUrl = import.meta.resolve('@cyberismo/migrations');
|
|
373
|
+
const migrationsIndexPath = fileURLToPath(migrationsPackageUrl);
|
|
374
|
+
const migrationsDistDir = dirname(migrationsIndexPath);
|
|
375
|
+
return join(migrationsDistDir, version.toString(), 'index.js');
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Execute all necessary migrations to bring project to target version.
|
|
380
|
+
* @param fromVersion Current project version
|
|
381
|
+
* @param toVersion Target version
|
|
382
|
+
* @param updateVersionCallback Callback to update project schema version after each migration
|
|
383
|
+
* @returns Overall migration result
|
|
384
|
+
*/
|
|
385
|
+
public async migrate(
|
|
386
|
+
fromVersion: number,
|
|
387
|
+
toVersion: number,
|
|
388
|
+
updateVersionCallback: (version: number) => Promise<void>,
|
|
389
|
+
): Promise<MigrationResult> {
|
|
390
|
+
const stepsDone: string[] = [];
|
|
391
|
+
|
|
392
|
+
// Step: Validate migration versions
|
|
393
|
+
const versionResult = this.validateMigrationVersions(
|
|
394
|
+
fromVersion,
|
|
395
|
+
toVersion,
|
|
396
|
+
stepsDone,
|
|
397
|
+
);
|
|
398
|
+
if (!versionResult.success) return versionResult;
|
|
399
|
+
stepsDone.push('version-validation');
|
|
400
|
+
|
|
401
|
+
// Step: Pre-migration validation
|
|
402
|
+
const validationResult = await this.preMigrateValidation(
|
|
403
|
+
fromVersion,
|
|
404
|
+
toVersion,
|
|
405
|
+
stepsDone,
|
|
406
|
+
);
|
|
407
|
+
if (!validationResult.success) return validationResult;
|
|
408
|
+
stepsDone.push('pre-validation');
|
|
409
|
+
|
|
410
|
+
// Step: Check disk space
|
|
411
|
+
const diskSpaceResult = await this.checkDiskSpace(
|
|
412
|
+
fromVersion,
|
|
413
|
+
toVersion,
|
|
414
|
+
stepsDone,
|
|
415
|
+
);
|
|
416
|
+
if (!diskSpaceResult.success) return diskSpaceResult;
|
|
417
|
+
stepsDone.push('disk-space-check');
|
|
418
|
+
|
|
419
|
+
// Step: Discover available migrations
|
|
420
|
+
const discoveryResult = this.availableMigrations(
|
|
421
|
+
fromVersion,
|
|
422
|
+
toVersion,
|
|
423
|
+
stepsDone,
|
|
424
|
+
);
|
|
425
|
+
if (!discoveryResult.success) return discoveryResult;
|
|
426
|
+
stepsDone.push('migration-versions');
|
|
427
|
+
|
|
428
|
+
const migrationVersions = discoveryResult.migrationVersions;
|
|
429
|
+
|
|
430
|
+
// Step(s): Execute migrations in sequence
|
|
431
|
+
let currentVersion = fromVersion;
|
|
432
|
+
try {
|
|
433
|
+
for (const targetVersion of migrationVersions) {
|
|
434
|
+
const migrationPath = this.migrationWorkerPath(targetVersion);
|
|
435
|
+
|
|
436
|
+
const result = await this.executeMigration(
|
|
437
|
+
migrationPath,
|
|
438
|
+
currentVersion,
|
|
439
|
+
targetVersion,
|
|
440
|
+
updateVersionCallback,
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
if (!result.success) {
|
|
444
|
+
return {
|
|
445
|
+
success: false,
|
|
446
|
+
message: result.message || 'Migration failed',
|
|
447
|
+
error: result.error,
|
|
448
|
+
stepsExecuted: stepsDone,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
stepsDone.push(
|
|
452
|
+
...result.stepsExecuted.map((step) => `v${targetVersion}:${step}`),
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
currentVersion = targetVersion;
|
|
456
|
+
this.logger.info(
|
|
457
|
+
{ targetVersion },
|
|
458
|
+
`Migration to version ${targetVersion} completed successfully`,
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
} catch (error) {
|
|
462
|
+
return {
|
|
463
|
+
success: false,
|
|
464
|
+
message:
|
|
465
|
+
error instanceof Error
|
|
466
|
+
? error.message
|
|
467
|
+
: `Migration timeout or error: ${error}`,
|
|
468
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
469
|
+
stepsExecuted: stepsDone,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
return {
|
|
473
|
+
success: true,
|
|
474
|
+
message: `Successfully migrated from version ${fromVersion} to ${currentVersion}`,
|
|
475
|
+
stepsExecuted: stepsDone,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
}
|