@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,264 @@
|
|
|
1
|
+
import * as pg from "pg";
|
|
2
|
+
import * as dotenv from "dotenv";
|
|
3
|
+
import { Logger } from "@nestjs/common";
|
|
4
|
+
import { execSync } from "child_process";
|
|
5
|
+
import { PrismaClient } from "@prisma/client";
|
|
6
|
+
|
|
7
|
+
dotenv.config();
|
|
8
|
+
|
|
9
|
+
export class DatabaseResetTool {
|
|
10
|
+
private readonly logger = new Logger("DatabaseResetTool");
|
|
11
|
+
private readonly pool: pg.Pool;
|
|
12
|
+
private readonly adminPool: pg.Pool;
|
|
13
|
+
private readonly prisma: PrismaClient;
|
|
14
|
+
|
|
15
|
+
constructor(private readonly databaseUrl: string) {
|
|
16
|
+
// Conexión a la base de datos específica
|
|
17
|
+
this.pool = new pg.Pool({
|
|
18
|
+
connectionString: this.databaseUrl,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Conexión a la base de datos postgres (admin)
|
|
22
|
+
const adminUrl = this.databaseUrl.replace(/\/[^/]+$/, "/postgres");
|
|
23
|
+
this.adminPool = new pg.Pool({
|
|
24
|
+
connectionString: adminUrl,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Cliente Prisma
|
|
28
|
+
this.prisma = new PrismaClient({
|
|
29
|
+
datasources: {
|
|
30
|
+
db: {
|
|
31
|
+
url: this.databaseUrl,
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async resetDatabase(recreateDb = false) {
|
|
38
|
+
try {
|
|
39
|
+
this.logger.log("Starting complete database reset process");
|
|
40
|
+
|
|
41
|
+
// Extraer el nombre de la base de datos de la URL
|
|
42
|
+
const dbName = this.extractDatabaseName(this.databaseUrl);
|
|
43
|
+
|
|
44
|
+
if (!dbName) {
|
|
45
|
+
throw new Error("Could not extract database name from connection URL");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 1. Eliminar todos los schemas personalizados y sus contenidos
|
|
49
|
+
await this.dropAllSchemas();
|
|
50
|
+
|
|
51
|
+
// 2. Eliminar todas las tablas del schema public
|
|
52
|
+
await this.dropAllPublicTables();
|
|
53
|
+
|
|
54
|
+
// 3. Eliminar todos los tipos personalizados (enums, etc.)
|
|
55
|
+
await this.dropAllCustomTypes();
|
|
56
|
+
|
|
57
|
+
if (recreateDb) {
|
|
58
|
+
// 4. Terminar todas las conexiones y eliminar la base de datos
|
|
59
|
+
await this.terminateConnections(dbName);
|
|
60
|
+
await this.adminPool.query(`DROP DATABASE IF EXISTS "${dbName}"`);
|
|
61
|
+
|
|
62
|
+
// 5. Recrear la base de datos
|
|
63
|
+
this.logger.log(`Recreating database: ${dbName}`);
|
|
64
|
+
await this.adminPool.query(`CREATE DATABASE "${dbName}"`);
|
|
65
|
+
|
|
66
|
+
// 6. Ejecutar prisma migrate para recrear el esquema base
|
|
67
|
+
this.logger.log("Running prisma migrate to recreate base schema");
|
|
68
|
+
try {
|
|
69
|
+
execSync(`DATABASE_URL="${this.databaseUrl}" npx prisma migrate deploy`, {
|
|
70
|
+
stdio: 'inherit'
|
|
71
|
+
});
|
|
72
|
+
} catch (migrationError) {
|
|
73
|
+
this.logger.error(`Error running prisma migrate: ${migrationError.message}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
this.logger.log("Database has been completely reset");
|
|
78
|
+
return true;
|
|
79
|
+
} catch (error) {
|
|
80
|
+
this.logger.error(
|
|
81
|
+
`Error resetting database: ${error.message}`,
|
|
82
|
+
error.stack
|
|
83
|
+
);
|
|
84
|
+
return false;
|
|
85
|
+
} finally {
|
|
86
|
+
await this.cleanup();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private async dropAllSchemas() {
|
|
91
|
+
this.logger.log("Dropping all custom schemas");
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
// Obtener todos los schemas excepto los del sistema
|
|
95
|
+
const schemas = await this.getCustomSchemas();
|
|
96
|
+
this.logger.log(`Found ${schemas.length} custom schemas to drop`);
|
|
97
|
+
|
|
98
|
+
// Desactivar restricciones de clave foránea temporalmente
|
|
99
|
+
await this.pool.query(`SET session_replication_role = 'replica'`);
|
|
100
|
+
|
|
101
|
+
// Eliminar cada schema y su contenido
|
|
102
|
+
for (const schema of schemas) {
|
|
103
|
+
try {
|
|
104
|
+
await this.pool.query(`DROP SCHEMA IF EXISTS "${schema}" CASCADE`);
|
|
105
|
+
this.logger.log(`Dropped schema: ${schema}`);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
this.logger.error(`Error dropping schema ${schema}: ${error.message}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Reactivar restricciones de clave foránea
|
|
112
|
+
await this.pool.query(`SET session_replication_role = 'origin'`);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
this.logger.error(`Error dropping schemas: ${error.message}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private async dropAllPublicTables() {
|
|
119
|
+
this.logger.log("Dropping all tables in public schema");
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
// Obtener todas las tablas del schema public
|
|
123
|
+
const tables = await this.getPublicTables();
|
|
124
|
+
this.logger.log(`Found ${tables.length} tables in public schema to drop`);
|
|
125
|
+
|
|
126
|
+
if (tables.length > 0) {
|
|
127
|
+
// Desactivar restricciones de clave foránea temporalmente
|
|
128
|
+
await this.pool.query(`SET session_replication_role = 'replica'`);
|
|
129
|
+
|
|
130
|
+
// Eliminar todas las tablas en una sola operación
|
|
131
|
+
const tableList = tables.map(table => `"${table}"`).join(", ");
|
|
132
|
+
if (tableList) {
|
|
133
|
+
await this.pool.query(`DROP TABLE IF EXISTS ${tableList} CASCADE`);
|
|
134
|
+
this.logger.log("Dropped all tables in public schema");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Reactivar restricciones de clave foránea
|
|
138
|
+
await this.pool.query(`SET session_replication_role = 'origin'`);
|
|
139
|
+
}
|
|
140
|
+
} catch (error) {
|
|
141
|
+
this.logger.error(`Error dropping public tables: ${error.message}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private async dropAllCustomTypes() {
|
|
146
|
+
this.logger.log("Dropping all custom types (enums, domains, etc.)");
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
// Obtener todos los tipos personalizados
|
|
150
|
+
const typesQuery = `
|
|
151
|
+
SELECT t.typname AS name
|
|
152
|
+
FROM pg_type t
|
|
153
|
+
JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
|
|
154
|
+
WHERE n.nspname = 'public'
|
|
155
|
+
AND t.typtype = 'e' -- enum types
|
|
156
|
+
`;
|
|
157
|
+
|
|
158
|
+
const typesResult = await this.pool.query(typesQuery);
|
|
159
|
+
const types = typesResult.rows.map(row => row.name);
|
|
160
|
+
|
|
161
|
+
this.logger.log(`Found ${types.length} custom types to drop`);
|
|
162
|
+
|
|
163
|
+
// Eliminar cada tipo
|
|
164
|
+
for (const type of types) {
|
|
165
|
+
try {
|
|
166
|
+
await this.pool.query(`DROP TYPE IF EXISTS "${type}" CASCADE`);
|
|
167
|
+
this.logger.log(`Dropped type: ${type}`);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
this.logger.error(`Error dropping type ${type}: ${error.message}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
} catch (error) {
|
|
173
|
+
this.logger.error(`Error dropping custom types: ${error.message}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private async getCustomSchemas(): Promise<string[]> {
|
|
178
|
+
const result = await this.pool.query(`
|
|
179
|
+
SELECT schema_name
|
|
180
|
+
FROM information_schema.schemata
|
|
181
|
+
WHERE schema_name NOT IN ('public', 'information_schema', 'pg_catalog', 'pg_toast')
|
|
182
|
+
AND schema_name NOT LIKE 'pg_%'
|
|
183
|
+
`);
|
|
184
|
+
|
|
185
|
+
return result.rows.map(row => row.schema_name);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private async getPublicTables(): Promise<string[]> {
|
|
189
|
+
const result = await this.pool.query(`
|
|
190
|
+
SELECT table_name
|
|
191
|
+
FROM information_schema.tables
|
|
192
|
+
WHERE table_schema = 'public'
|
|
193
|
+
AND table_type = 'BASE TABLE'
|
|
194
|
+
`);
|
|
195
|
+
|
|
196
|
+
return result.rows.map(row => row.table_name);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private extractDatabaseName(url: string): string | null {
|
|
200
|
+
// Extraer el nombre de la base de datos de la URL de conexión
|
|
201
|
+
const matches = url.match(/\/([^/?]+)($|\?)/);
|
|
202
|
+
return matches ? matches[1] : null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private async terminateConnections(dbName: string) {
|
|
206
|
+
this.logger.log(`Terminating all connections to database: ${dbName}`);
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
await this.adminPool.query(`
|
|
210
|
+
SELECT pg_terminate_backend(pg_stat_activity.pid)
|
|
211
|
+
FROM pg_stat_activity
|
|
212
|
+
WHERE pg_stat_activity.datname = $1
|
|
213
|
+
AND pid <> pg_backend_pid()
|
|
214
|
+
`, [dbName]);
|
|
215
|
+
|
|
216
|
+
this.logger.log("All connections terminated successfully");
|
|
217
|
+
} catch (error) {
|
|
218
|
+
this.logger.error(`Error terminating connections: ${error.message}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private async cleanup() {
|
|
223
|
+
this.logger.log("Cleaning up database connections");
|
|
224
|
+
await this.prisma.$disconnect();
|
|
225
|
+
await this.pool.end();
|
|
226
|
+
await this.adminPool.end();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Script para ejecutar desde línea de comandos
|
|
231
|
+
if (require.main === module) {
|
|
232
|
+
const run = async () => {
|
|
233
|
+
try {
|
|
234
|
+
// Load environment variables first
|
|
235
|
+
dotenv.config();
|
|
236
|
+
|
|
237
|
+
// Get database URL from command line args or .env
|
|
238
|
+
const databaseUrl = process.argv[2] || process.env.DATABASE_URL;
|
|
239
|
+
|
|
240
|
+
if (!databaseUrl) {
|
|
241
|
+
console.error("Error: DATABASE_URL not found in environment variables or command line arguments");
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Check for recreate flag
|
|
246
|
+
const recreateFlag = process.argv.includes('--recreate') || process.argv.includes('-r');
|
|
247
|
+
|
|
248
|
+
const resetTool = new DatabaseResetTool(databaseUrl);
|
|
249
|
+
const success = await resetTool.resetDatabase(recreateFlag);
|
|
250
|
+
|
|
251
|
+
if (!success) {
|
|
252
|
+
console.error("Database reset failed");
|
|
253
|
+
process.exit(1);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
process.exit(0);
|
|
257
|
+
} catch (error) {
|
|
258
|
+
console.error("Error:", error.message);
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
run();
|
|
264
|
+
}
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { PrismaClient } from "@prisma/client";
|
|
4
|
+
import * as dotenv from "dotenv";
|
|
5
|
+
import { Logger } from "@nestjs/common";
|
|
6
|
+
import * as pg from "pg";
|
|
7
|
+
import { MigrationErrorHandler } from "./error-handler";
|
|
8
|
+
|
|
9
|
+
dotenv.config();
|
|
10
|
+
|
|
11
|
+
export class FailedMigrationRetry {
|
|
12
|
+
private readonly logger = new Logger("FailedMigrationRetry");
|
|
13
|
+
private readonly targetPrisma: PrismaClient;
|
|
14
|
+
private readonly targetPool: pg.Pool;
|
|
15
|
+
private readonly errorHandler: MigrationErrorHandler;
|
|
16
|
+
private schemaCache: Record<string, any> = {};
|
|
17
|
+
|
|
18
|
+
constructor(private readonly errorLogPath: string) {
|
|
19
|
+
// Target database connection
|
|
20
|
+
this.targetPool = new pg.Pool({
|
|
21
|
+
connectionString: process.env.DATABASE_URL,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Target Prisma client
|
|
25
|
+
this.targetPrisma = new PrismaClient({
|
|
26
|
+
datasources: {
|
|
27
|
+
db: {
|
|
28
|
+
url: process.env.DATABASE_URL,
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Inicializar el manejador de errores
|
|
34
|
+
this.errorHandler = new MigrationErrorHandler();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async retryFailedMigrations() {
|
|
38
|
+
try {
|
|
39
|
+
this.logger.log(`Loading error log from: ${this.errorLogPath}`);
|
|
40
|
+
|
|
41
|
+
// Cargar el archivo de log de errores
|
|
42
|
+
if (!fs.existsSync(this.errorLogPath)) {
|
|
43
|
+
this.logger.error(`Error log file not found: ${this.errorLogPath}`);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const errorLogContent = fs.readFileSync(this.errorLogPath, "utf8");
|
|
48
|
+
const errorLog: Record<string, any[]> = JSON.parse(errorLogContent);
|
|
49
|
+
|
|
50
|
+
let totalErrors = 0;
|
|
51
|
+
let retrySuccessCount = 0;
|
|
52
|
+
let retryFailCount = 0;
|
|
53
|
+
|
|
54
|
+
// Procesar cada tenant
|
|
55
|
+
for (const tenantId in errorLog) {
|
|
56
|
+
const tenantErrors = errorLog[tenantId];
|
|
57
|
+
if (tenantErrors.length === 0) continue;
|
|
58
|
+
|
|
59
|
+
totalErrors += tenantErrors.length;
|
|
60
|
+
this.logger.log(
|
|
61
|
+
`Processing ${tenantErrors.length} errors for tenant: ${tenantId}`
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// Crear cliente Prisma específico para este tenant
|
|
65
|
+
const tenantPrisma = new PrismaClient({
|
|
66
|
+
datasources: {
|
|
67
|
+
db: {
|
|
68
|
+
url: `${process.env.DATABASE_URL}?schema=${tenantId}`,
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
transactionOptions: {
|
|
72
|
+
maxWait: 120000,
|
|
73
|
+
timeout: 120000,
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Procesar cada error
|
|
78
|
+
for (const errorEntry of tenantErrors) {
|
|
79
|
+
const { entity, recordId, data } = errorEntry;
|
|
80
|
+
|
|
81
|
+
this.logger.log(`Retrying ${entity} with ID ${recordId}`);
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
// Obtener el esquema de la tabla si no está en caché
|
|
85
|
+
if (!this.schemaCache[`${tenantId}.${entity}`]) {
|
|
86
|
+
this.schemaCache[`${tenantId}.${entity}`] =
|
|
87
|
+
await this.getTableSchema(entity, tenantId);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const tableSchema = this.schemaCache[`${tenantId}.${entity}`];
|
|
91
|
+
|
|
92
|
+
if (!tableSchema || tableSchema.length === 0) {
|
|
93
|
+
this.logger.warn(
|
|
94
|
+
`Schema not found for ${entity} in tenant ${tenantId}. Skipping.`
|
|
95
|
+
);
|
|
96
|
+
retryFailCount++;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Preparar los datos para la inserción/actualización
|
|
101
|
+
const createData: Record<string, any> = {};
|
|
102
|
+
const updateData: Record<string, any> = {};
|
|
103
|
+
|
|
104
|
+
// Obtener la lista de columnas
|
|
105
|
+
const columnNames = tableSchema.map((col) => col.column_name);
|
|
106
|
+
|
|
107
|
+
// Si no tenemos los datos originales, no podemos reintentar
|
|
108
|
+
if (!data) {
|
|
109
|
+
this.logger.warn(
|
|
110
|
+
`No data available for ${entity} with ID ${recordId}. Skipping.`
|
|
111
|
+
);
|
|
112
|
+
retryFailCount++;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Procesar cada columna
|
|
117
|
+
for (const column of tableSchema) {
|
|
118
|
+
const columnName = column.column_name;
|
|
119
|
+
|
|
120
|
+
// Omitir columnas que no están en los datos
|
|
121
|
+
if (!Object.prototype.hasOwnProperty.call(data, columnName)) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Manejar tipos especiales
|
|
126
|
+
if (column.data_type === "numeric" && data[columnName] !== null) {
|
|
127
|
+
// Convertir a número
|
|
128
|
+
const numericValue = parseFloat(data[columnName]);
|
|
129
|
+
if (!isNaN(numericValue)) {
|
|
130
|
+
createData[columnName] = numericValue;
|
|
131
|
+
updateData[columnName] = numericValue;
|
|
132
|
+
} else {
|
|
133
|
+
createData[columnName] = null;
|
|
134
|
+
updateData[columnName] = null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// Manejar tipos enum
|
|
138
|
+
else if (
|
|
139
|
+
column.data_type === "USER-DEFINED" &&
|
|
140
|
+
column.udt_name.startsWith("enum_")
|
|
141
|
+
) {
|
|
142
|
+
// Obtener valores válidos para el enum
|
|
143
|
+
const enumValues = await this.getEnumValues(column.udt_name);
|
|
144
|
+
|
|
145
|
+
// Inicializar _enumTypes si no existe
|
|
146
|
+
if (!createData._enumTypes) createData._enumTypes = {};
|
|
147
|
+
if (!updateData._enumTypes) updateData._enumTypes = {};
|
|
148
|
+
|
|
149
|
+
// Siempre guardar el tipo de enum para esta columna
|
|
150
|
+
createData._enumTypes[columnName] = column.udt_name;
|
|
151
|
+
updateData._enumTypes[columnName] = column.udt_name;
|
|
152
|
+
|
|
153
|
+
if (data[columnName] === null || data[columnName] === "") {
|
|
154
|
+
createData[columnName] = null;
|
|
155
|
+
updateData[columnName] = null;
|
|
156
|
+
}
|
|
157
|
+
// Si el valor es un string, intentar encontrar una coincidencia en los valores del enum
|
|
158
|
+
else if (typeof data[columnName] === "string") {
|
|
159
|
+
const matchingValue = this.findMatchingEnumValue(
|
|
160
|
+
data[columnName],
|
|
161
|
+
enumValues
|
|
162
|
+
);
|
|
163
|
+
if (matchingValue) {
|
|
164
|
+
createData[columnName] = matchingValue;
|
|
165
|
+
updateData[columnName] = matchingValue;
|
|
166
|
+
} else {
|
|
167
|
+
// Si no hay coincidencia, usar null
|
|
168
|
+
createData[columnName] = null;
|
|
169
|
+
updateData[columnName] = null;
|
|
170
|
+
this.logger.warn(
|
|
171
|
+
`No matching enum value found for ${columnName}="${data[columnName]}" in ${entity}. Using NULL.`
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
// Para otros tipos, usar el valor tal cual
|
|
176
|
+
const enumValue = data[columnName];
|
|
177
|
+
createData[columnName] = data[columnName];
|
|
178
|
+
updateData[columnName] = data[columnName];
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
// Para otros tipos de datos, usar el valor tal cual
|
|
182
|
+
createData[columnName] = data[columnName];
|
|
183
|
+
updateData[columnName] = data[columnName];
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Asegurarse de incluir el ID
|
|
188
|
+
if (columnNames.includes(recordId)) {
|
|
189
|
+
createData[recordId] = data[recordId];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Ejecutar la operación upsert usando SQL raw en lugar de acceso directo al modelo
|
|
193
|
+
// Verificar primero si el registro existe
|
|
194
|
+
const checkExistQuery = `
|
|
195
|
+
SELECT 1 FROM "${tenantId}"."${entity}"
|
|
196
|
+
WHERE "${recordId}" = $1
|
|
197
|
+
LIMIT 1
|
|
198
|
+
`;
|
|
199
|
+
|
|
200
|
+
const existResult = await this.targetPool.query(checkExistQuery, [
|
|
201
|
+
data[recordId],
|
|
202
|
+
]);
|
|
203
|
+
const recordExists = existResult.rows.length > 0;
|
|
204
|
+
|
|
205
|
+
if (recordExists) {
|
|
206
|
+
// Construir la consulta UPDATE
|
|
207
|
+
const updateFields = Object.keys(updateData)
|
|
208
|
+
.filter((key) => key !== recordId && key !== '_enumTypes') // Excluir el campo ID y _enumTypes
|
|
209
|
+
.map((key, index) => {
|
|
210
|
+
// Aplicar casting explícito para tipos enum
|
|
211
|
+
if (updateData._enumTypes && updateData._enumTypes[key]) {
|
|
212
|
+
return `"${key}" = $${index + 2}::${updateData._enumTypes[key]}`;
|
|
213
|
+
}
|
|
214
|
+
return `"${key}" = $${index + 2}`;
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
if (updateFields.length > 0) {
|
|
218
|
+
const updateQuery = `
|
|
219
|
+
UPDATE "${tenantId}"."${entity}"
|
|
220
|
+
SET ${updateFields.join(", ")}
|
|
221
|
+
WHERE "${recordId}" = $1
|
|
222
|
+
`;
|
|
223
|
+
|
|
224
|
+
const updateValues = [
|
|
225
|
+
data[recordId],
|
|
226
|
+
...Object.keys(updateData)
|
|
227
|
+
.filter((key) => key !== recordId && key !== '_enumTypes')
|
|
228
|
+
.map((key) => updateData[key]),
|
|
229
|
+
];
|
|
230
|
+
|
|
231
|
+
await this.targetPool.query(updateQuery, updateValues);
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
// Construir la consulta INSERT
|
|
235
|
+
const insertFields = Object.keys(createData)
|
|
236
|
+
.filter(key => key !== '_enumTypes')
|
|
237
|
+
.map((key) => `"${key}"`);
|
|
238
|
+
|
|
239
|
+
const insertPlaceholders = Object.keys(createData)
|
|
240
|
+
.filter(key => key !== '_enumTypes')
|
|
241
|
+
.map((key, index) => {
|
|
242
|
+
// Aplicar casting explícito para tipos enum
|
|
243
|
+
if (createData._enumTypes && createData._enumTypes[key]) {
|
|
244
|
+
return `$${index + 1}::${createData._enumTypes[key]}`;
|
|
245
|
+
}
|
|
246
|
+
return `$${index + 1}`;
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const insertQuery = `
|
|
250
|
+
INSERT INTO "${tenantId}"."${entity}" (${insertFields.join(", ")})
|
|
251
|
+
VALUES (${insertPlaceholders.join(", ")})
|
|
252
|
+
`;
|
|
253
|
+
|
|
254
|
+
const insertValues = Object.keys(createData)
|
|
255
|
+
.filter(key => key !== '_enumTypes')
|
|
256
|
+
.map((key) => createData[key]);
|
|
257
|
+
|
|
258
|
+
await this.targetPool.query(insertQuery, insertValues);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
this.logger.log(
|
|
262
|
+
`Successfully migrated ${entity} with ID ${recordId}`
|
|
263
|
+
);
|
|
264
|
+
retrySuccessCount++;
|
|
265
|
+
} catch (error) {
|
|
266
|
+
this.logger.error(
|
|
267
|
+
`Error retrying ${entity} with ID ${recordId}: ${error.message}`
|
|
268
|
+
);
|
|
269
|
+
// Registrar el error en el nuevo log
|
|
270
|
+
this.errorHandler.logError(tenantId, entity, recordId, error, data);
|
|
271
|
+
retryFailCount++;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Cerrar la conexión del cliente tenant
|
|
276
|
+
await tenantPrisma.$disconnect();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Mostrar resumen
|
|
280
|
+
this.logger.log(`
|
|
281
|
+
Retry Summary:
|
|
282
|
+
- Total errors: ${totalErrors}
|
|
283
|
+
- Successfully retried: ${retrySuccessCount}
|
|
284
|
+
- Failed retries: ${retryFailCount}
|
|
285
|
+
`);
|
|
286
|
+
|
|
287
|
+
// Generar informe de errores restantes
|
|
288
|
+
const remainingErrorsReport = this.errorHandler.generateErrorReport();
|
|
289
|
+
this.logger.log("Remaining Errors Report:\n" + remainingErrorsReport);
|
|
290
|
+
} catch (error) {
|
|
291
|
+
this.logger.error(
|
|
292
|
+
`Error in retry process: ${error.message}`,
|
|
293
|
+
error.stack
|
|
294
|
+
);
|
|
295
|
+
} finally {
|
|
296
|
+
await this.cleanup();
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private async getTableSchema(
|
|
301
|
+
tableName: string,
|
|
302
|
+
schemaName: string
|
|
303
|
+
): Promise<any[]> {
|
|
304
|
+
try {
|
|
305
|
+
const schemaQuery = `
|
|
306
|
+
SELECT column_name, data_type, column_default, is_nullable, udt_name
|
|
307
|
+
FROM information_schema.columns
|
|
308
|
+
WHERE table_name = $1
|
|
309
|
+
AND table_schema = $2
|
|
310
|
+
ORDER BY ordinal_position
|
|
311
|
+
`;
|
|
312
|
+
|
|
313
|
+
const result = await this.targetPool.query(schemaQuery, [
|
|
314
|
+
tableName,
|
|
315
|
+
schemaName,
|
|
316
|
+
]);
|
|
317
|
+
return result.rows;
|
|
318
|
+
} catch (error) {
|
|
319
|
+
this.logger.error(
|
|
320
|
+
`Error getting schema for table ${tableName} in schema ${schemaName}: ${error.message}`
|
|
321
|
+
);
|
|
322
|
+
return [];
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private async getEnumValues(enumTypeName: string): Promise<string[]> {
|
|
327
|
+
try {
|
|
328
|
+
const enumQuery = `
|
|
329
|
+
SELECT e.enumlabel
|
|
330
|
+
FROM pg_enum e
|
|
331
|
+
JOIN pg_type t ON e.enumtypid = t.oid
|
|
332
|
+
WHERE t.typname = $1
|
|
333
|
+
ORDER BY e.enumsortorder
|
|
334
|
+
`;
|
|
335
|
+
|
|
336
|
+
const result = await this.targetPool.query(enumQuery, [enumTypeName]);
|
|
337
|
+
return result.rows.map((row) => row.enumlabel);
|
|
338
|
+
} catch (error) {
|
|
339
|
+
this.logger.error(
|
|
340
|
+
`Error getting enum values for ${enumTypeName}: ${error.message}`
|
|
341
|
+
);
|
|
342
|
+
return [];
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
private findMatchingEnumValue(
|
|
347
|
+
value: string,
|
|
348
|
+
enumValues: string[]
|
|
349
|
+
): string | null {
|
|
350
|
+
// Buscar coincidencia exacta
|
|
351
|
+
if (enumValues.includes(value)) {
|
|
352
|
+
return value;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Buscar coincidencia sin distinción de mayúsculas/minúsculas
|
|
356
|
+
const lowerValue = value.toLowerCase();
|
|
357
|
+
for (const enumValue of enumValues) {
|
|
358
|
+
if (enumValue.toLowerCase() === lowerValue) {
|
|
359
|
+
return enumValue;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Buscar coincidencia parcial
|
|
364
|
+
const similarValues = enumValues.filter(
|
|
365
|
+
(v) =>
|
|
366
|
+
v.toLowerCase().includes(lowerValue) ||
|
|
367
|
+
lowerValue.includes(v.toLowerCase())
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
if (similarValues.length === 1) {
|
|
371
|
+
return similarValues[0];
|
|
372
|
+
} else if (similarValues.length > 1) {
|
|
373
|
+
// Si hay múltiples coincidencias, usar la más cercana por longitud
|
|
374
|
+
return similarValues.reduce((prev, curr) =>
|
|
375
|
+
Math.abs(curr.length - value.length) <
|
|
376
|
+
Math.abs(prev.length - value.length)
|
|
377
|
+
? curr
|
|
378
|
+
: prev
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
private async cleanup() {
|
|
386
|
+
this.logger.log("Cleaning up database connections");
|
|
387
|
+
await this.targetPrisma.$disconnect();
|
|
388
|
+
await this.targetPool.end();
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Script para ejecutar desde línea de comandos
|
|
393
|
+
if (require.main === module) {
|
|
394
|
+
const run = async () => {
|
|
395
|
+
try {
|
|
396
|
+
// Obtener la ruta del archivo de log de errores
|
|
397
|
+
const errorLogPath = process.argv[2];
|
|
398
|
+
|
|
399
|
+
if (!errorLogPath) {
|
|
400
|
+
console.error(
|
|
401
|
+
"Error: No error log file path provided. Please provide the path to the error log file."
|
|
402
|
+
);
|
|
403
|
+
process.exit(1);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const retryTool = new FailedMigrationRetry(errorLogPath);
|
|
407
|
+
await retryTool.retryFailedMigrations();
|
|
408
|
+
process.exit(0);
|
|
409
|
+
} catch (error) {
|
|
410
|
+
console.error("Error:", error.message);
|
|
411
|
+
process.exit(1);
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
run();
|
|
416
|
+
}
|