chore(db): reset total de datos vivos
Deja únicamente el usuario admin y la estructura mínima del producto; elimina todos los datos vivos para la carga definitiva.
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
/**
|
||||
* One-time full live-data reset requested before loading the definitive source files.
|
||||
*
|
||||
* The database is left with only the single `admin` user and the minimum product
|
||||
* scaffolding required to keep authentication/authorization and the core asset
|
||||
* model functional. Every business, operational, imported, catalog, history,
|
||||
* document, inspection and tenant/company row is removed.
|
||||
*
|
||||
* Fresh migration replays/CI do not have the live `admin` account at this point,
|
||||
* so this migration intentionally no-ops there.
|
||||
*/
|
||||
export class FullLiveDataReset1790103000000 implements MigrationInterface {
|
||||
name = 'FullLiveDataReset1790103000000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
const adminRows = (await queryRunner.query(`
|
||||
SELECT id, username
|
||||
FROM users
|
||||
WHERE lower(btrim(username))='admin'
|
||||
ORDER BY id
|
||||
`)) as Array<{ id: string; username: string }>;
|
||||
|
||||
if (adminRows.length === 0) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('[full-live-reset] skipped: no live admin account on migration replay');
|
||||
return;
|
||||
}
|
||||
|
||||
if (adminRows.length !== 1) {
|
||||
throw new Error(
|
||||
`Full live reset aborted: expected exactly one username admin, found ${adminRows.length}`,
|
||||
);
|
||||
}
|
||||
|
||||
const adminId = adminRows[0].id;
|
||||
|
||||
// These are product/schema scaffolding, not customer/business data.
|
||||
// Everything else in public is disposable live data for this reset.
|
||||
const structuralTables = [
|
||||
'roles',
|
||||
'permissions',
|
||||
'role_permissions',
|
||||
'asset_types',
|
||||
'asset_attribute_definitions',
|
||||
'asset_type_parent_rules',
|
||||
] as const;
|
||||
|
||||
const preservedTables = new Set<string>([
|
||||
'typeorm_migrations',
|
||||
'users',
|
||||
'user_roles',
|
||||
...structuralTables,
|
||||
]);
|
||||
|
||||
const structuralCounts = new Map<string, string>();
|
||||
for (const table of structuralTables) {
|
||||
const safeTable = `"${table.replace(/"/g, '""')}"`;
|
||||
const rows = (await queryRunner.query(
|
||||
`SELECT count(*)::text AS total FROM ${safeTable}`,
|
||||
)) as Array<{ total: string }>;
|
||||
structuralCounts.set(table, rows[0]?.total ?? '0');
|
||||
}
|
||||
|
||||
const adminRolesBefore = (await queryRunner.query(
|
||||
`SELECT count(*)::text AS total FROM user_roles WHERE user_id=$1`,
|
||||
[adminId],
|
||||
)) as Array<{ total: string }>;
|
||||
const adminRoleCount = adminRolesBefore[0]?.total ?? '0';
|
||||
if (adminRoleCount === '0') {
|
||||
throw new Error('Full live reset aborted: admin has no assigned role');
|
||||
}
|
||||
|
||||
const tableRows = (await queryRunner.query(`
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema='public'
|
||||
AND table_type='BASE TABLE'
|
||||
ORDER BY table_name
|
||||
`)) as Array<{ table_name: string }>;
|
||||
|
||||
const disposableTables = tableRows
|
||||
.map((row) => row.table_name)
|
||||
.filter((table) => !preservedTables.has(table));
|
||||
|
||||
if (disposableTables.length > 0) {
|
||||
const quoted = disposableTables
|
||||
.map((table) => `"${table.replace(/"/g, '""')}"`)
|
||||
.join(', ');
|
||||
await queryRunner.query(`TRUNCATE TABLE ${quoted} RESTART IDENTITY CASCADE`);
|
||||
}
|
||||
|
||||
// Keep only the owner's administrator account. user_roles for other users
|
||||
// are removed through their FK cascade.
|
||||
await queryRunner.query(`DELETE FROM users WHERE id<>$1`, [adminId]);
|
||||
|
||||
// Invalidate any previous login state and unlock the preserved account.
|
||||
await queryRunner.query(
|
||||
`
|
||||
UPDATE users
|
||||
SET failed_login_attempts=0,
|
||||
locked_until=NULL,
|
||||
last_login_at=NULL,
|
||||
updated_at=CURRENT_TIMESTAMP
|
||||
WHERE id=$1
|
||||
`,
|
||||
[adminId],
|
||||
);
|
||||
|
||||
const finalUsers = (await queryRunner.query(`
|
||||
SELECT
|
||||
count(*)::text AS total,
|
||||
count(*) FILTER (WHERE lower(btrim(username))='admin')::text AS admins
|
||||
FROM users
|
||||
`)) as Array<{ total: string; admins: string }>;
|
||||
|
||||
if (finalUsers[0]?.total !== '1' || finalUsers[0]?.admins !== '1') {
|
||||
throw new Error('Full live reset verification failed: users table is not admin-only');
|
||||
}
|
||||
|
||||
const finalAdminRoles = (await queryRunner.query(
|
||||
`
|
||||
SELECT
|
||||
count(*) FILTER (WHERE user_id=$1)::text AS total,
|
||||
count(*) FILTER (WHERE user_id<>$1)::text AS foreign_users
|
||||
FROM user_roles
|
||||
`,
|
||||
[adminId],
|
||||
)) as Array<{ total: string; foreign_users: string }>;
|
||||
|
||||
if (
|
||||
finalAdminRoles[0]?.total !== adminRoleCount ||
|
||||
finalAdminRoles[0]?.foreign_users !== '0'
|
||||
) {
|
||||
throw new Error('Full live reset verification failed: admin role assignments changed');
|
||||
}
|
||||
|
||||
for (const table of structuralTables) {
|
||||
const safeTable = `"${table.replace(/"/g, '""')}"`;
|
||||
const rows = (await queryRunner.query(
|
||||
`SELECT count(*)::text AS total FROM ${safeTable}`,
|
||||
)) as Array<{ total: string }>;
|
||||
const before = structuralCounts.get(table) ?? '0';
|
||||
if (rows[0]?.total !== before) {
|
||||
throw new Error(
|
||||
`Full live reset verification failed: structural table ${table} changed (${before} -> ${rows[0]?.total ?? 'unknown'})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const table of disposableTables) {
|
||||
const safeTable = `"${table.replace(/"/g, '""')}"`;
|
||||
const rows = (await queryRunner.query(
|
||||
`SELECT count(*)::text AS total FROM ${safeTable}`,
|
||||
)) as Array<{ total: string }>;
|
||||
if (rows[0]?.total !== '0') {
|
||||
throw new Error(`Full live reset verification failed: ${table} is not empty`);
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
`[full-live-reset] kept admin=${adminRows[0].username} (${adminId}); preserved ${structuralTables.length} product tables; cleared ${disposableTables.length} data tables`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(): Promise<void> {
|
||||
throw new Error(
|
||||
'FullLiveDataReset is intentionally destructive; restore the automatic deploy PRE database backup instead.',
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user