c
This commit is contained in:
374
scripts/wipeCompany.js
Normal file
374
scripts/wipeCompany.js
Normal file
@@ -0,0 +1,374 @@
|
||||
import readline from 'readline';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import chalk from 'chalk';
|
||||
import { getProvider, closeStore } from '../src/db/store.js';
|
||||
import env from '../src/config/env.js';
|
||||
|
||||
function prompt(question) {
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
return new Promise((resolve) => {
|
||||
rl.question(question, (answer) => {
|
||||
rl.close();
|
||||
resolve(answer.trim());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function getAllDescendantCategoryIds(provider, rootCategoryId) {
|
||||
if (!rootCategoryId) return [];
|
||||
const catIds = [rootCategoryId];
|
||||
let queue = [rootCategoryId];
|
||||
|
||||
while (queue.length > 0) {
|
||||
const placeholders = queue.map((_, i) => `@p${i}`).join(',');
|
||||
const params = Object.fromEntries(queue.map((id, i) => [`p${i}`, id]));
|
||||
const res = await provider.query(
|
||||
`SELECT id FROM category WHERE parent_id IN (${placeholders})`,
|
||||
params
|
||||
);
|
||||
queue = res.rows.map((r) => r.id);
|
||||
catIds.push(...queue);
|
||||
}
|
||||
return catIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sweeps the storage directory for image files on disk not linked to any pic record.
|
||||
*/
|
||||
async function cleanStorageDiskFiles(provider) {
|
||||
const activePics = await provider.query('SELECT hash FROM pic');
|
||||
const validHashes = new Set(activePics.rows.map((r) => r.hash));
|
||||
|
||||
const storageDir = path.resolve(process.cwd(), 'data/storage');
|
||||
const subdirs = ['fullbinary', 'thumbbinary', 'thumbavif'];
|
||||
let removedFiles = 0;
|
||||
|
||||
for (const sub of subdirs) {
|
||||
const dirPath = path.join(storageDir, sub);
|
||||
if (!fs.existsSync(dirPath)) continue;
|
||||
|
||||
const files = fs.readdirSync(dirPath);
|
||||
for (const file of files) {
|
||||
const hash = file.replace(/\.[^.]+$/, '');
|
||||
if (!validHashes.has(hash)) {
|
||||
try {
|
||||
fs.unlinkSync(path.join(dirPath, file));
|
||||
removedFiles++;
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
}
|
||||
return removedFiles;
|
||||
}
|
||||
|
||||
async function wipeCompany(companyIdentifier, autoConfirm = false) {
|
||||
const provider = await getProvider();
|
||||
const isSqlite = env.dbProvider === 'sqlite' || env.dbProvider === 'better-sqlite3';
|
||||
|
||||
// 1. Locate company
|
||||
let compRes;
|
||||
const numId = Number(companyIdentifier);
|
||||
if (Number.isInteger(numId) && numId > 0) {
|
||||
compRes = await provider.query('SELECT id, name, root_category_id FROM company WHERE id = @id', { id: numId });
|
||||
} else {
|
||||
compRes = await provider.query('SELECT id, name, root_category_id FROM company WHERE name = @name', { name: companyIdentifier });
|
||||
}
|
||||
|
||||
if (!compRes.rows[0]) {
|
||||
console.error(chalk.red(`\nCompany "${companyIdentifier}" not found.\n`));
|
||||
const allCompanies = await provider.query('SELECT id, name FROM company ORDER BY id');
|
||||
console.log(chalk.yellow('Existing companies:'));
|
||||
for (const c of allCompanies.rows) {
|
||||
console.log(` - [ID: ${c.id}] ${c.name}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const company = compRes.rows[0];
|
||||
const companyId = company.id;
|
||||
const companyName = company.name;
|
||||
const rootCatId = company.root_category_id;
|
||||
|
||||
console.log(chalk.bold.yellow(`\n⚠️ WARNING: You are about to completely wipe company:`));
|
||||
console.log(` ID: ${chalk.bold.white(companyId)}`);
|
||||
console.log(` Name: ${chalk.bold.white(companyName)}\n`);
|
||||
|
||||
if (!autoConfirm) {
|
||||
const confirmation = await prompt(
|
||||
chalk.red(`To proceed, type the exact company name "${companyName}": `)
|
||||
);
|
||||
if (confirmation !== companyName) {
|
||||
console.log(chalk.gray('Aborted. Nothing was deleted.'));
|
||||
await closeStore();
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(chalk.cyan(`\nStarting wipe for "${companyName}" (ID: ${companyId})...`));
|
||||
|
||||
// 2. Collect candidate category IDs
|
||||
const categoryIds = await getAllDescendantCategoryIds(provider, rootCatId);
|
||||
console.log(` Found ${categoryIds.length} categories in company tree.`);
|
||||
|
||||
// 3. Begin deletion in dependency order
|
||||
await provider.transaction(async (tx) => {
|
||||
if (isSqlite) {
|
||||
await tx.query('PRAGMA defer_foreign_keys = ON');
|
||||
}
|
||||
|
||||
// Wipe google_auth users who had this company selected or belonged to it
|
||||
const authRes = await tx.query(`
|
||||
DELETE FROM google_auth
|
||||
WHERE selected_company = @companyId
|
||||
OR id IN (SELECT google_auth_id FROM companyuser WHERE company_id = @companyId)
|
||||
`, { companyId });
|
||||
console.log(` Deleted ${authRes.rowsAffected || 0} associated google_auth user(s).`);
|
||||
|
||||
// Manifest, Attachments, Warehouse & Ledger
|
||||
await tx.query(`DELETE FROM manifestattachment WHERE manifest_id IN (SELECT id FROM manifest WHERE company_id = @companyId)`, { companyId });
|
||||
await tx.query(`DELETE FROM ledger WHERE warehouse_id IN (SELECT id FROM warehouse WHERE company_id = @companyId)`, { companyId });
|
||||
await tx.query(`DELETE FROM ledger WHERE manifestarticle_id IN (SELECT ma.id FROM manifestarticle ma JOIN manifest m ON m.id = ma.manifest_id WHERE m.company_id = @companyId)`, { companyId });
|
||||
await tx.query(`DELETE FROM manifestarticle WHERE manifest_id IN (SELECT id FROM manifest WHERE company_id = @companyId)`, { companyId });
|
||||
await tx.query('DELETE FROM warehouse WHERE company_id = @companyId', { companyId });
|
||||
await tx.query('DELETE FROM manifest WHERE company_id = @companyId', { companyId });
|
||||
|
||||
// Hardware / Settings / History
|
||||
await tx.query(`DELETE FROM posterminalcategory WHERE posterminal_id IN (SELECT id FROM posterminal WHERE company_id = @companyId)`, { companyId });
|
||||
await tx.query('DELETE FROM posterminal WHERE company_id = @companyId', { companyId });
|
||||
await tx.query('DELETE FROM printer WHERE company_id = @companyId', { companyId });
|
||||
await tx.query('DELETE FROM labeltemplate WHERE company_id = @companyId', { companyId });
|
||||
await tx.query('DELETE FROM companysetting WHERE company_id = @companyId', { companyId });
|
||||
await tx.query('DELETE FROM companyhistory WHERE company_id = @companyId', { companyId });
|
||||
await tx.query('DELETE FROM numbers WHERE company_id = @companyId', { companyId });
|
||||
await tx.query('DELETE FROM articletombstone WHERE company_id = @companyId', { companyId });
|
||||
await tx.query('DELETE FROM modelvfs WHERE company_id = @companyId', { companyId });
|
||||
await tx.query(`DELETE FROM modelconversation WHERE companyuser_id IN (SELECT id FROM companyuser WHERE company_id = @companyId)`, { companyId });
|
||||
|
||||
// Articles & Sub-tables
|
||||
const artCount = (await tx.query('SELECT COUNT(*) as cnt FROM article WHERE company_id = @companyId', { companyId })).rows[0].cnt;
|
||||
await tx.query('DELETE FROM articleean WHERE article_id IN (SELECT id FROM article WHERE company_id = @companyId)', { companyId });
|
||||
await tx.query('DELETE FROM articletext WHERE article_id IN (SELECT id FROM article WHERE company_id = @companyId)', { companyId });
|
||||
await tx.query('DELETE FROM articlepic WHERE article_id IN (SELECT id FROM article WHERE company_id = @companyId)', { companyId });
|
||||
await tx.query('DELETE FROM articlehistory WHERE article_id IN (SELECT id FROM article WHERE company_id = @companyId)', { companyId });
|
||||
await tx.query('DELETE FROM price WHERE article_id IN (SELECT id FROM article WHERE company_id = @companyId)', { companyId });
|
||||
await tx.query('DELETE FROM articlesupplier WHERE article_id IN (SELECT id FROM article WHERE company_id = @companyId)', { companyId });
|
||||
await tx.query('DELETE FROM articleproperty WHERE article_id IN (SELECT id FROM article WHERE company_id = @companyId)', { companyId });
|
||||
await tx.query('DELETE FROM articleattributevalue WHERE article_id IN (SELECT id FROM article WHERE company_id = @companyId)', { companyId });
|
||||
await tx.query('DELETE FROM articlelistarticle WHERE article_id IN (SELECT id FROM article WHERE company_id = @companyId)', { companyId });
|
||||
await tx.query('DELETE FROM optionslistarticle WHERE article_id IN (SELECT id FROM article WHERE company_id = @companyId)', { companyId });
|
||||
await tx.query('DELETE FROM categoryarticle WHERE article_id IN (SELECT id FROM article WHERE company_id = @companyId)', { companyId });
|
||||
await tx.query('DELETE FROM article WHERE company_id = @companyId', { companyId });
|
||||
console.log(` Deleted ${artCount} articles and all child rows.`);
|
||||
|
||||
// Clean orphaned optionslist and articlelist
|
||||
const optRes = await tx.query(`
|
||||
DELETE FROM optionslist
|
||||
WHERE id NOT IN (SELECT optionslist_id FROM article WHERE optionslist_id IS NOT NULL)
|
||||
`);
|
||||
const artListRes = await tx.query(`
|
||||
DELETE FROM articlelist
|
||||
WHERE id NOT IN (SELECT articlelist_id FROM article WHERE articlelist_id IS NOT NULL)
|
||||
`);
|
||||
console.log(` Cleaned ${optRes.rowsAffected || 0} optionslist and ${artListRes.rowsAffected || 0} articlelist entries.`);
|
||||
|
||||
// Brand & Unit
|
||||
await tx.query('DELETE FROM brandpic WHERE brand_id IN (SELECT id FROM brand WHERE company_id = @companyId)', { companyId });
|
||||
await tx.query('DELETE FROM brand WHERE company_id = @companyId', { companyId });
|
||||
await tx.query('DELETE FROM unittext WHERE unit_id IN (SELECT id FROM unit WHERE company_id = @companyId)', { companyId });
|
||||
await tx.query('DELETE FROM unit WHERE company_id = @companyId', { companyId });
|
||||
|
||||
// Properties & Attributes defined by company
|
||||
await tx.query('DELETE FROM propertyname WHERE company_id = @companyId', { companyId });
|
||||
await tx.query('DELETE FROM attributename WHERE company_id = @companyId', { companyId });
|
||||
|
||||
// Category tree
|
||||
if (categoryIds.length > 0) {
|
||||
const catIn = categoryIds.join(',');
|
||||
await tx.query(`DELETE FROM categoryarticle WHERE category_id IN (${catIn})`);
|
||||
await tx.query(`DELETE FROM categorypic WHERE category_id IN (${catIn})`);
|
||||
await tx.query(`DELETE FROM categorytext WHERE category_id IN (${catIn})`);
|
||||
await tx.query(`DELETE FROM categoryproperty WHERE category_id IN (${catIn})`);
|
||||
await tx.query(`DELETE FROM categoryattributevalue WHERE category_id IN (${catIn})`);
|
||||
|
||||
// Detach root_category_id from company first
|
||||
await tx.query('UPDATE company SET root_category_id = NULL WHERE id = @companyId', { companyId });
|
||||
|
||||
// Delete categories from leaf to root
|
||||
const sortedDescCats = [...categoryIds].reverse();
|
||||
for (const catId of sortedDescCats) {
|
||||
await tx.query('DELETE FROM category WHERE id = @catId', { catId });
|
||||
}
|
||||
console.log(` Deleted ${categoryIds.length} categories.`);
|
||||
}
|
||||
|
||||
// Entities & Customers
|
||||
await tx.query('DELETE FROM entityaddress WHERE entity_id IN (SELECT id FROM entity WHERE company_id = @companyId)', { companyId });
|
||||
await tx.query('DELETE FROM entitycontact WHERE entity_id IN (SELECT id FROM entity WHERE company_id = @companyId)', { companyId });
|
||||
await tx.query('DELETE FROM entity WHERE company_id = @companyId', { companyId });
|
||||
await tx.query('DELETE FROM customergroup WHERE company_id = @companyId', { companyId });
|
||||
|
||||
// Company User & Company itself
|
||||
await tx.query('DELETE FROM companyuser WHERE company_id = @companyId', { companyId });
|
||||
await tx.query('DELETE FROM company WHERE id = @companyId', { companyId });
|
||||
console.log(` Deleted company record.`);
|
||||
});
|
||||
|
||||
// 4. Clean up ALL orphaned pictures across the database
|
||||
const orphanedPics = await provider.query(`
|
||||
SELECT id, hash FROM pic
|
||||
WHERE id NOT IN (SELECT pic_id FROM articlepic)
|
||||
AND id NOT IN (SELECT pic_id FROM categorypic)
|
||||
AND id NOT IN (SELECT pic_id FROM brandpic)
|
||||
`);
|
||||
|
||||
if (orphanedPics.rows.length > 0) {
|
||||
const orphanIds = orphanedPics.rows.map((r) => r.id).join(',');
|
||||
await provider.query(`DELETE FROM pic WHERE id IN (${orphanIds})`);
|
||||
console.log(` Deleted ${orphanedPics.rows.length} orphaned rows from pic table.`);
|
||||
}
|
||||
|
||||
// 5. Clean up files on disk
|
||||
const removedFiles = await cleanStorageDiskFiles(provider);
|
||||
if (removedFiles > 0) {
|
||||
console.log(` Removed ${removedFiles} orphaned image files from data/storage/`);
|
||||
}
|
||||
|
||||
// 6. Remove data/vfs/<companyId> and data/storage/vfs/<companyId>
|
||||
const vfsDir = path.resolve(process.cwd(), 'data/vfs', String(companyId));
|
||||
if (fs.existsSync(vfsDir)) {
|
||||
fs.rmSync(vfsDir, { recursive: true, force: true });
|
||||
console.log(` Removed VFS directory at data/vfs/${companyId}`);
|
||||
}
|
||||
|
||||
const storageVfsDir = path.resolve(process.cwd(), 'data/storage/vfs', String(companyId));
|
||||
if (fs.existsSync(storageVfsDir)) {
|
||||
fs.rmSync(storageVfsDir, { recursive: true, force: true });
|
||||
console.log(` Removed VFS storage directory at data/storage/vfs/${companyId}`);
|
||||
}
|
||||
|
||||
// 7. Remove import directory from disk if exists
|
||||
const importDir = path.resolve(process.cwd(), 'data/import', String(companyId));
|
||||
if (fs.existsSync(importDir)) {
|
||||
fs.rmSync(importDir, { recursive: true, force: true });
|
||||
console.log(` Removed import files at data/import/${companyId}`);
|
||||
}
|
||||
|
||||
console.log(chalk.green(`\n✔ Company "${companyName}" (ID: ${companyId}) completely wiped.\n`));
|
||||
await closeStore();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean all orphaned storage, vfs, and pic records across the entire database.
|
||||
*/
|
||||
async function cleanAllOrphans() {
|
||||
const provider = await getProvider();
|
||||
console.log(chalk.cyan('\nCleaning all orphaned pics, optionslists, articlelists, and storage files...'));
|
||||
|
||||
const remainingCompanies = await provider.query('SELECT COUNT(*) as cnt FROM company');
|
||||
const compCount = remainingCompanies.rows[0].cnt;
|
||||
|
||||
// Delete orphaned pics
|
||||
const orphanedPics = await provider.query(`
|
||||
SELECT id, hash FROM pic
|
||||
WHERE id NOT IN (SELECT pic_id FROM articlepic)
|
||||
AND id NOT IN (SELECT pic_id FROM categorypic)
|
||||
AND id NOT IN (SELECT pic_id FROM brandpic)
|
||||
`);
|
||||
|
||||
if (orphanedPics.rows.length > 0) {
|
||||
const orphanIds = orphanedPics.rows.map((r) => r.id).join(',');
|
||||
await provider.query(`DELETE FROM pic WHERE id IN (${orphanIds})`);
|
||||
console.log(` Deleted ${orphanedPics.rows.length} orphaned rows from pic table.`);
|
||||
}
|
||||
|
||||
// Delete orphaned optionslist and articlelist
|
||||
const optRes = await provider.query(`
|
||||
DELETE FROM optionslist
|
||||
WHERE id NOT IN (SELECT optionslist_id FROM article WHERE optionslist_id IS NOT NULL)
|
||||
`);
|
||||
const artListRes = await provider.query(`
|
||||
DELETE FROM articlelist
|
||||
WHERE id NOT IN (SELECT articlelist_id FROM article WHERE articlelist_id IS NOT NULL)
|
||||
`);
|
||||
console.log(` Cleaned ${optRes.rowsAffected || 0} optionslist and ${artListRes.rowsAffected || 0} articlelist rows.`);
|
||||
|
||||
// Wipe google_auth users who have no selected_company or point to nonexistent company
|
||||
const authRes = await provider.query(`
|
||||
DELETE FROM google_auth
|
||||
WHERE selected_company IS NULL
|
||||
OR selected_company NOT IN (SELECT id FROM company)
|
||||
`);
|
||||
console.log(` Cleaned ${authRes.rowsAffected || 0} google_auth user(s) with no valid company.`);
|
||||
|
||||
// Storage files on disk
|
||||
const removedFiles = await cleanStorageDiskFiles(provider);
|
||||
console.log(` Removed ${removedFiles} orphaned image files from data/storage/`);
|
||||
|
||||
// If 0 companies remain, clear any leftover folders in data/vfs and data/import
|
||||
if (compCount === 0) {
|
||||
const vfsDir = path.resolve(process.cwd(), 'data/vfs');
|
||||
if (fs.existsSync(vfsDir)) {
|
||||
for (const f of fs.readdirSync(vfsDir)) {
|
||||
fs.rmSync(path.join(vfsDir, f), { recursive: true, force: true });
|
||||
}
|
||||
console.log(` Cleaned all entries in data/vfs/`);
|
||||
}
|
||||
const storageVfs = path.resolve(process.cwd(), 'data/storage/vfs');
|
||||
if (fs.existsSync(storageVfs)) {
|
||||
for (const f of fs.readdirSync(storageVfs)) {
|
||||
fs.rmSync(path.join(storageVfs, f), { recursive: true, force: true });
|
||||
}
|
||||
console.log(` Cleaned all entries in data/storage/vfs/`);
|
||||
}
|
||||
const importDir = path.resolve(process.cwd(), 'data/import');
|
||||
if (fs.existsSync(importDir)) {
|
||||
for (const f of fs.readdirSync(importDir)) {
|
||||
fs.rmSync(path.join(importDir, f), { recursive: true, force: true });
|
||||
}
|
||||
console.log(` Cleaned all entries in data/import/`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(chalk.green('\n✔ Cleanup complete.\n'));
|
||||
await closeStore();
|
||||
}
|
||||
|
||||
async function listCompanies() {
|
||||
const provider = await getProvider();
|
||||
const res = await provider.query(`
|
||||
SELECT c.id, c.name,
|
||||
(SELECT COUNT(*) FROM article a WHERE a.company_id = c.id) AS articles,
|
||||
(SELECT COUNT(*) FROM companyuser cu WHERE cu.company_id = c.id) AS users
|
||||
FROM company c
|
||||
ORDER BY c.id
|
||||
`);
|
||||
|
||||
console.log(chalk.bold('\nAvailable companies:'));
|
||||
console.table(res.rows.map((r) => ({
|
||||
ID: r.id,
|
||||
Name: r.name,
|
||||
Articles: r.articles,
|
||||
Users: r.users,
|
||||
})));
|
||||
console.log(`Usage:`);
|
||||
console.log(` npm run wipe-company <companyId-or-name> [--yes]`);
|
||||
console.log(` npm run wipe-company -- --clean-orphans\n`);
|
||||
await closeStore();
|
||||
}
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const autoConfirm = args.includes('--yes') || args.includes('-y');
|
||||
const shouldCleanOrphans = args.includes('--clean-orphans');
|
||||
const target = args.find((a) => !a.startsWith('-'));
|
||||
|
||||
if (shouldCleanOrphans) {
|
||||
await cleanAllOrphans();
|
||||
} else if (!target) {
|
||||
await listCompanies();
|
||||
} else {
|
||||
await wipeCompany(target, autoConfirm);
|
||||
}
|
||||
Reference in New Issue
Block a user