/** * Dependency Checker Utility * * Verifies if a record can be safely deleted by checking for child references. * Provides warnings and prevents accidental deletion of records with dependencies. */ import { query } from '../config/database'; // Types export interface DependencyRule { childTable: string; childColumn: string; displayName: string; } export interface DependencyCheckResult { canDelete: boolean; hasChildren: boolean; dependencies: { table: string; displayName: string; count: number; }[]; totalChildren: number; warning?: string; } export interface DeleteResult { success: boolean; deleted: boolean; message: string; hadDependencies?: boolean; dependencyDetails?: DependencyCheckResult; } /** * Dependency map for all tables that have child references * Key: parent table name * Value: array of child table references */ const DEPENDENCY_MAP: Record = { // Analysis Core 'dimension': [{ childTable: 'subdimension', childColumn: 'dimension_id', displayName: 'Subdimensiuni' }], 'subdimension': [{ childTable: 'technique', childColumn: 'subdimension_id', displayName: 'Tehnici' }], 'technique': [ { childTable: 'technique_indicator', childColumn: 'technique_id', displayName: 'Indicatori' }, { childTable: 'technique_validation_rule', childColumn: 'technique_id', displayName: 'Reguli de validare' } ], // Source Assessment 'platform_modifier': [{ childTable: 'platform', childColumn: 'platform_modifier_id', displayName: 'Platforme' }], 'source_type': [{ childTable: 'domain_attribute', childColumn: 'source_type_id', displayName: 'Atribute domeniu' }], 'source_credibility': [{ childTable: 'domain_attribute', childColumn: 'source_credibility_id', displayName: 'Atribute domeniu' }], 'domain_age_score': [{ childTable: 'domain_attribute', childColumn: 'domain_age_score', displayName: 'Atribute domeniu' }], 'domain_risk_level': [{ childTable: 'domain_attribute', childColumn: 'domain_risk_level_id', displayName: 'Atribute domeniu' }], 'domain_red_flag': [{ childTable: 'domain_attribute', childColumn: 'domain_red_flag_id', displayName: 'Atribute domeniu' }], 'author_classification': [{ childTable: 'author', childColumn: 'author_classification_id', displayName: 'Autori' }], 'author_credibility': [{ childTable: 'author', childColumn: 'author_credibility_id', displayName: 'Autori' }], // Source hierarchy 'source': [ { childTable: 'platform', childColumn: 'platform_id', displayName: 'Platforme' }, { childTable: 'domain', childColumn: 'domain_id', displayName: 'Domenii' }, { childTable: 'author', childColumn: 'author_id', displayName: 'Autori' } ], 'domain': [{ childTable: 'domain_attribute', childColumn: 'domain_id', displayName: 'Atribute domeniu' }] }; /** * Tables that can be deleted without any child checks */ const LEAF_TABLES = new Set([ 'technique_indicator', 'technique_validation_rule', 'platform', 'domain_attribute', 'author', 'claim', 'claim_type', 'confidence', 'interpretation', 'verdict_category', 'risk_mapping', 'severity_assessment', 'component_weight', 'weight_scenario', 'multiplier', 'source_assessment', 'notification', 'email_server' ]); /** * Check if a table has dependency rules defined */ export function hasDependencyRules(tableName: string): boolean { return tableName in DEPENDENCY_MAP; } /** * Check if a table is a leaf table (no children possible) */ export function isLeafTable(tableName: string): boolean { return LEAF_TABLES.has(tableName); } /** * Get dependency rules for a table */ export function getDependencyRules(tableName: string): DependencyRule[] { return DEPENDENCY_MAP[tableName] || []; } /** * Check dependencies for a specific record before deletion * * @param tableName - The table name to check * @param idColumn - The primary key column name * @param idValue - The primary key value * @returns DependencyCheckResult with details about children */ export async function checkDependencies( tableName: string, idColumn: string, idValue: number | string ): Promise { // If it's a leaf table, it can always be deleted if (isLeafTable(tableName)) { return { canDelete: true, hasChildren: false, dependencies: [], totalChildren: 0 }; } const rules = getDependencyRules(tableName); // If no rules defined, assume it can be deleted if (rules.length === 0) { return { canDelete: true, hasChildren: false, dependencies: [], totalChildren: 0 }; } const dependencies: { table: string; displayName: string; count: number }[] = []; let totalChildren = 0; // Check each dependency rule for (const rule of rules) { const countQuery = `SELECT COUNT(*) as count FROM ${rule.childTable} WHERE ${rule.childColumn} = $1`; const result = await query<{ count: string }>(countQuery, [idValue]); const count = parseInt(result[0]?.count || '0', 10); if (count > 0) { dependencies.push({ table: rule.childTable, displayName: rule.displayName, count }); totalChildren += count; } } const hasChildren = totalChildren > 0; return { canDelete: !hasChildren, hasChildren, dependencies, totalChildren, warning: hasChildren ? `Nu se poate șterge: există ${totalChildren} înregistrări dependente (${dependencies.map(d => `${d.count} ${d.displayName}`).join(', ')})` : undefined }; } /** * Safe delete function - checks dependencies before deleting * * @param tableName - Table to delete from * @param idColumn - Primary key column name * @param idValue - Primary key value * @param force - If true, attempts to delete even with children (will fail at DB level) * @returns DeleteResult with success status and details */ export async function safeDelete( tableName: string, idColumn: string, idValue: number | string, force: boolean = false ): Promise { // First check dependencies const depCheck = await checkDependencies(tableName, idColumn, idValue); if (depCheck.hasChildren && !force) { return { success: false, deleted: false, message: depCheck.warning || 'Record has dependencies and cannot be deleted', hadDependencies: true, dependencyDetails: depCheck }; } // Attempt to delete try { const deleteQuery = `DELETE FROM ${tableName} WHERE ${idColumn} = $1 RETURNING *`; const result = await query(deleteQuery, [idValue]); if (result.length === 0) { return { success: false, deleted: false, message: 'Înregistrarea nu a fost găsită' }; } return { success: true, deleted: true, message: 'Înregistrarea a fost ștearsă cu succes' }; } catch (error: any) { // Handle FK violation errors from PostgreSQL if (error.code === '23503') { // foreign_key_violation return { success: false, deleted: false, message: 'Nu se poate șterge: există înregistrări dependente în baza de date', hadDependencies: true }; } throw error; } } /** * Get a summary of all dependencies for a table (useful for UI display) */ export function getDependencySummary(tableName: string): { hasRules: boolean; isLeaf: boolean; rules: DependencyRule[]; canHaveChildren: boolean; } { const isLeaf = isLeafTable(tableName); const rules = getDependencyRules(tableName); return { hasRules: rules.length > 0, isLeaf, rules, canHaveChildren: !isLeaf && rules.length > 0 }; } /** * Batch check dependencies for multiple records */ export async function batchCheckDependencies( tableName: string, idColumn: string, idValues: (number | string)[] ): Promise> { const results = new Map(); // For leaf tables, all can be deleted if (isLeafTable(tableName)) { for (const id of idValues) { results.set(id, { canDelete: true, hasChildren: false, dependencies: [], totalChildren: 0 }); } return results; } // Check each record for (const id of idValues) { const check = await checkDependencies(tableName, idColumn, id); results.set(id, check); } return results; } export default { checkDependencies, safeDelete, hasDependencyRules, isLeafTable, getDependencyRules, getDependencySummary, batchCheckDependencies };