@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,563 +1,563 @@
1
- import { Logger } from "@nestjs/common";
2
- import { Pool } from "pg";
3
- import * as path from "path";
4
- import * as fs from "fs";
5
- import * as dotenv from "dotenv";
6
-
7
- dotenv.config();
8
-
9
- /**
10
- * Clase para sincronizar valores entre enums específicos y enums consolidados
11
- * Esta herramienta asegura que los valores en los enums específicos (deprecados)
12
- * sean compatibles con los enums consolidados durante la migración.
13
- */
14
- export class EnumValueSynchronizer {
15
- private readonly logger = new Logger("EnumValueSynchronizer");
16
- private readonly pool: Pool;
17
-
18
- // Mapeo de enums específicos a enums consolidados
19
- private readonly enumMappings: Record<string, string> = {
20
- enum_credit_requests_status: "enum_transaction_status",
21
- enum_global_user_transactions_status: "enum_transaction_status",
22
- enum_global_user_transactions_transaction_type: "enum_transaction_type",
23
- enum_invoices_status: "enum_transaction_status",
24
- enum_payin_status: "enum_transaction_status",
25
- enum_payout_status: "enum_transaction_status",
26
- enum_pending_references_status: "enum_transaction_status",
27
- enum_toku_status: "enum_transaction_status",
28
- enum_transaction_updates_new_status: "enum_transaction_status",
29
- enum_transactions_account_type: "enum_account_type",
30
- enum_transactions_status: "enum_transaction_status",
31
- enum_transactions_transaction_type: "enum_transaction_type",
32
- enum_transactions_typetransaction: "enum_transaction_type",
33
- enum_transactions_user_type_account: "enum_account_type",
34
- enum_transactions_usertypeaccount: "enum_account_type",
35
- };
36
-
37
- // Mapeo de valores de texto a valores de enum válidos
38
- private readonly valueNormalizationMap: Record<
39
- string,
40
- Record<string, string>
41
- > = {
42
- // Mapeos para enum_transaction_status
43
- enum_transaction_status: {
44
- PENDING: "PENDING",
45
- pending: "PENDING",
46
- COMPLETED: "COMPLETED",
47
- completed: "COMPLETED",
48
- FAILED: "FAILED",
49
- failed: "FAILED",
50
- PROCESSING: "PROCESSING",
51
- processing: "PROCESSING",
52
- CANCELLED: "CANCELLED",
53
- cancelled: "CANCELLED",
54
- canceled: "CANCELLED",
55
- CANCELED: "CANCELLED",
56
- REJECTED: "REJECTED",
57
- rejected: "REJECTED",
58
- APPROVED: "APPROVED",
59
- approved: "APPROVED",
60
- EXPIRED: "EXPIRED",
61
- expired: "EXPIRED",
62
- REFUNDED: "REFUNDED",
63
- refunded: "REFUNDED",
64
- PARTIAL_REFUND: "PARTIAL_REFUND",
65
- partial_refund: "PARTIAL_REFUND",
66
- },
67
- // Mapeos para enum_transaction_type
68
- enum_transaction_type: {
69
- DEPOSIT: "DEPOSIT",
70
- deposit: "DEPOSIT",
71
- WITHDRAWAL: "WITHDRAWAL",
72
- withdrawal: "WITHDRAWAL",
73
- TRANSFER: "TRANSFER",
74
- transfer: "TRANSFER",
75
- PAYMENT: "PAYMENT",
76
- payment: "PAYMENT",
77
- REFUND: "REFUND",
78
- refund: "REFUND",
79
- FEE: "FEE",
80
- fee: "FEE",
81
- ADJUSTMENT: "ADJUSTMENT",
82
- adjustment: "ADJUSTMENT",
83
- },
84
- // Mapeos para enum_account_type
85
- enum_account_type: {
86
- PERSONAL: "PERSONAL",
87
- personal: "PERSONAL",
88
- BUSINESS: "BUSINESS",
89
- business: "BUSINESS",
90
- MERCHANT: "MERCHANT",
91
- merchant: "MERCHANT",
92
- PLATFORM: "PLATFORM",
93
- platform: "PLATFORM",
94
- },
95
- };
96
-
97
- // Caché para valores de enum
98
- private enumValuesCache: Record<string, string[]> = {};
99
-
100
- constructor(
101
- private readonly sourceUrl: string = process.env.SOURCE_DATABASE_URL,
102
- private readonly targetUrl: string = process.env.DATABASE_URL
103
- ) {
104
- this.pool = new Pool({
105
- connectionString: this.targetUrl,
106
- });
107
- }
108
-
109
- /**
110
- * Sincroniza los valores entre enums específicos y enums consolidados
111
- * y normaliza los valores de texto a valores de enum válidos
112
- */
113
- async synchronizeEnumValues() {
114
- try {
115
- this.logger.log("Iniciando sincronización de valores de enum");
116
-
117
- // Crear directorio para logs si no existe
118
- const logsDir = path.join(process.cwd(), "migration-logs");
119
- if (!fs.existsSync(logsDir)) {
120
- fs.mkdirSync(logsDir, { recursive: true });
121
- }
122
-
123
- // Obtener todos los tipos enum en la base de datos
124
- const enumsResult = await this.pool.query(`
125
- SELECT t.typname AS enum_name
126
- FROM pg_type t
127
- JOIN pg_namespace n ON t.typnamespace = n.oid
128
- WHERE t.typtype = 'e' -- enum types
129
- AND n.nspname = 'public'
130
- ORDER BY t.typname
131
- `);
132
-
133
- this.logger.log(
134
- `Encontrados ${enumsResult.rows.length} tipos enum en la base de datos`
135
- );
136
-
137
- // Procesar cada enum específico y su correspondiente enum consolidado
138
- for (const [specificEnum, consolidatedEnum] of Object.entries(
139
- this.enumMappings
140
- )) {
141
- await this.synchronizeSpecificEnum(specificEnum, consolidatedEnum);
142
- }
143
-
144
- // Después de sincronizar los enums, normalizar los valores en las tablas
145
- await this.normalizeEnumValuesInTables();
146
-
147
- this.logger.log("Sincronización de valores de enum completada con éxito");
148
- } catch (error) {
149
- this.logger.error(
150
- `Error durante la sincronización de valores de enum: ${error.message}`,
151
- error.stack
152
- );
153
- throw error;
154
- } finally {
155
- await this.pool.end();
156
- }
157
- }
158
-
159
- /**
160
- * Sincroniza los valores entre un enum específico y su enum consolidado
161
- */
162
- private async synchronizeSpecificEnum(
163
- specificEnum: string,
164
- consolidatedEnum: string
165
- ) {
166
- try {
167
- this.logger.log(
168
- `Sincronizando valores entre ${specificEnum} y ${consolidatedEnum}`
169
- );
170
-
171
- // Verificar si ambos enums existen
172
- const specificEnumExists = await this.enumExists(specificEnum);
173
- const consolidatedEnumExists = await this.enumExists(consolidatedEnum);
174
-
175
- if (!specificEnumExists || !consolidatedEnumExists) {
176
- this.logger.warn(
177
- `No se puede sincronizar: ${
178
- !specificEnumExists ? specificEnum : consolidatedEnum
179
- } no existe`
180
- );
181
- return;
182
- }
183
-
184
- // Obtener valores de ambos enums
185
- const specificValues = await this.getEnumValues(specificEnum);
186
- const consolidatedValues = await this.getEnumValues(consolidatedEnum);
187
-
188
- this.logger.log(
189
- `Valores en ${specificEnum}: ${specificValues.join(", ")}\n` +
190
- `Valores en ${consolidatedEnum}: ${consolidatedValues.join(", ")}`
191
- );
192
-
193
- // Verificar valores que están en el enum específico pero no en el consolidado
194
- const missingValues = specificValues.filter(
195
- (value) => !consolidatedValues.includes(value)
196
- );
197
-
198
- if (missingValues.length === 0) {
199
- this.logger.log(
200
- `No hay valores faltantes entre ${specificEnum} y ${consolidatedEnum}`
201
- );
202
- return;
203
- }
204
-
205
- this.logger.log(
206
- `Encontrados ${
207
- missingValues.length
208
- } valores en ${specificEnum} que faltan en ${consolidatedEnum}: ${missingValues.join(
209
- ", "
210
- )}`
211
- );
212
-
213
- // Agregar los valores faltantes al enum consolidado
214
- for (const value of missingValues) {
215
- await this.addValueToEnum(consolidatedEnum, value);
216
- }
217
-
218
- this.logger.log(
219
- `Sincronización entre ${specificEnum} y ${consolidatedEnum} completada`
220
- );
221
- } catch (error) {
222
- this.logger.error(
223
- `Error sincronizando ${specificEnum} con ${consolidatedEnum}: ${error.message}`
224
- );
225
- }
226
- }
227
-
228
- /**
229
- * Verifica si un enum existe en la base de datos
230
- */
231
- private async enumExists(enumName: string): Promise<boolean> {
232
- const result = await this.pool.query(
233
- `
234
- SELECT 1
235
- FROM pg_type t
236
- JOIN pg_namespace n ON t.typnamespace = n.oid
237
- WHERE t.typname = $1
238
- AND t.typtype = 'e' -- enum types
239
- AND n.nspname = 'public'
240
- `,
241
- [enumName]
242
- );
243
-
244
- return result.rows.length > 0;
245
- }
246
-
247
- /**
248
- * Obtiene los valores de un enum
249
- */
250
- private async getEnumValues(enumName: string): Promise<string[]> {
251
- const result = await this.pool.query(
252
- `
253
- SELECT e.enumlabel
254
- FROM pg_enum e
255
- JOIN pg_type t ON e.enumtypid = t.oid
256
- JOIN pg_namespace n ON t.typnamespace = n.oid
257
- WHERE t.typname = $1
258
- AND n.nspname = 'public'
259
- ORDER BY e.enumsortorder
260
- `,
261
- [enumName]
262
- );
263
-
264
- return result.rows.map((row) => row.enumlabel);
265
- }
266
-
267
- /**
268
- * Agrega un valor a un enum existente
269
- */
270
- private async addValueToEnum(enumName: string, value: string) {
271
- try {
272
- // Escapar comillas simples en el valor
273
- const escapedValue = value.replace(/'/g, "''");
274
-
275
- // Agregar el valor al enum
276
- await this.pool.query(`
277
- ALTER TYPE ${enumName} ADD VALUE IF NOT EXISTS '${escapedValue}'
278
- `);
279
-
280
- // Actualizar la caché de valores de enum
281
- if (this.enumValuesCache[enumName]) {
282
- if (!this.enumValuesCache[enumName].includes(value)) {
283
- this.enumValuesCache[enumName].push(value);
284
- }
285
- }
286
-
287
- this.logger.log(`Valor '${value}' agregado al enum ${enumName}`);
288
- } catch (error) {
289
- this.logger.error(
290
- `Error agregando valor '${value}' al enum ${enumName}: ${error.message}`
291
- );
292
- throw error;
293
- }
294
- }
295
-
296
- /**
297
- * Normaliza los valores de enum en todas las tablas
298
- */
299
- private async normalizeEnumValuesInTables() {
300
- this.logger.log("Iniciando normalización de valores de enum en tablas");
301
-
302
- try {
303
- // Obtener todos los schemas excepto los del sistema
304
- const schemas = await this.getSchemas();
305
- this.logger.log(`Encontrados ${schemas.length} schemas para procesar`);
306
-
307
- // Para cada schema, normalizar los valores de enum en sus tablas
308
- for (const schema of schemas) {
309
- await this.normalizeEnumValuesInSchema(schema);
310
- }
311
-
312
- this.logger.log("Normalización de valores de enum en tablas completada");
313
- } catch (error) {
314
- this.logger.error(
315
- `Error durante la normalización de valores de enum: ${error.message}`,
316
- error.stack
317
- );
318
- }
319
- }
320
-
321
- /**
322
- * Obtiene todos los schemas de la base de datos excepto los del sistema
323
- */
324
- private async getSchemas(): Promise<string[]> {
325
- const result = await this.pool.query(`
326
- SELECT schema_name
327
- FROM information_schema.schemata
328
- WHERE schema_name NOT IN ('information_schema', 'pg_catalog', 'pg_toast')
329
- AND schema_name NOT LIKE 'pg_%'
330
- `);
331
-
332
- return result.rows.map((row) => row.schema_name);
333
- }
334
-
335
- /**
336
- * Normaliza los valores de enum en un schema específico
337
- */
338
- private async normalizeEnumValuesInSchema(schema: string) {
339
- this.logger.log(`Procesando valores de enum en schema: ${schema}`);
340
-
341
- try {
342
- // Obtener todas las columnas enum en este schema
343
- const enumColumnsQuery = `
344
- SELECT
345
- c.table_name,
346
- c.column_name,
347
- c.udt_name
348
- FROM
349
- information_schema.columns c
350
- WHERE
351
- c.table_schema = $1
352
- AND c.data_type = 'USER-DEFINED'
353
- AND c.udt_name LIKE 'enum_%'
354
- `;
355
-
356
- const enumColumnsResult = await this.pool.query(enumColumnsQuery, [
357
- schema,
358
- ]);
359
-
360
- if (enumColumnsResult.rows.length === 0) {
361
- this.logger.log(`No se encontraron columnas enum en schema: ${schema}`);
362
- return;
363
- }
364
-
365
- this.logger.log(
366
- `Encontradas ${enumColumnsResult.rows.length} columnas enum en schema: ${schema}`
367
- );
368
-
369
- // Procesar cada columna enum
370
- for (const row of enumColumnsResult.rows) {
371
- const tableName = row.table_name;
372
- const columnName = row.column_name;
373
- const enumTypeName = row.udt_name;
374
-
375
- await this.normalizeEnumColumn(
376
- schema,
377
- tableName,
378
- columnName,
379
- enumTypeName
380
- );
381
- }
382
-
383
- this.logger.log(
384
- `Completado procesamiento de valores enum para schema: ${schema}`
385
- );
386
- } catch (error) {
387
- this.logger.error(
388
- `Error procesando valores enum para schema ${schema}: ${error.message}`,
389
- error.stack
390
- );
391
- }
392
- }
393
-
394
- /**
395
- * Normaliza los valores en una columna enum específica
396
- */
397
- private async normalizeEnumColumn(
398
- schema: string,
399
- tableName: string,
400
- columnName: string,
401
- enumTypeName: string
402
- ) {
403
- this.logger.log(
404
- `Normalizando columna enum ${columnName} (${enumTypeName}) en tabla ${schema}.${tableName}`
405
- );
406
-
407
- try {
408
- // Obtener los valores válidos para el enum si no están en caché
409
- if (!this.enumValuesCache[enumTypeName]) {
410
- this.enumValuesCache[enumTypeName] = await this.getEnumValues(
411
- enumTypeName
412
- );
413
- }
414
- const validEnumValues = this.enumValuesCache[enumTypeName];
415
-
416
- if (validEnumValues.length === 0) {
417
- this.logger.warn(
418
- `No se encontraron valores para el enum ${enumTypeName}. Omitiendo.`
419
- );
420
- return;
421
- }
422
-
423
- // Obtener valores actuales en la columna
424
- const currentValuesQuery = `
425
- SELECT DISTINCT "${columnName}"
426
- FROM "${schema}"."${tableName}"
427
- WHERE "${columnName}" IS NOT NULL
428
- `;
429
-
430
- const currentValuesResult = await this.pool.query(currentValuesQuery);
431
- const currentValues = currentValuesResult.rows.map((r) => r[columnName]);
432
-
433
- // Encontrar valores inválidos
434
- const invalidValues = currentValues.filter(
435
- (val) => !validEnumValues.includes(val)
436
- );
437
-
438
- if (invalidValues.length === 0) {
439
- this.logger.log(
440
- `No se encontraron valores inválidos para columna enum ${columnName} en tabla ${schema}.${tableName}`
441
- );
442
- return;
443
- }
444
-
445
- this.logger.log(
446
- `Encontrados ${
447
- invalidValues.length
448
- } valores inválidos para enum ${enumTypeName} en ${schema}.${tableName}: ${invalidValues.join(
449
- ", "
450
- )}`
451
- );
452
-
453
- // Para cada valor inválido, intentar normalizarlo
454
- for (const invalidValue of invalidValues) {
455
- const normalizedValue = this.getNormalizedEnumValue(
456
- enumTypeName,
457
- invalidValue
458
- );
459
-
460
- if (normalizedValue && normalizedValue !== invalidValue) {
461
- // Actualizar los registros con el valor normalizado
462
- const updateQuery = `
463
- UPDATE "${schema}"."${tableName}"
464
- SET "${columnName}" = $1
465
- WHERE "${columnName}" = $2
466
- `;
467
-
468
- const result = await this.pool.query(updateQuery, [
469
- normalizedValue,
470
- invalidValue,
471
- ]);
472
-
473
- this.logger.log(
474
- `Actualizado valor '${invalidValue}' a '${normalizedValue}' en ${result.rowCount} filas de ${schema}.${tableName}.${columnName}`
475
- );
476
-
477
- // Si el valor normalizado no está en el enum, agregarlo
478
- if (!validEnumValues.includes(normalizedValue)) {
479
- await this.addValueToEnum(enumTypeName, normalizedValue);
480
- }
481
- } else if (!normalizedValue) {
482
- // Si no se puede normalizar, establecer a NULL
483
- const updateQuery = `
484
- UPDATE "${schema}"."${tableName}"
485
- SET "${columnName}" = NULL
486
- WHERE "${columnName}" = $1
487
- `;
488
-
489
- const result = await this.pool.query(updateQuery, [invalidValue]);
490
-
491
- this.logger.log(
492
- `Establecido valor '${invalidValue}' a NULL en ${result.rowCount} filas de ${schema}.${tableName}.${columnName}`
493
- );
494
- }
495
- }
496
- } catch (error) {
497
- this.logger.error(
498
- `Error normalizando columna enum ${columnName} en ${schema}.${tableName}: ${error.message}`,
499
- error.stack
500
- );
501
- }
502
- }
503
-
504
- /**
505
- * Obtiene un valor de enum normalizado basado en mapeos predefinidos
506
- */
507
- private getNormalizedEnumValue(
508
- enumTypeName: string,
509
- value: string
510
- ): string | null {
511
- // Si el valor ya es válido, devolverlo tal cual
512
- if (this.enumValuesCache[enumTypeName]?.includes(value)) {
513
- return value;
514
- }
515
-
516
- // Buscar en el mapa de normalización
517
- if (
518
- this.valueNormalizationMap[enumTypeName] &&
519
- this.valueNormalizationMap[enumTypeName][value]
520
- ) {
521
- return this.valueNormalizationMap[enumTypeName][value];
522
- }
523
-
524
- // Intentar normalizar basado en similitud (convertir a mayúsculas, quitar espacios, etc.)
525
- const normalizedInput = value.toUpperCase().replace(/\s+/g, "_");
526
-
527
- // Verificar si la versión normalizada existe en el mapa
528
- if (
529
- this.valueNormalizationMap[enumTypeName] &&
530
- this.valueNormalizationMap[enumTypeName][normalizedInput]
531
- ) {
532
- return this.valueNormalizationMap[enumTypeName][normalizedInput];
533
- }
534
-
535
- // Buscar coincidencias parciales en los valores válidos
536
- const validValues = this.enumValuesCache[enumTypeName] || [];
537
- for (const validValue of validValues) {
538
- // Comparar ignorando mayúsculas/minúsculas
539
- if (validValue.toLowerCase() === value.toLowerCase()) {
540
- return validValue;
541
- }
542
- }
543
-
544
- // Si no se encuentra coincidencia, devolver null
545
- return null;
546
- }
547
- }
548
-
549
- // Ejecutar el script si se llama directamente
550
- if (require.main === module) {
551
- const run = async () => {
552
- const synchronizer = new EnumValueSynchronizer(
553
- process.env.SOURCE_DATABASE_URL,
554
- process.env.DATABASE_URL
555
- );
556
- await synchronizer.synchronizeEnumValues();
557
- };
558
-
559
- run().catch((error) => {
560
- console.error("Error ejecutando sincronización de valores de enum:", error);
561
- process.exit(1);
562
- });
563
- }
1
+ import { Logger } from "@nestjs/common";
2
+ import { Pool } from "pg";
3
+ import * as path from "path";
4
+ import * as fs from "fs";
5
+ import * as dotenv from "dotenv";
6
+
7
+ dotenv.config();
8
+
9
+ /**
10
+ * Clase para sincronizar valores entre enums específicos y enums consolidados
11
+ * Esta herramienta asegura que los valores en los enums específicos (deprecados)
12
+ * sean compatibles con los enums consolidados durante la migración.
13
+ */
14
+ export class EnumValueSynchronizer {
15
+ private readonly logger = new Logger("EnumValueSynchronizer");
16
+ private readonly pool: Pool;
17
+
18
+ // Mapeo de enums específicos a enums consolidados
19
+ private readonly enumMappings: Record<string, string> = {
20
+ enum_credit_requests_status: "enum_transaction_status",
21
+ enum_global_user_transactions_status: "enum_transaction_status",
22
+ enum_global_user_transactions_transaction_type: "enum_transaction_type",
23
+ enum_invoices_status: "enum_transaction_status",
24
+ enum_payin_status: "enum_transaction_status",
25
+ enum_payout_status: "enum_transaction_status",
26
+ enum_pending_references_status: "enum_transaction_status",
27
+ enum_toku_status: "enum_transaction_status",
28
+ enum_transaction_updates_new_status: "enum_transaction_status",
29
+ enum_transactions_account_type: "enum_account_type",
30
+ enum_transactions_status: "enum_transaction_status",
31
+ enum_transactions_transaction_type: "enum_transaction_type",
32
+ enum_transactions_typetransaction: "enum_transaction_type",
33
+ enum_transactions_user_type_account: "enum_account_type",
34
+ enum_transactions_usertypeaccount: "enum_account_type",
35
+ };
36
+
37
+ // Mapeo de valores de texto a valores de enum válidos
38
+ private readonly valueNormalizationMap: Record<
39
+ string,
40
+ Record<string, string>
41
+ > = {
42
+ // Mapeos para enum_transaction_status
43
+ enum_transaction_status: {
44
+ PENDING: "PENDING",
45
+ pending: "PENDING",
46
+ COMPLETED: "COMPLETED",
47
+ completed: "COMPLETED",
48
+ FAILED: "FAILED",
49
+ failed: "FAILED",
50
+ PROCESSING: "PROCESSING",
51
+ processing: "PROCESSING",
52
+ CANCELLED: "CANCELLED",
53
+ cancelled: "CANCELLED",
54
+ canceled: "CANCELLED",
55
+ CANCELED: "CANCELLED",
56
+ REJECTED: "REJECTED",
57
+ rejected: "REJECTED",
58
+ APPROVED: "APPROVED",
59
+ approved: "APPROVED",
60
+ EXPIRED: "EXPIRED",
61
+ expired: "EXPIRED",
62
+ REFUNDED: "REFUNDED",
63
+ refunded: "REFUNDED",
64
+ PARTIAL_REFUND: "PARTIAL_REFUND",
65
+ partial_refund: "PARTIAL_REFUND",
66
+ },
67
+ // Mapeos para enum_transaction_type
68
+ enum_transaction_type: {
69
+ DEPOSIT: "DEPOSIT",
70
+ deposit: "DEPOSIT",
71
+ WITHDRAWAL: "WITHDRAWAL",
72
+ withdrawal: "WITHDRAWAL",
73
+ TRANSFER: "TRANSFER",
74
+ transfer: "TRANSFER",
75
+ PAYMENT: "PAYMENT",
76
+ payment: "PAYMENT",
77
+ REFUND: "REFUND",
78
+ refund: "REFUND",
79
+ FEE: "FEE",
80
+ fee: "FEE",
81
+ ADJUSTMENT: "ADJUSTMENT",
82
+ adjustment: "ADJUSTMENT",
83
+ },
84
+ // Mapeos para enum_account_type
85
+ enum_account_type: {
86
+ PERSONAL: "PERSONAL",
87
+ personal: "PERSONAL",
88
+ BUSINESS: "BUSINESS",
89
+ business: "BUSINESS",
90
+ MERCHANT: "MERCHANT",
91
+ merchant: "MERCHANT",
92
+ PLATFORM: "PLATFORM",
93
+ platform: "PLATFORM",
94
+ },
95
+ };
96
+
97
+ // Caché para valores de enum
98
+ private enumValuesCache: Record<string, string[]> = {};
99
+
100
+ constructor(
101
+ private readonly sourceUrl: string = process.env.SOURCE_DATABASE_URL,
102
+ private readonly targetUrl: string = process.env.DATABASE_URL
103
+ ) {
104
+ this.pool = new Pool({
105
+ connectionString: this.targetUrl,
106
+ });
107
+ }
108
+
109
+ /**
110
+ * Sincroniza los valores entre enums específicos y enums consolidados
111
+ * y normaliza los valores de texto a valores de enum válidos
112
+ */
113
+ async synchronizeEnumValues() {
114
+ try {
115
+ this.logger.log("Iniciando sincronización de valores de enum");
116
+
117
+ // Crear directorio para logs si no existe
118
+ const logsDir = path.join(process.cwd(), "migration-logs");
119
+ if (!fs.existsSync(logsDir)) {
120
+ fs.mkdirSync(logsDir, { recursive: true });
121
+ }
122
+
123
+ // Obtener todos los tipos enum en la base de datos
124
+ const enumsResult = await this.pool.query(`
125
+ SELECT t.typname AS enum_name
126
+ FROM pg_type t
127
+ JOIN pg_namespace n ON t.typnamespace = n.oid
128
+ WHERE t.typtype = 'e' -- enum types
129
+ AND n.nspname = 'public'
130
+ ORDER BY t.typname
131
+ `);
132
+
133
+ this.logger.log(
134
+ `Encontrados ${enumsResult.rows.length} tipos enum en la base de datos`
135
+ );
136
+
137
+ // Procesar cada enum específico y su correspondiente enum consolidado
138
+ for (const [specificEnum, consolidatedEnum] of Object.entries(
139
+ this.enumMappings
140
+ )) {
141
+ await this.synchronizeSpecificEnum(specificEnum, consolidatedEnum);
142
+ }
143
+
144
+ // Después de sincronizar los enums, normalizar los valores en las tablas
145
+ await this.normalizeEnumValuesInTables();
146
+
147
+ this.logger.log("Sincronización de valores de enum completada con éxito");
148
+ } catch (error) {
149
+ this.logger.error(
150
+ `Error durante la sincronización de valores de enum: ${error.message}`,
151
+ error.stack
152
+ );
153
+ throw error;
154
+ } finally {
155
+ await this.pool.end();
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Sincroniza los valores entre un enum específico y su enum consolidado
161
+ */
162
+ private async synchronizeSpecificEnum(
163
+ specificEnum: string,
164
+ consolidatedEnum: string
165
+ ) {
166
+ try {
167
+ this.logger.log(
168
+ `Sincronizando valores entre ${specificEnum} y ${consolidatedEnum}`
169
+ );
170
+
171
+ // Verificar si ambos enums existen
172
+ const specificEnumExists = await this.enumExists(specificEnum);
173
+ const consolidatedEnumExists = await this.enumExists(consolidatedEnum);
174
+
175
+ if (!specificEnumExists || !consolidatedEnumExists) {
176
+ this.logger.warn(
177
+ `No se puede sincronizar: ${
178
+ !specificEnumExists ? specificEnum : consolidatedEnum
179
+ } no existe`
180
+ );
181
+ return;
182
+ }
183
+
184
+ // Obtener valores de ambos enums
185
+ const specificValues = await this.getEnumValues(specificEnum);
186
+ const consolidatedValues = await this.getEnumValues(consolidatedEnum);
187
+
188
+ this.logger.log(
189
+ `Valores en ${specificEnum}: ${specificValues.join(", ")}\n` +
190
+ `Valores en ${consolidatedEnum}: ${consolidatedValues.join(", ")}`
191
+ );
192
+
193
+ // Verificar valores que están en el enum específico pero no en el consolidado
194
+ const missingValues = specificValues.filter(
195
+ (value) => !consolidatedValues.includes(value)
196
+ );
197
+
198
+ if (missingValues.length === 0) {
199
+ this.logger.log(
200
+ `No hay valores faltantes entre ${specificEnum} y ${consolidatedEnum}`
201
+ );
202
+ return;
203
+ }
204
+
205
+ this.logger.log(
206
+ `Encontrados ${
207
+ missingValues.length
208
+ } valores en ${specificEnum} que faltan en ${consolidatedEnum}: ${missingValues.join(
209
+ ", "
210
+ )}`
211
+ );
212
+
213
+ // Agregar los valores faltantes al enum consolidado
214
+ for (const value of missingValues) {
215
+ await this.addValueToEnum(consolidatedEnum, value);
216
+ }
217
+
218
+ this.logger.log(
219
+ `Sincronización entre ${specificEnum} y ${consolidatedEnum} completada`
220
+ );
221
+ } catch (error) {
222
+ this.logger.error(
223
+ `Error sincronizando ${specificEnum} con ${consolidatedEnum}: ${error.message}`
224
+ );
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Verifica si un enum existe en la base de datos
230
+ */
231
+ private async enumExists(enumName: string): Promise<boolean> {
232
+ const result = await this.pool.query(
233
+ `
234
+ SELECT 1
235
+ FROM pg_type t
236
+ JOIN pg_namespace n ON t.typnamespace = n.oid
237
+ WHERE t.typname = $1
238
+ AND t.typtype = 'e' -- enum types
239
+ AND n.nspname = 'public'
240
+ `,
241
+ [enumName]
242
+ );
243
+
244
+ return result.rows.length > 0;
245
+ }
246
+
247
+ /**
248
+ * Obtiene los valores de un enum
249
+ */
250
+ private async getEnumValues(enumName: string): Promise<string[]> {
251
+ const result = await this.pool.query(
252
+ `
253
+ SELECT e.enumlabel
254
+ FROM pg_enum e
255
+ JOIN pg_type t ON e.enumtypid = t.oid
256
+ JOIN pg_namespace n ON t.typnamespace = n.oid
257
+ WHERE t.typname = $1
258
+ AND n.nspname = 'public'
259
+ ORDER BY e.enumsortorder
260
+ `,
261
+ [enumName]
262
+ );
263
+
264
+ return result.rows.map((row) => row.enumlabel);
265
+ }
266
+
267
+ /**
268
+ * Agrega un valor a un enum existente
269
+ */
270
+ private async addValueToEnum(enumName: string, value: string) {
271
+ try {
272
+ // Escapar comillas simples en el valor
273
+ const escapedValue = value.replace(/'/g, "''");
274
+
275
+ // Agregar el valor al enum
276
+ await this.pool.query(`
277
+ ALTER TYPE ${enumName} ADD VALUE IF NOT EXISTS '${escapedValue}'
278
+ `);
279
+
280
+ // Actualizar la caché de valores de enum
281
+ if (this.enumValuesCache[enumName]) {
282
+ if (!this.enumValuesCache[enumName].includes(value)) {
283
+ this.enumValuesCache[enumName].push(value);
284
+ }
285
+ }
286
+
287
+ this.logger.log(`Valor '${value}' agregado al enum ${enumName}`);
288
+ } catch (error) {
289
+ this.logger.error(
290
+ `Error agregando valor '${value}' al enum ${enumName}: ${error.message}`
291
+ );
292
+ throw error;
293
+ }
294
+ }
295
+
296
+ /**
297
+ * Normaliza los valores de enum en todas las tablas
298
+ */
299
+ private async normalizeEnumValuesInTables() {
300
+ this.logger.log("Iniciando normalización de valores de enum en tablas");
301
+
302
+ try {
303
+ // Obtener todos los schemas excepto los del sistema
304
+ const schemas = await this.getSchemas();
305
+ this.logger.log(`Encontrados ${schemas.length} schemas para procesar`);
306
+
307
+ // Para cada schema, normalizar los valores de enum en sus tablas
308
+ for (const schema of schemas) {
309
+ await this.normalizeEnumValuesInSchema(schema);
310
+ }
311
+
312
+ this.logger.log("Normalización de valores de enum en tablas completada");
313
+ } catch (error) {
314
+ this.logger.error(
315
+ `Error durante la normalización de valores de enum: ${error.message}`,
316
+ error.stack
317
+ );
318
+ }
319
+ }
320
+
321
+ /**
322
+ * Obtiene todos los schemas de la base de datos excepto los del sistema
323
+ */
324
+ private async getSchemas(): Promise<string[]> {
325
+ const result = await this.pool.query(`
326
+ SELECT schema_name
327
+ FROM information_schema.schemata
328
+ WHERE schema_name NOT IN ('information_schema', 'pg_catalog', 'pg_toast')
329
+ AND schema_name NOT LIKE 'pg_%'
330
+ `);
331
+
332
+ return result.rows.map((row) => row.schema_name);
333
+ }
334
+
335
+ /**
336
+ * Normaliza los valores de enum en un schema específico
337
+ */
338
+ private async normalizeEnumValuesInSchema(schema: string) {
339
+ this.logger.log(`Procesando valores de enum en schema: ${schema}`);
340
+
341
+ try {
342
+ // Obtener todas las columnas enum en este schema
343
+ const enumColumnsQuery = `
344
+ SELECT
345
+ c.table_name,
346
+ c.column_name,
347
+ c.udt_name
348
+ FROM
349
+ information_schema.columns c
350
+ WHERE
351
+ c.table_schema = $1
352
+ AND c.data_type = 'USER-DEFINED'
353
+ AND c.udt_name LIKE 'enum_%'
354
+ `;
355
+
356
+ const enumColumnsResult = await this.pool.query(enumColumnsQuery, [
357
+ schema,
358
+ ]);
359
+
360
+ if (enumColumnsResult.rows.length === 0) {
361
+ this.logger.log(`No se encontraron columnas enum en schema: ${schema}`);
362
+ return;
363
+ }
364
+
365
+ this.logger.log(
366
+ `Encontradas ${enumColumnsResult.rows.length} columnas enum en schema: ${schema}`
367
+ );
368
+
369
+ // Procesar cada columna enum
370
+ for (const row of enumColumnsResult.rows) {
371
+ const tableName = row.table_name;
372
+ const columnName = row.column_name;
373
+ const enumTypeName = row.udt_name;
374
+
375
+ await this.normalizeEnumColumn(
376
+ schema,
377
+ tableName,
378
+ columnName,
379
+ enumTypeName
380
+ );
381
+ }
382
+
383
+ this.logger.log(
384
+ `Completado procesamiento de valores enum para schema: ${schema}`
385
+ );
386
+ } catch (error) {
387
+ this.logger.error(
388
+ `Error procesando valores enum para schema ${schema}: ${error.message}`,
389
+ error.stack
390
+ );
391
+ }
392
+ }
393
+
394
+ /**
395
+ * Normaliza los valores en una columna enum específica
396
+ */
397
+ private async normalizeEnumColumn(
398
+ schema: string,
399
+ tableName: string,
400
+ columnName: string,
401
+ enumTypeName: string
402
+ ) {
403
+ this.logger.log(
404
+ `Normalizando columna enum ${columnName} (${enumTypeName}) en tabla ${schema}.${tableName}`
405
+ );
406
+
407
+ try {
408
+ // Obtener los valores válidos para el enum si no están en caché
409
+ if (!this.enumValuesCache[enumTypeName]) {
410
+ this.enumValuesCache[enumTypeName] = await this.getEnumValues(
411
+ enumTypeName
412
+ );
413
+ }
414
+ const validEnumValues = this.enumValuesCache[enumTypeName];
415
+
416
+ if (validEnumValues.length === 0) {
417
+ this.logger.warn(
418
+ `No se encontraron valores para el enum ${enumTypeName}. Omitiendo.`
419
+ );
420
+ return;
421
+ }
422
+
423
+ // Obtener valores actuales en la columna
424
+ const currentValuesQuery = `
425
+ SELECT DISTINCT "${columnName}"
426
+ FROM "${schema}"."${tableName}"
427
+ WHERE "${columnName}" IS NOT NULL
428
+ `;
429
+
430
+ const currentValuesResult = await this.pool.query(currentValuesQuery);
431
+ const currentValues = currentValuesResult.rows.map((r) => r[columnName]);
432
+
433
+ // Encontrar valores inválidos
434
+ const invalidValues = currentValues.filter(
435
+ (val) => !validEnumValues.includes(val)
436
+ );
437
+
438
+ if (invalidValues.length === 0) {
439
+ this.logger.log(
440
+ `No se encontraron valores inválidos para columna enum ${columnName} en tabla ${schema}.${tableName}`
441
+ );
442
+ return;
443
+ }
444
+
445
+ this.logger.log(
446
+ `Encontrados ${
447
+ invalidValues.length
448
+ } valores inválidos para enum ${enumTypeName} en ${schema}.${tableName}: ${invalidValues.join(
449
+ ", "
450
+ )}`
451
+ );
452
+
453
+ // Para cada valor inválido, intentar normalizarlo
454
+ for (const invalidValue of invalidValues) {
455
+ const normalizedValue = this.getNormalizedEnumValue(
456
+ enumTypeName,
457
+ invalidValue
458
+ );
459
+
460
+ if (normalizedValue && normalizedValue !== invalidValue) {
461
+ // Actualizar los registros con el valor normalizado
462
+ const updateQuery = `
463
+ UPDATE "${schema}"."${tableName}"
464
+ SET "${columnName}" = $1
465
+ WHERE "${columnName}" = $2
466
+ `;
467
+
468
+ const result = await this.pool.query(updateQuery, [
469
+ normalizedValue,
470
+ invalidValue,
471
+ ]);
472
+
473
+ this.logger.log(
474
+ `Actualizado valor '${invalidValue}' a '${normalizedValue}' en ${result.rowCount} filas de ${schema}.${tableName}.${columnName}`
475
+ );
476
+
477
+ // Si el valor normalizado no está en el enum, agregarlo
478
+ if (!validEnumValues.includes(normalizedValue)) {
479
+ await this.addValueToEnum(enumTypeName, normalizedValue);
480
+ }
481
+ } else if (!normalizedValue) {
482
+ // Si no se puede normalizar, establecer a NULL
483
+ const updateQuery = `
484
+ UPDATE "${schema}"."${tableName}"
485
+ SET "${columnName}" = NULL
486
+ WHERE "${columnName}" = $1
487
+ `;
488
+
489
+ const result = await this.pool.query(updateQuery, [invalidValue]);
490
+
491
+ this.logger.log(
492
+ `Establecido valor '${invalidValue}' a NULL en ${result.rowCount} filas de ${schema}.${tableName}.${columnName}`
493
+ );
494
+ }
495
+ }
496
+ } catch (error) {
497
+ this.logger.error(
498
+ `Error normalizando columna enum ${columnName} en ${schema}.${tableName}: ${error.message}`,
499
+ error.stack
500
+ );
501
+ }
502
+ }
503
+
504
+ /**
505
+ * Obtiene un valor de enum normalizado basado en mapeos predefinidos
506
+ */
507
+ private getNormalizedEnumValue(
508
+ enumTypeName: string,
509
+ value: string
510
+ ): string | null {
511
+ // Si el valor ya es válido, devolverlo tal cual
512
+ if (this.enumValuesCache[enumTypeName]?.includes(value)) {
513
+ return value;
514
+ }
515
+
516
+ // Buscar en el mapa de normalización
517
+ if (
518
+ this.valueNormalizationMap[enumTypeName] &&
519
+ this.valueNormalizationMap[enumTypeName][value]
520
+ ) {
521
+ return this.valueNormalizationMap[enumTypeName][value];
522
+ }
523
+
524
+ // Intentar normalizar basado en similitud (convertir a mayúsculas, quitar espacios, etc.)
525
+ const normalizedInput = value.toUpperCase().replace(/\s+/g, "_");
526
+
527
+ // Verificar si la versión normalizada existe en el mapa
528
+ if (
529
+ this.valueNormalizationMap[enumTypeName] &&
530
+ this.valueNormalizationMap[enumTypeName][normalizedInput]
531
+ ) {
532
+ return this.valueNormalizationMap[enumTypeName][normalizedInput];
533
+ }
534
+
535
+ // Buscar coincidencias parciales en los valores válidos
536
+ const validValues = this.enumValuesCache[enumTypeName] || [];
537
+ for (const validValue of validValues) {
538
+ // Comparar ignorando mayúsculas/minúsculas
539
+ if (validValue.toLowerCase() === value.toLowerCase()) {
540
+ return validValue;
541
+ }
542
+ }
543
+
544
+ // Si no se encuentra coincidencia, devolver null
545
+ return null;
546
+ }
547
+ }
548
+
549
+ // Ejecutar el script si se llama directamente
550
+ if (require.main === module) {
551
+ const run = async () => {
552
+ const synchronizer = new EnumValueSynchronizer(
553
+ process.env.SOURCE_DATABASE_URL,
554
+ process.env.DATABASE_URL
555
+ );
556
+ await synchronizer.synchronizeEnumValues();
557
+ };
558
+
559
+ run().catch((error) => {
560
+ console.error("Error ejecutando sincronización de valores de enum:", error);
561
+ process.exit(1);
562
+ });
563
+ }