@javalabs/prisma-client 1.0.16 → 1.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/.dockerignore +14 -0
- package/Dockerfile +23 -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/package.json +44 -44
- package/prisma/migrations/add_accepts_partial_payments_to_users.sql +19 -0
- package/prisma/migrations/add_amount_received_to_manual_payments.sql +19 -0
- package/prisma/migrations/add_commission_fields.sql +33 -0
- package/prisma/migrations/add_uuid_to_transactions.sql +13 -13
- package/prisma/migrations/complete_partial_payments_migration.sql +53 -0
- package/prisma/migrations/create_settlements_table.sql +60 -0
- package/prisma/schema.prisma +56 -4
- 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,416 +1,416 @@
|
|
|
1
|
-
import * as fs from "fs";
|
|
2
|
-
import * as path from "path";
|
|
3
|
-
import { PrismaClient } from "@prisma/client";
|
|
4
|
-
import * as dotenv from "dotenv";
|
|
5
|
-
import { Logger } from "@nestjs/common";
|
|
6
|
-
import * as pg from "pg";
|
|
7
|
-
import { MigrationErrorHandler } from "./error-handler";
|
|
8
|
-
|
|
9
|
-
dotenv.config();
|
|
10
|
-
|
|
11
|
-
export class FailedMigrationRetry {
|
|
12
|
-
private readonly logger = new Logger("FailedMigrationRetry");
|
|
13
|
-
private readonly targetPrisma: PrismaClient;
|
|
14
|
-
private readonly targetPool: pg.Pool;
|
|
15
|
-
private readonly errorHandler: MigrationErrorHandler;
|
|
16
|
-
private schemaCache: Record<string, any> = {};
|
|
17
|
-
|
|
18
|
-
constructor(private readonly errorLogPath: string) {
|
|
19
|
-
// Target database connection
|
|
20
|
-
this.targetPool = new pg.Pool({
|
|
21
|
-
connectionString: process.env.DATABASE_URL,
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
// Target Prisma client
|
|
25
|
-
this.targetPrisma = new PrismaClient({
|
|
26
|
-
datasources: {
|
|
27
|
-
db: {
|
|
28
|
-
url: process.env.DATABASE_URL,
|
|
29
|
-
},
|
|
30
|
-
},
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
// Inicializar el manejador de errores
|
|
34
|
-
this.errorHandler = new MigrationErrorHandler();
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async retryFailedMigrations() {
|
|
38
|
-
try {
|
|
39
|
-
this.logger.log(`Loading error log from: ${this.errorLogPath}`);
|
|
40
|
-
|
|
41
|
-
// Cargar el archivo de log de errores
|
|
42
|
-
if (!fs.existsSync(this.errorLogPath)) {
|
|
43
|
-
this.logger.error(`Error log file not found: ${this.errorLogPath}`);
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const errorLogContent = fs.readFileSync(this.errorLogPath, "utf8");
|
|
48
|
-
const errorLog: Record<string, any[]> = JSON.parse(errorLogContent);
|
|
49
|
-
|
|
50
|
-
let totalErrors = 0;
|
|
51
|
-
let retrySuccessCount = 0;
|
|
52
|
-
let retryFailCount = 0;
|
|
53
|
-
|
|
54
|
-
// Procesar cada tenant
|
|
55
|
-
for (const tenantId in errorLog) {
|
|
56
|
-
const tenantErrors = errorLog[tenantId];
|
|
57
|
-
if (tenantErrors.length === 0) continue;
|
|
58
|
-
|
|
59
|
-
totalErrors += tenantErrors.length;
|
|
60
|
-
this.logger.log(
|
|
61
|
-
`Processing ${tenantErrors.length} errors for tenant: ${tenantId}`
|
|
62
|
-
);
|
|
63
|
-
|
|
64
|
-
// Crear cliente Prisma específico para este tenant
|
|
65
|
-
const tenantPrisma = new PrismaClient({
|
|
66
|
-
datasources: {
|
|
67
|
-
db: {
|
|
68
|
-
url: `${process.env.DATABASE_URL}?schema=${tenantId}`,
|
|
69
|
-
},
|
|
70
|
-
},
|
|
71
|
-
transactionOptions: {
|
|
72
|
-
maxWait: 120000,
|
|
73
|
-
timeout: 120000,
|
|
74
|
-
},
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
// Procesar cada error
|
|
78
|
-
for (const errorEntry of tenantErrors) {
|
|
79
|
-
const { entity, recordId, data } = errorEntry;
|
|
80
|
-
|
|
81
|
-
this.logger.log(`Retrying ${entity} with ID ${recordId}`);
|
|
82
|
-
|
|
83
|
-
try {
|
|
84
|
-
// Obtener el esquema de la tabla si no está en caché
|
|
85
|
-
if (!this.schemaCache[`${tenantId}.${entity}`]) {
|
|
86
|
-
this.schemaCache[`${tenantId}.${entity}`] =
|
|
87
|
-
await this.getTableSchema(entity, tenantId);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const tableSchema = this.schemaCache[`${tenantId}.${entity}`];
|
|
91
|
-
|
|
92
|
-
if (!tableSchema || tableSchema.length === 0) {
|
|
93
|
-
this.logger.warn(
|
|
94
|
-
`Schema not found for ${entity} in tenant ${tenantId}. Skipping.`
|
|
95
|
-
);
|
|
96
|
-
retryFailCount++;
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Preparar los datos para la inserción/actualización
|
|
101
|
-
const createData: Record<string, any> = {};
|
|
102
|
-
const updateData: Record<string, any> = {};
|
|
103
|
-
|
|
104
|
-
// Obtener la lista de columnas
|
|
105
|
-
const columnNames = tableSchema.map((col) => col.column_name);
|
|
106
|
-
|
|
107
|
-
// Si no tenemos los datos originales, no podemos reintentar
|
|
108
|
-
if (!data) {
|
|
109
|
-
this.logger.warn(
|
|
110
|
-
`No data available for ${entity} with ID ${recordId}. Skipping.`
|
|
111
|
-
);
|
|
112
|
-
retryFailCount++;
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Procesar cada columna
|
|
117
|
-
for (const column of tableSchema) {
|
|
118
|
-
const columnName = column.column_name;
|
|
119
|
-
|
|
120
|
-
// Omitir columnas que no están en los datos
|
|
121
|
-
if (!Object.prototype.hasOwnProperty.call(data, columnName)) {
|
|
122
|
-
continue;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Manejar tipos especiales
|
|
126
|
-
if (column.data_type === "numeric" && data[columnName] !== null) {
|
|
127
|
-
// Convertir a número
|
|
128
|
-
const numericValue = parseFloat(data[columnName]);
|
|
129
|
-
if (!isNaN(numericValue)) {
|
|
130
|
-
createData[columnName] = numericValue;
|
|
131
|
-
updateData[columnName] = numericValue;
|
|
132
|
-
} else {
|
|
133
|
-
createData[columnName] = null;
|
|
134
|
-
updateData[columnName] = null;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
// Manejar tipos enum
|
|
138
|
-
else if (
|
|
139
|
-
column.data_type === "USER-DEFINED" &&
|
|
140
|
-
column.udt_name.startsWith("enum_")
|
|
141
|
-
) {
|
|
142
|
-
// Obtener valores válidos para el enum
|
|
143
|
-
const enumValues = await this.getEnumValues(column.udt_name);
|
|
144
|
-
|
|
145
|
-
// Inicializar _enumTypes si no existe
|
|
146
|
-
if (!createData._enumTypes) createData._enumTypes = {};
|
|
147
|
-
if (!updateData._enumTypes) updateData._enumTypes = {};
|
|
148
|
-
|
|
149
|
-
// Siempre guardar el tipo de enum para esta columna
|
|
150
|
-
createData._enumTypes[columnName] = column.udt_name;
|
|
151
|
-
updateData._enumTypes[columnName] = column.udt_name;
|
|
152
|
-
|
|
153
|
-
if (data[columnName] === null || data[columnName] === "") {
|
|
154
|
-
createData[columnName] = null;
|
|
155
|
-
updateData[columnName] = null;
|
|
156
|
-
}
|
|
157
|
-
// Si el valor es un string, intentar encontrar una coincidencia en los valores del enum
|
|
158
|
-
else if (typeof data[columnName] === "string") {
|
|
159
|
-
const matchingValue = this.findMatchingEnumValue(
|
|
160
|
-
data[columnName],
|
|
161
|
-
enumValues
|
|
162
|
-
);
|
|
163
|
-
if (matchingValue) {
|
|
164
|
-
createData[columnName] = matchingValue;
|
|
165
|
-
updateData[columnName] = matchingValue;
|
|
166
|
-
} else {
|
|
167
|
-
// Si no hay coincidencia, usar null
|
|
168
|
-
createData[columnName] = null;
|
|
169
|
-
updateData[columnName] = null;
|
|
170
|
-
this.logger.warn(
|
|
171
|
-
`No matching enum value found for ${columnName}="${data[columnName]}" in ${entity}. Using NULL.`
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
} else {
|
|
175
|
-
// Para otros tipos, usar el valor tal cual
|
|
176
|
-
const enumValue = data[columnName];
|
|
177
|
-
createData[columnName] = data[columnName];
|
|
178
|
-
updateData[columnName] = data[columnName];
|
|
179
|
-
}
|
|
180
|
-
} else {
|
|
181
|
-
// Para otros tipos de datos, usar el valor tal cual
|
|
182
|
-
createData[columnName] = data[columnName];
|
|
183
|
-
updateData[columnName] = data[columnName];
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Asegurarse de incluir el ID
|
|
188
|
-
if (columnNames.includes(recordId)) {
|
|
189
|
-
createData[recordId] = data[recordId];
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Ejecutar la operación upsert usando SQL raw en lugar de acceso directo al modelo
|
|
193
|
-
// Verificar primero si el registro existe
|
|
194
|
-
const checkExistQuery = `
|
|
195
|
-
SELECT 1 FROM "${tenantId}"."${entity}"
|
|
196
|
-
WHERE "${recordId}" = $1
|
|
197
|
-
LIMIT 1
|
|
198
|
-
`;
|
|
199
|
-
|
|
200
|
-
const existResult = await this.targetPool.query(checkExistQuery, [
|
|
201
|
-
data[recordId],
|
|
202
|
-
]);
|
|
203
|
-
const recordExists = existResult.rows.length > 0;
|
|
204
|
-
|
|
205
|
-
if (recordExists) {
|
|
206
|
-
// Construir la consulta UPDATE
|
|
207
|
-
const updateFields = Object.keys(updateData)
|
|
208
|
-
.filter((key) => key !== recordId && key !== '_enumTypes') // Excluir el campo ID y _enumTypes
|
|
209
|
-
.map((key, index) => {
|
|
210
|
-
// Aplicar casting explícito para tipos enum
|
|
211
|
-
if (updateData._enumTypes && updateData._enumTypes[key]) {
|
|
212
|
-
return `"${key}" = $${index + 2}::${updateData._enumTypes[key]}`;
|
|
213
|
-
}
|
|
214
|
-
return `"${key}" = $${index + 2}`;
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
if (updateFields.length > 0) {
|
|
218
|
-
const updateQuery = `
|
|
219
|
-
UPDATE "${tenantId}"."${entity}"
|
|
220
|
-
SET ${updateFields.join(", ")}
|
|
221
|
-
WHERE "${recordId}" = $1
|
|
222
|
-
`;
|
|
223
|
-
|
|
224
|
-
const updateValues = [
|
|
225
|
-
data[recordId],
|
|
226
|
-
...Object.keys(updateData)
|
|
227
|
-
.filter((key) => key !== recordId && key !== '_enumTypes')
|
|
228
|
-
.map((key) => updateData[key]),
|
|
229
|
-
];
|
|
230
|
-
|
|
231
|
-
await this.targetPool.query(updateQuery, updateValues);
|
|
232
|
-
}
|
|
233
|
-
} else {
|
|
234
|
-
// Construir la consulta INSERT
|
|
235
|
-
const insertFields = Object.keys(createData)
|
|
236
|
-
.filter(key => key !== '_enumTypes')
|
|
237
|
-
.map((key) => `"${key}"`);
|
|
238
|
-
|
|
239
|
-
const insertPlaceholders = Object.keys(createData)
|
|
240
|
-
.filter(key => key !== '_enumTypes')
|
|
241
|
-
.map((key, index) => {
|
|
242
|
-
// Aplicar casting explícito para tipos enum
|
|
243
|
-
if (createData._enumTypes && createData._enumTypes[key]) {
|
|
244
|
-
return `$${index + 1}::${createData._enumTypes[key]}`;
|
|
245
|
-
}
|
|
246
|
-
return `$${index + 1}`;
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
const insertQuery = `
|
|
250
|
-
INSERT INTO "${tenantId}"."${entity}" (${insertFields.join(", ")})
|
|
251
|
-
VALUES (${insertPlaceholders.join(", ")})
|
|
252
|
-
`;
|
|
253
|
-
|
|
254
|
-
const insertValues = Object.keys(createData)
|
|
255
|
-
.filter(key => key !== '_enumTypes')
|
|
256
|
-
.map((key) => createData[key]);
|
|
257
|
-
|
|
258
|
-
await this.targetPool.query(insertQuery, insertValues);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
this.logger.log(
|
|
262
|
-
`Successfully migrated ${entity} with ID ${recordId}`
|
|
263
|
-
);
|
|
264
|
-
retrySuccessCount++;
|
|
265
|
-
} catch (error) {
|
|
266
|
-
this.logger.error(
|
|
267
|
-
`Error retrying ${entity} with ID ${recordId}: ${error.message}`
|
|
268
|
-
);
|
|
269
|
-
// Registrar el error en el nuevo log
|
|
270
|
-
this.errorHandler.logError(tenantId, entity, recordId, error, data);
|
|
271
|
-
retryFailCount++;
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// Cerrar la conexión del cliente tenant
|
|
276
|
-
await tenantPrisma.$disconnect();
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// Mostrar resumen
|
|
280
|
-
this.logger.log(`
|
|
281
|
-
Retry Summary:
|
|
282
|
-
- Total errors: ${totalErrors}
|
|
283
|
-
- Successfully retried: ${retrySuccessCount}
|
|
284
|
-
- Failed retries: ${retryFailCount}
|
|
285
|
-
`);
|
|
286
|
-
|
|
287
|
-
// Generar informe de errores restantes
|
|
288
|
-
const remainingErrorsReport = this.errorHandler.generateErrorReport();
|
|
289
|
-
this.logger.log("Remaining Errors Report:\n" + remainingErrorsReport);
|
|
290
|
-
} catch (error) {
|
|
291
|
-
this.logger.error(
|
|
292
|
-
`Error in retry process: ${error.message}`,
|
|
293
|
-
error.stack
|
|
294
|
-
);
|
|
295
|
-
} finally {
|
|
296
|
-
await this.cleanup();
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
private async getTableSchema(
|
|
301
|
-
tableName: string,
|
|
302
|
-
schemaName: string
|
|
303
|
-
): Promise<any[]> {
|
|
304
|
-
try {
|
|
305
|
-
const schemaQuery = `
|
|
306
|
-
SELECT column_name, data_type, column_default, is_nullable, udt_name
|
|
307
|
-
FROM information_schema.columns
|
|
308
|
-
WHERE table_name = $1
|
|
309
|
-
AND table_schema = $2
|
|
310
|
-
ORDER BY ordinal_position
|
|
311
|
-
`;
|
|
312
|
-
|
|
313
|
-
const result = await this.targetPool.query(schemaQuery, [
|
|
314
|
-
tableName,
|
|
315
|
-
schemaName,
|
|
316
|
-
]);
|
|
317
|
-
return result.rows;
|
|
318
|
-
} catch (error) {
|
|
319
|
-
this.logger.error(
|
|
320
|
-
`Error getting schema for table ${tableName} in schema ${schemaName}: ${error.message}`
|
|
321
|
-
);
|
|
322
|
-
return [];
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
private async getEnumValues(enumTypeName: string): Promise<string[]> {
|
|
327
|
-
try {
|
|
328
|
-
const enumQuery = `
|
|
329
|
-
SELECT e.enumlabel
|
|
330
|
-
FROM pg_enum e
|
|
331
|
-
JOIN pg_type t ON e.enumtypid = t.oid
|
|
332
|
-
WHERE t.typname = $1
|
|
333
|
-
ORDER BY e.enumsortorder
|
|
334
|
-
`;
|
|
335
|
-
|
|
336
|
-
const result = await this.targetPool.query(enumQuery, [enumTypeName]);
|
|
337
|
-
return result.rows.map((row) => row.enumlabel);
|
|
338
|
-
} catch (error) {
|
|
339
|
-
this.logger.error(
|
|
340
|
-
`Error getting enum values for ${enumTypeName}: ${error.message}`
|
|
341
|
-
);
|
|
342
|
-
return [];
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
private findMatchingEnumValue(
|
|
347
|
-
value: string,
|
|
348
|
-
enumValues: string[]
|
|
349
|
-
): string | null {
|
|
350
|
-
// Buscar coincidencia exacta
|
|
351
|
-
if (enumValues.includes(value)) {
|
|
352
|
-
return value;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// Buscar coincidencia sin distinción de mayúsculas/minúsculas
|
|
356
|
-
const lowerValue = value.toLowerCase();
|
|
357
|
-
for (const enumValue of enumValues) {
|
|
358
|
-
if (enumValue.toLowerCase() === lowerValue) {
|
|
359
|
-
return enumValue;
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// Buscar coincidencia parcial
|
|
364
|
-
const similarValues = enumValues.filter(
|
|
365
|
-
(v) =>
|
|
366
|
-
v.toLowerCase().includes(lowerValue) ||
|
|
367
|
-
lowerValue.includes(v.toLowerCase())
|
|
368
|
-
);
|
|
369
|
-
|
|
370
|
-
if (similarValues.length === 1) {
|
|
371
|
-
return similarValues[0];
|
|
372
|
-
} else if (similarValues.length > 1) {
|
|
373
|
-
// Si hay múltiples coincidencias, usar la más cercana por longitud
|
|
374
|
-
return similarValues.reduce((prev, curr) =>
|
|
375
|
-
Math.abs(curr.length - value.length) <
|
|
376
|
-
Math.abs(prev.length - value.length)
|
|
377
|
-
? curr
|
|
378
|
-
: prev
|
|
379
|
-
);
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
return null;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
private async cleanup() {
|
|
386
|
-
this.logger.log("Cleaning up database connections");
|
|
387
|
-
await this.targetPrisma.$disconnect();
|
|
388
|
-
await this.targetPool.end();
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// Script para ejecutar desde línea de comandos
|
|
393
|
-
if (require.main === module) {
|
|
394
|
-
const run = async () => {
|
|
395
|
-
try {
|
|
396
|
-
// Obtener la ruta del archivo de log de errores
|
|
397
|
-
const errorLogPath = process.argv[2];
|
|
398
|
-
|
|
399
|
-
if (!errorLogPath) {
|
|
400
|
-
console.error(
|
|
401
|
-
"Error: No error log file path provided. Please provide the path to the error log file."
|
|
402
|
-
);
|
|
403
|
-
process.exit(1);
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
const retryTool = new FailedMigrationRetry(errorLogPath);
|
|
407
|
-
await retryTool.retryFailedMigrations();
|
|
408
|
-
process.exit(0);
|
|
409
|
-
} catch (error) {
|
|
410
|
-
console.error("Error:", error.message);
|
|
411
|
-
process.exit(1);
|
|
412
|
-
}
|
|
413
|
-
};
|
|
414
|
-
|
|
415
|
-
run();
|
|
416
|
-
}
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { PrismaClient } from "@prisma/client";
|
|
4
|
+
import * as dotenv from "dotenv";
|
|
5
|
+
import { Logger } from "@nestjs/common";
|
|
6
|
+
import * as pg from "pg";
|
|
7
|
+
import { MigrationErrorHandler } from "./error-handler";
|
|
8
|
+
|
|
9
|
+
dotenv.config();
|
|
10
|
+
|
|
11
|
+
export class FailedMigrationRetry {
|
|
12
|
+
private readonly logger = new Logger("FailedMigrationRetry");
|
|
13
|
+
private readonly targetPrisma: PrismaClient;
|
|
14
|
+
private readonly targetPool: pg.Pool;
|
|
15
|
+
private readonly errorHandler: MigrationErrorHandler;
|
|
16
|
+
private schemaCache: Record<string, any> = {};
|
|
17
|
+
|
|
18
|
+
constructor(private readonly errorLogPath: string) {
|
|
19
|
+
// Target database connection
|
|
20
|
+
this.targetPool = new pg.Pool({
|
|
21
|
+
connectionString: process.env.DATABASE_URL,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Target Prisma client
|
|
25
|
+
this.targetPrisma = new PrismaClient({
|
|
26
|
+
datasources: {
|
|
27
|
+
db: {
|
|
28
|
+
url: process.env.DATABASE_URL,
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Inicializar el manejador de errores
|
|
34
|
+
this.errorHandler = new MigrationErrorHandler();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async retryFailedMigrations() {
|
|
38
|
+
try {
|
|
39
|
+
this.logger.log(`Loading error log from: ${this.errorLogPath}`);
|
|
40
|
+
|
|
41
|
+
// Cargar el archivo de log de errores
|
|
42
|
+
if (!fs.existsSync(this.errorLogPath)) {
|
|
43
|
+
this.logger.error(`Error log file not found: ${this.errorLogPath}`);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const errorLogContent = fs.readFileSync(this.errorLogPath, "utf8");
|
|
48
|
+
const errorLog: Record<string, any[]> = JSON.parse(errorLogContent);
|
|
49
|
+
|
|
50
|
+
let totalErrors = 0;
|
|
51
|
+
let retrySuccessCount = 0;
|
|
52
|
+
let retryFailCount = 0;
|
|
53
|
+
|
|
54
|
+
// Procesar cada tenant
|
|
55
|
+
for (const tenantId in errorLog) {
|
|
56
|
+
const tenantErrors = errorLog[tenantId];
|
|
57
|
+
if (tenantErrors.length === 0) continue;
|
|
58
|
+
|
|
59
|
+
totalErrors += tenantErrors.length;
|
|
60
|
+
this.logger.log(
|
|
61
|
+
`Processing ${tenantErrors.length} errors for tenant: ${tenantId}`
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// Crear cliente Prisma específico para este tenant
|
|
65
|
+
const tenantPrisma = new PrismaClient({
|
|
66
|
+
datasources: {
|
|
67
|
+
db: {
|
|
68
|
+
url: `${process.env.DATABASE_URL}?schema=${tenantId}`,
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
transactionOptions: {
|
|
72
|
+
maxWait: 120000,
|
|
73
|
+
timeout: 120000,
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Procesar cada error
|
|
78
|
+
for (const errorEntry of tenantErrors) {
|
|
79
|
+
const { entity, recordId, data } = errorEntry;
|
|
80
|
+
|
|
81
|
+
this.logger.log(`Retrying ${entity} with ID ${recordId}`);
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
// Obtener el esquema de la tabla si no está en caché
|
|
85
|
+
if (!this.schemaCache[`${tenantId}.${entity}`]) {
|
|
86
|
+
this.schemaCache[`${tenantId}.${entity}`] =
|
|
87
|
+
await this.getTableSchema(entity, tenantId);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const tableSchema = this.schemaCache[`${tenantId}.${entity}`];
|
|
91
|
+
|
|
92
|
+
if (!tableSchema || tableSchema.length === 0) {
|
|
93
|
+
this.logger.warn(
|
|
94
|
+
`Schema not found for ${entity} in tenant ${tenantId}. Skipping.`
|
|
95
|
+
);
|
|
96
|
+
retryFailCount++;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Preparar los datos para la inserción/actualización
|
|
101
|
+
const createData: Record<string, any> = {};
|
|
102
|
+
const updateData: Record<string, any> = {};
|
|
103
|
+
|
|
104
|
+
// Obtener la lista de columnas
|
|
105
|
+
const columnNames = tableSchema.map((col) => col.column_name);
|
|
106
|
+
|
|
107
|
+
// Si no tenemos los datos originales, no podemos reintentar
|
|
108
|
+
if (!data) {
|
|
109
|
+
this.logger.warn(
|
|
110
|
+
`No data available for ${entity} with ID ${recordId}. Skipping.`
|
|
111
|
+
);
|
|
112
|
+
retryFailCount++;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Procesar cada columna
|
|
117
|
+
for (const column of tableSchema) {
|
|
118
|
+
const columnName = column.column_name;
|
|
119
|
+
|
|
120
|
+
// Omitir columnas que no están en los datos
|
|
121
|
+
if (!Object.prototype.hasOwnProperty.call(data, columnName)) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Manejar tipos especiales
|
|
126
|
+
if (column.data_type === "numeric" && data[columnName] !== null) {
|
|
127
|
+
// Convertir a número
|
|
128
|
+
const numericValue = parseFloat(data[columnName]);
|
|
129
|
+
if (!isNaN(numericValue)) {
|
|
130
|
+
createData[columnName] = numericValue;
|
|
131
|
+
updateData[columnName] = numericValue;
|
|
132
|
+
} else {
|
|
133
|
+
createData[columnName] = null;
|
|
134
|
+
updateData[columnName] = null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// Manejar tipos enum
|
|
138
|
+
else if (
|
|
139
|
+
column.data_type === "USER-DEFINED" &&
|
|
140
|
+
column.udt_name.startsWith("enum_")
|
|
141
|
+
) {
|
|
142
|
+
// Obtener valores válidos para el enum
|
|
143
|
+
const enumValues = await this.getEnumValues(column.udt_name);
|
|
144
|
+
|
|
145
|
+
// Inicializar _enumTypes si no existe
|
|
146
|
+
if (!createData._enumTypes) createData._enumTypes = {};
|
|
147
|
+
if (!updateData._enumTypes) updateData._enumTypes = {};
|
|
148
|
+
|
|
149
|
+
// Siempre guardar el tipo de enum para esta columna
|
|
150
|
+
createData._enumTypes[columnName] = column.udt_name;
|
|
151
|
+
updateData._enumTypes[columnName] = column.udt_name;
|
|
152
|
+
|
|
153
|
+
if (data[columnName] === null || data[columnName] === "") {
|
|
154
|
+
createData[columnName] = null;
|
|
155
|
+
updateData[columnName] = null;
|
|
156
|
+
}
|
|
157
|
+
// Si el valor es un string, intentar encontrar una coincidencia en los valores del enum
|
|
158
|
+
else if (typeof data[columnName] === "string") {
|
|
159
|
+
const matchingValue = this.findMatchingEnumValue(
|
|
160
|
+
data[columnName],
|
|
161
|
+
enumValues
|
|
162
|
+
);
|
|
163
|
+
if (matchingValue) {
|
|
164
|
+
createData[columnName] = matchingValue;
|
|
165
|
+
updateData[columnName] = matchingValue;
|
|
166
|
+
} else {
|
|
167
|
+
// Si no hay coincidencia, usar null
|
|
168
|
+
createData[columnName] = null;
|
|
169
|
+
updateData[columnName] = null;
|
|
170
|
+
this.logger.warn(
|
|
171
|
+
`No matching enum value found for ${columnName}="${data[columnName]}" in ${entity}. Using NULL.`
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
// Para otros tipos, usar el valor tal cual
|
|
176
|
+
const enumValue = data[columnName];
|
|
177
|
+
createData[columnName] = data[columnName];
|
|
178
|
+
updateData[columnName] = data[columnName];
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
// Para otros tipos de datos, usar el valor tal cual
|
|
182
|
+
createData[columnName] = data[columnName];
|
|
183
|
+
updateData[columnName] = data[columnName];
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Asegurarse de incluir el ID
|
|
188
|
+
if (columnNames.includes(recordId)) {
|
|
189
|
+
createData[recordId] = data[recordId];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Ejecutar la operación upsert usando SQL raw en lugar de acceso directo al modelo
|
|
193
|
+
// Verificar primero si el registro existe
|
|
194
|
+
const checkExistQuery = `
|
|
195
|
+
SELECT 1 FROM "${tenantId}"."${entity}"
|
|
196
|
+
WHERE "${recordId}" = $1
|
|
197
|
+
LIMIT 1
|
|
198
|
+
`;
|
|
199
|
+
|
|
200
|
+
const existResult = await this.targetPool.query(checkExistQuery, [
|
|
201
|
+
data[recordId],
|
|
202
|
+
]);
|
|
203
|
+
const recordExists = existResult.rows.length > 0;
|
|
204
|
+
|
|
205
|
+
if (recordExists) {
|
|
206
|
+
// Construir la consulta UPDATE
|
|
207
|
+
const updateFields = Object.keys(updateData)
|
|
208
|
+
.filter((key) => key !== recordId && key !== '_enumTypes') // Excluir el campo ID y _enumTypes
|
|
209
|
+
.map((key, index) => {
|
|
210
|
+
// Aplicar casting explícito para tipos enum
|
|
211
|
+
if (updateData._enumTypes && updateData._enumTypes[key]) {
|
|
212
|
+
return `"${key}" = $${index + 2}::${updateData._enumTypes[key]}`;
|
|
213
|
+
}
|
|
214
|
+
return `"${key}" = $${index + 2}`;
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
if (updateFields.length > 0) {
|
|
218
|
+
const updateQuery = `
|
|
219
|
+
UPDATE "${tenantId}"."${entity}"
|
|
220
|
+
SET ${updateFields.join(", ")}
|
|
221
|
+
WHERE "${recordId}" = $1
|
|
222
|
+
`;
|
|
223
|
+
|
|
224
|
+
const updateValues = [
|
|
225
|
+
data[recordId],
|
|
226
|
+
...Object.keys(updateData)
|
|
227
|
+
.filter((key) => key !== recordId && key !== '_enumTypes')
|
|
228
|
+
.map((key) => updateData[key]),
|
|
229
|
+
];
|
|
230
|
+
|
|
231
|
+
await this.targetPool.query(updateQuery, updateValues);
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
// Construir la consulta INSERT
|
|
235
|
+
const insertFields = Object.keys(createData)
|
|
236
|
+
.filter(key => key !== '_enumTypes')
|
|
237
|
+
.map((key) => `"${key}"`);
|
|
238
|
+
|
|
239
|
+
const insertPlaceholders = Object.keys(createData)
|
|
240
|
+
.filter(key => key !== '_enumTypes')
|
|
241
|
+
.map((key, index) => {
|
|
242
|
+
// Aplicar casting explícito para tipos enum
|
|
243
|
+
if (createData._enumTypes && createData._enumTypes[key]) {
|
|
244
|
+
return `$${index + 1}::${createData._enumTypes[key]}`;
|
|
245
|
+
}
|
|
246
|
+
return `$${index + 1}`;
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const insertQuery = `
|
|
250
|
+
INSERT INTO "${tenantId}"."${entity}" (${insertFields.join(", ")})
|
|
251
|
+
VALUES (${insertPlaceholders.join(", ")})
|
|
252
|
+
`;
|
|
253
|
+
|
|
254
|
+
const insertValues = Object.keys(createData)
|
|
255
|
+
.filter(key => key !== '_enumTypes')
|
|
256
|
+
.map((key) => createData[key]);
|
|
257
|
+
|
|
258
|
+
await this.targetPool.query(insertQuery, insertValues);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
this.logger.log(
|
|
262
|
+
`Successfully migrated ${entity} with ID ${recordId}`
|
|
263
|
+
);
|
|
264
|
+
retrySuccessCount++;
|
|
265
|
+
} catch (error) {
|
|
266
|
+
this.logger.error(
|
|
267
|
+
`Error retrying ${entity} with ID ${recordId}: ${error.message}`
|
|
268
|
+
);
|
|
269
|
+
// Registrar el error en el nuevo log
|
|
270
|
+
this.errorHandler.logError(tenantId, entity, recordId, error, data);
|
|
271
|
+
retryFailCount++;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Cerrar la conexión del cliente tenant
|
|
276
|
+
await tenantPrisma.$disconnect();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Mostrar resumen
|
|
280
|
+
this.logger.log(`
|
|
281
|
+
Retry Summary:
|
|
282
|
+
- Total errors: ${totalErrors}
|
|
283
|
+
- Successfully retried: ${retrySuccessCount}
|
|
284
|
+
- Failed retries: ${retryFailCount}
|
|
285
|
+
`);
|
|
286
|
+
|
|
287
|
+
// Generar informe de errores restantes
|
|
288
|
+
const remainingErrorsReport = this.errorHandler.generateErrorReport();
|
|
289
|
+
this.logger.log("Remaining Errors Report:\n" + remainingErrorsReport);
|
|
290
|
+
} catch (error) {
|
|
291
|
+
this.logger.error(
|
|
292
|
+
`Error in retry process: ${error.message}`,
|
|
293
|
+
error.stack
|
|
294
|
+
);
|
|
295
|
+
} finally {
|
|
296
|
+
await this.cleanup();
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private async getTableSchema(
|
|
301
|
+
tableName: string,
|
|
302
|
+
schemaName: string
|
|
303
|
+
): Promise<any[]> {
|
|
304
|
+
try {
|
|
305
|
+
const schemaQuery = `
|
|
306
|
+
SELECT column_name, data_type, column_default, is_nullable, udt_name
|
|
307
|
+
FROM information_schema.columns
|
|
308
|
+
WHERE table_name = $1
|
|
309
|
+
AND table_schema = $2
|
|
310
|
+
ORDER BY ordinal_position
|
|
311
|
+
`;
|
|
312
|
+
|
|
313
|
+
const result = await this.targetPool.query(schemaQuery, [
|
|
314
|
+
tableName,
|
|
315
|
+
schemaName,
|
|
316
|
+
]);
|
|
317
|
+
return result.rows;
|
|
318
|
+
} catch (error) {
|
|
319
|
+
this.logger.error(
|
|
320
|
+
`Error getting schema for table ${tableName} in schema ${schemaName}: ${error.message}`
|
|
321
|
+
);
|
|
322
|
+
return [];
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private async getEnumValues(enumTypeName: string): Promise<string[]> {
|
|
327
|
+
try {
|
|
328
|
+
const enumQuery = `
|
|
329
|
+
SELECT e.enumlabel
|
|
330
|
+
FROM pg_enum e
|
|
331
|
+
JOIN pg_type t ON e.enumtypid = t.oid
|
|
332
|
+
WHERE t.typname = $1
|
|
333
|
+
ORDER BY e.enumsortorder
|
|
334
|
+
`;
|
|
335
|
+
|
|
336
|
+
const result = await this.targetPool.query(enumQuery, [enumTypeName]);
|
|
337
|
+
return result.rows.map((row) => row.enumlabel);
|
|
338
|
+
} catch (error) {
|
|
339
|
+
this.logger.error(
|
|
340
|
+
`Error getting enum values for ${enumTypeName}: ${error.message}`
|
|
341
|
+
);
|
|
342
|
+
return [];
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
private findMatchingEnumValue(
|
|
347
|
+
value: string,
|
|
348
|
+
enumValues: string[]
|
|
349
|
+
): string | null {
|
|
350
|
+
// Buscar coincidencia exacta
|
|
351
|
+
if (enumValues.includes(value)) {
|
|
352
|
+
return value;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Buscar coincidencia sin distinción de mayúsculas/minúsculas
|
|
356
|
+
const lowerValue = value.toLowerCase();
|
|
357
|
+
for (const enumValue of enumValues) {
|
|
358
|
+
if (enumValue.toLowerCase() === lowerValue) {
|
|
359
|
+
return enumValue;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Buscar coincidencia parcial
|
|
364
|
+
const similarValues = enumValues.filter(
|
|
365
|
+
(v) =>
|
|
366
|
+
v.toLowerCase().includes(lowerValue) ||
|
|
367
|
+
lowerValue.includes(v.toLowerCase())
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
if (similarValues.length === 1) {
|
|
371
|
+
return similarValues[0];
|
|
372
|
+
} else if (similarValues.length > 1) {
|
|
373
|
+
// Si hay múltiples coincidencias, usar la más cercana por longitud
|
|
374
|
+
return similarValues.reduce((prev, curr) =>
|
|
375
|
+
Math.abs(curr.length - value.length) <
|
|
376
|
+
Math.abs(prev.length - value.length)
|
|
377
|
+
? curr
|
|
378
|
+
: prev
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
private async cleanup() {
|
|
386
|
+
this.logger.log("Cleaning up database connections");
|
|
387
|
+
await this.targetPrisma.$disconnect();
|
|
388
|
+
await this.targetPool.end();
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Script para ejecutar desde línea de comandos
|
|
393
|
+
if (require.main === module) {
|
|
394
|
+
const run = async () => {
|
|
395
|
+
try {
|
|
396
|
+
// Obtener la ruta del archivo de log de errores
|
|
397
|
+
const errorLogPath = process.argv[2];
|
|
398
|
+
|
|
399
|
+
if (!errorLogPath) {
|
|
400
|
+
console.error(
|
|
401
|
+
"Error: No error log file path provided. Please provide the path to the error log file."
|
|
402
|
+
);
|
|
403
|
+
process.exit(1);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const retryTool = new FailedMigrationRetry(errorLogPath);
|
|
407
|
+
await retryTool.retryFailedMigrations();
|
|
408
|
+
process.exit(0);
|
|
409
|
+
} catch (error) {
|
|
410
|
+
console.error("Error:", error.message);
|
|
411
|
+
process.exit(1);
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
run();
|
|
416
|
+
}
|