@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.
- package/.github/CODEOWNERS +1 -0
- package/README.md +269 -269
- package/dist/index.d.ts +1 -1
- package/dist/prisma.service.d.ts +1 -1
- package/dist/scripts/add-uuid-to-table.js +32 -32
- package/dist/scripts/data-migration/batch-migrator.js +12 -12
- package/dist/scripts/data-migration/data-transformer.js +14 -14
- package/dist/scripts/data-migration/dependency-resolver.js +23 -23
- package/dist/scripts/data-migration/entity-discovery.js +68 -68
- package/dist/scripts/data-migration/foreign-key-manager.js +23 -23
- package/dist/scripts/data-migration/migration-tool.js +5 -5
- package/dist/scripts/data-migration/schema-utils.js +74 -74
- package/dist/scripts/data-migration/typecast-manager.js +4 -4
- package/dist/scripts/database-initializer.js +5 -5
- package/dist/scripts/drop-database.js +5 -5
- package/dist/scripts/fix-data-types.js +53 -53
- package/dist/scripts/fix-enum-values.js +34 -34
- package/dist/scripts/fix-schema-discrepancies.js +40 -40
- package/dist/scripts/fix-table-indexes.js +81 -81
- package/dist/scripts/migrate-schema-structure.js +4 -4
- package/dist/scripts/migrate-uuid.js +19 -19
- package/dist/scripts/post-migration-validator.js +49 -49
- package/dist/scripts/pre-migration-validator.js +107 -107
- package/dist/scripts/reset-database.js +21 -21
- package/dist/scripts/retry-failed-migrations.js +28 -28
- package/dist/scripts/run-migration.js +5 -5
- package/dist/scripts/schema-sync.js +18 -18
- package/dist/scripts/sequence-sync-cli.js +55 -55
- package/dist/scripts/sequence-synchronizer.js +20 -20
- package/dist/scripts/sync-enum-types.js +30 -30
- package/dist/scripts/sync-enum-values.js +52 -52
- package/dist/scripts/truncate-database.js +10 -10
- package/dist/scripts/verify-migration-setup.js +10 -10
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/migration-config.json +63 -63
- package/migration-config.json.bk +95 -95
- package/migrations/add_reserved_amount.sql +8 -0
- package/package.json +44 -44
- package/prisma/migrations/add_uuid_to_transactions.sql +13 -13
- package/prisma/schema.prisma +601 -554
- package/src/index.ts +23 -23
- package/src/prisma-factory.service.ts +40 -40
- package/src/prisma.module.ts +9 -9
- package/src/prisma.service.ts +16 -16
- package/src/scripts/add-uuid-to-table.ts +138 -138
- package/src/scripts/create-tenant-schemas.ts +145 -145
- package/src/scripts/data-migration/batch-migrator.ts +248 -248
- package/src/scripts/data-migration/data-transformer.ts +426 -426
- package/src/scripts/data-migration/db-connector.ts +120 -120
- package/src/scripts/data-migration/dependency-resolver.ts +174 -174
- package/src/scripts/data-migration/entity-discovery.ts +196 -196
- package/src/scripts/data-migration/foreign-key-manager.ts +277 -277
- package/src/scripts/data-migration/migration-config.json +63 -63
- package/src/scripts/data-migration/migration-tool.ts +509 -509
- package/src/scripts/data-migration/schema-utils.ts +248 -248
- package/src/scripts/data-migration/tenant-migrator.ts +201 -201
- package/src/scripts/data-migration/typecast-manager.ts +193 -193
- package/src/scripts/data-migration/types.ts +113 -113
- package/src/scripts/database-initializer.ts +49 -49
- package/src/scripts/drop-database.ts +104 -104
- package/src/scripts/dump-source-db.sh +61 -61
- package/src/scripts/encrypt-user-passwords.ts +36 -36
- package/src/scripts/error-handler.ts +117 -117
- package/src/scripts/fix-data-types.ts +241 -241
- package/src/scripts/fix-enum-values.ts +357 -357
- package/src/scripts/fix-schema-discrepancies.ts +317 -317
- package/src/scripts/fix-table-indexes.ts +601 -601
- package/src/scripts/migrate-schema-structure.ts +90 -90
- package/src/scripts/migrate-uuid.ts +76 -76
- package/src/scripts/post-migration-validator.ts +526 -526
- package/src/scripts/pre-migration-validator.ts +610 -610
- package/src/scripts/reset-database.ts +263 -263
- package/src/scripts/retry-failed-migrations.ts +416 -416
- package/src/scripts/run-migration.ts +707 -707
- package/src/scripts/schema-sync.ts +128 -128
- package/src/scripts/sequence-sync-cli.ts +416 -416
- package/src/scripts/sequence-synchronizer.ts +127 -127
- package/src/scripts/sync-enum-types.ts +170 -170
- package/src/scripts/sync-enum-values.ts +563 -563
- package/src/scripts/truncate-database.ts +123 -123
- package/src/scripts/verify-migration-setup.ts +135 -135
- package/tsconfig.json +17 -17
- package/dist/scripts/data-migration/dependency-manager.d.ts +0 -9
- package/dist/scripts/data-migration/dependency-manager.js +0 -86
- package/dist/scripts/data-migration/dependency-manager.js.map +0 -1
- package/dist/scripts/data-migration/migration-config.json +0 -63
- package/dist/scripts/data-migration/migration-phases.d.ts +0 -5
- package/dist/scripts/data-migration/migration-phases.js +0 -55
- package/dist/scripts/data-migration/migration-phases.js.map +0 -1
- package/dist/scripts/data-migration/multi-source-migrator.d.ts +0 -17
- package/dist/scripts/data-migration/multi-source-migrator.js +0 -130
- package/dist/scripts/data-migration/multi-source-migrator.js.map +0 -1
- package/dist/scripts/data-migration/phase-generator.d.ts +0 -15
- package/dist/scripts/data-migration/phase-generator.js +0 -187
- package/dist/scripts/data-migration/phase-generator.js.map +0 -1
- package/dist/scripts/data-migration.d.ts +0 -22
- package/dist/scripts/data-migration.js +0 -593
- package/dist/scripts/data-migration.js.map +0 -1
- package/dist/scripts/multi-db-migration.d.ts +0 -1
- package/dist/scripts/multi-db-migration.js +0 -55
- package/dist/scripts/multi-db-migration.js.map +0 -1
|
@@ -1,357 +1,357 @@
|
|
|
1
|
-
import * as pg from "pg";
|
|
2
|
-
import * as dotenv from "dotenv";
|
|
3
|
-
import { Logger } from "@nestjs/common";
|
|
4
|
-
import * as fs from "fs";
|
|
5
|
-
import * as path from "path";
|
|
6
|
-
|
|
7
|
-
dotenv.config();
|
|
8
|
-
|
|
9
|
-
export class EnumFixer {
|
|
10
|
-
private readonly logger = new Logger("EnumFixer");
|
|
11
|
-
private readonly pool: pg.Pool;
|
|
12
|
-
private enumValuesCache: Record<string, string[]> = {};
|
|
13
|
-
// Fix the type definition to match how it's actually used
|
|
14
|
-
private mappingsCache: Record<string, string | null> = {};
|
|
15
|
-
private readonly mappingsFilePath: string;
|
|
16
|
-
|
|
17
|
-
constructor(
|
|
18
|
-
private readonly databaseUrl: string = process.env.DATABASE_URL,
|
|
19
|
-
private readonly saveMapping: boolean = true
|
|
20
|
-
) {
|
|
21
|
-
this.pool = new pg.Pool({
|
|
22
|
-
connectionString: this.databaseUrl,
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
// Crear directorio para mappings si no existe
|
|
26
|
-
const mappingsDir = path.join(process.cwd(), "migration-logs");
|
|
27
|
-
if (!fs.existsSync(mappingsDir)) {
|
|
28
|
-
fs.mkdirSync(mappingsDir, { recursive: true });
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Archivo para guardar los mappings
|
|
32
|
-
const timestamp = new Date()
|
|
33
|
-
.toISOString()
|
|
34
|
-
.replace(/:/g, "-")
|
|
35
|
-
.replace(/\..+/, "");
|
|
36
|
-
this.mappingsFilePath = path.join(
|
|
37
|
-
mappingsDir,
|
|
38
|
-
`enum-mappings-${timestamp}.json`
|
|
39
|
-
);
|
|
40
|
-
|
|
41
|
-
// Inicializar archivo de mappings
|
|
42
|
-
if (this.saveMapping) {
|
|
43
|
-
this.saveMappings();
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async fixEnumValues() {
|
|
48
|
-
try {
|
|
49
|
-
this.logger.log("Starting enum values fixing process");
|
|
50
|
-
|
|
51
|
-
// Obtener todos los schemas excepto los del sistema
|
|
52
|
-
const schemas = await this.getSchemas();
|
|
53
|
-
this.logger.log(`Found ${schemas.length} schemas to process`);
|
|
54
|
-
|
|
55
|
-
// Para cada schema, procesar sus campos enum
|
|
56
|
-
for (const schema of schemas) {
|
|
57
|
-
await this.fixEnumValuesForSchema(schema);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Guardar los mappings finales
|
|
61
|
-
if (this.saveMapping) {
|
|
62
|
-
this.saveMappings();
|
|
63
|
-
this.logger.log(`Enum mappings saved to: ${this.mappingsFilePath}`);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
this.logger.log("Enum values fixing completed successfully");
|
|
67
|
-
} catch (error) {
|
|
68
|
-
this.logger.error(
|
|
69
|
-
`Error during enum values fixing: ${error.message}`,
|
|
70
|
-
error.stack
|
|
71
|
-
);
|
|
72
|
-
} finally {
|
|
73
|
-
await this.cleanup();
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
private async getSchemas(): Promise<string[]> {
|
|
78
|
-
const result = await this.pool.query(`
|
|
79
|
-
SELECT schema_name
|
|
80
|
-
FROM information_schema.schemata
|
|
81
|
-
WHERE schema_name NOT IN ('information_schema', 'pg_catalog', 'pg_toast')
|
|
82
|
-
AND schema_name NOT LIKE 'pg_%'
|
|
83
|
-
`);
|
|
84
|
-
|
|
85
|
-
return result.rows.map((row) => row.schema_name);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
private async fixEnumValuesForSchema(schema: string) {
|
|
89
|
-
this.logger.log(`Processing enum fields for schema: ${schema}`);
|
|
90
|
-
|
|
91
|
-
try {
|
|
92
|
-
// Obtener todas las columnas enum en este schema
|
|
93
|
-
const enumColumnsQuery = `
|
|
94
|
-
SELECT
|
|
95
|
-
c.table_name,
|
|
96
|
-
c.column_name,
|
|
97
|
-
c.udt_name
|
|
98
|
-
FROM
|
|
99
|
-
information_schema.columns c
|
|
100
|
-
WHERE
|
|
101
|
-
c.table_schema = $1
|
|
102
|
-
AND c.data_type = 'USER-DEFINED'
|
|
103
|
-
AND c.udt_name LIKE 'enum_%'
|
|
104
|
-
`;
|
|
105
|
-
|
|
106
|
-
const enumColumnsResult = await this.pool.query(enumColumnsQuery, [
|
|
107
|
-
schema,
|
|
108
|
-
]);
|
|
109
|
-
|
|
110
|
-
if (enumColumnsResult.rows.length === 0) {
|
|
111
|
-
this.logger.log(`No enum fields found in schema: ${schema}`);
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
this.logger.log(
|
|
116
|
-
`Found ${enumColumnsResult.rows.length} enum fields in schema: ${schema}`
|
|
117
|
-
);
|
|
118
|
-
|
|
119
|
-
// Procesar cada columna enum
|
|
120
|
-
for (const row of enumColumnsResult.rows) {
|
|
121
|
-
const tableName = row.table_name;
|
|
122
|
-
const columnName = row.column_name;
|
|
123
|
-
const enumTypeName = row.udt_name;
|
|
124
|
-
|
|
125
|
-
await this.fixEnumColumn(schema, tableName, columnName, enumTypeName);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
this.logger.log(`Completed processing enum fields for schema: ${schema}`);
|
|
129
|
-
} catch (error) {
|
|
130
|
-
this.logger.error(
|
|
131
|
-
`Error processing enum fields for schema ${schema}: ${error.message}`,
|
|
132
|
-
error.stack
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
private async fixEnumColumn(
|
|
138
|
-
schema: string,
|
|
139
|
-
tableName: string,
|
|
140
|
-
columnName: string,
|
|
141
|
-
enumTypeName: string
|
|
142
|
-
) {
|
|
143
|
-
this.logger.log(
|
|
144
|
-
`Fixing enum column ${columnName} (${enumTypeName}) in table ${schema}.${tableName}`
|
|
145
|
-
);
|
|
146
|
-
|
|
147
|
-
try {
|
|
148
|
-
// Obtener los valores válidos para el enum
|
|
149
|
-
let validEnumValues: string[];
|
|
150
|
-
if (this.enumValuesCache[enumTypeName]) {
|
|
151
|
-
validEnumValues = this.enumValuesCache[enumTypeName];
|
|
152
|
-
} else {
|
|
153
|
-
validEnumValues = await this.getEnumValues(enumTypeName);
|
|
154
|
-
this.enumValuesCache[enumTypeName] = validEnumValues;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (validEnumValues.length === 0) {
|
|
158
|
-
this.logger.warn(
|
|
159
|
-
`No enum values found for type ${enumTypeName}. Skipping.`
|
|
160
|
-
);
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
this.logger.log(
|
|
165
|
-
`Valid values for ${enumTypeName}: ${validEnumValues.join(", ")}`
|
|
166
|
-
);
|
|
167
|
-
|
|
168
|
-
// Obtener valores actuales en la columna
|
|
169
|
-
const currentValuesQuery = `
|
|
170
|
-
SELECT DISTINCT "${columnName}"
|
|
171
|
-
FROM "${schema}"."${tableName}"
|
|
172
|
-
WHERE "${columnName}" IS NOT NULL
|
|
173
|
-
`;
|
|
174
|
-
|
|
175
|
-
const currentValuesResult = await this.pool.query(currentValuesQuery);
|
|
176
|
-
const currentValues = currentValuesResult.rows.map((r) => r[columnName]);
|
|
177
|
-
|
|
178
|
-
// Encontrar valores inválidos
|
|
179
|
-
const invalidValues = currentValues.filter(
|
|
180
|
-
(val) => !validEnumValues.includes(val)
|
|
181
|
-
);
|
|
182
|
-
|
|
183
|
-
if (invalidValues.length === 0) {
|
|
184
|
-
this.logger.log(
|
|
185
|
-
`No invalid values found for enum column ${columnName} in table ${schema}.${tableName}`
|
|
186
|
-
);
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
this.logger.log(
|
|
191
|
-
`Found ${
|
|
192
|
-
invalidValues.length
|
|
193
|
-
} invalid values for enum ${enumTypeName} in ${schema}.${tableName}: ${invalidValues.join(
|
|
194
|
-
", "
|
|
195
|
-
)}`
|
|
196
|
-
);
|
|
197
|
-
|
|
198
|
-
// Para cada valor inválido, intentar mapearlo a uno válido o establecerlo a NULL
|
|
199
|
-
for (const invalidValue of invalidValues) {
|
|
200
|
-
// Intentar encontrar un mapping existente
|
|
201
|
-
const mappingKey = `${enumTypeName}:${invalidValue}`;
|
|
202
|
-
let mappedValue: string | null = null;
|
|
203
|
-
|
|
204
|
-
if (this.mappingsCache[mappingKey]) {
|
|
205
|
-
mappedValue = this.mappingsCache[mappingKey];
|
|
206
|
-
this.logger.log(
|
|
207
|
-
`Using cached mapping for ${invalidValue} -> ${mappedValue}`
|
|
208
|
-
);
|
|
209
|
-
} else {
|
|
210
|
-
// Intentar encontrar una coincidencia sin distinción de mayúsculas/minúsculas
|
|
211
|
-
const matchingValidValue = validEnumValues.find(
|
|
212
|
-
(v) => v.toLowerCase() === invalidValue.toLowerCase()
|
|
213
|
-
);
|
|
214
|
-
|
|
215
|
-
if (matchingValidValue) {
|
|
216
|
-
mappedValue = matchingValidValue;
|
|
217
|
-
this.logger.log(
|
|
218
|
-
`Found case-insensitive match for ${invalidValue} -> ${mappedValue}`
|
|
219
|
-
);
|
|
220
|
-
} else {
|
|
221
|
-
// Intentar encontrar una coincidencia parcial
|
|
222
|
-
const similarValues = validEnumValues.filter(
|
|
223
|
-
(v) =>
|
|
224
|
-
v.toLowerCase().includes(invalidValue.toLowerCase()) ||
|
|
225
|
-
invalidValue.toLowerCase().includes(v.toLowerCase())
|
|
226
|
-
);
|
|
227
|
-
|
|
228
|
-
if (similarValues.length === 1) {
|
|
229
|
-
mappedValue = similarValues[0];
|
|
230
|
-
this.logger.log(
|
|
231
|
-
`Found partial match for ${invalidValue} -> ${mappedValue}`
|
|
232
|
-
);
|
|
233
|
-
} else if (similarValues.length > 1) {
|
|
234
|
-
// Si hay múltiples coincidencias, usar la más cercana por longitud
|
|
235
|
-
mappedValue = similarValues.reduce((prev, curr) =>
|
|
236
|
-
Math.abs(curr.length - invalidValue.length) <
|
|
237
|
-
Math.abs(prev.length - invalidValue.length)
|
|
238
|
-
? curr
|
|
239
|
-
: prev
|
|
240
|
-
);
|
|
241
|
-
this.logger.log(
|
|
242
|
-
`Found multiple partial matches for ${invalidValue}, using closest: ${mappedValue}`
|
|
243
|
-
);
|
|
244
|
-
} else {
|
|
245
|
-
// Si no hay coincidencias, usar NULL
|
|
246
|
-
mappedValue = null;
|
|
247
|
-
this.logger.log(
|
|
248
|
-
`No match found for ${invalidValue}, will set to NULL`
|
|
249
|
-
);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// Guardar el mapping para uso futuro
|
|
254
|
-
this.mappingsCache[mappingKey] = mappedValue;
|
|
255
|
-
if (this.saveMapping) {
|
|
256
|
-
this.saveMappings();
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// Actualizar los registros con el valor mapeado usando CAST explícito
|
|
261
|
-
const updateQuery =
|
|
262
|
-
mappedValue !== null
|
|
263
|
-
? `
|
|
264
|
-
UPDATE "${schema}"."${tableName}"
|
|
265
|
-
SET "${columnName}" = CAST($1 AS "${enumTypeName}")
|
|
266
|
-
WHERE "${columnName}" = $2
|
|
267
|
-
`
|
|
268
|
-
: `
|
|
269
|
-
UPDATE "${schema}"."${tableName}"
|
|
270
|
-
SET "${columnName}" = NULL
|
|
271
|
-
WHERE "${columnName}" = $1
|
|
272
|
-
`;
|
|
273
|
-
|
|
274
|
-
const updateParams =
|
|
275
|
-
mappedValue !== null ? [mappedValue, invalidValue] : [invalidValue];
|
|
276
|
-
|
|
277
|
-
const updateResult = await this.pool.query(updateQuery, updateParams);
|
|
278
|
-
|
|
279
|
-
this.logger.log(
|
|
280
|
-
`Updated ${
|
|
281
|
-
updateResult.rowCount
|
|
282
|
-
} rows in ${schema}.${tableName} with ${invalidValue} -> ${
|
|
283
|
-
mappedValue || "NULL"
|
|
284
|
-
} with explicit CAST`
|
|
285
|
-
);
|
|
286
|
-
}
|
|
287
|
-
} catch (error) {
|
|
288
|
-
this.logger.error(
|
|
289
|
-
`Error fixing enum column ${columnName} in table ${schema}.${tableName}: ${error.message}`,
|
|
290
|
-
error.stack
|
|
291
|
-
);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
private async getEnumValues(enumTypeName: string): Promise<string[]> {
|
|
296
|
-
try {
|
|
297
|
-
const enumQuery = `
|
|
298
|
-
SELECT e.enumlabel
|
|
299
|
-
FROM pg_enum e
|
|
300
|
-
JOIN pg_type t ON e.enumtypid = t.oid
|
|
301
|
-
WHERE t.typname = $1
|
|
302
|
-
ORDER BY e.enumsortorder
|
|
303
|
-
`;
|
|
304
|
-
|
|
305
|
-
const result = await this.pool.query(enumQuery, [enumTypeName]);
|
|
306
|
-
return result.rows.map((row) => row.enumlabel);
|
|
307
|
-
} catch (error) {
|
|
308
|
-
this.logger.error(
|
|
309
|
-
`Error getting enum values for ${enumTypeName}: ${error.message}`
|
|
310
|
-
);
|
|
311
|
-
return [];
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
private saveMappings(): void {
|
|
316
|
-
try {
|
|
317
|
-
fs.writeFileSync(
|
|
318
|
-
this.mappingsFilePath,
|
|
319
|
-
JSON.stringify(this.mappingsCache, null, 2),
|
|
320
|
-
"utf8"
|
|
321
|
-
);
|
|
322
|
-
} catch (error) {
|
|
323
|
-
this.logger.error(`Error saving enum mappings: ${error.message}`);
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
private async cleanup() {
|
|
328
|
-
this.logger.log("Cleaning up database connections");
|
|
329
|
-
await this.pool.end();
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// Script para ejecutar desde línea de comandos
|
|
334
|
-
if (require.main === module) {
|
|
335
|
-
const run = async () => {
|
|
336
|
-
try {
|
|
337
|
-
// Obtener la URL de la base de datos desde los argumentos o .env
|
|
338
|
-
const databaseUrl = process.argv[2] || process.env.DATABASE_URL;
|
|
339
|
-
|
|
340
|
-
if (!databaseUrl) {
|
|
341
|
-
console.error(
|
|
342
|
-
"Error: No database URL provided. Please provide a database URL as an argument or set DATABASE_URL environment variable."
|
|
343
|
-
);
|
|
344
|
-
process.exit(1);
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
const enumFixer = new EnumFixer(databaseUrl);
|
|
348
|
-
await enumFixer.fixEnumValues();
|
|
349
|
-
process.exit(0);
|
|
350
|
-
} catch (error) {
|
|
351
|
-
console.error("Error:", error.message);
|
|
352
|
-
process.exit(1);
|
|
353
|
-
}
|
|
354
|
-
};
|
|
355
|
-
|
|
356
|
-
run();
|
|
357
|
-
}
|
|
1
|
+
import * as pg from "pg";
|
|
2
|
+
import * as dotenv from "dotenv";
|
|
3
|
+
import { Logger } from "@nestjs/common";
|
|
4
|
+
import * as fs from "fs";
|
|
5
|
+
import * as path from "path";
|
|
6
|
+
|
|
7
|
+
dotenv.config();
|
|
8
|
+
|
|
9
|
+
export class EnumFixer {
|
|
10
|
+
private readonly logger = new Logger("EnumFixer");
|
|
11
|
+
private readonly pool: pg.Pool;
|
|
12
|
+
private enumValuesCache: Record<string, string[]> = {};
|
|
13
|
+
// Fix the type definition to match how it's actually used
|
|
14
|
+
private mappingsCache: Record<string, string | null> = {};
|
|
15
|
+
private readonly mappingsFilePath: string;
|
|
16
|
+
|
|
17
|
+
constructor(
|
|
18
|
+
private readonly databaseUrl: string = process.env.DATABASE_URL,
|
|
19
|
+
private readonly saveMapping: boolean = true
|
|
20
|
+
) {
|
|
21
|
+
this.pool = new pg.Pool({
|
|
22
|
+
connectionString: this.databaseUrl,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Crear directorio para mappings si no existe
|
|
26
|
+
const mappingsDir = path.join(process.cwd(), "migration-logs");
|
|
27
|
+
if (!fs.existsSync(mappingsDir)) {
|
|
28
|
+
fs.mkdirSync(mappingsDir, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Archivo para guardar los mappings
|
|
32
|
+
const timestamp = new Date()
|
|
33
|
+
.toISOString()
|
|
34
|
+
.replace(/:/g, "-")
|
|
35
|
+
.replace(/\..+/, "");
|
|
36
|
+
this.mappingsFilePath = path.join(
|
|
37
|
+
mappingsDir,
|
|
38
|
+
`enum-mappings-${timestamp}.json`
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
// Inicializar archivo de mappings
|
|
42
|
+
if (this.saveMapping) {
|
|
43
|
+
this.saveMappings();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async fixEnumValues() {
|
|
48
|
+
try {
|
|
49
|
+
this.logger.log("Starting enum values fixing process");
|
|
50
|
+
|
|
51
|
+
// Obtener todos los schemas excepto los del sistema
|
|
52
|
+
const schemas = await this.getSchemas();
|
|
53
|
+
this.logger.log(`Found ${schemas.length} schemas to process`);
|
|
54
|
+
|
|
55
|
+
// Para cada schema, procesar sus campos enum
|
|
56
|
+
for (const schema of schemas) {
|
|
57
|
+
await this.fixEnumValuesForSchema(schema);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Guardar los mappings finales
|
|
61
|
+
if (this.saveMapping) {
|
|
62
|
+
this.saveMappings();
|
|
63
|
+
this.logger.log(`Enum mappings saved to: ${this.mappingsFilePath}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
this.logger.log("Enum values fixing completed successfully");
|
|
67
|
+
} catch (error) {
|
|
68
|
+
this.logger.error(
|
|
69
|
+
`Error during enum values fixing: ${error.message}`,
|
|
70
|
+
error.stack
|
|
71
|
+
);
|
|
72
|
+
} finally {
|
|
73
|
+
await this.cleanup();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private async getSchemas(): Promise<string[]> {
|
|
78
|
+
const result = await this.pool.query(`
|
|
79
|
+
SELECT schema_name
|
|
80
|
+
FROM information_schema.schemata
|
|
81
|
+
WHERE schema_name NOT IN ('information_schema', 'pg_catalog', 'pg_toast')
|
|
82
|
+
AND schema_name NOT LIKE 'pg_%'
|
|
83
|
+
`);
|
|
84
|
+
|
|
85
|
+
return result.rows.map((row) => row.schema_name);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private async fixEnumValuesForSchema(schema: string) {
|
|
89
|
+
this.logger.log(`Processing enum fields for schema: ${schema}`);
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
// Obtener todas las columnas enum en este schema
|
|
93
|
+
const enumColumnsQuery = `
|
|
94
|
+
SELECT
|
|
95
|
+
c.table_name,
|
|
96
|
+
c.column_name,
|
|
97
|
+
c.udt_name
|
|
98
|
+
FROM
|
|
99
|
+
information_schema.columns c
|
|
100
|
+
WHERE
|
|
101
|
+
c.table_schema = $1
|
|
102
|
+
AND c.data_type = 'USER-DEFINED'
|
|
103
|
+
AND c.udt_name LIKE 'enum_%'
|
|
104
|
+
`;
|
|
105
|
+
|
|
106
|
+
const enumColumnsResult = await this.pool.query(enumColumnsQuery, [
|
|
107
|
+
schema,
|
|
108
|
+
]);
|
|
109
|
+
|
|
110
|
+
if (enumColumnsResult.rows.length === 0) {
|
|
111
|
+
this.logger.log(`No enum fields found in schema: ${schema}`);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
this.logger.log(
|
|
116
|
+
`Found ${enumColumnsResult.rows.length} enum fields in schema: ${schema}`
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// Procesar cada columna enum
|
|
120
|
+
for (const row of enumColumnsResult.rows) {
|
|
121
|
+
const tableName = row.table_name;
|
|
122
|
+
const columnName = row.column_name;
|
|
123
|
+
const enumTypeName = row.udt_name;
|
|
124
|
+
|
|
125
|
+
await this.fixEnumColumn(schema, tableName, columnName, enumTypeName);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
this.logger.log(`Completed processing enum fields for schema: ${schema}`);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
this.logger.error(
|
|
131
|
+
`Error processing enum fields for schema ${schema}: ${error.message}`,
|
|
132
|
+
error.stack
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private async fixEnumColumn(
|
|
138
|
+
schema: string,
|
|
139
|
+
tableName: string,
|
|
140
|
+
columnName: string,
|
|
141
|
+
enumTypeName: string
|
|
142
|
+
) {
|
|
143
|
+
this.logger.log(
|
|
144
|
+
`Fixing enum column ${columnName} (${enumTypeName}) in table ${schema}.${tableName}`
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
// Obtener los valores válidos para el enum
|
|
149
|
+
let validEnumValues: string[];
|
|
150
|
+
if (this.enumValuesCache[enumTypeName]) {
|
|
151
|
+
validEnumValues = this.enumValuesCache[enumTypeName];
|
|
152
|
+
} else {
|
|
153
|
+
validEnumValues = await this.getEnumValues(enumTypeName);
|
|
154
|
+
this.enumValuesCache[enumTypeName] = validEnumValues;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (validEnumValues.length === 0) {
|
|
158
|
+
this.logger.warn(
|
|
159
|
+
`No enum values found for type ${enumTypeName}. Skipping.`
|
|
160
|
+
);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
this.logger.log(
|
|
165
|
+
`Valid values for ${enumTypeName}: ${validEnumValues.join(", ")}`
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
// Obtener valores actuales en la columna
|
|
169
|
+
const currentValuesQuery = `
|
|
170
|
+
SELECT DISTINCT "${columnName}"
|
|
171
|
+
FROM "${schema}"."${tableName}"
|
|
172
|
+
WHERE "${columnName}" IS NOT NULL
|
|
173
|
+
`;
|
|
174
|
+
|
|
175
|
+
const currentValuesResult = await this.pool.query(currentValuesQuery);
|
|
176
|
+
const currentValues = currentValuesResult.rows.map((r) => r[columnName]);
|
|
177
|
+
|
|
178
|
+
// Encontrar valores inválidos
|
|
179
|
+
const invalidValues = currentValues.filter(
|
|
180
|
+
(val) => !validEnumValues.includes(val)
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
if (invalidValues.length === 0) {
|
|
184
|
+
this.logger.log(
|
|
185
|
+
`No invalid values found for enum column ${columnName} in table ${schema}.${tableName}`
|
|
186
|
+
);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
this.logger.log(
|
|
191
|
+
`Found ${
|
|
192
|
+
invalidValues.length
|
|
193
|
+
} invalid values for enum ${enumTypeName} in ${schema}.${tableName}: ${invalidValues.join(
|
|
194
|
+
", "
|
|
195
|
+
)}`
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
// Para cada valor inválido, intentar mapearlo a uno válido o establecerlo a NULL
|
|
199
|
+
for (const invalidValue of invalidValues) {
|
|
200
|
+
// Intentar encontrar un mapping existente
|
|
201
|
+
const mappingKey = `${enumTypeName}:${invalidValue}`;
|
|
202
|
+
let mappedValue: string | null = null;
|
|
203
|
+
|
|
204
|
+
if (this.mappingsCache[mappingKey]) {
|
|
205
|
+
mappedValue = this.mappingsCache[mappingKey];
|
|
206
|
+
this.logger.log(
|
|
207
|
+
`Using cached mapping for ${invalidValue} -> ${mappedValue}`
|
|
208
|
+
);
|
|
209
|
+
} else {
|
|
210
|
+
// Intentar encontrar una coincidencia sin distinción de mayúsculas/minúsculas
|
|
211
|
+
const matchingValidValue = validEnumValues.find(
|
|
212
|
+
(v) => v.toLowerCase() === invalidValue.toLowerCase()
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
if (matchingValidValue) {
|
|
216
|
+
mappedValue = matchingValidValue;
|
|
217
|
+
this.logger.log(
|
|
218
|
+
`Found case-insensitive match for ${invalidValue} -> ${mappedValue}`
|
|
219
|
+
);
|
|
220
|
+
} else {
|
|
221
|
+
// Intentar encontrar una coincidencia parcial
|
|
222
|
+
const similarValues = validEnumValues.filter(
|
|
223
|
+
(v) =>
|
|
224
|
+
v.toLowerCase().includes(invalidValue.toLowerCase()) ||
|
|
225
|
+
invalidValue.toLowerCase().includes(v.toLowerCase())
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
if (similarValues.length === 1) {
|
|
229
|
+
mappedValue = similarValues[0];
|
|
230
|
+
this.logger.log(
|
|
231
|
+
`Found partial match for ${invalidValue} -> ${mappedValue}`
|
|
232
|
+
);
|
|
233
|
+
} else if (similarValues.length > 1) {
|
|
234
|
+
// Si hay múltiples coincidencias, usar la más cercana por longitud
|
|
235
|
+
mappedValue = similarValues.reduce((prev, curr) =>
|
|
236
|
+
Math.abs(curr.length - invalidValue.length) <
|
|
237
|
+
Math.abs(prev.length - invalidValue.length)
|
|
238
|
+
? curr
|
|
239
|
+
: prev
|
|
240
|
+
);
|
|
241
|
+
this.logger.log(
|
|
242
|
+
`Found multiple partial matches for ${invalidValue}, using closest: ${mappedValue}`
|
|
243
|
+
);
|
|
244
|
+
} else {
|
|
245
|
+
// Si no hay coincidencias, usar NULL
|
|
246
|
+
mappedValue = null;
|
|
247
|
+
this.logger.log(
|
|
248
|
+
`No match found for ${invalidValue}, will set to NULL`
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Guardar el mapping para uso futuro
|
|
254
|
+
this.mappingsCache[mappingKey] = mappedValue;
|
|
255
|
+
if (this.saveMapping) {
|
|
256
|
+
this.saveMappings();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Actualizar los registros con el valor mapeado usando CAST explícito
|
|
261
|
+
const updateQuery =
|
|
262
|
+
mappedValue !== null
|
|
263
|
+
? `
|
|
264
|
+
UPDATE "${schema}"."${tableName}"
|
|
265
|
+
SET "${columnName}" = CAST($1 AS "${enumTypeName}")
|
|
266
|
+
WHERE "${columnName}" = $2
|
|
267
|
+
`
|
|
268
|
+
: `
|
|
269
|
+
UPDATE "${schema}"."${tableName}"
|
|
270
|
+
SET "${columnName}" = NULL
|
|
271
|
+
WHERE "${columnName}" = $1
|
|
272
|
+
`;
|
|
273
|
+
|
|
274
|
+
const updateParams =
|
|
275
|
+
mappedValue !== null ? [mappedValue, invalidValue] : [invalidValue];
|
|
276
|
+
|
|
277
|
+
const updateResult = await this.pool.query(updateQuery, updateParams);
|
|
278
|
+
|
|
279
|
+
this.logger.log(
|
|
280
|
+
`Updated ${
|
|
281
|
+
updateResult.rowCount
|
|
282
|
+
} rows in ${schema}.${tableName} with ${invalidValue} -> ${
|
|
283
|
+
mappedValue || "NULL"
|
|
284
|
+
} with explicit CAST`
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
} catch (error) {
|
|
288
|
+
this.logger.error(
|
|
289
|
+
`Error fixing enum column ${columnName} in table ${schema}.${tableName}: ${error.message}`,
|
|
290
|
+
error.stack
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private async getEnumValues(enumTypeName: string): Promise<string[]> {
|
|
296
|
+
try {
|
|
297
|
+
const enumQuery = `
|
|
298
|
+
SELECT e.enumlabel
|
|
299
|
+
FROM pg_enum e
|
|
300
|
+
JOIN pg_type t ON e.enumtypid = t.oid
|
|
301
|
+
WHERE t.typname = $1
|
|
302
|
+
ORDER BY e.enumsortorder
|
|
303
|
+
`;
|
|
304
|
+
|
|
305
|
+
const result = await this.pool.query(enumQuery, [enumTypeName]);
|
|
306
|
+
return result.rows.map((row) => row.enumlabel);
|
|
307
|
+
} catch (error) {
|
|
308
|
+
this.logger.error(
|
|
309
|
+
`Error getting enum values for ${enumTypeName}: ${error.message}`
|
|
310
|
+
);
|
|
311
|
+
return [];
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private saveMappings(): void {
|
|
316
|
+
try {
|
|
317
|
+
fs.writeFileSync(
|
|
318
|
+
this.mappingsFilePath,
|
|
319
|
+
JSON.stringify(this.mappingsCache, null, 2),
|
|
320
|
+
"utf8"
|
|
321
|
+
);
|
|
322
|
+
} catch (error) {
|
|
323
|
+
this.logger.error(`Error saving enum mappings: ${error.message}`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private async cleanup() {
|
|
328
|
+
this.logger.log("Cleaning up database connections");
|
|
329
|
+
await this.pool.end();
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Script para ejecutar desde línea de comandos
|
|
334
|
+
if (require.main === module) {
|
|
335
|
+
const run = async () => {
|
|
336
|
+
try {
|
|
337
|
+
// Obtener la URL de la base de datos desde los argumentos o .env
|
|
338
|
+
const databaseUrl = process.argv[2] || process.env.DATABASE_URL;
|
|
339
|
+
|
|
340
|
+
if (!databaseUrl) {
|
|
341
|
+
console.error(
|
|
342
|
+
"Error: No database URL provided. Please provide a database URL as an argument or set DATABASE_URL environment variable."
|
|
343
|
+
);
|
|
344
|
+
process.exit(1);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const enumFixer = new EnumFixer(databaseUrl);
|
|
348
|
+
await enumFixer.fixEnumValues();
|
|
349
|
+
process.exit(0);
|
|
350
|
+
} catch (error) {
|
|
351
|
+
console.error("Error:", error.message);
|
|
352
|
+
process.exit(1);
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
run();
|
|
357
|
+
}
|