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