@zintrust/d1-migrator 0.4.0
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/README.md +871 -0
- package/dist/cli/DataMigrator.d.ts +104 -0
- package/dist/cli/DataMigrator.d.ts.map +1 -0
- package/dist/cli/DataMigrator.js +431 -0
- package/dist/cli/MigrateToD1Command.d.ts +52 -0
- package/dist/cli/MigrateToD1Command.d.ts.map +1 -0
- package/dist/cli/MigrateToD1Command.js +600 -0
- package/dist/cli/ProgressTracker.d.ts +32 -0
- package/dist/cli/ProgressTracker.d.ts.map +1 -0
- package/dist/cli/ProgressTracker.js +95 -0
- package/dist/cli/SchemaAnalyzer.d.ts +130 -0
- package/dist/cli/SchemaAnalyzer.d.ts.map +1 -0
- package/dist/cli/SchemaAnalyzer.js +660 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/schema/SchemaBuilder.d.ts +51 -0
- package/dist/schema/SchemaBuilder.d.ts.map +1 -0
- package/dist/schema/SchemaBuilder.js +165 -0
- package/dist/schema/TypeConverter.d.ts +35 -0
- package/dist/schema/TypeConverter.d.ts.map +1 -0
- package/dist/schema/TypeConverter.js +187 -0
- package/dist/schema/Validator.d.ts +74 -0
- package/dist/schema/Validator.d.ts.map +1 -0
- package/dist/schema/Validator.js +225 -0
- package/dist/types.d.ts +145 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/utils/CheckpointManager.d.ts +48 -0
- package/dist/utils/CheckpointManager.d.ts.map +1 -0
- package/dist/utils/CheckpointManager.js +191 -0
- package/dist/utils/DataValidator.d.ts +46 -0
- package/dist/utils/DataValidator.d.ts.map +1 -0
- package/dist/utils/DataValidator.js +139 -0
- package/package.json +37 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema Validator
|
|
3
|
+
* Validates schemas and provides detailed error reporting
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* SchemaValidator - Sealed namespace for schema validation
|
|
7
|
+
* Provides comprehensive schema validation utilities
|
|
8
|
+
*/
|
|
9
|
+
export const SchemaValidator = Object.freeze({
|
|
10
|
+
/**
|
|
11
|
+
* Validate complete schema
|
|
12
|
+
*/
|
|
13
|
+
validateSchema(tables) {
|
|
14
|
+
const errors = [];
|
|
15
|
+
const warnings = [];
|
|
16
|
+
tables.forEach((table, index) => {
|
|
17
|
+
const tableValidation = SchemaValidator.validateTable(table);
|
|
18
|
+
errors.push(...tableValidation.errors.map((error) => `Table ${index + 1} (${table.name}): ${error}`));
|
|
19
|
+
warnings.push(...tableValidation.warnings.map((warning) => `Table ${index + 1} (${table.name}): ${warning}`));
|
|
20
|
+
});
|
|
21
|
+
// Check for duplicate table names
|
|
22
|
+
const tableNames = tables.map((t) => t.name.toLowerCase());
|
|
23
|
+
const duplicates = tableNames.filter((name, index) => tableNames.indexOf(name) !== index);
|
|
24
|
+
if (duplicates.length > 0) {
|
|
25
|
+
errors.push(`Duplicate table names: ${[...new Set(duplicates)].join(', ')}`);
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
valid: errors.length === 0,
|
|
29
|
+
errors,
|
|
30
|
+
warnings,
|
|
31
|
+
};
|
|
32
|
+
},
|
|
33
|
+
/**
|
|
34
|
+
* Validate single table
|
|
35
|
+
*/
|
|
36
|
+
validateTable(table) {
|
|
37
|
+
const errors = [];
|
|
38
|
+
const warnings = [];
|
|
39
|
+
// Validate table name
|
|
40
|
+
if (!table.name || table.name.trim() === '') {
|
|
41
|
+
errors.push('Table name is required');
|
|
42
|
+
}
|
|
43
|
+
else if (!/^\w*$/.test(table.name)) {
|
|
44
|
+
errors.push(`Invalid table name: ${table.name}. Must start with letter or underscore and contain only letters, numbers, and underscores`);
|
|
45
|
+
}
|
|
46
|
+
else if (table.name.length > 64) {
|
|
47
|
+
errors.push(`Table name too long: ${table.name}. Maximum 64 characters`);
|
|
48
|
+
}
|
|
49
|
+
// Validate columns
|
|
50
|
+
if (!table.columns || table.columns.length === 0) {
|
|
51
|
+
errors.push('Table must have at least one column');
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
const columnNames = table.columns.map((c) => c.name.toLowerCase());
|
|
55
|
+
const duplicateColumns = columnNames.filter((name, index) => columnNames.indexOf(name) !== index);
|
|
56
|
+
if (duplicateColumns.length > 0) {
|
|
57
|
+
errors.push(`Duplicate column names: ${[...new Set(duplicateColumns)].join(', ')}`);
|
|
58
|
+
}
|
|
59
|
+
table.columns.forEach((column, colIndex) => {
|
|
60
|
+
const columnValidation = SchemaValidator.validateColumn(column);
|
|
61
|
+
errors.push(...columnValidation.errors.map((error) => `Column ${colIndex + 1} (${column.name}): ${error}`));
|
|
62
|
+
warnings.push(...columnValidation.warnings.map((warning) => `Column ${colIndex + 1} (${column.name}): ${warning}`));
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
// Validate primary key
|
|
66
|
+
if (table.primaryKey) {
|
|
67
|
+
const hasPrimaryKeyColumn = table.columns.some((column) => column.name === table.primaryKey);
|
|
68
|
+
if (!hasPrimaryKeyColumn) {
|
|
69
|
+
errors.push(`Primary key column '${table.primaryKey}' not found in table definition`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
valid: errors.length === 0,
|
|
74
|
+
errors,
|
|
75
|
+
warnings,
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
/**
|
|
79
|
+
* Validate single column
|
|
80
|
+
*/
|
|
81
|
+
validateColumn(column) {
|
|
82
|
+
const errors = [];
|
|
83
|
+
const warnings = [];
|
|
84
|
+
// Validate column name
|
|
85
|
+
if (!column.name || column.name.trim() === '') {
|
|
86
|
+
errors.push('Column name is required');
|
|
87
|
+
}
|
|
88
|
+
else if (!/^\w*$/.test(column.name)) {
|
|
89
|
+
errors.push(`Invalid column name: ${column.name}. Must start with letter or underscore and contain only letters, numbers, and underscores`);
|
|
90
|
+
}
|
|
91
|
+
else if (column.name.length > 64) {
|
|
92
|
+
errors.push(`Column name too long: ${column.name}. Maximum 64 characters`);
|
|
93
|
+
}
|
|
94
|
+
// Validate column type
|
|
95
|
+
if (!column.type || column.type.trim() === '') {
|
|
96
|
+
errors.push('Column type is required');
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
const validTypes = [
|
|
100
|
+
'integer',
|
|
101
|
+
'text',
|
|
102
|
+
'real',
|
|
103
|
+
'numeric',
|
|
104
|
+
'blob',
|
|
105
|
+
'varchar',
|
|
106
|
+
'char',
|
|
107
|
+
'date',
|
|
108
|
+
'datetime',
|
|
109
|
+
'boolean',
|
|
110
|
+
];
|
|
111
|
+
const normalizedType = column.type.toLowerCase();
|
|
112
|
+
if (!validTypes.includes(normalizedType)) {
|
|
113
|
+
warnings.push(`Potentially unsupported column type: ${column.type}. SQLite/D1 may not fully support this type`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// Validate nullable
|
|
117
|
+
if (column.nullable !== undefined && typeof column.nullable !== 'boolean') {
|
|
118
|
+
errors.push('Nullable property must be a boolean');
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
valid: errors.length === 0,
|
|
122
|
+
errors,
|
|
123
|
+
warnings,
|
|
124
|
+
};
|
|
125
|
+
},
|
|
126
|
+
/**
|
|
127
|
+
* Check for schema compatibility issues
|
|
128
|
+
*/
|
|
129
|
+
checkCompatibility(issues) {
|
|
130
|
+
const blocking = [];
|
|
131
|
+
const warnings = [];
|
|
132
|
+
const { sourceDriver, tables } = issues;
|
|
133
|
+
tables.forEach((table) => {
|
|
134
|
+
table.columns.forEach((column) => {
|
|
135
|
+
const type = column.type.toLowerCase();
|
|
136
|
+
const compatibilityWarning = SchemaValidator.checkColumnTypeCompatibility(type, sourceDriver, table.name, column.name);
|
|
137
|
+
if (compatibilityWarning) {
|
|
138
|
+
warnings.push(compatibilityWarning);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
return { blocking, warnings };
|
|
143
|
+
},
|
|
144
|
+
/**
|
|
145
|
+
* Check column type compatibility for specific driver
|
|
146
|
+
*/
|
|
147
|
+
checkColumnTypeCompatibility(type, sourceDriver, tableName, columnName) {
|
|
148
|
+
switch (sourceDriver) {
|
|
149
|
+
case 'mysql':
|
|
150
|
+
return SchemaValidator.checkMySQLCompatibility(type, tableName, columnName);
|
|
151
|
+
case 'postgresql':
|
|
152
|
+
return SchemaValidator.checkPostgreSQLCompatibility(type, tableName, columnName);
|
|
153
|
+
case 'sqlserver':
|
|
154
|
+
return SchemaValidator.checkSQLServerCompatibility(type, tableName, columnName);
|
|
155
|
+
default:
|
|
156
|
+
return SchemaValidator.checkGeneralCompatibility(type, tableName, columnName);
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
/**
|
|
160
|
+
* Check MySQL-specific compatibility
|
|
161
|
+
*/
|
|
162
|
+
checkMySQLCompatibility(type, tableName, columnName) {
|
|
163
|
+
if (type.includes('enum') || type.includes('set')) {
|
|
164
|
+
return `MySQL ENUM/SET types will be converted to TEXT in table: ${tableName}.${columnName}`;
|
|
165
|
+
}
|
|
166
|
+
if (type.includes('json')) {
|
|
167
|
+
return `MySQL JSON types will be converted to TEXT in table: ${tableName}.${columnName}`;
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
},
|
|
171
|
+
/**
|
|
172
|
+
* Check PostgreSQL-specific compatibility
|
|
173
|
+
*/
|
|
174
|
+
checkPostgreSQLCompatibility(type, tableName, columnName) {
|
|
175
|
+
if (type.includes('uuid')) {
|
|
176
|
+
return `PostgreSQL UUID will be converted to TEXT in table: ${tableName}.${columnName}`;
|
|
177
|
+
}
|
|
178
|
+
if (type.includes('jsonb')) {
|
|
179
|
+
return `PostgreSQL JSONB will be converted to TEXT in table: ${tableName}.${columnName}`;
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
},
|
|
183
|
+
/**
|
|
184
|
+
* Check SQL Server-specific compatibility
|
|
185
|
+
*/
|
|
186
|
+
checkSQLServerCompatibility(type, tableName, columnName) {
|
|
187
|
+
if (type.includes('uniqueidentifier')) {
|
|
188
|
+
return `SQL Server UNIQUEIDENTIFIER will be converted to TEXT in table: ${tableName}.${columnName}`;
|
|
189
|
+
}
|
|
190
|
+
if (type.includes('varbinary') || type.includes('image')) {
|
|
191
|
+
return `SQL Server binary types will be converted to BLOB in table: ${tableName}.${columnName}`;
|
|
192
|
+
}
|
|
193
|
+
return null;
|
|
194
|
+
},
|
|
195
|
+
/**
|
|
196
|
+
* Check general compatibility issues
|
|
197
|
+
*/
|
|
198
|
+
checkGeneralCompatibility(type, tableName, columnName) {
|
|
199
|
+
if (type.includes('decimal') || type.includes('numeric')) {
|
|
200
|
+
return `Decimal/numeric types may lose precision when converted to REAL in table: ${tableName}.${columnName}`;
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
203
|
+
},
|
|
204
|
+
/**
|
|
205
|
+
* Generate validation report
|
|
206
|
+
*/
|
|
207
|
+
generateReport(validation) {
|
|
208
|
+
let report = '# Schema Validation Report\n\n';
|
|
209
|
+
report += `## Status: ${validation.valid ? 'VALID' : 'INVALID'}\n\n`;
|
|
210
|
+
if (validation.errors.length > 0) {
|
|
211
|
+
report += `## Errors (${validation.errors.length})\n\n`;
|
|
212
|
+
validation.errors.forEach((error, index) => {
|
|
213
|
+
report += `${index + 1}. ${error}\n`;
|
|
214
|
+
});
|
|
215
|
+
report += '\n';
|
|
216
|
+
}
|
|
217
|
+
if (validation.warnings.length > 0) {
|
|
218
|
+
report += `## Warnings (${validation.warnings.length})\n\n`;
|
|
219
|
+
validation.warnings.forEach((warning, index) => {
|
|
220
|
+
report += `${index + 1}. ${warning}\n`;
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
return report;
|
|
224
|
+
},
|
|
225
|
+
});
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D1 Migrator Types
|
|
3
|
+
* Type definitions for migration operations
|
|
4
|
+
*/
|
|
5
|
+
export type SourceDatabaseDriver = 'mysql' | 'postgresql' | 'sqlite' | 'sqlserver';
|
|
6
|
+
export interface MigrationConfig {
|
|
7
|
+
sourceConnection: string;
|
|
8
|
+
sourceDriver: SourceDatabaseDriver;
|
|
9
|
+
targetDatabase: string;
|
|
10
|
+
targetType: 'd1' | 'd1-remote';
|
|
11
|
+
batchSize?: number;
|
|
12
|
+
checkpointInterval?: number;
|
|
13
|
+
dryRun?: boolean;
|
|
14
|
+
interactive?: boolean;
|
|
15
|
+
migrationId?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface MigrationState {
|
|
18
|
+
id: string;
|
|
19
|
+
status: 'pending' | 'running' | 'paused' | 'completed' | 'failed';
|
|
20
|
+
startTime: Date;
|
|
21
|
+
lastCheckpoint?: Date;
|
|
22
|
+
totalTables: number;
|
|
23
|
+
completedTables: number;
|
|
24
|
+
totalRows: number;
|
|
25
|
+
completedRows: number;
|
|
26
|
+
errors: MigrationError[];
|
|
27
|
+
config: MigrationConfig;
|
|
28
|
+
}
|
|
29
|
+
export interface CheckpointData {
|
|
30
|
+
migrationId: string;
|
|
31
|
+
table: string;
|
|
32
|
+
lastProcessedId?: string | number;
|
|
33
|
+
processedRows: number;
|
|
34
|
+
totalRows: number;
|
|
35
|
+
checksum?: string;
|
|
36
|
+
timestamp: Date;
|
|
37
|
+
batchIndex: number;
|
|
38
|
+
}
|
|
39
|
+
export interface SchemaAnalysisResult {
|
|
40
|
+
tables: TableSchema[];
|
|
41
|
+
dependencies: TableDependency[];
|
|
42
|
+
conflicts: SchemaConflict[];
|
|
43
|
+
warnings: SchemaWarning[];
|
|
44
|
+
}
|
|
45
|
+
export interface DatabaseSchema {
|
|
46
|
+
tables: TableSchema[];
|
|
47
|
+
relationships: TableRelationship[];
|
|
48
|
+
constraints: TableConstraint[];
|
|
49
|
+
}
|
|
50
|
+
export interface TableRelationship {
|
|
51
|
+
sourceTable: string;
|
|
52
|
+
targetTable: string;
|
|
53
|
+
sourceColumn: string;
|
|
54
|
+
targetColumn: string;
|
|
55
|
+
type: 'one-to-one' | 'one-to-many' | 'many-to-many';
|
|
56
|
+
}
|
|
57
|
+
export interface TableConstraint {
|
|
58
|
+
table: string;
|
|
59
|
+
type: 'primary_key' | 'foreign_key' | 'unique' | 'check' | 'not_null';
|
|
60
|
+
columns: string[];
|
|
61
|
+
definition?: string;
|
|
62
|
+
}
|
|
63
|
+
export interface TableSchema {
|
|
64
|
+
primaryKey: string;
|
|
65
|
+
name: string;
|
|
66
|
+
columns: ColumnSchema[];
|
|
67
|
+
indexes: IndexSchema[];
|
|
68
|
+
foreignKeys: ForeignKeySchema[];
|
|
69
|
+
primaryKeys: string[];
|
|
70
|
+
rowCount?: number;
|
|
71
|
+
}
|
|
72
|
+
export interface ColumnSchema {
|
|
73
|
+
name: string;
|
|
74
|
+
type: string;
|
|
75
|
+
nullable: boolean;
|
|
76
|
+
defaultValue?: unknown;
|
|
77
|
+
autoIncrement?: boolean;
|
|
78
|
+
maxLength?: number;
|
|
79
|
+
precision?: number;
|
|
80
|
+
scale?: number;
|
|
81
|
+
}
|
|
82
|
+
export interface IndexSchema {
|
|
83
|
+
name: string;
|
|
84
|
+
columns: string[];
|
|
85
|
+
unique: boolean;
|
|
86
|
+
primary?: boolean;
|
|
87
|
+
}
|
|
88
|
+
export interface ForeignKeySchema {
|
|
89
|
+
name: string;
|
|
90
|
+
column: string;
|
|
91
|
+
referencedTable: string;
|
|
92
|
+
referencedColumn: string;
|
|
93
|
+
onDelete?: 'CASCADE' | 'SET NULL' | 'RESTRICT';
|
|
94
|
+
onUpdate?: 'CASCADE' | 'SET NULL' | 'RESTRICT';
|
|
95
|
+
}
|
|
96
|
+
export interface TableDependency {
|
|
97
|
+
table: string;
|
|
98
|
+
dependsOn: string[];
|
|
99
|
+
level: number;
|
|
100
|
+
}
|
|
101
|
+
export interface SchemaConflict {
|
|
102
|
+
type: 'unsupported_type' | 'size_limitation' | 'constraint_incompatible';
|
|
103
|
+
table: string;
|
|
104
|
+
column?: string;
|
|
105
|
+
description: string;
|
|
106
|
+
severity: 'error' | 'warning';
|
|
107
|
+
suggestion?: string;
|
|
108
|
+
}
|
|
109
|
+
export interface SchemaWarning {
|
|
110
|
+
type: 'data_loss_risk' | 'performance_impact' | 'manual_review';
|
|
111
|
+
table: string;
|
|
112
|
+
description: string;
|
|
113
|
+
suggestion?: string;
|
|
114
|
+
}
|
|
115
|
+
export interface MigrationProgress {
|
|
116
|
+
migrationId: string;
|
|
117
|
+
currentTable: string;
|
|
118
|
+
table: string;
|
|
119
|
+
status: 'pending' | 'processing' | 'completed' | 'failed';
|
|
120
|
+
processedRows: number;
|
|
121
|
+
totalTables: number;
|
|
122
|
+
totalRows: number;
|
|
123
|
+
percentage: number;
|
|
124
|
+
errors: Record<string, string>;
|
|
125
|
+
startTime?: Date;
|
|
126
|
+
endTime?: Date;
|
|
127
|
+
}
|
|
128
|
+
export interface MigrationError {
|
|
129
|
+
table?: string;
|
|
130
|
+
batch?: number;
|
|
131
|
+
error: string;
|
|
132
|
+
timestamp: Date;
|
|
133
|
+
retryCount: number;
|
|
134
|
+
resolved: boolean;
|
|
135
|
+
}
|
|
136
|
+
export interface DataValidationResult {
|
|
137
|
+
table: string;
|
|
138
|
+
sourceCount: number;
|
|
139
|
+
targetCount: number;
|
|
140
|
+
checksumMatch: boolean;
|
|
141
|
+
missingRows?: string[];
|
|
142
|
+
extraRows?: string[];
|
|
143
|
+
errors: string[];
|
|
144
|
+
}
|
|
145
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checkpoint Manager
|
|
3
|
+
* File-based persistent state tracking for resumable operations
|
|
4
|
+
*/
|
|
5
|
+
import type { CheckpointData, MigrationError, MigrationState } from '../types';
|
|
6
|
+
/**
|
|
7
|
+
* CheckpointManager - Sealed namespace for migration state management
|
|
8
|
+
* Provides file-based persistent storage for migration progress
|
|
9
|
+
*/
|
|
10
|
+
export declare const CheckpointManager: Readonly<{
|
|
11
|
+
/**
|
|
12
|
+
* Initialize migration directory
|
|
13
|
+
*/
|
|
14
|
+
initDirectory(): Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* Save migration state to file
|
|
17
|
+
*/
|
|
18
|
+
saveState(state: MigrationState): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* Load migration state from file
|
|
21
|
+
*/
|
|
22
|
+
loadState(migrationId: string): Promise<MigrationState | null>;
|
|
23
|
+
/**
|
|
24
|
+
* Save checkpoint data
|
|
25
|
+
*/
|
|
26
|
+
saveCheckpoint(checkpoint: CheckpointData): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Load checkpoint data
|
|
29
|
+
*/
|
|
30
|
+
loadCheckpoint(migrationId: string, table: string): Promise<CheckpointData | null>;
|
|
31
|
+
/**
|
|
32
|
+
* Get all checkpoints for a migration
|
|
33
|
+
*/
|
|
34
|
+
getAllCheckpoints(migrationId: string): Promise<CheckpointData[]>;
|
|
35
|
+
/**
|
|
36
|
+
* Clean up migration files
|
|
37
|
+
*/
|
|
38
|
+
cleanup(migrationId: string): Promise<void>;
|
|
39
|
+
/**
|
|
40
|
+
* List all migrations
|
|
41
|
+
*/
|
|
42
|
+
listMigrations(): Promise<string[]>;
|
|
43
|
+
/**
|
|
44
|
+
* Log migration error
|
|
45
|
+
*/
|
|
46
|
+
logError(migrationId: string, errorData: MigrationError): Promise<void>;
|
|
47
|
+
}>;
|
|
48
|
+
//# sourceMappingURL=CheckpointManager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CheckpointManager.d.ts","sourceRoot":"","sources":["../../src/utils/CheckpointManager.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAM/E;;;GAGG;AACH,eAAO,MAAM,iBAAiB;IAC5B;;OAEG;qBACoB,OAAO,CAAC,IAAI,CAAC;IASpC;;OAEG;qBACoB,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAerD;;OAEG;2BAC0B,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;IAoBpE;;OAEG;+BAC8B,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAkB/D;;OAEG;gCAC+B,MAAM,SAAS,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;IAcxF;;OAEG;mCACkC,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAuCvE;;OAEG;yBACwB,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAwBjD;;OAEG;sBACqB,OAAO,CAAC,MAAM,EAAE,CAAC;IAgBzC;;OAEG;0BACyB,MAAM,aAAa,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;EAuB7E,CAAC"}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checkpoint Manager
|
|
3
|
+
* File-based persistent state tracking for resumable operations
|
|
4
|
+
*/
|
|
5
|
+
import { ErrorFactory, Logger, NodeSingletons } from '@zintrust/core';
|
|
6
|
+
const MIGRATION_DIR = '.zintrust/migration';
|
|
7
|
+
const path = NodeSingletons.path;
|
|
8
|
+
const { fsPromises, mkdir, readFile, rm, writeFile } = NodeSingletons.fs;
|
|
9
|
+
/**
|
|
10
|
+
* CheckpointManager - Sealed namespace for migration state management
|
|
11
|
+
* Provides file-based persistent storage for migration progress
|
|
12
|
+
*/
|
|
13
|
+
export const CheckpointManager = Object.freeze({
|
|
14
|
+
/**
|
|
15
|
+
* Initialize migration directory
|
|
16
|
+
*/
|
|
17
|
+
async initDirectory() {
|
|
18
|
+
try {
|
|
19
|
+
await mkdir(MIGRATION_DIR, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
catch (initError) {
|
|
22
|
+
Logger.error('Failed to create migration directory:', initError);
|
|
23
|
+
throw ErrorFactory.createConfigError('Cannot create migration directory');
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
/**
|
|
27
|
+
* Save migration state to file
|
|
28
|
+
*/
|
|
29
|
+
async saveState(state) {
|
|
30
|
+
await CheckpointManager.initDirectory();
|
|
31
|
+
const filePath = path.join(MIGRATION_DIR, `migration-${state.id}.json`);
|
|
32
|
+
const content = JSON.stringify(state, null, 2);
|
|
33
|
+
try {
|
|
34
|
+
await writeFile(filePath, content, 'utf-8');
|
|
35
|
+
Logger.info(`Migration state saved: ${filePath}`);
|
|
36
|
+
}
|
|
37
|
+
catch (writeError) {
|
|
38
|
+
Logger.error('Failed to save migration state:', writeError);
|
|
39
|
+
throw ErrorFactory.createConfigError('Cannot save migration state');
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
/**
|
|
43
|
+
* Load migration state from file
|
|
44
|
+
*/
|
|
45
|
+
async loadState(migrationId) {
|
|
46
|
+
const filePath = NodeSingletons.path.join(MIGRATION_DIR, `migration-${migrationId}.json`);
|
|
47
|
+
try {
|
|
48
|
+
const content = await readFile(filePath, 'utf-8');
|
|
49
|
+
const state = JSON.parse(content);
|
|
50
|
+
// Convert date strings back to Date objects
|
|
51
|
+
state.startTime = new Date(state.startTime);
|
|
52
|
+
if (state.lastCheckpoint) {
|
|
53
|
+
state.lastCheckpoint = new Date(state.lastCheckpoint);
|
|
54
|
+
}
|
|
55
|
+
return state;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
Logger.warn(`Migration state not found: ${filePath}`);
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
/**
|
|
63
|
+
* Save checkpoint data
|
|
64
|
+
*/
|
|
65
|
+
async saveCheckpoint(checkpoint) {
|
|
66
|
+
await CheckpointManager.initDirectory();
|
|
67
|
+
const filePath = path.join(MIGRATION_DIR, `checkpoint-${checkpoint.migrationId}-${checkpoint.table}.json`);
|
|
68
|
+
const content = JSON.stringify(checkpoint, null, 2);
|
|
69
|
+
try {
|
|
70
|
+
await writeFile(filePath, content, 'utf-8');
|
|
71
|
+
Logger.debug(`Checkpoint saved: ${filePath}`);
|
|
72
|
+
}
|
|
73
|
+
catch (writeError) {
|
|
74
|
+
Logger.error('Failed to save checkpoint:', writeError);
|
|
75
|
+
throw ErrorFactory.createConfigError('Cannot save checkpoint');
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
/**
|
|
79
|
+
* Load checkpoint data
|
|
80
|
+
*/
|
|
81
|
+
async loadCheckpoint(migrationId, table) {
|
|
82
|
+
const filePath = path.join(MIGRATION_DIR, `checkpoint-${migrationId}-${table}.json`);
|
|
83
|
+
try {
|
|
84
|
+
const content = await readFile(filePath, 'utf-8');
|
|
85
|
+
const checkpoint = JSON.parse(content);
|
|
86
|
+
checkpoint.timestamp = new Date(checkpoint.timestamp);
|
|
87
|
+
return checkpoint;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
Logger.debug(`Checkpoint not found: ${filePath}`);
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
/**
|
|
95
|
+
* Get all checkpoints for a migration
|
|
96
|
+
*/
|
|
97
|
+
async getAllCheckpoints(migrationId) {
|
|
98
|
+
await CheckpointManager.initDirectory();
|
|
99
|
+
try {
|
|
100
|
+
const files = await fsPromises.readdir(MIGRATION_DIR);
|
|
101
|
+
const checkpointFiles = files.filter((file) => file.startsWith(`checkpoint-${migrationId}-`) && file.endsWith('.json'));
|
|
102
|
+
const checkpoints = [];
|
|
103
|
+
// Process files in parallel for better performance
|
|
104
|
+
const checkpointPromises = checkpointFiles.map(async (file) => {
|
|
105
|
+
try {
|
|
106
|
+
const filePath = path.join(MIGRATION_DIR, file);
|
|
107
|
+
const content = await readFile(filePath, 'utf-8');
|
|
108
|
+
const checkpoint = JSON.parse(content);
|
|
109
|
+
checkpoint.timestamp = new Date(checkpoint.timestamp);
|
|
110
|
+
return checkpoint;
|
|
111
|
+
}
|
|
112
|
+
catch (parseError) {
|
|
113
|
+
Logger.warn(`Failed to load checkpoint file ${file}:`, parseError);
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
const results = await Promise.allSettled(checkpointPromises);
|
|
118
|
+
results.forEach((result) => {
|
|
119
|
+
if (result.status === 'fulfilled' && result.value !== null) {
|
|
120
|
+
checkpoints.push(result.value);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
return checkpoints.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
|
124
|
+
}
|
|
125
|
+
catch (readError) {
|
|
126
|
+
Logger.error('Failed to list checkpoints:', readError);
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
/**
|
|
131
|
+
* Clean up migration files
|
|
132
|
+
*/
|
|
133
|
+
async cleanup(migrationId) {
|
|
134
|
+
try {
|
|
135
|
+
const files = await fsPromises.readdir(MIGRATION_DIR);
|
|
136
|
+
const migrationFiles = files.filter((file) => file.includes(migrationId) && file.endsWith('.json'));
|
|
137
|
+
// Process cleanup in parallel
|
|
138
|
+
const cleanupPromises = migrationFiles.map(async (file) => {
|
|
139
|
+
try {
|
|
140
|
+
const filePath = path.join(MIGRATION_DIR, file);
|
|
141
|
+
await rm(filePath);
|
|
142
|
+
Logger.debug(`Cleaned up migration file: ${file}`);
|
|
143
|
+
}
|
|
144
|
+
catch (cleanupError) {
|
|
145
|
+
Logger.warn(`Failed to cleanup file ${file}:`, cleanupError);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
await Promise.allSettled(cleanupPromises);
|
|
149
|
+
}
|
|
150
|
+
catch (readError) {
|
|
151
|
+
Logger.error('Failed to cleanup migration files:', readError);
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
/**
|
|
155
|
+
* List all migrations
|
|
156
|
+
*/
|
|
157
|
+
async listMigrations() {
|
|
158
|
+
try {
|
|
159
|
+
const files = await fsPromises.readdir(MIGRATION_DIR);
|
|
160
|
+
const migrationFiles = files.filter((file) => file.startsWith('migration-') && file.endsWith('.json'));
|
|
161
|
+
return migrationFiles.map((file) => file.replace('migration-', '').replace('.json', ''));
|
|
162
|
+
}
|
|
163
|
+
catch (readError) {
|
|
164
|
+
Logger.error('Failed to list migrations:', readError);
|
|
165
|
+
return [];
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
/**
|
|
169
|
+
* Log migration error
|
|
170
|
+
*/
|
|
171
|
+
async logError(migrationId, errorData) {
|
|
172
|
+
await CheckpointManager.initDirectory();
|
|
173
|
+
const filePath = path.join(MIGRATION_DIR, `errors-${migrationId}.json`);
|
|
174
|
+
try {
|
|
175
|
+
let errors = [];
|
|
176
|
+
try {
|
|
177
|
+
const content = await readFile(filePath, 'utf-8');
|
|
178
|
+
errors = JSON.parse(content);
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
// File doesn't exist, start with empty array
|
|
182
|
+
}
|
|
183
|
+
errors.push(errorData);
|
|
184
|
+
await writeFile(filePath, JSON.stringify(errors, null, 2), 'utf-8');
|
|
185
|
+
Logger.error(`Migration error logged: ${errorData.error}`);
|
|
186
|
+
}
|
|
187
|
+
catch (writeError) {
|
|
188
|
+
Logger.error('Failed to log migration error:', writeError);
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data Validator
|
|
3
|
+
* Integrity checks and validation for migrated data
|
|
4
|
+
*/
|
|
5
|
+
import type { DataValidationResult } from '../types';
|
|
6
|
+
/**
|
|
7
|
+
* DataValidator - Sealed namespace for data integrity validation
|
|
8
|
+
* Provides comprehensive validation of migrated data
|
|
9
|
+
*/
|
|
10
|
+
export declare const DataValidator: Readonly<{
|
|
11
|
+
/**
|
|
12
|
+
* Validate data integrity between source and target
|
|
13
|
+
*/
|
|
14
|
+
validateDataIntegrity(sourceCount: number, targetCount: number, tableName: string): Promise<boolean>;
|
|
15
|
+
/**
|
|
16
|
+
* Generate checksum for data validation
|
|
17
|
+
*/
|
|
18
|
+
generateChecksum(data: unknown[]): Promise<string>;
|
|
19
|
+
/**
|
|
20
|
+
* Validate data checksums
|
|
21
|
+
*/
|
|
22
|
+
validateChecksum(sourceData: unknown[], targetData: unknown[], tableName: string): Promise<boolean>;
|
|
23
|
+
/**
|
|
24
|
+
* Comprehensive data validation
|
|
25
|
+
*/
|
|
26
|
+
validateTable(sourceData: unknown[], targetData: unknown[], tableName: string): Promise<DataValidationResult>;
|
|
27
|
+
/**
|
|
28
|
+
* Validate schema compatibility
|
|
29
|
+
*/
|
|
30
|
+
validateSchemaCompatibility(sourceSchema: unknown, targetSchema: unknown, tableName: string): {
|
|
31
|
+
valid: boolean;
|
|
32
|
+
errors: string[];
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Sanitize table name for D1 compatibility
|
|
36
|
+
*/
|
|
37
|
+
sanitizeTableName(tableName: string): string;
|
|
38
|
+
/**
|
|
39
|
+
* Validate column type conversion
|
|
40
|
+
*/
|
|
41
|
+
validateColumnType(sourceType: string, targetType: string, tableName: string, columnName: string): {
|
|
42
|
+
valid: boolean;
|
|
43
|
+
warning?: string;
|
|
44
|
+
};
|
|
45
|
+
}>;
|
|
46
|
+
//# sourceMappingURL=DataValidator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DataValidator.d.ts","sourceRoot":"","sources":["../../src/utils/DataValidator.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAErD;;;GAGG;AACH,eAAO,MAAM,aAAa;IACxB;;OAEG;uCAEY,MAAM,eACN,MAAM,aACR,MAAM,GAChB,OAAO,CAAC,OAAO,CAAC;IAcnB;;OAEG;2BAC0B,OAAO,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IAMxD;;OAEG;iCAEW,OAAO,EAAE,cACT,OAAO,EAAE,aACV,MAAM,GAChB,OAAO,CAAC,OAAO,CAAC;IAiBnB;;OAEG;8BAEW,OAAO,EAAE,cACT,OAAO,EAAE,aACV,MAAM,GAChB,OAAO,CAAC,oBAAoB,CAAC;IAsChC;;OAEG;8CAEa,OAAO,gBACP,OAAO,aACV,MAAM,GAChB;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,EAAE,CAAA;KAAE;IAsBvC;;OAEG;iCAC0B,MAAM,GAAG,MAAM;IAe5C;;OAEG;mCAEW,MAAM,cACN,MAAM,aACP,MAAM,cACL,MAAM,GACjB;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE;EA2BvC,CAAC"}
|