@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.
Files changed (101) hide show
  1. package/.github/CODEOWNERS +1 -0
  2. package/README.md +269 -269
  3. package/dist/index.d.ts +1 -1
  4. package/dist/prisma.service.d.ts +1 -1
  5. package/dist/scripts/add-uuid-to-table.js +32 -32
  6. package/dist/scripts/data-migration/batch-migrator.js +12 -12
  7. package/dist/scripts/data-migration/data-transformer.js +14 -14
  8. package/dist/scripts/data-migration/dependency-resolver.js +23 -23
  9. package/dist/scripts/data-migration/entity-discovery.js +68 -68
  10. package/dist/scripts/data-migration/foreign-key-manager.js +23 -23
  11. package/dist/scripts/data-migration/migration-tool.js +5 -5
  12. package/dist/scripts/data-migration/schema-utils.js +74 -74
  13. package/dist/scripts/data-migration/typecast-manager.js +4 -4
  14. package/dist/scripts/database-initializer.js +5 -5
  15. package/dist/scripts/drop-database.js +5 -5
  16. package/dist/scripts/fix-data-types.js +53 -53
  17. package/dist/scripts/fix-enum-values.js +34 -34
  18. package/dist/scripts/fix-schema-discrepancies.js +40 -40
  19. package/dist/scripts/fix-table-indexes.js +81 -81
  20. package/dist/scripts/migrate-schema-structure.js +4 -4
  21. package/dist/scripts/migrate-uuid.js +19 -19
  22. package/dist/scripts/post-migration-validator.js +49 -49
  23. package/dist/scripts/pre-migration-validator.js +107 -107
  24. package/dist/scripts/reset-database.js +21 -21
  25. package/dist/scripts/retry-failed-migrations.js +28 -28
  26. package/dist/scripts/run-migration.js +5 -5
  27. package/dist/scripts/schema-sync.js +18 -18
  28. package/dist/scripts/sequence-sync-cli.js +55 -55
  29. package/dist/scripts/sequence-synchronizer.js +20 -20
  30. package/dist/scripts/sync-enum-types.js +30 -30
  31. package/dist/scripts/sync-enum-values.js +52 -52
  32. package/dist/scripts/truncate-database.js +10 -10
  33. package/dist/scripts/verify-migration-setup.js +10 -10
  34. package/dist/tsconfig.tsbuildinfo +1 -1
  35. package/migration-config.json +63 -63
  36. package/migration-config.json.bk +95 -95
  37. package/migrations/add_reserved_amount.sql +8 -0
  38. package/package.json +44 -44
  39. package/prisma/migrations/add_uuid_to_transactions.sql +13 -13
  40. package/prisma/schema.prisma +601 -554
  41. package/src/index.ts +23 -23
  42. package/src/prisma-factory.service.ts +40 -40
  43. package/src/prisma.module.ts +9 -9
  44. package/src/prisma.service.ts +16 -16
  45. package/src/scripts/add-uuid-to-table.ts +138 -138
  46. package/src/scripts/create-tenant-schemas.ts +145 -145
  47. package/src/scripts/data-migration/batch-migrator.ts +248 -248
  48. package/src/scripts/data-migration/data-transformer.ts +426 -426
  49. package/src/scripts/data-migration/db-connector.ts +120 -120
  50. package/src/scripts/data-migration/dependency-resolver.ts +174 -174
  51. package/src/scripts/data-migration/entity-discovery.ts +196 -196
  52. package/src/scripts/data-migration/foreign-key-manager.ts +277 -277
  53. package/src/scripts/data-migration/migration-config.json +63 -63
  54. package/src/scripts/data-migration/migration-tool.ts +509 -509
  55. package/src/scripts/data-migration/schema-utils.ts +248 -248
  56. package/src/scripts/data-migration/tenant-migrator.ts +201 -201
  57. package/src/scripts/data-migration/typecast-manager.ts +193 -193
  58. package/src/scripts/data-migration/types.ts +113 -113
  59. package/src/scripts/database-initializer.ts +49 -49
  60. package/src/scripts/drop-database.ts +104 -104
  61. package/src/scripts/dump-source-db.sh +61 -61
  62. package/src/scripts/encrypt-user-passwords.ts +36 -36
  63. package/src/scripts/error-handler.ts +117 -117
  64. package/src/scripts/fix-data-types.ts +241 -241
  65. package/src/scripts/fix-enum-values.ts +357 -357
  66. package/src/scripts/fix-schema-discrepancies.ts +317 -317
  67. package/src/scripts/fix-table-indexes.ts +601 -601
  68. package/src/scripts/migrate-schema-structure.ts +90 -90
  69. package/src/scripts/migrate-uuid.ts +76 -76
  70. package/src/scripts/post-migration-validator.ts +526 -526
  71. package/src/scripts/pre-migration-validator.ts +610 -610
  72. package/src/scripts/reset-database.ts +263 -263
  73. package/src/scripts/retry-failed-migrations.ts +416 -416
  74. package/src/scripts/run-migration.ts +707 -707
  75. package/src/scripts/schema-sync.ts +128 -128
  76. package/src/scripts/sequence-sync-cli.ts +416 -416
  77. package/src/scripts/sequence-synchronizer.ts +127 -127
  78. package/src/scripts/sync-enum-types.ts +170 -170
  79. package/src/scripts/sync-enum-values.ts +563 -563
  80. package/src/scripts/truncate-database.ts +123 -123
  81. package/src/scripts/verify-migration-setup.ts +135 -135
  82. package/tsconfig.json +17 -17
  83. package/dist/scripts/data-migration/dependency-manager.d.ts +0 -9
  84. package/dist/scripts/data-migration/dependency-manager.js +0 -86
  85. package/dist/scripts/data-migration/dependency-manager.js.map +0 -1
  86. package/dist/scripts/data-migration/migration-config.json +0 -63
  87. package/dist/scripts/data-migration/migration-phases.d.ts +0 -5
  88. package/dist/scripts/data-migration/migration-phases.js +0 -55
  89. package/dist/scripts/data-migration/migration-phases.js.map +0 -1
  90. package/dist/scripts/data-migration/multi-source-migrator.d.ts +0 -17
  91. package/dist/scripts/data-migration/multi-source-migrator.js +0 -130
  92. package/dist/scripts/data-migration/multi-source-migrator.js.map +0 -1
  93. package/dist/scripts/data-migration/phase-generator.d.ts +0 -15
  94. package/dist/scripts/data-migration/phase-generator.js +0 -187
  95. package/dist/scripts/data-migration/phase-generator.js.map +0 -1
  96. package/dist/scripts/data-migration.d.ts +0 -22
  97. package/dist/scripts/data-migration.js +0 -593
  98. package/dist/scripts/data-migration.js.map +0 -1
  99. package/dist/scripts/multi-db-migration.d.ts +0 -1
  100. package/dist/scripts/multi-db-migration.js +0 -55
  101. 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
+ }