@javalabs/prisma-client 1.0.0
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/README.md +220 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -0
- package/dist/prisma-factory.service.d.ts +9 -0
- package/dist/prisma-factory.service.js +47 -0
- package/dist/prisma-factory.service.js.map +1 -0
- package/dist/prisma.module.d.ts +2 -0
- package/dist/prisma.module.js +23 -0
- package/dist/prisma.module.js.map +1 -0
- package/dist/prisma.service.d.ts +6 -0
- package/dist/prisma.service.js +27 -0
- package/dist/prisma.service.js.map +1 -0
- package/dist/scripts/create-tenant-schemas.d.ts +1 -0
- package/dist/scripts/create-tenant-schemas.js +117 -0
- package/dist/scripts/create-tenant-schemas.js.map +1 -0
- package/dist/scripts/data-migration/batch-migrator.d.ts +25 -0
- package/dist/scripts/data-migration/batch-migrator.js +333 -0
- package/dist/scripts/data-migration/batch-migrator.js.map +1 -0
- package/dist/scripts/data-migration/data-transformer.d.ts +17 -0
- package/dist/scripts/data-migration/data-transformer.js +242 -0
- package/dist/scripts/data-migration/data-transformer.js.map +1 -0
- package/dist/scripts/data-migration/db-connector.d.ts +7 -0
- package/dist/scripts/data-migration/db-connector.js +58 -0
- package/dist/scripts/data-migration/db-connector.js.map +1 -0
- package/dist/scripts/data-migration/dependency-manager.d.ts +9 -0
- package/dist/scripts/data-migration/dependency-manager.js +86 -0
- package/dist/scripts/data-migration/dependency-manager.js.map +1 -0
- package/dist/scripts/data-migration/dependency-resolver.d.ts +18 -0
- package/dist/scripts/data-migration/dependency-resolver.js +251 -0
- package/dist/scripts/data-migration/dependency-resolver.js.map +1 -0
- package/dist/scripts/data-migration/entity-discovery.d.ts +11 -0
- package/dist/scripts/data-migration/entity-discovery.js +152 -0
- package/dist/scripts/data-migration/entity-discovery.js.map +1 -0
- package/dist/scripts/data-migration/foreign-key-manager.d.ts +17 -0
- package/dist/scripts/data-migration/foreign-key-manager.js +70 -0
- package/dist/scripts/data-migration/foreign-key-manager.js.map +1 -0
- package/dist/scripts/data-migration/migration-phases.d.ts +5 -0
- package/dist/scripts/data-migration/migration-phases.js +55 -0
- package/dist/scripts/data-migration/migration-phases.js.map +1 -0
- package/dist/scripts/data-migration/migration-tool.d.ts +29 -0
- package/dist/scripts/data-migration/migration-tool.js +250 -0
- package/dist/scripts/data-migration/migration-tool.js.map +1 -0
- package/dist/scripts/data-migration/phase-generator.d.ts +15 -0
- package/dist/scripts/data-migration/phase-generator.js +187 -0
- package/dist/scripts/data-migration/phase-generator.js.map +1 -0
- package/dist/scripts/data-migration/schema-utils.d.ts +18 -0
- package/dist/scripts/data-migration/schema-utils.js +164 -0
- package/dist/scripts/data-migration/schema-utils.js.map +1 -0
- package/dist/scripts/data-migration/tenant-migrator.d.ts +15 -0
- package/dist/scripts/data-migration/tenant-migrator.js +110 -0
- package/dist/scripts/data-migration/tenant-migrator.js.map +1 -0
- package/dist/scripts/data-migration/typecast-manager.d.ts +5 -0
- package/dist/scripts/data-migration/typecast-manager.js +35 -0
- package/dist/scripts/data-migration/typecast-manager.js.map +1 -0
- package/dist/scripts/data-migration/types.d.ts +34 -0
- package/dist/scripts/data-migration/types.js +3 -0
- package/dist/scripts/data-migration/types.js.map +1 -0
- package/dist/scripts/data-migration.d.ts +22 -0
- package/dist/scripts/data-migration.js +593 -0
- package/dist/scripts/data-migration.js.map +1 -0
- package/dist/scripts/drop-database.d.ts +10 -0
- package/dist/scripts/drop-database.js +81 -0
- package/dist/scripts/drop-database.js.map +1 -0
- package/dist/scripts/error-handler.d.ts +12 -0
- package/dist/scripts/error-handler.js +82 -0
- package/dist/scripts/error-handler.js.map +1 -0
- package/dist/scripts/fix-data-types.d.ts +10 -0
- package/dist/scripts/fix-data-types.js +185 -0
- package/dist/scripts/fix-data-types.js.map +1 -0
- package/dist/scripts/fix-enum-values.d.ts +17 -0
- package/dist/scripts/fix-enum-values.js +234 -0
- package/dist/scripts/fix-enum-values.js.map +1 -0
- package/dist/scripts/fix-schema-discrepancies.d.ts +21 -0
- package/dist/scripts/fix-schema-discrepancies.js +240 -0
- package/dist/scripts/fix-schema-discrepancies.js.map +1 -0
- package/dist/scripts/migrate-schema-structure.d.ts +1 -0
- package/dist/scripts/migrate-schema-structure.js +76 -0
- package/dist/scripts/migrate-schema-structure.js.map +1 -0
- package/dist/scripts/post-migration-validator.d.ts +21 -0
- package/dist/scripts/post-migration-validator.js +341 -0
- package/dist/scripts/post-migration-validator.js.map +1 -0
- package/dist/scripts/pre-migration-validator.d.ts +25 -0
- package/dist/scripts/pre-migration-validator.js +491 -0
- package/dist/scripts/pre-migration-validator.js.map +1 -0
- package/dist/scripts/reset-database.d.ts +17 -0
- package/dist/scripts/reset-database.js +202 -0
- package/dist/scripts/reset-database.js.map +1 -0
- package/dist/scripts/retry-failed-migrations.d.ts +14 -0
- package/dist/scripts/retry-failed-migrations.js +301 -0
- package/dist/scripts/retry-failed-migrations.js.map +1 -0
- package/dist/scripts/run-migration.d.ts +1 -0
- package/dist/scripts/run-migration.js +525 -0
- package/dist/scripts/run-migration.js.map +1 -0
- package/dist/scripts/schema-sync.d.ts +1 -0
- package/dist/scripts/schema-sync.js +85 -0
- package/dist/scripts/schema-sync.js.map +1 -0
- package/dist/scripts/sync-enum-types.d.ts +13 -0
- package/dist/scripts/sync-enum-types.js +139 -0
- package/dist/scripts/sync-enum-types.js.map +1 -0
- package/dist/scripts/sync-enum-values.d.ts +20 -0
- package/dist/scripts/sync-enum-values.js +336 -0
- package/dist/scripts/sync-enum-values.js.map +1 -0
- package/dist/scripts/truncate-database.d.ts +10 -0
- package/dist/scripts/truncate-database.js +100 -0
- package/dist/scripts/truncate-database.js.map +1 -0
- package/dist/scripts/verify-migration-setup.d.ts +11 -0
- package/dist/scripts/verify-migration-setup.js +120 -0
- package/dist/scripts/verify-migration-setup.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/migration-config-public.json +95 -0
- package/migration-config.json +95 -0
- package/package.json +33 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +360 -0
- package/src/index.ts +23 -0
- package/src/prisma-factory.service.ts +41 -0
- package/src/prisma.module.ts +10 -0
- package/src/prisma.service.ts +17 -0
- package/src/scripts/create-tenant-schemas.ts +146 -0
- package/src/scripts/data-migration/batch-migrator.ts +569 -0
- package/src/scripts/data-migration/data-transformer.ts +377 -0
- package/src/scripts/data-migration/db-connector.ts +67 -0
- package/src/scripts/data-migration/dependency-resolver.ts +319 -0
- package/src/scripts/data-migration/entity-discovery.ts +197 -0
- package/src/scripts/data-migration/foreign-key-manager.ts +95 -0
- package/src/scripts/data-migration/migration-tool.ts +357 -0
- package/src/scripts/data-migration/schema-utils.ts +186 -0
- package/src/scripts/data-migration/tenant-migrator.ts +194 -0
- package/src/scripts/data-migration/typecast-manager.ts +38 -0
- package/src/scripts/data-migration/types.ts +40 -0
- package/src/scripts/drop-database.ts +105 -0
- package/src/scripts/dump-source-db.sh +62 -0
- package/src/scripts/dumps/source_dump_20250413_112626.sql +1527 -0
- package/src/scripts/error-handler.ts +118 -0
- package/src/scripts/fix-data-types.ts +242 -0
- package/src/scripts/fix-enum-values.ts +357 -0
- package/src/scripts/fix-schema-discrepancies.ts +318 -0
- package/src/scripts/migrate-schema-structure.ts +90 -0
- package/src/scripts/post-migration-validator.ts +427 -0
- package/src/scripts/pre-migration-validator.ts +611 -0
- package/src/scripts/reset-database.ts +264 -0
- package/src/scripts/retry-failed-migrations.ts +416 -0
- package/src/scripts/run-migration.ts +691 -0
- package/src/scripts/schema-sync.ts +129 -0
- package/src/scripts/sync-enum-types.ts +171 -0
- package/src/scripts/sync-enum-values.ts +563 -0
- package/src/scripts/truncate-database.ts +124 -0
- package/src/scripts/verify-migration-setup.ts +136 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { Logger } from '@nestjs/common';
|
|
4
|
+
|
|
5
|
+
export class MigrationErrorHandler {
|
|
6
|
+
private readonly logger = new Logger('MigrationErrorHandler');
|
|
7
|
+
private errorLog: Record<string, any[]> = {};
|
|
8
|
+
private errorLogPath: string;
|
|
9
|
+
|
|
10
|
+
constructor(private readonly logDir: string = path.join(process.cwd(), 'migration-logs')) {
|
|
11
|
+
// Crear directorio de logs si no existe
|
|
12
|
+
if (!fs.existsSync(this.logDir)) {
|
|
13
|
+
fs.mkdirSync(this.logDir, { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Crear nombre de archivo con timestamp
|
|
17
|
+
const timestamp = new Date().toISOString().replace(/:/g, '-').replace(/\..+/, '');
|
|
18
|
+
this.errorLogPath = path.join(this.logDir, `migration-errors-${timestamp}.json`);
|
|
19
|
+
|
|
20
|
+
// Inicializar archivo de log
|
|
21
|
+
this.saveErrorLog();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Registra un error durante la migración
|
|
26
|
+
*/
|
|
27
|
+
logError(tenantId: string, entityName: string, recordId: any, error: Error, data?: any): void {
|
|
28
|
+
if (!this.errorLog[tenantId]) {
|
|
29
|
+
this.errorLog[tenantId] = [];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
this.errorLog[tenantId].push({
|
|
33
|
+
timestamp: new Date().toISOString(),
|
|
34
|
+
entity: entityName,
|
|
35
|
+
recordId,
|
|
36
|
+
error: {
|
|
37
|
+
message: error.message,
|
|
38
|
+
stack: error.stack,
|
|
39
|
+
},
|
|
40
|
+
data: data || null,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Guardar el log después de cada error
|
|
44
|
+
this.saveErrorLog();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Guarda el log de errores en un archivo JSON
|
|
49
|
+
*/
|
|
50
|
+
private saveErrorLog(): void {
|
|
51
|
+
try {
|
|
52
|
+
fs.writeFileSync(
|
|
53
|
+
this.errorLogPath,
|
|
54
|
+
JSON.stringify(this.errorLog, null, 2),
|
|
55
|
+
'utf8'
|
|
56
|
+
);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
this.logger.error(`Error saving error log: ${error.message}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Genera un informe de errores
|
|
64
|
+
*/
|
|
65
|
+
generateErrorReport(): string {
|
|
66
|
+
let totalErrors = 0;
|
|
67
|
+
let report = 'Migration Error Report\n';
|
|
68
|
+
report += '=====================\n\n';
|
|
69
|
+
|
|
70
|
+
for (const tenantId in this.errorLog) {
|
|
71
|
+
const tenantErrors = this.errorLog[tenantId];
|
|
72
|
+
if (tenantErrors.length === 0) continue;
|
|
73
|
+
|
|
74
|
+
totalErrors += tenantErrors.length;
|
|
75
|
+
report += `Tenant: ${tenantId}\n`;
|
|
76
|
+
report += `Errors: ${tenantErrors.length}\n\n`;
|
|
77
|
+
|
|
78
|
+
// Agrupar errores por entidad
|
|
79
|
+
const entityErrors: Record<string, number> = {};
|
|
80
|
+
for (const error of tenantErrors) {
|
|
81
|
+
entityErrors[error.entity] = (entityErrors[error.entity] || 0) + 1;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
report += 'Errors by entity:\n';
|
|
85
|
+
for (const entity in entityErrors) {
|
|
86
|
+
report += ` - ${entity}: ${entityErrors[entity]}\n`;
|
|
87
|
+
}
|
|
88
|
+
report += '\n';
|
|
89
|
+
|
|
90
|
+
// Mostrar los primeros 5 errores como ejemplo
|
|
91
|
+
report += 'Sample errors:\n';
|
|
92
|
+
for (let i = 0; i < Math.min(5, tenantErrors.length); i++) {
|
|
93
|
+
const error = tenantErrors[i];
|
|
94
|
+
report += ` - ${error.entity} (ID: ${error.recordId}): ${error.error.message}\n`;
|
|
95
|
+
}
|
|
96
|
+
report += '\n';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
report += `Total errors: ${totalErrors}\n`;
|
|
100
|
+
report += `Full error log saved to: ${this.errorLogPath}\n`;
|
|
101
|
+
|
|
102
|
+
return report;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Obtiene la lista de errores para un tenant específico
|
|
107
|
+
*/
|
|
108
|
+
getErrorsForTenant(tenantId: string): any[] {
|
|
109
|
+
return this.errorLog[tenantId] || [];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Obtiene todos los errores
|
|
114
|
+
*/
|
|
115
|
+
getAllErrors(): Record<string, any[]> {
|
|
116
|
+
return this.errorLog;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { PrismaClient } from "@prisma/client";
|
|
2
|
+
import * as dotenv from "dotenv";
|
|
3
|
+
import { Logger } from "@nestjs/common";
|
|
4
|
+
import * as pg from "pg";
|
|
5
|
+
|
|
6
|
+
dotenv.config();
|
|
7
|
+
|
|
8
|
+
export class DataTypeFixer {
|
|
9
|
+
private readonly logger = new Logger("DataTypeFixer");
|
|
10
|
+
private readonly sourcePool: pg.Pool;
|
|
11
|
+
private readonly targetPool: pg.Pool;
|
|
12
|
+
|
|
13
|
+
constructor() {
|
|
14
|
+
// Source database connection
|
|
15
|
+
this.sourcePool = new pg.Pool({
|
|
16
|
+
connectionString: process.env.SOURCE_DATABASE_URL,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// Target database connection
|
|
20
|
+
this.targetPool = new pg.Pool({
|
|
21
|
+
connectionString: process.env.DATABASE_URL,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async fixDataTypes() {
|
|
26
|
+
try {
|
|
27
|
+
this.logger.log("Starting data type fixing process");
|
|
28
|
+
|
|
29
|
+
// Get all schemas (tenants) from the target database
|
|
30
|
+
const schemasQuery = `
|
|
31
|
+
SELECT schema_name
|
|
32
|
+
FROM information_schema.schemata
|
|
33
|
+
WHERE schema_name NOT IN ('public', 'information_schema', 'pg_catalog', 'pg_toast')
|
|
34
|
+
AND schema_name NOT LIKE 'pg_%';
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
const schemasResult = await this.targetPool.query(schemasQuery);
|
|
38
|
+
const schemas = schemasResult.rows.map(row => row.schema_name);
|
|
39
|
+
|
|
40
|
+
this.logger.log(`Found ${schemas.length} schemas to process`);
|
|
41
|
+
|
|
42
|
+
// Process each schema
|
|
43
|
+
for (const schema of schemas) {
|
|
44
|
+
await this.fixNumericFieldsForSchema(schema);
|
|
45
|
+
await this.fixEnumFieldsForSchema(schema);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
this.logger.log("Data type fixing completed successfully");
|
|
49
|
+
} catch (error) {
|
|
50
|
+
this.logger.error(
|
|
51
|
+
`Error during data type fixing: ${error.message}`,
|
|
52
|
+
error.stack
|
|
53
|
+
);
|
|
54
|
+
} finally {
|
|
55
|
+
await this.cleanup();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private async fixNumericFieldsForSchema(schema: string) {
|
|
60
|
+
this.logger.log(`Processing numeric fields for schema: ${schema}`);
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
// Get all tables with numeric columns in this schema
|
|
64
|
+
const numericColumnsQuery = `
|
|
65
|
+
SELECT
|
|
66
|
+
table_name,
|
|
67
|
+
column_name
|
|
68
|
+
FROM
|
|
69
|
+
information_schema.columns
|
|
70
|
+
WHERE
|
|
71
|
+
table_schema = $1
|
|
72
|
+
AND data_type = 'numeric'
|
|
73
|
+
`;
|
|
74
|
+
|
|
75
|
+
const numericColumnsResult = await this.targetPool.query(numericColumnsQuery, [schema]);
|
|
76
|
+
|
|
77
|
+
// Process each table with numeric columns
|
|
78
|
+
for (const row of numericColumnsResult.rows) {
|
|
79
|
+
const tableName = row.table_name;
|
|
80
|
+
const columnName = row.column_name;
|
|
81
|
+
|
|
82
|
+
this.logger.log(`Fixing numeric column ${columnName} in table ${schema}.${tableName}`);
|
|
83
|
+
|
|
84
|
+
// Update the column to cast text values to numeric
|
|
85
|
+
const updateQuery = `
|
|
86
|
+
UPDATE "${schema}"."${tableName}"
|
|
87
|
+
SET "${columnName}" = CASE
|
|
88
|
+
WHEN "${columnName}" IS NULL THEN NULL
|
|
89
|
+
WHEN "${columnName}" = '' THEN NULL
|
|
90
|
+
ELSE CAST("${columnName}" AS NUMERIC)
|
|
91
|
+
END
|
|
92
|
+
WHERE "${columnName}" IS NOT NULL
|
|
93
|
+
AND "${columnName}" != ''
|
|
94
|
+
AND "${columnName}" ~ '^[0-9]+(\.[0-9]+)?$';
|
|
95
|
+
`;
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
await this.targetPool.query(updateQuery);
|
|
99
|
+
this.logger.log(`Successfully fixed numeric column ${columnName} in table ${schema}.${tableName}`);
|
|
100
|
+
} catch (updateError) {
|
|
101
|
+
this.logger.error(
|
|
102
|
+
`Error fixing numeric column ${columnName} in table ${schema}.${tableName}: ${updateError.message}`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
this.logger.log(`Completed processing numeric fields for schema: ${schema}`);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
this.logger.error(
|
|
110
|
+
`Error processing numeric fields for schema ${schema}: ${error.message}`,
|
|
111
|
+
error.stack
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private async fixEnumFieldsForSchema(schema: string) {
|
|
117
|
+
this.logger.log(`Processing enum fields for schema: ${schema}`);
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
// Get all tables with enum columns in this schema
|
|
121
|
+
const enumColumnsQuery = `
|
|
122
|
+
SELECT
|
|
123
|
+
c.table_name,
|
|
124
|
+
c.column_name,
|
|
125
|
+
c.udt_name
|
|
126
|
+
FROM
|
|
127
|
+
information_schema.columns c
|
|
128
|
+
WHERE
|
|
129
|
+
c.table_schema = $1
|
|
130
|
+
AND c.data_type = 'USER-DEFINED'
|
|
131
|
+
AND c.udt_name LIKE 'enum_%'
|
|
132
|
+
`;
|
|
133
|
+
|
|
134
|
+
const enumColumnsResult = await this.targetPool.query(enumColumnsQuery, [schema]);
|
|
135
|
+
|
|
136
|
+
// Process each table with enum columns
|
|
137
|
+
for (const row of enumColumnsResult.rows) {
|
|
138
|
+
const tableName = row.table_name;
|
|
139
|
+
const columnName = row.column_name;
|
|
140
|
+
const enumTypeName = row.udt_name;
|
|
141
|
+
|
|
142
|
+
this.logger.log(`Fixing enum column ${columnName} (${enumTypeName}) in table ${schema}.${tableName}`);
|
|
143
|
+
|
|
144
|
+
// Get the valid enum values
|
|
145
|
+
const enumValuesQuery = `
|
|
146
|
+
SELECT e.enumlabel
|
|
147
|
+
FROM pg_enum e
|
|
148
|
+
JOIN pg_type t ON e.enumtypid = t.oid
|
|
149
|
+
WHERE t.typname = $1
|
|
150
|
+
ORDER BY e.enumsortorder
|
|
151
|
+
`;
|
|
152
|
+
|
|
153
|
+
const enumValuesResult = await this.targetPool.query(enumValuesQuery, [enumTypeName]);
|
|
154
|
+
const validEnumValues = enumValuesResult.rows.map(r => r.enumlabel);
|
|
155
|
+
|
|
156
|
+
if (validEnumValues.length === 0) {
|
|
157
|
+
this.logger.warn(`No enum values found for type ${enumTypeName}. Skipping.`);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
this.logger.log(`Valid values for ${enumTypeName}: ${validEnumValues.join(', ')}`);
|
|
162
|
+
|
|
163
|
+
// Get current values in the column
|
|
164
|
+
const currentValuesQuery = `
|
|
165
|
+
SELECT DISTINCT "${columnName}"
|
|
166
|
+
FROM "${schema}"."${tableName}"
|
|
167
|
+
WHERE "${columnName}" IS NOT NULL
|
|
168
|
+
`;
|
|
169
|
+
|
|
170
|
+
const currentValuesResult = await this.targetPool.query(currentValuesQuery);
|
|
171
|
+
const currentValues = currentValuesResult.rows.map(r => r[columnName]);
|
|
172
|
+
|
|
173
|
+
// Find invalid values
|
|
174
|
+
const invalidValues = currentValues.filter(val => !validEnumValues.includes(val));
|
|
175
|
+
|
|
176
|
+
if (invalidValues.length > 0) {
|
|
177
|
+
this.logger.log(`Found invalid values for enum ${enumTypeName}: ${invalidValues.join(', ')}`);
|
|
178
|
+
|
|
179
|
+
// For each invalid value, try to map it to a valid one or set to NULL
|
|
180
|
+
for (const invalidValue of invalidValues) {
|
|
181
|
+
// Try to find a case-insensitive match
|
|
182
|
+
const matchingValidValue = validEnumValues.find(
|
|
183
|
+
v => v.toLowerCase() === invalidValue.toLowerCase()
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
let updateQuery;
|
|
187
|
+
if (matchingValidValue) {
|
|
188
|
+
// If we found a case-insensitive match, use it
|
|
189
|
+
updateQuery = `
|
|
190
|
+
UPDATE "${schema}"."${tableName}"
|
|
191
|
+
SET "${columnName}" = $1
|
|
192
|
+
WHERE "${columnName}" = $2
|
|
193
|
+
`;
|
|
194
|
+
await this.targetPool.query(updateQuery, [matchingValidValue, invalidValue]);
|
|
195
|
+
this.logger.log(`Updated invalid value "${invalidValue}" to "${matchingValidValue}"`);
|
|
196
|
+
} else {
|
|
197
|
+
// Otherwise set to NULL or default value
|
|
198
|
+
updateQuery = `
|
|
199
|
+
UPDATE "${schema}"."${tableName}"
|
|
200
|
+
SET "${columnName}" = NULL
|
|
201
|
+
WHERE "${columnName}" = $1
|
|
202
|
+
`;
|
|
203
|
+
await this.targetPool.query(updateQuery, [invalidValue]);
|
|
204
|
+
this.logger.log(`Set invalid value "${invalidValue}" to NULL`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
} else {
|
|
208
|
+
this.logger.log(`No invalid enum values found for ${columnName} in ${schema}.${tableName}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
this.logger.log(`Completed processing enum fields for schema: ${schema}`);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
this.logger.error(
|
|
215
|
+
`Error processing enum fields for schema ${schema}: ${error.message}`,
|
|
216
|
+
error.stack
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private async cleanup() {
|
|
222
|
+
this.logger.log("Cleaning up database connections");
|
|
223
|
+
await this.sourcePool.end();
|
|
224
|
+
await this.targetPool.end();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Script para ejecutar desde línea de comandos
|
|
229
|
+
if (require.main === module) {
|
|
230
|
+
const run = async () => {
|
|
231
|
+
try {
|
|
232
|
+
const fixer = new DataTypeFixer();
|
|
233
|
+
await fixer.fixDataTypes();
|
|
234
|
+
process.exit(0);
|
|
235
|
+
} catch (error) {
|
|
236
|
+
console.error("Error:", error.message);
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
run();
|
|
242
|
+
}
|
|
@@ -0,0 +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
|
+
}
|