ante-erp-cli 1.6.12 → 1.6.14
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/CHANGELOG.md +14 -0
- package/package.json +1 -1
- package/src/commands/clone-db.js +83 -7
- package/src/utils/postgres.js +332 -9
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,20 @@ All notable changes to the ANTE CLI will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **Misleading success messages in `clone-db` command** - Now shows accurate operation status
|
|
12
|
+
- Added result tracking for Prisma generate and migrate operations
|
|
13
|
+
- Summary now displays actual success/failure instead of always showing success
|
|
14
|
+
- Improved error messages: explains that migrations may not be needed after pg_restore
|
|
15
|
+
- Shows "⚠ Prisma migrations skipped (schema already exists from restore)" when appropriate
|
|
16
|
+
- **ESLint warnings** - Removed unused variables and imports
|
|
17
|
+
- Removed unused `join` import from docker.js
|
|
18
|
+
- Removed unused `needsSudo` variable from update-cli.js
|
|
19
|
+
- Removed unused `getInstallDir` import from doctor.js
|
|
20
|
+
- Added eslint-disable comments for intentionally unused parameters
|
|
21
|
+
|
|
8
22
|
## [1.6.0] - 2025-01-28
|
|
9
23
|
|
|
10
24
|
### Added
|
package/package.json
CHANGED
package/src/commands/clone-db.js
CHANGED
|
@@ -9,7 +9,10 @@ import {
|
|
|
9
9
|
parsePostgresUrl,
|
|
10
10
|
testConnection,
|
|
11
11
|
dumpDatabase,
|
|
12
|
-
restoreDatabase
|
|
12
|
+
restoreDatabase,
|
|
13
|
+
disableRLS,
|
|
14
|
+
enableRLS,
|
|
15
|
+
verifyDataRestored
|
|
13
16
|
} from '../utils/postgres.js';
|
|
14
17
|
|
|
15
18
|
/**
|
|
@@ -187,8 +190,20 @@ export async function cloneDb(sourceUrl, options = {}) {
|
|
|
187
190
|
console.log('');
|
|
188
191
|
}
|
|
189
192
|
|
|
193
|
+
// Step 5A: Disable RLS on target database (to allow data insertion)
|
|
194
|
+
console.log(chalk.yellow('Step 5/7: Preparing target database...'));
|
|
195
|
+
const rlsDisableSpinner = ora('Disabling Row-Level Security policies...').start();
|
|
196
|
+
const rlsDisableResult = await disableRLS(localInfo, composeFile);
|
|
197
|
+
|
|
198
|
+
if (!rlsDisableResult.success) {
|
|
199
|
+
rlsDisableSpinner.warn(chalk.yellow('Could not disable RLS (this is normal for non-Supabase databases)'));
|
|
200
|
+
} else {
|
|
201
|
+
rlsDisableSpinner.succeed(chalk.green(`RLS disabled on ${rlsDisableResult.tablesAffected} tables`));
|
|
202
|
+
}
|
|
203
|
+
console.log('');
|
|
204
|
+
|
|
190
205
|
// Step 6: Restore to local database
|
|
191
|
-
console.log(chalk.yellow('Step
|
|
206
|
+
console.log(chalk.yellow('Step 6/7: Restoring to local database...'));
|
|
192
207
|
console.log(chalk.gray('This may take several minutes...'));
|
|
193
208
|
console.log('');
|
|
194
209
|
|
|
@@ -203,9 +218,46 @@ export async function cloneDb(sourceUrl, options = {}) {
|
|
|
203
218
|
restoreSpinner.succeed(chalk.green('Database restore completed'));
|
|
204
219
|
console.log('');
|
|
205
220
|
|
|
221
|
+
// Step 6A: Verify data was restored
|
|
222
|
+
const verifySpinner = ora('Verifying data restoration...').start();
|
|
223
|
+
const verifyResult = await verifyDataRestored(localInfo, composeFile);
|
|
224
|
+
|
|
225
|
+
if (!verifyResult.success) {
|
|
226
|
+
verifySpinner.fail(chalk.red('Data verification failed'));
|
|
227
|
+
console.log(chalk.yellow(' Warning: Could not verify data restoration'));
|
|
228
|
+
} else {
|
|
229
|
+
verifySpinner.succeed(chalk.green(`Verified: ${verifyResult.tableCount} tables, ${verifyResult.totalRows.toLocaleString()} rows restored`));
|
|
230
|
+
|
|
231
|
+
// Warn if no data was restored
|
|
232
|
+
if (verifyResult.totalRows === 0) {
|
|
233
|
+
console.log(chalk.red('\n⚠️ WARNING: No data was restored!'));
|
|
234
|
+
console.log(chalk.yellow(' Possible causes:'));
|
|
235
|
+
console.log(chalk.gray(' - Source database was empty'));
|
|
236
|
+
console.log(chalk.gray(' - RLS policies blocked data insertion'));
|
|
237
|
+
console.log(chalk.gray(' - Permission issues prevented data restore'));
|
|
238
|
+
console.log(chalk.gray(' - Data restore errors were silently ignored\n'));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
console.log('');
|
|
242
|
+
|
|
243
|
+
// Step 6B: Re-enable RLS on target database
|
|
244
|
+
const rlsEnableSpinner = ora('Re-enabling Row-Level Security policies...').start();
|
|
245
|
+
const rlsEnableResult = await enableRLS(localInfo, composeFile);
|
|
246
|
+
|
|
247
|
+
if (!rlsEnableResult.success) {
|
|
248
|
+
rlsEnableSpinner.warn(chalk.yellow('Could not re-enable RLS'));
|
|
249
|
+
} else {
|
|
250
|
+
rlsEnableSpinner.succeed(chalk.green(`RLS re-enabled on ${rlsEnableResult.tablesAffected} tables`));
|
|
251
|
+
}
|
|
252
|
+
console.log('');
|
|
253
|
+
|
|
254
|
+
// Track Prisma operation results
|
|
255
|
+
let generateSuccess = false;
|
|
256
|
+
let migrateSuccess = false;
|
|
257
|
+
|
|
206
258
|
// Step 7: Run Prisma operations (if not skipped)
|
|
207
259
|
if (!options.noPrisma) {
|
|
208
|
-
console.log(chalk.yellow('Step
|
|
260
|
+
console.log(chalk.yellow('Step 7/7: Running Prisma operations...'));
|
|
209
261
|
|
|
210
262
|
// Generate Prisma client
|
|
211
263
|
const generateSpinner = ora('Generating Prisma client...').start();
|
|
@@ -216,9 +268,10 @@ export async function cloneDb(sourceUrl, options = {}) {
|
|
|
216
268
|
['npx', 'prisma', 'generate']
|
|
217
269
|
);
|
|
218
270
|
generateSpinner.succeed(chalk.green('Prisma client generated'));
|
|
271
|
+
generateSuccess = true;
|
|
219
272
|
} catch (error) {
|
|
220
273
|
generateSpinner.fail(chalk.red('Prisma generate failed'));
|
|
221
|
-
console.log(chalk.yellow(' Warning: You may need to run "
|
|
274
|
+
console.log(chalk.yellow(' Warning: You may need to run "npx prisma generate" manually in the backend container'));
|
|
222
275
|
}
|
|
223
276
|
|
|
224
277
|
// Run Prisma migrations
|
|
@@ -230,9 +283,10 @@ export async function cloneDb(sourceUrl, options = {}) {
|
|
|
230
283
|
['npx', 'prisma', 'migrate', 'deploy']
|
|
231
284
|
);
|
|
232
285
|
migrateSpinner.succeed(chalk.green('Prisma migrations completed'));
|
|
286
|
+
migrateSuccess = true;
|
|
233
287
|
} catch (error) {
|
|
234
288
|
migrateSpinner.fail(chalk.red('Prisma migrate failed'));
|
|
235
|
-
console.log(chalk.yellow('
|
|
289
|
+
console.log(chalk.yellow(' Note: Database schema already exists from restore. Migrations may not be needed.'));
|
|
236
290
|
}
|
|
237
291
|
console.log('');
|
|
238
292
|
}
|
|
@@ -245,10 +299,32 @@ export async function cloneDb(sourceUrl, options = {}) {
|
|
|
245
299
|
console.log(chalk.blue('Summary:'));
|
|
246
300
|
console.log(chalk.gray(` ✓ Source database dumped to: ${dumpFile}`));
|
|
247
301
|
console.log(chalk.gray(` ✓ Restored to local database: ${localInfo.database}`));
|
|
302
|
+
|
|
303
|
+
// Show data restoration summary
|
|
304
|
+
if (verifyResult && verifyResult.success) {
|
|
305
|
+
if (verifyResult.totalRows > 0) {
|
|
306
|
+
console.log(chalk.green(` ✓ Data restored: ${verifyResult.totalRows.toLocaleString()} rows across ${verifyResult.tableCount} tables`));
|
|
307
|
+
} else {
|
|
308
|
+
console.log(chalk.red(` ✗ No data restored (${verifyResult.tableCount} tables created but empty)`));
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
248
312
|
if (!options.noPrisma) {
|
|
249
|
-
|
|
250
|
-
|
|
313
|
+
// Show Prisma generate status
|
|
314
|
+
if (generateSuccess) {
|
|
315
|
+
console.log(chalk.gray(' ✓ Prisma client generated'));
|
|
316
|
+
} else {
|
|
317
|
+
console.log(chalk.yellow(' ⚠ Prisma client generation failed - run manually if needed'));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Show Prisma migrate status
|
|
321
|
+
if (migrateSuccess) {
|
|
322
|
+
console.log(chalk.gray(' ✓ Prisma migrations applied'));
|
|
323
|
+
} else {
|
|
324
|
+
console.log(chalk.yellow(' ⚠ Prisma migrations skipped (schema already exists from restore)'));
|
|
325
|
+
}
|
|
251
326
|
}
|
|
327
|
+
|
|
252
328
|
console.log('');
|
|
253
329
|
console.log(chalk.cyan('Your local database is now cloned from the source!'));
|
|
254
330
|
console.log(chalk.gray('You can start your services with: ante start'));
|
package/src/utils/postgres.js
CHANGED
|
@@ -87,7 +87,7 @@ export async function testConnection(connectionInfo, composeFile = null) {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
dockerArgs.push(
|
|
90
|
-
'postgres:
|
|
90
|
+
'postgres:15-alpine',
|
|
91
91
|
'psql',
|
|
92
92
|
'-h', testHost,
|
|
93
93
|
'-p', port,
|
|
@@ -129,13 +129,13 @@ export async function dumpDatabase(connectionInfo, outputFile) {
|
|
|
129
129
|
// Convert to absolute path for Docker volume mount
|
|
130
130
|
const absoluteBackupDir = resolve(backupDir);
|
|
131
131
|
|
|
132
|
-
// Use Docker with PostgreSQL
|
|
132
|
+
// Use Docker with PostgreSQL 15 to match ANTE installation
|
|
133
133
|
await execa('docker', [
|
|
134
134
|
'run',
|
|
135
135
|
'--rm',
|
|
136
136
|
'-e', `PGPASSWORD=${password}`,
|
|
137
137
|
'-v', `${absoluteBackupDir}:/backups`,
|
|
138
|
-
'postgres:
|
|
138
|
+
'postgres:15-alpine',
|
|
139
139
|
'pg_dump',
|
|
140
140
|
'-h', host,
|
|
141
141
|
'-p', port,
|
|
@@ -227,8 +227,9 @@ export async function restoreDatabase(dumpFile, connectionInfo, dropSchema = tru
|
|
|
227
227
|
});
|
|
228
228
|
|
|
229
229
|
// Restore from inside the container
|
|
230
|
+
let restoreResult;
|
|
230
231
|
try {
|
|
231
|
-
await execa('docker', [
|
|
232
|
+
restoreResult = await execa('docker', [
|
|
232
233
|
'compose',
|
|
233
234
|
'-f', composeFile,
|
|
234
235
|
'exec',
|
|
@@ -244,8 +245,24 @@ export async function restoreDatabase(dumpFile, connectionInfo, dropSchema = tru
|
|
|
244
245
|
'/tmp/' + dumpFileName
|
|
245
246
|
], {
|
|
246
247
|
timeout: 600000, // 10 minute timeout
|
|
247
|
-
reject: false
|
|
248
|
+
reject: false, // Don't reject on non-zero exit (pg_restore often exits with warnings)
|
|
249
|
+
all: true // Capture both stdout and stderr
|
|
248
250
|
});
|
|
251
|
+
|
|
252
|
+
// Check for critical errors in output
|
|
253
|
+
const output = (restoreResult.stdout || '') + (restoreResult.stderr || '');
|
|
254
|
+
if (output.includes('FATAL:') || output.includes('ERROR:')) {
|
|
255
|
+
console.error('\n⚠️ pg_restore reported errors:');
|
|
256
|
+
console.error(output);
|
|
257
|
+
|
|
258
|
+
// Check for specific error patterns that indicate data restoration failed
|
|
259
|
+
if (output.includes('permission denied') ||
|
|
260
|
+
output.includes('must be owner') ||
|
|
261
|
+
output.includes('RLS policy') ||
|
|
262
|
+
output.includes('no data was returned by command')) {
|
|
263
|
+
throw new Error('Data restoration may have failed due to permission or RLS policy issues. Check output above.');
|
|
264
|
+
}
|
|
265
|
+
}
|
|
249
266
|
} finally {
|
|
250
267
|
// Clean up: Remove dump file from container
|
|
251
268
|
try {
|
|
@@ -283,7 +300,7 @@ export async function restoreDatabase(dumpFile, connectionInfo, dropSchema = tru
|
|
|
283
300
|
}
|
|
284
301
|
|
|
285
302
|
dockerArgs.push(
|
|
286
|
-
'postgres:
|
|
303
|
+
'postgres:15-alpine',
|
|
287
304
|
'psql',
|
|
288
305
|
'-h', psqlHost,
|
|
289
306
|
'-p', port,
|
|
@@ -310,7 +327,7 @@ export async function restoreDatabase(dumpFile, connectionInfo, dropSchema = tru
|
|
|
310
327
|
}
|
|
311
328
|
|
|
312
329
|
restoreArgs.push(
|
|
313
|
-
'postgres:
|
|
330
|
+
'postgres:15-alpine',
|
|
314
331
|
'pg_restore',
|
|
315
332
|
'-h', restoreHost,
|
|
316
333
|
'-p', port,
|
|
@@ -322,11 +339,28 @@ export async function restoreDatabase(dumpFile, connectionInfo, dropSchema = tru
|
|
|
322
339
|
`/backups/${join('/', dumpFileName)}`
|
|
323
340
|
);
|
|
324
341
|
|
|
342
|
+
let restoreResult;
|
|
325
343
|
try {
|
|
326
|
-
await execa('docker', restoreArgs, {
|
|
344
|
+
restoreResult = await execa('docker', restoreArgs, {
|
|
327
345
|
timeout: 600000, // 10 minute timeout
|
|
328
|
-
reject: false
|
|
346
|
+
reject: false, // Don't reject on non-zero exit (pg_restore often exits with warnings)
|
|
347
|
+
all: true // Capture both stdout and stderr
|
|
329
348
|
});
|
|
349
|
+
|
|
350
|
+
// Check for critical errors in output
|
|
351
|
+
const output = (restoreResult.stdout || '') + (restoreResult.stderr || '');
|
|
352
|
+
if (output.includes('FATAL:') || output.includes('ERROR:')) {
|
|
353
|
+
console.error('\n⚠️ pg_restore reported errors:');
|
|
354
|
+
console.error(output);
|
|
355
|
+
|
|
356
|
+
// Check for specific error patterns that indicate data restoration failed
|
|
357
|
+
if (output.includes('permission denied') ||
|
|
358
|
+
output.includes('must be owner') ||
|
|
359
|
+
output.includes('RLS policy') ||
|
|
360
|
+
output.includes('no data was returned by command')) {
|
|
361
|
+
throw new Error('Data restoration may have failed due to permission or RLS policy issues. Check output above.');
|
|
362
|
+
}
|
|
363
|
+
}
|
|
330
364
|
} catch (error) {
|
|
331
365
|
// pg_restore might return non-zero even on successful restore due to warnings
|
|
332
366
|
// We'll consider it successful if no critical error occurred
|
|
@@ -385,3 +419,292 @@ export async function runPrismaMigrate(backendDir) {
|
|
|
385
419
|
};
|
|
386
420
|
}
|
|
387
421
|
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Disable Row-Level Security on all tables (for Supabase databases)
|
|
425
|
+
* @param {Object} connectionInfo - Connection details
|
|
426
|
+
* @param {string} composeFile - Optional path to docker-compose.yml
|
|
427
|
+
* @returns {Promise<{success: boolean, tablesAffected: number, error?: string}>}
|
|
428
|
+
*/
|
|
429
|
+
export async function disableRLS(connectionInfo, composeFile = null) {
|
|
430
|
+
try {
|
|
431
|
+
const { user, password, database, schema } = connectionInfo;
|
|
432
|
+
|
|
433
|
+
const query = `
|
|
434
|
+
DO $$
|
|
435
|
+
DECLARE
|
|
436
|
+
r RECORD;
|
|
437
|
+
count INTEGER := 0;
|
|
438
|
+
BEGIN
|
|
439
|
+
FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = '${schema}')
|
|
440
|
+
LOOP
|
|
441
|
+
EXECUTE 'ALTER TABLE ' || quote_ident('${schema}') || '.' || quote_ident(r.tablename) || ' DISABLE ROW LEVEL SECURITY';
|
|
442
|
+
count := count + 1;
|
|
443
|
+
END LOOP;
|
|
444
|
+
RAISE NOTICE 'Disabled RLS on % tables', count;
|
|
445
|
+
END $$;
|
|
446
|
+
`;
|
|
447
|
+
|
|
448
|
+
let result;
|
|
449
|
+
if (composeFile) {
|
|
450
|
+
result = await execa('docker', [
|
|
451
|
+
'compose',
|
|
452
|
+
'-f', composeFile,
|
|
453
|
+
'exec',
|
|
454
|
+
'-T',
|
|
455
|
+
'-e', `PGPASSWORD=${password}`,
|
|
456
|
+
'postgres',
|
|
457
|
+
'psql',
|
|
458
|
+
'-U', user,
|
|
459
|
+
'-d', database,
|
|
460
|
+
'-c', query
|
|
461
|
+
], {
|
|
462
|
+
timeout: 30000,
|
|
463
|
+
all: true
|
|
464
|
+
});
|
|
465
|
+
} else {
|
|
466
|
+
const { host, port } = connectionInfo;
|
|
467
|
+
let psqlHost = host;
|
|
468
|
+
const dockerArgs = ['run', '--rm', '-e', `PGPASSWORD=${password}`];
|
|
469
|
+
|
|
470
|
+
if (host === 'localhost' || host === '127.0.0.1') {
|
|
471
|
+
psqlHost = 'host.docker.internal';
|
|
472
|
+
dockerArgs.push('--add-host=host.docker.internal:host-gateway');
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
dockerArgs.push(
|
|
476
|
+
'postgres:15-alpine',
|
|
477
|
+
'psql',
|
|
478
|
+
'-h', psqlHost,
|
|
479
|
+
'-p', port,
|
|
480
|
+
'-U', user,
|
|
481
|
+
'-d', database,
|
|
482
|
+
'-c', query
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
result = await execa('docker', dockerArgs, {
|
|
486
|
+
timeout: 30000,
|
|
487
|
+
all: true
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Extract table count from NOTICE message
|
|
492
|
+
const noticeMatch = result.all?.match(/Disabled RLS on (\d+) tables/);
|
|
493
|
+
const tablesAffected = noticeMatch ? parseInt(noticeMatch[1]) : 0;
|
|
494
|
+
|
|
495
|
+
return {
|
|
496
|
+
success: true,
|
|
497
|
+
tablesAffected
|
|
498
|
+
};
|
|
499
|
+
} catch (error) {
|
|
500
|
+
return {
|
|
501
|
+
success: false,
|
|
502
|
+
tablesAffected: 0,
|
|
503
|
+
error: error.message || 'Failed to disable RLS'
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Enable Row-Level Security on all tables (for Supabase databases)
|
|
510
|
+
* @param {Object} connectionInfo - Connection details
|
|
511
|
+
* @param {string} composeFile - Optional path to docker-compose.yml
|
|
512
|
+
* @returns {Promise<{success: boolean, tablesAffected: number, error?: string}>}
|
|
513
|
+
*/
|
|
514
|
+
export async function enableRLS(connectionInfo, composeFile = null) {
|
|
515
|
+
try {
|
|
516
|
+
const { user, password, database, schema } = connectionInfo;
|
|
517
|
+
|
|
518
|
+
const query = `
|
|
519
|
+
DO $$
|
|
520
|
+
DECLARE
|
|
521
|
+
r RECORD;
|
|
522
|
+
count INTEGER := 0;
|
|
523
|
+
BEGIN
|
|
524
|
+
FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = '${schema}')
|
|
525
|
+
LOOP
|
|
526
|
+
EXECUTE 'ALTER TABLE ' || quote_ident('${schema}') || '.' || quote_ident(r.tablename) || ' ENABLE ROW LEVEL SECURITY';
|
|
527
|
+
count := count + 1;
|
|
528
|
+
END LOOP;
|
|
529
|
+
RAISE NOTICE 'Enabled RLS on % tables', count;
|
|
530
|
+
END $$;
|
|
531
|
+
`;
|
|
532
|
+
|
|
533
|
+
let result;
|
|
534
|
+
if (composeFile) {
|
|
535
|
+
result = await execa('docker', [
|
|
536
|
+
'compose',
|
|
537
|
+
'-f', composeFile,
|
|
538
|
+
'exec',
|
|
539
|
+
'-T',
|
|
540
|
+
'-e', `PGPASSWORD=${password}`,
|
|
541
|
+
'postgres',
|
|
542
|
+
'psql',
|
|
543
|
+
'-U', user,
|
|
544
|
+
'-d', database,
|
|
545
|
+
'-c', query
|
|
546
|
+
], {
|
|
547
|
+
timeout: 30000,
|
|
548
|
+
all: true
|
|
549
|
+
});
|
|
550
|
+
} else {
|
|
551
|
+
const { host, port } = connectionInfo;
|
|
552
|
+
let psqlHost = host;
|
|
553
|
+
const dockerArgs = ['run', '--rm', '-e', `PGPASSWORD=${password}`];
|
|
554
|
+
|
|
555
|
+
if (host === 'localhost' || host === '127.0.0.1') {
|
|
556
|
+
psqlHost = 'host.docker.internal';
|
|
557
|
+
dockerArgs.push('--add-host=host.docker.internal:host-gateway');
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
dockerArgs.push(
|
|
561
|
+
'postgres:15-alpine',
|
|
562
|
+
'psql',
|
|
563
|
+
'-h', psqlHost,
|
|
564
|
+
'-p', port,
|
|
565
|
+
'-U', user,
|
|
566
|
+
'-d', database,
|
|
567
|
+
'-c', query
|
|
568
|
+
);
|
|
569
|
+
|
|
570
|
+
result = await execa('docker', dockerArgs, {
|
|
571
|
+
timeout: 30000,
|
|
572
|
+
all: true
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Extract table count from NOTICE message
|
|
577
|
+
const noticeMatch = result.all?.match(/Enabled RLS on (\d+) tables/);
|
|
578
|
+
const tablesAffected = noticeMatch ? parseInt(noticeMatch[1]) : 0;
|
|
579
|
+
|
|
580
|
+
return {
|
|
581
|
+
success: true,
|
|
582
|
+
tablesAffected
|
|
583
|
+
};
|
|
584
|
+
} catch (error) {
|
|
585
|
+
return {
|
|
586
|
+
success: false,
|
|
587
|
+
tablesAffected: 0,
|
|
588
|
+
error: error.message || 'Failed to enable RLS'
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Verify data was restored by counting rows in all tables
|
|
595
|
+
* @param {Object} connectionInfo - Connection details
|
|
596
|
+
* @param {string} composeFile - Optional path to docker-compose.yml
|
|
597
|
+
* @returns {Promise<{success: boolean, tableCount: number, totalRows: number, error?: string}>}
|
|
598
|
+
*/
|
|
599
|
+
export async function verifyDataRestored(connectionInfo, composeFile = null) {
|
|
600
|
+
try {
|
|
601
|
+
const { user, password, database, schema } = connectionInfo;
|
|
602
|
+
|
|
603
|
+
// Count tables
|
|
604
|
+
const tableCountQuery = `SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = '${schema}'`;
|
|
605
|
+
|
|
606
|
+
// Count total rows across all tables (excluding _prisma_migrations)
|
|
607
|
+
const rowCountQuery = `
|
|
608
|
+
SELECT COALESCE(SUM(n_live_tup), 0)::bigint as total_rows
|
|
609
|
+
FROM pg_stat_user_tables
|
|
610
|
+
WHERE schemaname = '${schema}' AND relname != '_prisma_migrations'
|
|
611
|
+
`;
|
|
612
|
+
|
|
613
|
+
let tableCountResult, rowCountResult;
|
|
614
|
+
|
|
615
|
+
if (composeFile) {
|
|
616
|
+
// Get table count
|
|
617
|
+
tableCountResult = await execa('docker', [
|
|
618
|
+
'compose',
|
|
619
|
+
'-f', composeFile,
|
|
620
|
+
'exec',
|
|
621
|
+
'-T',
|
|
622
|
+
'-e', `PGPASSWORD=${password}`,
|
|
623
|
+
'postgres',
|
|
624
|
+
'psql',
|
|
625
|
+
'-U', user,
|
|
626
|
+
'-d', database,
|
|
627
|
+
'-t', // Tuples only (no headers)
|
|
628
|
+
'-c', tableCountQuery
|
|
629
|
+
], {
|
|
630
|
+
timeout: 10000
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
// Get row count
|
|
634
|
+
rowCountResult = await execa('docker', [
|
|
635
|
+
'compose',
|
|
636
|
+
'-f', composeFile,
|
|
637
|
+
'exec',
|
|
638
|
+
'-T',
|
|
639
|
+
'-e', `PGPASSWORD=${password}`,
|
|
640
|
+
'postgres',
|
|
641
|
+
'psql',
|
|
642
|
+
'-U', user,
|
|
643
|
+
'-d', database,
|
|
644
|
+
'-t',
|
|
645
|
+
'-c', rowCountQuery
|
|
646
|
+
], {
|
|
647
|
+
timeout: 10000
|
|
648
|
+
});
|
|
649
|
+
} else {
|
|
650
|
+
const { host, port } = connectionInfo;
|
|
651
|
+
let psqlHost = host;
|
|
652
|
+
const dockerArgs = ['run', '--rm', '-e', `PGPASSWORD=${password}`];
|
|
653
|
+
|
|
654
|
+
if (host === 'localhost' || host === '127.0.0.1') {
|
|
655
|
+
psqlHost = 'host.docker.internal';
|
|
656
|
+
dockerArgs.push('--add-host=host.docker.internal:host-gateway');
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Get table count
|
|
660
|
+
const tableCountArgs = [...dockerArgs];
|
|
661
|
+
tableCountArgs.push(
|
|
662
|
+
'postgres:15-alpine',
|
|
663
|
+
'psql',
|
|
664
|
+
'-h', psqlHost,
|
|
665
|
+
'-p', port,
|
|
666
|
+
'-U', user,
|
|
667
|
+
'-d', database,
|
|
668
|
+
'-t',
|
|
669
|
+
'-c', tableCountQuery
|
|
670
|
+
);
|
|
671
|
+
|
|
672
|
+
tableCountResult = await execa('docker', tableCountArgs, {
|
|
673
|
+
timeout: 10000
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
// Get row count
|
|
677
|
+
const rowCountArgs = [...dockerArgs];
|
|
678
|
+
rowCountArgs.push(
|
|
679
|
+
'postgres:15-alpine',
|
|
680
|
+
'psql',
|
|
681
|
+
'-h', psqlHost,
|
|
682
|
+
'-p', port,
|
|
683
|
+
'-U', user,
|
|
684
|
+
'-d', database,
|
|
685
|
+
'-t',
|
|
686
|
+
'-c', rowCountQuery
|
|
687
|
+
);
|
|
688
|
+
|
|
689
|
+
rowCountResult = await execa('docker', rowCountArgs, {
|
|
690
|
+
timeout: 10000
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const tableCount = parseInt(tableCountResult.stdout.trim());
|
|
695
|
+
const totalRows = parseInt(rowCountResult.stdout.trim() || '0');
|
|
696
|
+
|
|
697
|
+
return {
|
|
698
|
+
success: true,
|
|
699
|
+
tableCount,
|
|
700
|
+
totalRows
|
|
701
|
+
};
|
|
702
|
+
} catch (error) {
|
|
703
|
+
return {
|
|
704
|
+
success: false,
|
|
705
|
+
tableCount: 0,
|
|
706
|
+
totalRows: 0,
|
|
707
|
+
error: error.message || 'Failed to verify data'
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
}
|