@javalabs/prisma-client 1.0.18 → 1.0.20
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/.github/CODEOWNERS +1 -0
- package/README.md +269 -269
- package/dist/index.d.ts +1 -1
- package/dist/prisma.service.d.ts +1 -1
- package/dist/scripts/add-uuid-to-table.js +32 -32
- package/dist/scripts/data-migration/batch-migrator.js +12 -12
- package/dist/scripts/data-migration/data-transformer.js +14 -14
- package/dist/scripts/data-migration/dependency-resolver.js +23 -23
- package/dist/scripts/data-migration/entity-discovery.js +68 -68
- package/dist/scripts/data-migration/foreign-key-manager.js +23 -23
- package/dist/scripts/data-migration/migration-tool.js +5 -5
- package/dist/scripts/data-migration/schema-utils.js +74 -74
- package/dist/scripts/data-migration/typecast-manager.js +4 -4
- package/dist/scripts/database-initializer.js +5 -5
- package/dist/scripts/drop-database.js +5 -5
- package/dist/scripts/fix-data-types.js +53 -53
- package/dist/scripts/fix-enum-values.js +34 -34
- package/dist/scripts/fix-schema-discrepancies.js +40 -40
- package/dist/scripts/fix-table-indexes.js +81 -81
- package/dist/scripts/migrate-schema-structure.js +4 -4
- package/dist/scripts/migrate-uuid.js +19 -19
- package/dist/scripts/post-migration-validator.js +49 -49
- package/dist/scripts/pre-migration-validator.js +107 -107
- package/dist/scripts/reset-database.js +21 -21
- package/dist/scripts/retry-failed-migrations.js +28 -28
- package/dist/scripts/run-migration.js +5 -5
- package/dist/scripts/schema-sync.js +18 -18
- package/dist/scripts/sequence-sync-cli.js +55 -55
- package/dist/scripts/sequence-synchronizer.js +20 -20
- package/dist/scripts/sync-enum-types.js +30 -30
- package/dist/scripts/sync-enum-values.js +52 -52
- package/dist/scripts/truncate-database.js +10 -10
- package/dist/scripts/verify-migration-setup.js +10 -10
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/migration-config.json +63 -63
- package/migration-config.json.bk +95 -95
- package/migrations/add_reserved_amount.sql +8 -0
- package/package.json +44 -44
- package/prisma/migrations/add_uuid_to_transactions.sql +13 -13
- package/prisma/schema.prisma +601 -554
- package/src/index.ts +23 -23
- package/src/prisma-factory.service.ts +40 -40
- package/src/prisma.module.ts +9 -9
- package/src/prisma.service.ts +16 -16
- package/src/scripts/add-uuid-to-table.ts +138 -138
- package/src/scripts/create-tenant-schemas.ts +145 -145
- package/src/scripts/data-migration/batch-migrator.ts +248 -248
- package/src/scripts/data-migration/data-transformer.ts +426 -426
- package/src/scripts/data-migration/db-connector.ts +120 -120
- package/src/scripts/data-migration/dependency-resolver.ts +174 -174
- package/src/scripts/data-migration/entity-discovery.ts +196 -196
- package/src/scripts/data-migration/foreign-key-manager.ts +277 -277
- package/src/scripts/data-migration/migration-config.json +63 -63
- package/src/scripts/data-migration/migration-tool.ts +509 -509
- package/src/scripts/data-migration/schema-utils.ts +248 -248
- package/src/scripts/data-migration/tenant-migrator.ts +201 -201
- package/src/scripts/data-migration/typecast-manager.ts +193 -193
- package/src/scripts/data-migration/types.ts +113 -113
- package/src/scripts/database-initializer.ts +49 -49
- package/src/scripts/drop-database.ts +104 -104
- package/src/scripts/dump-source-db.sh +61 -61
- package/src/scripts/encrypt-user-passwords.ts +36 -36
- package/src/scripts/error-handler.ts +117 -117
- package/src/scripts/fix-data-types.ts +241 -241
- package/src/scripts/fix-enum-values.ts +357 -357
- package/src/scripts/fix-schema-discrepancies.ts +317 -317
- package/src/scripts/fix-table-indexes.ts +601 -601
- package/src/scripts/migrate-schema-structure.ts +90 -90
- package/src/scripts/migrate-uuid.ts +76 -76
- package/src/scripts/post-migration-validator.ts +526 -526
- package/src/scripts/pre-migration-validator.ts +610 -610
- package/src/scripts/reset-database.ts +263 -263
- package/src/scripts/retry-failed-migrations.ts +416 -416
- package/src/scripts/run-migration.ts +707 -707
- package/src/scripts/schema-sync.ts +128 -128
- package/src/scripts/sequence-sync-cli.ts +416 -416
- package/src/scripts/sequence-synchronizer.ts +127 -127
- package/src/scripts/sync-enum-types.ts +170 -170
- package/src/scripts/sync-enum-values.ts +563 -563
- package/src/scripts/truncate-database.ts +123 -123
- package/src/scripts/verify-migration-setup.ts +135 -135
- package/tsconfig.json +17 -17
- package/dist/scripts/data-migration/dependency-manager.d.ts +0 -9
- package/dist/scripts/data-migration/dependency-manager.js +0 -86
- package/dist/scripts/data-migration/dependency-manager.js.map +0 -1
- package/dist/scripts/data-migration/migration-config.json +0 -63
- package/dist/scripts/data-migration/migration-phases.d.ts +0 -5
- package/dist/scripts/data-migration/migration-phases.js +0 -55
- package/dist/scripts/data-migration/migration-phases.js.map +0 -1
- package/dist/scripts/data-migration/multi-source-migrator.d.ts +0 -17
- package/dist/scripts/data-migration/multi-source-migrator.js +0 -130
- package/dist/scripts/data-migration/multi-source-migrator.js.map +0 -1
- package/dist/scripts/data-migration/phase-generator.d.ts +0 -15
- package/dist/scripts/data-migration/phase-generator.js +0 -187
- package/dist/scripts/data-migration/phase-generator.js.map +0 -1
- package/dist/scripts/data-migration.d.ts +0 -22
- package/dist/scripts/data-migration.js +0 -593
- package/dist/scripts/data-migration.js.map +0 -1
- package/dist/scripts/multi-db-migration.d.ts +0 -1
- package/dist/scripts/multi-db-migration.js +0 -55
- package/dist/scripts/multi-db-migration.js.map +0 -1
|
@@ -1,318 +1,318 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import * as path from 'path';
|
|
3
|
-
import * as pg from 'pg';
|
|
4
|
-
import { Logger } from '@nestjs/common';
|
|
5
|
-
import * as dotenv from 'dotenv';
|
|
6
|
-
|
|
7
|
-
dotenv.config();
|
|
8
|
-
|
|
9
|
-
interface ColumnMismatch {
|
|
10
|
-
type: string;
|
|
11
|
-
message: string;
|
|
12
|
-
severity: string;
|
|
13
|
-
details: {
|
|
14
|
-
table: string;
|
|
15
|
-
column: string;
|
|
16
|
-
constraint_type: string;
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
interface MigrationReport {
|
|
21
|
-
timestamp: string;
|
|
22
|
-
sourceDatabase: string;
|
|
23
|
-
targetDatabase: string;
|
|
24
|
-
issueCount: number;
|
|
25
|
-
issues: ColumnMismatch[];
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export class SchemaDiscrepancyFixer {
|
|
29
|
-
private readonly logger = new Logger('SchemaDiscrepancyFixer');
|
|
30
|
-
private readonly sourcePool: pg.Pool;
|
|
31
|
-
private readonly targetPool: pg.Pool;
|
|
32
|
-
private readonly columnTypeCache: Record<string, Record<string, string>> = {};
|
|
33
|
-
|
|
34
|
-
constructor(
|
|
35
|
-
private readonly sourceUrl: string = process.env.SOURCE_DATABASE_URL,
|
|
36
|
-
private readonly targetUrl: string = process.env.DATABASE_URL,
|
|
37
|
-
private readonly reportPath?: string
|
|
38
|
-
) {
|
|
39
|
-
if (!this.sourceUrl) {
|
|
40
|
-
throw new Error('SOURCE_DATABASE_URL environment variable is required');
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (!this.targetUrl) {
|
|
44
|
-
throw new Error('DATABASE_URL environment variable is required');
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
this.sourcePool = new pg.Pool({
|
|
48
|
-
connectionString: this.sourceUrl,
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
this.targetPool = new pg.Pool({
|
|
52
|
-
connectionString: this.targetUrl,
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async fixDiscrepancies(): Promise<void> {
|
|
57
|
-
try {
|
|
58
|
-
// Encontrar el informe de pre-migración más reciente si no se especificó uno
|
|
59
|
-
const reportFile = this.reportPath || await this.findLatestReport();
|
|
60
|
-
if (!reportFile) {
|
|
61
|
-
this.logger.error('No pre-migration report found');
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
this.logger.log(`Using pre-migration report: ${reportFile}`);
|
|
66
|
-
const reportContent = fs.readFileSync(reportFile, 'utf8');
|
|
67
|
-
const report: MigrationReport = JSON.parse(reportContent);
|
|
68
|
-
|
|
69
|
-
// Filtrar solo los problemas de tipo COLUMN_MISMATCH
|
|
70
|
-
const columnMismatches = report.issues.filter(
|
|
71
|
-
issue => issue.type === 'COLUMN_MISMATCH'
|
|
72
|
-
);
|
|
73
|
-
|
|
74
|
-
if (columnMismatches.length === 0) {
|
|
75
|
-
this.logger.log('No column mismatches to fix');
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
this.logger.log(`Found ${columnMismatches.length} column mismatches to fix`);
|
|
80
|
-
|
|
81
|
-
// Agrupar por tabla para optimizar las consultas
|
|
82
|
-
const tableGroups = this.groupByTable(columnMismatches);
|
|
83
|
-
|
|
84
|
-
// Procesar cada tabla
|
|
85
|
-
for (const [table, columns] of Object.entries(tableGroups)) {
|
|
86
|
-
await this.fixTableColumns(table, columns);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
this.logger.log('Schema discrepancies fixed successfully');
|
|
90
|
-
} catch (error) {
|
|
91
|
-
this.logger.error(`Error fixing schema discrepancies: ${error.message}`);
|
|
92
|
-
throw error;
|
|
93
|
-
} finally {
|
|
94
|
-
await this.cleanup();
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
private async findLatestReport(): Promise<string | null> {
|
|
99
|
-
const logsDir = path.join(process.cwd(), 'migration-logs');
|
|
100
|
-
if (!fs.existsSync(logsDir)) {
|
|
101
|
-
return null;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const files = fs.readdirSync(logsDir);
|
|
105
|
-
const reportFiles = files.filter(file => file.startsWith('pre-migration-report-'));
|
|
106
|
-
|
|
107
|
-
if (reportFiles.length === 0) {
|
|
108
|
-
return null;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Ordenar por fecha (más reciente primero)
|
|
112
|
-
reportFiles.sort().reverse();
|
|
113
|
-
return path.join(logsDir, reportFiles[0]);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
private groupByTable(mismatches: ColumnMismatch[]): Record<string, ColumnMismatch[]> {
|
|
117
|
-
return mismatches.reduce((groups, mismatch) => {
|
|
118
|
-
const table = mismatch.details.table;
|
|
119
|
-
if (!groups[table]) {
|
|
120
|
-
groups[table] = [];
|
|
121
|
-
}
|
|
122
|
-
groups[table].push(mismatch);
|
|
123
|
-
return groups;
|
|
124
|
-
}, {} as Record<string, ColumnMismatch[]>);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
private async fixTableColumns(table: string, columns: ColumnMismatch[]): Promise<void> {
|
|
128
|
-
try {
|
|
129
|
-
this.logger.log(`Fixing columns for table "${table}"`);
|
|
130
|
-
|
|
131
|
-
// Obtener los esquemas en la base de datos de destino
|
|
132
|
-
const schemas = await this.getTargetSchemas();
|
|
133
|
-
|
|
134
|
-
// Obtener los tipos de columna de la base de datos de origen
|
|
135
|
-
await this.cacheSourceColumnTypes(table, columns.map(c => c.details.column));
|
|
136
|
-
|
|
137
|
-
for (const schema of schemas) {
|
|
138
|
-
this.logger.log(`Processing schema "${schema}" for table "${table}"`);
|
|
139
|
-
|
|
140
|
-
// Verificar si la tabla existe en este esquema
|
|
141
|
-
const tableExists = await this.checkTableExists(schema, table);
|
|
142
|
-
if (!tableExists) {
|
|
143
|
-
this.logger.warn(`Table "${table}" does not exist in schema "${schema}". Skipping.`);
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Añadir las columnas faltantes
|
|
148
|
-
for (const column of columns) {
|
|
149
|
-
const columnName = column.details.column;
|
|
150
|
-
const columnType = this.columnTypeCache[table]?.[columnName];
|
|
151
|
-
|
|
152
|
-
if (!columnType) {
|
|
153
|
-
this.logger.warn(`Could not determine type for column "${columnName}" in table "${table}". Skipping.`);
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Verificar si la columna ya existe
|
|
158
|
-
const columnExists = await this.checkColumnExists(schema, table, columnName);
|
|
159
|
-
if (columnExists) {
|
|
160
|
-
this.logger.log(`Column "${columnName}" already exists in "${schema}"."${table}". Skipping.`);
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Añadir la columna
|
|
165
|
-
await this.addColumn(schema, table, columnName, columnType);
|
|
166
|
-
|
|
167
|
-
// Añadir restricción UNIQUE si es necesario
|
|
168
|
-
if (column.details.constraint_type === 'UNIQUE') {
|
|
169
|
-
await this.addUniqueConstraint(schema, table, columnName);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
this.logger.log(`Fixed columns for table "${table}" successfully`);
|
|
175
|
-
} catch (error) {
|
|
176
|
-
this.logger.error(`Error fixing columns for table "${table}": ${error.message}`);
|
|
177
|
-
throw error;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
private async getTargetSchemas(): Promise<string[]> {
|
|
182
|
-
const query = `
|
|
183
|
-
SELECT schema_name
|
|
184
|
-
FROM information_schema.schemata
|
|
185
|
-
WHERE schema_name NOT IN ('information_schema', 'pg_catalog', 'pg_toast')
|
|
186
|
-
AND schema_name NOT LIKE 'pg_%'
|
|
187
|
-
`;
|
|
188
|
-
|
|
189
|
-
const result = await this.targetPool.query(query);
|
|
190
|
-
return result.rows.map(row => row.schema_name);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
private async cacheSourceColumnTypes(table: string, columns: string[]): Promise<void> {
|
|
194
|
-
if (!this.columnTypeCache[table]) {
|
|
195
|
-
this.columnTypeCache[table] = {};
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const columnsStr = columns.map(c => `'${c}'`).join(',');
|
|
199
|
-
const query = `
|
|
200
|
-
SELECT column_name, data_type,
|
|
201
|
-
CASE
|
|
202
|
-
WHEN data_type = 'character varying' THEN data_type || '(' || character_maximum_length || ')'
|
|
203
|
-
WHEN data_type = 'numeric' THEN
|
|
204
|
-
CASE
|
|
205
|
-
WHEN numeric_precision IS NOT NULL AND numeric_scale IS NOT NULL
|
|
206
|
-
THEN data_type || '(' || numeric_precision || ',' || numeric_scale || ')'
|
|
207
|
-
ELSE data_type
|
|
208
|
-
END
|
|
209
|
-
ELSE data_type
|
|
210
|
-
END as full_data_type
|
|
211
|
-
FROM information_schema.columns
|
|
212
|
-
WHERE table_schema = 'public'
|
|
213
|
-
AND table_name = $1
|
|
214
|
-
AND column_name IN (${columnsStr})
|
|
215
|
-
`;
|
|
216
|
-
|
|
217
|
-
const result = await this.sourcePool.query(query, [table]);
|
|
218
|
-
|
|
219
|
-
for (const row of result.rows) {
|
|
220
|
-
this.columnTypeCache[table][row.column_name] = row.full_data_type;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
private async checkTableExists(schema: string, table: string): Promise<boolean> {
|
|
225
|
-
const query = `
|
|
226
|
-
SELECT EXISTS (
|
|
227
|
-
SELECT FROM information_schema.tables
|
|
228
|
-
WHERE table_schema = $1
|
|
229
|
-
AND table_name = $2
|
|
230
|
-
);
|
|
231
|
-
`;
|
|
232
|
-
|
|
233
|
-
const result = await this.targetPool.query(query, [schema, table]);
|
|
234
|
-
return result.rows[0].exists;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
private async checkColumnExists(schema: string, table: string, column: string): Promise<boolean> {
|
|
238
|
-
const query = `
|
|
239
|
-
SELECT EXISTS (
|
|
240
|
-
SELECT FROM information_schema.columns
|
|
241
|
-
WHERE table_schema = $1
|
|
242
|
-
AND table_name = $2
|
|
243
|
-
AND column_name = $3
|
|
244
|
-
);
|
|
245
|
-
`;
|
|
246
|
-
|
|
247
|
-
const result = await this.targetPool.query(query, [schema, table, column]);
|
|
248
|
-
return result.rows[0].exists;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
private async addColumn(schema: string, table: string, column: string, type: string): Promise<void> {
|
|
252
|
-
try {
|
|
253
|
-
const query = `
|
|
254
|
-
ALTER TABLE "${schema}"."${table}"
|
|
255
|
-
ADD COLUMN IF NOT EXISTS "${column}" ${type};
|
|
256
|
-
`;
|
|
257
|
-
|
|
258
|
-
await this.targetPool.query(query);
|
|
259
|
-
this.logger.log(`Added column "${column}" to "${schema}"."${table}" with type "${type}"`);
|
|
260
|
-
} catch (error) {
|
|
261
|
-
this.logger.error(`Error adding column "${column}" to "${schema}"."${table}": ${error.message}`);
|
|
262
|
-
throw error;
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
private async addUniqueConstraint(schema: string, table: string, column: string): Promise<void> {
|
|
267
|
-
try {
|
|
268
|
-
const constraintName = `${table}_${column}_unique`;
|
|
269
|
-
const query = `
|
|
270
|
-
ALTER TABLE "${schema}"."${table}"
|
|
271
|
-
ADD CONSTRAINT "${constraintName}" UNIQUE ("${column}");
|
|
272
|
-
`;
|
|
273
|
-
|
|
274
|
-
await this.targetPool.query(query);
|
|
275
|
-
this.logger.log(`Added UNIQUE constraint on "${column}" to "${schema}"."${table}"`);
|
|
276
|
-
} catch (error) {
|
|
277
|
-
// Si la restricción ya existe, ignoramos el error
|
|
278
|
-
if (error.message.includes('already exists')) {
|
|
279
|
-
this.logger.log(`UNIQUE constraint on "${column}" already exists for "${schema}"."${table}"`);
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
|
-
this.logger.error(`Error adding UNIQUE constraint on "${column}" to "${schema}"."${table}": ${error.message}`);
|
|
283
|
-
throw error;
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
private async cleanup(): Promise<void> {
|
|
288
|
-
try {
|
|
289
|
-
await this.sourcePool.end();
|
|
290
|
-
await this.targetPool.end();
|
|
291
|
-
} catch (error) {
|
|
292
|
-
this.logger.error(`Error during cleanup: ${error.message}`);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// Función para ejecutar el script directamente
|
|
298
|
-
async function run() {
|
|
299
|
-
try {
|
|
300
|
-
const reportPath = process.argv[2]; // Opcional: ruta al informe
|
|
301
|
-
const fixer = new SchemaDiscrepancyFixer(
|
|
302
|
-
process.env.SOURCE_DATABASE_URL,
|
|
303
|
-
process.env.DATABASE_URL,
|
|
304
|
-
reportPath
|
|
305
|
-
);
|
|
306
|
-
|
|
307
|
-
await fixer.fixDiscrepancies();
|
|
308
|
-
process.exit(0);
|
|
309
|
-
} catch (error) {
|
|
310
|
-
console.error('Error:', error.message);
|
|
311
|
-
process.exit(1);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// Ejecutar si se llama directamente
|
|
316
|
-
if (require.main === module) {
|
|
317
|
-
run();
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as pg from 'pg';
|
|
4
|
+
import { Logger } from '@nestjs/common';
|
|
5
|
+
import * as dotenv from 'dotenv';
|
|
6
|
+
|
|
7
|
+
dotenv.config();
|
|
8
|
+
|
|
9
|
+
interface ColumnMismatch {
|
|
10
|
+
type: string;
|
|
11
|
+
message: string;
|
|
12
|
+
severity: string;
|
|
13
|
+
details: {
|
|
14
|
+
table: string;
|
|
15
|
+
column: string;
|
|
16
|
+
constraint_type: string;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface MigrationReport {
|
|
21
|
+
timestamp: string;
|
|
22
|
+
sourceDatabase: string;
|
|
23
|
+
targetDatabase: string;
|
|
24
|
+
issueCount: number;
|
|
25
|
+
issues: ColumnMismatch[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class SchemaDiscrepancyFixer {
|
|
29
|
+
private readonly logger = new Logger('SchemaDiscrepancyFixer');
|
|
30
|
+
private readonly sourcePool: pg.Pool;
|
|
31
|
+
private readonly targetPool: pg.Pool;
|
|
32
|
+
private readonly columnTypeCache: Record<string, Record<string, string>> = {};
|
|
33
|
+
|
|
34
|
+
constructor(
|
|
35
|
+
private readonly sourceUrl: string = process.env.SOURCE_DATABASE_URL,
|
|
36
|
+
private readonly targetUrl: string = process.env.DATABASE_URL,
|
|
37
|
+
private readonly reportPath?: string
|
|
38
|
+
) {
|
|
39
|
+
if (!this.sourceUrl) {
|
|
40
|
+
throw new Error('SOURCE_DATABASE_URL environment variable is required');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!this.targetUrl) {
|
|
44
|
+
throw new Error('DATABASE_URL environment variable is required');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
this.sourcePool = new pg.Pool({
|
|
48
|
+
connectionString: this.sourceUrl,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
this.targetPool = new pg.Pool({
|
|
52
|
+
connectionString: this.targetUrl,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async fixDiscrepancies(): Promise<void> {
|
|
57
|
+
try {
|
|
58
|
+
// Encontrar el informe de pre-migración más reciente si no se especificó uno
|
|
59
|
+
const reportFile = this.reportPath || await this.findLatestReport();
|
|
60
|
+
if (!reportFile) {
|
|
61
|
+
this.logger.error('No pre-migration report found');
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this.logger.log(`Using pre-migration report: ${reportFile}`);
|
|
66
|
+
const reportContent = fs.readFileSync(reportFile, 'utf8');
|
|
67
|
+
const report: MigrationReport = JSON.parse(reportContent);
|
|
68
|
+
|
|
69
|
+
// Filtrar solo los problemas de tipo COLUMN_MISMATCH
|
|
70
|
+
const columnMismatches = report.issues.filter(
|
|
71
|
+
issue => issue.type === 'COLUMN_MISMATCH'
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
if (columnMismatches.length === 0) {
|
|
75
|
+
this.logger.log('No column mismatches to fix');
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
this.logger.log(`Found ${columnMismatches.length} column mismatches to fix`);
|
|
80
|
+
|
|
81
|
+
// Agrupar por tabla para optimizar las consultas
|
|
82
|
+
const tableGroups = this.groupByTable(columnMismatches);
|
|
83
|
+
|
|
84
|
+
// Procesar cada tabla
|
|
85
|
+
for (const [table, columns] of Object.entries(tableGroups)) {
|
|
86
|
+
await this.fixTableColumns(table, columns);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
this.logger.log('Schema discrepancies fixed successfully');
|
|
90
|
+
} catch (error) {
|
|
91
|
+
this.logger.error(`Error fixing schema discrepancies: ${error.message}`);
|
|
92
|
+
throw error;
|
|
93
|
+
} finally {
|
|
94
|
+
await this.cleanup();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private async findLatestReport(): Promise<string | null> {
|
|
99
|
+
const logsDir = path.join(process.cwd(), 'migration-logs');
|
|
100
|
+
if (!fs.existsSync(logsDir)) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const files = fs.readdirSync(logsDir);
|
|
105
|
+
const reportFiles = files.filter(file => file.startsWith('pre-migration-report-'));
|
|
106
|
+
|
|
107
|
+
if (reportFiles.length === 0) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Ordenar por fecha (más reciente primero)
|
|
112
|
+
reportFiles.sort().reverse();
|
|
113
|
+
return path.join(logsDir, reportFiles[0]);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private groupByTable(mismatches: ColumnMismatch[]): Record<string, ColumnMismatch[]> {
|
|
117
|
+
return mismatches.reduce((groups, mismatch) => {
|
|
118
|
+
const table = mismatch.details.table;
|
|
119
|
+
if (!groups[table]) {
|
|
120
|
+
groups[table] = [];
|
|
121
|
+
}
|
|
122
|
+
groups[table].push(mismatch);
|
|
123
|
+
return groups;
|
|
124
|
+
}, {} as Record<string, ColumnMismatch[]>);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private async fixTableColumns(table: string, columns: ColumnMismatch[]): Promise<void> {
|
|
128
|
+
try {
|
|
129
|
+
this.logger.log(`Fixing columns for table "${table}"`);
|
|
130
|
+
|
|
131
|
+
// Obtener los esquemas en la base de datos de destino
|
|
132
|
+
const schemas = await this.getTargetSchemas();
|
|
133
|
+
|
|
134
|
+
// Obtener los tipos de columna de la base de datos de origen
|
|
135
|
+
await this.cacheSourceColumnTypes(table, columns.map(c => c.details.column));
|
|
136
|
+
|
|
137
|
+
for (const schema of schemas) {
|
|
138
|
+
this.logger.log(`Processing schema "${schema}" for table "${table}"`);
|
|
139
|
+
|
|
140
|
+
// Verificar si la tabla existe en este esquema
|
|
141
|
+
const tableExists = await this.checkTableExists(schema, table);
|
|
142
|
+
if (!tableExists) {
|
|
143
|
+
this.logger.warn(`Table "${table}" does not exist in schema "${schema}". Skipping.`);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Añadir las columnas faltantes
|
|
148
|
+
for (const column of columns) {
|
|
149
|
+
const columnName = column.details.column;
|
|
150
|
+
const columnType = this.columnTypeCache[table]?.[columnName];
|
|
151
|
+
|
|
152
|
+
if (!columnType) {
|
|
153
|
+
this.logger.warn(`Could not determine type for column "${columnName}" in table "${table}". Skipping.`);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Verificar si la columna ya existe
|
|
158
|
+
const columnExists = await this.checkColumnExists(schema, table, columnName);
|
|
159
|
+
if (columnExists) {
|
|
160
|
+
this.logger.log(`Column "${columnName}" already exists in "${schema}"."${table}". Skipping.`);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Añadir la columna
|
|
165
|
+
await this.addColumn(schema, table, columnName, columnType);
|
|
166
|
+
|
|
167
|
+
// Añadir restricción UNIQUE si es necesario
|
|
168
|
+
if (column.details.constraint_type === 'UNIQUE') {
|
|
169
|
+
await this.addUniqueConstraint(schema, table, columnName);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
this.logger.log(`Fixed columns for table "${table}" successfully`);
|
|
175
|
+
} catch (error) {
|
|
176
|
+
this.logger.error(`Error fixing columns for table "${table}": ${error.message}`);
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private async getTargetSchemas(): Promise<string[]> {
|
|
182
|
+
const query = `
|
|
183
|
+
SELECT schema_name
|
|
184
|
+
FROM information_schema.schemata
|
|
185
|
+
WHERE schema_name NOT IN ('information_schema', 'pg_catalog', 'pg_toast')
|
|
186
|
+
AND schema_name NOT LIKE 'pg_%'
|
|
187
|
+
`;
|
|
188
|
+
|
|
189
|
+
const result = await this.targetPool.query(query);
|
|
190
|
+
return result.rows.map(row => row.schema_name);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private async cacheSourceColumnTypes(table: string, columns: string[]): Promise<void> {
|
|
194
|
+
if (!this.columnTypeCache[table]) {
|
|
195
|
+
this.columnTypeCache[table] = {};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const columnsStr = columns.map(c => `'${c}'`).join(',');
|
|
199
|
+
const query = `
|
|
200
|
+
SELECT column_name, data_type,
|
|
201
|
+
CASE
|
|
202
|
+
WHEN data_type = 'character varying' THEN data_type || '(' || character_maximum_length || ')'
|
|
203
|
+
WHEN data_type = 'numeric' THEN
|
|
204
|
+
CASE
|
|
205
|
+
WHEN numeric_precision IS NOT NULL AND numeric_scale IS NOT NULL
|
|
206
|
+
THEN data_type || '(' || numeric_precision || ',' || numeric_scale || ')'
|
|
207
|
+
ELSE data_type
|
|
208
|
+
END
|
|
209
|
+
ELSE data_type
|
|
210
|
+
END as full_data_type
|
|
211
|
+
FROM information_schema.columns
|
|
212
|
+
WHERE table_schema = 'public'
|
|
213
|
+
AND table_name = $1
|
|
214
|
+
AND column_name IN (${columnsStr})
|
|
215
|
+
`;
|
|
216
|
+
|
|
217
|
+
const result = await this.sourcePool.query(query, [table]);
|
|
218
|
+
|
|
219
|
+
for (const row of result.rows) {
|
|
220
|
+
this.columnTypeCache[table][row.column_name] = row.full_data_type;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private async checkTableExists(schema: string, table: string): Promise<boolean> {
|
|
225
|
+
const query = `
|
|
226
|
+
SELECT EXISTS (
|
|
227
|
+
SELECT FROM information_schema.tables
|
|
228
|
+
WHERE table_schema = $1
|
|
229
|
+
AND table_name = $2
|
|
230
|
+
);
|
|
231
|
+
`;
|
|
232
|
+
|
|
233
|
+
const result = await this.targetPool.query(query, [schema, table]);
|
|
234
|
+
return result.rows[0].exists;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private async checkColumnExists(schema: string, table: string, column: string): Promise<boolean> {
|
|
238
|
+
const query = `
|
|
239
|
+
SELECT EXISTS (
|
|
240
|
+
SELECT FROM information_schema.columns
|
|
241
|
+
WHERE table_schema = $1
|
|
242
|
+
AND table_name = $2
|
|
243
|
+
AND column_name = $3
|
|
244
|
+
);
|
|
245
|
+
`;
|
|
246
|
+
|
|
247
|
+
const result = await this.targetPool.query(query, [schema, table, column]);
|
|
248
|
+
return result.rows[0].exists;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private async addColumn(schema: string, table: string, column: string, type: string): Promise<void> {
|
|
252
|
+
try {
|
|
253
|
+
const query = `
|
|
254
|
+
ALTER TABLE "${schema}"."${table}"
|
|
255
|
+
ADD COLUMN IF NOT EXISTS "${column}" ${type};
|
|
256
|
+
`;
|
|
257
|
+
|
|
258
|
+
await this.targetPool.query(query);
|
|
259
|
+
this.logger.log(`Added column "${column}" to "${schema}"."${table}" with type "${type}"`);
|
|
260
|
+
} catch (error) {
|
|
261
|
+
this.logger.error(`Error adding column "${column}" to "${schema}"."${table}": ${error.message}`);
|
|
262
|
+
throw error;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private async addUniqueConstraint(schema: string, table: string, column: string): Promise<void> {
|
|
267
|
+
try {
|
|
268
|
+
const constraintName = `${table}_${column}_unique`;
|
|
269
|
+
const query = `
|
|
270
|
+
ALTER TABLE "${schema}"."${table}"
|
|
271
|
+
ADD CONSTRAINT "${constraintName}" UNIQUE ("${column}");
|
|
272
|
+
`;
|
|
273
|
+
|
|
274
|
+
await this.targetPool.query(query);
|
|
275
|
+
this.logger.log(`Added UNIQUE constraint on "${column}" to "${schema}"."${table}"`);
|
|
276
|
+
} catch (error) {
|
|
277
|
+
// Si la restricción ya existe, ignoramos el error
|
|
278
|
+
if (error.message.includes('already exists')) {
|
|
279
|
+
this.logger.log(`UNIQUE constraint on "${column}" already exists for "${schema}"."${table}"`);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
this.logger.error(`Error adding UNIQUE constraint on "${column}" to "${schema}"."${table}": ${error.message}`);
|
|
283
|
+
throw error;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private async cleanup(): Promise<void> {
|
|
288
|
+
try {
|
|
289
|
+
await this.sourcePool.end();
|
|
290
|
+
await this.targetPool.end();
|
|
291
|
+
} catch (error) {
|
|
292
|
+
this.logger.error(`Error during cleanup: ${error.message}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Función para ejecutar el script directamente
|
|
298
|
+
async function run() {
|
|
299
|
+
try {
|
|
300
|
+
const reportPath = process.argv[2]; // Opcional: ruta al informe
|
|
301
|
+
const fixer = new SchemaDiscrepancyFixer(
|
|
302
|
+
process.env.SOURCE_DATABASE_URL,
|
|
303
|
+
process.env.DATABASE_URL,
|
|
304
|
+
reportPath
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
await fixer.fixDiscrepancies();
|
|
308
|
+
process.exit(0);
|
|
309
|
+
} catch (error) {
|
|
310
|
+
console.error('Error:', error.message);
|
|
311
|
+
process.exit(1);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Ejecutar si se llama directamente
|
|
316
|
+
if (require.main === module) {
|
|
317
|
+
run();
|
|
318
318
|
}
|