feat: add registry repair framework

- add shared registry validation service
- add transaction-safe repair CLI
- refactor doctor to use validator
- improve restore integrity checks
- add registry repair integration tests
- strengthen global slug consistency
This commit is contained in:
thakares committed 2026-06-29 16:53:05 +05:30
1 parent 2761863c14
commit f4947489af
12 files changed
+796 -255

No files matched your search

+9 -7
View File
@@ -356,17 +356,19 @@ impl Db {
{
let system_conn = db.system.lock().unwrap();
let users_conn = db.users.lock().unwrap();
match crate::db::users::verify_global_slug_registry_integrity(
match crate::services::registry_validator::RegistryValidator::scan(
&system_conn,
&users_conn,
&config.data_dir,
None,
) {
Ok((errors, warnings)) => {
for err in errors {
tracing::error!("Global registry integrity error: {}", err);
}
for warn in warnings {
tracing::warn!("Global registry integrity warning: {}", warn);
Ok(issues) => {
for issue in issues {
tracing::error!(
"Global registry integrity issue: {:?} for slug {}",
issue.issue_type,
issue.slug
);
}
}
Err(e) => {
-210
View File
@@ -722,216 +722,6 @@ pub fn cleanup_stale_reservations(
Ok(cleaned_count)
}
pub fn verify_global_slug_registry_integrity(
system_conn: &Connection,
users_conn: &Connection,
data_dir: &std::path::Path,
) -> Result<(Vec<String>, Vec<String>), Box<dyn std::error::Error>> {
use chrono::{DateTime, Utc};
let mut errors = Vec::new();
let mut warnings = Vec::new();
// 1. Check duplicate slugs
let total_count: i64 =
system_conn.query_row("SELECT COUNT(*) FROM global_slugs;", [], |r| r.get(0))?;
let distinct_count: i64 =
system_conn.query_row("SELECT COUNT(DISTINCT slug) FROM global_slugs;", [], |r| {
r.get(0)
})?;
if total_count != distinct_count {
errors.push(format!(
"Duplicate slugs found in global_slugs table (total rows: {}, distinct slugs: {})",
total_count, distinct_count
));
}
// 2. Scan all global slugs
let mut stmt = system_conn.prepare(
"SELECT slug, owner_user_id, target_type, target_id, created_at, status FROM global_slugs;",
)?;
let mut rows = stmt.query([])?;
while let Some(row) = rows.next()? {
let slug: String = row.get(0)?;
let owner_user_id: i64 = row.get(1)?;
let target_type: String = row.get(2)?;
let target_id: String = row.get(3)?;
let created_at_str: String = row.get(4)?;
let status: String = row.get(5)?;
// Target type check
if target_type != "url" && target_type != "page" {
errors.push(format!(
"Slug '{}' has invalid target_type '{}'",
slug, target_type
));
}
// Status check
if status != "active" && status != "disabled" && status != "reserving" {
errors.push(format!("Slug '{}' has invalid status '{}'", slug, status));
}
// Check owner
let owner_exists: bool = users_conn
.query_row(
"SELECT EXISTS(SELECT 1 FROM users WHERE id = ?1);",
[owner_user_id],
|r| r.get(0),
)
.unwrap_or(false);
if !owner_exists {
errors.push(format!(
"Slug '{}' references missing owner user ID {}",
slug, owner_user_id
));
continue;
}
// Stale warning check
if status == "reserving" {
if let Ok(created_at) = DateTime::parse_from_rfc3339(&created_at_str) {
let age = Utc::now().signed_duration_since(created_at.with_timezone(&Utc));
if age > chrono::Duration::minutes(15) {
warnings.push(format!(
"Reserving slug '{}' has been stale for over 15 minutes",
slug
));
}
}
}
// Check target record exists for active / disabled (and reserving with target_id)
if status == "active"
|| status == "disabled"
|| (status == "reserving" && !target_id.is_empty())
{
let content_db_path = if owner_user_id == 1 {
data_dir.join("users").join("1").join("content.db")
} else {
data_dir
.join("users")
.join(owner_user_id.to_string())
.join("content.db")
};
if !content_db_path.exists() {
errors.push(format!(
"Slug '{}' owner content database does not exist at {:?}",
slug, content_db_path
));
} else {
match Connection::open(&content_db_path) {
Ok(conn) => {
let exists = if target_type == "url" {
conn.query_row(
"SELECT EXISTS(SELECT 1 FROM urls WHERE id = ?1);",
[&target_id],
|r| r.get(0),
)
.unwrap_or(false)
} else if target_type == "page" {
conn.query_row(
"SELECT EXISTS(SELECT 1 FROM landing_pages WHERE id = ?1);",
[&target_id],
|r| r.get(0),
)
.unwrap_or(false)
} else {
false
};
if !exists {
errors.push(format!("Slug '{}' (type: '{}', id: '{}') references missing target record in owner's content database", slug, target_type, target_id));
}
}
Err(e) => {
errors.push(format!(
"Slug '{}' owner content database could not be opened: {}",
slug, e
));
}
}
}
}
}
// 3. Admin Content Reverse Consistency Check (Legacy DB)
let admin_content_db_path = data_dir.join("users").join("1").join("content.db");
if admin_content_db_path.exists() {
if let Ok(admin_content_conn) = Connection::open(&admin_content_db_path) {
// Check URLs
if let Ok(mut stmt) = admin_content_conn.prepare("SELECT code, id FROM urls;") {
if let Ok(mut rows) = stmt.query([]) {
while let Ok(Some(row)) = rows.next() {
let code: String = row.get(0).unwrap_or_default();
let id: String = row.get(1).unwrap_or_default();
let exists: bool = system_conn.query_row(
"SELECT EXISTS(SELECT 1 FROM global_slugs WHERE slug = ?1 AND owner_user_id = 1 AND target_id = ?2);",
rusqlite::params![code, id],
|r| r.get(0)
).unwrap_or(false);
if !exists {
warnings.push(format!("Orphaned admin URL detected in legacy content DB: code='{}', id='{}' is missing from global_slugs", code, id));
}
}
}
}
// Check Landing Pages
if let Ok(mut stmt) = admin_content_conn.prepare("SELECT code, id FROM landing_pages;")
{
if let Ok(mut rows) = stmt.query([]) {
while let Ok(Some(row)) = rows.next() {
let code: String = row.get(0).unwrap_or_default();
let id: String = row.get(1).unwrap_or_default();
let exists: bool = system_conn.query_row(
"SELECT EXISTS(SELECT 1 FROM global_slugs WHERE slug = ?1 AND owner_user_id = 1 AND target_id = ?2);",
rusqlite::params![code, id],
|r| r.get(0)
).unwrap_or(false);
if !exists {
warnings.push(format!("Orphaned admin Landing Page detected in legacy content DB: code='{}', id='{}' is missing from global_slugs", code, id));
}
}
}
}
}
}
// 4. Check for admin content in non-legacy tenant DBs
if let Ok(mut stmt) = users_conn
.prepare("SELECT id, username FROM users WHERE account_type = 'admin' AND id != 1;")
{
if let Ok(mut rows) = stmt.query([]) {
while let Ok(Some(row)) = rows.next() {
let id: i64 = row.get(0).unwrap_or(0);
let username: String = row.get(1).unwrap_or_default();
let tenant_db_path = data_dir
.join("users")
.join(id.to_string())
.join("content.db");
if tenant_db_path.exists() {
if let Ok(conn) = Connection::open(&tenant_db_path) {
let url_count: i64 = conn
.query_row("SELECT COUNT(*) FROM urls;", [], |r| r.get(0))
.unwrap_or(0);
let page_count: i64 = conn
.query_row("SELECT COUNT(*) FROM landing_pages;", [], |r| r.get(0))
.unwrap_or(0);
if url_count > 0 || page_count > 0 {
warnings.push(format!("Admin user '{}' (ID {}) has content in isolated tenant DB ({} URLs, {} pages). Admin content should be in legacy DB 1.", username, id, url_count, page_count));
}
}
}
}
}
}
Ok((errors, warnings))
}
pub fn register_restored_user_slugs(
system_conn: &Connection,
target_user_id: i64,