@javalabs/prisma-client 1.0.27 → 1.0.29

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.
Files changed (50) hide show
  1. package/.github/CODEOWNERS +1 -1
  2. package/README.md +269 -269
  3. package/migration-config.json +63 -63
  4. package/migration-config.json.bk +95 -95
  5. package/migrations/add_reserved_amount.sql +7 -7
  6. package/package.json +44 -44
  7. package/prisma/migrations/add_uuid_to_transactions.sql +13 -13
  8. package/prisma/schema.prisma +609 -601
  9. package/src/index.ts +23 -23
  10. package/src/prisma-factory.service.ts +40 -40
  11. package/src/prisma.module.ts +9 -9
  12. package/src/prisma.service.ts +16 -16
  13. package/src/scripts/add-uuid-to-table.ts +138 -138
  14. package/src/scripts/create-tenant-schemas.ts +145 -145
  15. package/src/scripts/data-migration/batch-migrator.ts +248 -248
  16. package/src/scripts/data-migration/data-transformer.ts +426 -426
  17. package/src/scripts/data-migration/db-connector.ts +120 -120
  18. package/src/scripts/data-migration/dependency-resolver.ts +174 -174
  19. package/src/scripts/data-migration/entity-discovery.ts +196 -196
  20. package/src/scripts/data-migration/foreign-key-manager.ts +277 -277
  21. package/src/scripts/data-migration/migration-config.json +63 -63
  22. package/src/scripts/data-migration/migration-tool.ts +509 -509
  23. package/src/scripts/data-migration/schema-utils.ts +248 -248
  24. package/src/scripts/data-migration/tenant-migrator.ts +201 -201
  25. package/src/scripts/data-migration/typecast-manager.ts +193 -193
  26. package/src/scripts/data-migration/types.ts +113 -113
  27. package/src/scripts/database-initializer.ts +49 -49
  28. package/src/scripts/drop-database.ts +104 -104
  29. package/src/scripts/dump-source-db.sh +61 -61
  30. package/src/scripts/encrypt-user-passwords.ts +36 -36
  31. package/src/scripts/error-handler.ts +117 -117
  32. package/src/scripts/fix-data-types.ts +241 -241
  33. package/src/scripts/fix-enum-values.ts +357 -357
  34. package/src/scripts/fix-schema-discrepancies.ts +317 -317
  35. package/src/scripts/fix-table-indexes.ts +601 -601
  36. package/src/scripts/migrate-schema-structure.ts +90 -90
  37. package/src/scripts/migrate-uuid.ts +76 -76
  38. package/src/scripts/post-migration-validator.ts +526 -526
  39. package/src/scripts/pre-migration-validator.ts +610 -610
  40. package/src/scripts/reset-database.ts +263 -263
  41. package/src/scripts/retry-failed-migrations.ts +416 -416
  42. package/src/scripts/run-migration.ts +707 -707
  43. package/src/scripts/schema-sync.ts +128 -128
  44. package/src/scripts/sequence-sync-cli.ts +416 -416
  45. package/src/scripts/sequence-synchronizer.ts +127 -127
  46. package/src/scripts/sync-enum-types.ts +170 -170
  47. package/src/scripts/sync-enum-values.ts +563 -563
  48. package/src/scripts/truncate-database.ts +123 -123
  49. package/src/scripts/verify-migration-setup.ts +135 -135
  50. package/tsconfig.json +17 -17
@@ -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
+ }