377 lines
8.7 KiB
TypeScript
377 lines
8.7 KiB
TypeScript
/**
|
|
* 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<string, DependencyRule[]> = {
|
|
// 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<DependencyCheckResult> {
|
|
// 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<DeleteResult> {
|
|
// 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<Map<number | string, DependencyCheckResult>> {
|
|
const results = new Map<number | string, DependencyCheckResult>();
|
|
|
|
// 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
|
|
};
|