Initial public release

This commit is contained in:
thakares committed 2026-06-11 20:19:03 +05:30
commit c1107147b4
92 files changed
+10562

No files matched your search

+384
View File
@@ -0,0 +1,384 @@
use rusqlite::{Connection, params};
use uuid::Uuid;
use chrono::Utc;
use crate::models::{Url, LandingPage};
// Helper: Associate tags with a URL
fn associate_tags(conn: &Connection, url_id: &str, tags: &[String]) -> rusqlite::Result<()> {
conn.execute("DELETE FROM url_tags WHERE url_id = ?1;", params![url_id])?;
for tag_name in tags {
let tag_name = tag_name.trim().to_lowercase();
if tag_name.is_empty() {
continue;
}
// Insert tag if it doesn't exist
conn.execute(
"INSERT OR IGNORE INTO tags (id, name) VALUES (?1, ?2);",
params![Uuid::new_v4().to_string(), tag_name],
)?;
// Get tag id
let tag_id: String = conn.query_row(
"SELECT id FROM tags WHERE name = ?1;",
params![tag_name],
|row| row.get(0),
)?;
// Insert association
conn.execute(
"INSERT OR IGNORE INTO url_tags (url_id, tag_id) VALUES (?1, ?2);",
params![url_id, tag_id],
)?;
}
Ok(())
}
// Helper: Get tags for a URL
pub fn get_tags_for_url(conn: &Connection, url_id: &str) -> rusqlite::Result<Vec<String>> {
let mut stmt = conn.prepare(
"SELECT t.name FROM tags t JOIN url_tags ut ON t.id = ut.tag_id WHERE ut.url_id = ?1 ORDER BY t.name;"
)?;
let rows = stmt.query_map(params![url_id], |row| row.get::<_, String>(0))?;
let mut tags = Vec::new();
for tag in rows {
tags.push(tag?);
}
Ok(tags)
}
pub fn create_url(
conn: &Connection,
code: &str,
destination: &str,
title: Option<&str>,
description: Option<&str>,
tags: &[String],
) -> rusqlite::Result<Url> {
let id = Uuid::new_v4().to_string();
let now = Utc::now().to_rfc3339();
let status = "healthy".to_string();
conn.execute(
"INSERT INTO urls (id, code, destination, title, description, status, created_at, updated_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8);",
params![id, code, destination, title, description, status, now, now],
)?;
associate_tags(conn, &id, tags)?;
Ok(Url {
id,
code: code.to_string(),
destination: destination.to_string(),
title: title.map(|s| s.to_string()),
description: description.map(|s| s.to_string()),
status,
created_at: now.clone(),
updated_at: now,
tags: tags.to_vec(),
})
}
pub fn get_url_by_id(conn: &Connection, id: &str) -> rusqlite::Result<Option<Url>> {
let mut stmt = conn.prepare(
"SELECT id, code, destination, title, description, status, created_at, updated_at FROM urls WHERE id = ?1;"
)?;
let mut rows = stmt.query(params![id])?;
if let Some(row) = rows.next()? {
let url_id: String = row.get(0)?;
let tags = get_tags_for_url(conn, &url_id)?;
Ok(Some(Url {
id: url_id,
code: row.get(1)?,
destination: row.get(2)?,
title: row.get(3)?,
description: row.get(4)?,
status: row.get(5)?,
created_at: row.get(6)?,
updated_at: row.get(7)?,
tags,
}))
} else {
Ok(None)
}
}
pub fn get_url_by_code(conn: &Connection, code: &str) -> rusqlite::Result<Option<Url>> {
let mut stmt = conn.prepare(
"SELECT id, code, destination, title, description, status, created_at, updated_at FROM urls WHERE code = ?1;"
)?;
let mut rows = stmt.query(params![code])?;
if let Some(row) = rows.next()? {
let url_id: String = row.get(0)?;
let tags = get_tags_for_url(conn, &url_id)?;
Ok(Some(Url {
id: url_id,
code: row.get(1)?,
destination: row.get(2)?,
title: row.get(3)?,
description: row.get(4)?,
status: row.get(5)?,
created_at: row.get(6)?,
updated_at: row.get(7)?,
tags,
}))
} else {
Ok(None)
}
}
pub fn update_url(
conn: &Connection,
id: &str,
destination: &str,
title: Option<&str>,
description: Option<&str>,
status: &str,
tags: &[String],
) -> rusqlite::Result<Option<Url>> {
let now = Utc::now().to_rfc3339();
let count = conn.execute(
"UPDATE urls SET destination = ?1, title = ?2, description = ?3, status = ?4, updated_at = ?5 WHERE id = ?6;",
params![destination, title, description, status, now, id],
)?;
if count == 0 {
return Ok(None);
}
associate_tags(conn, id, tags)?;
get_url_by_id(conn, id)
}
pub fn delete_url(conn: &Connection, id: &str) -> rusqlite::Result<bool> {
let count = conn.execute("DELETE FROM urls WHERE id = ?1;", params![id])?;
Ok(count > 0)
}
pub fn list_urls(
conn: &Connection,
limit: i64,
offset: i64,
tag_filter: Option<&str>,
) -> rusqlite::Result<Vec<Url>> {
let mut urls = Vec::new();
if let Some(tag) = tag_filter {
let tag_name = tag.trim().to_lowercase();
let mut stmt = conn.prepare(
"SELECT u.id, u.code, u.destination, u.title, u.description, u.status, u.created_at, u.updated_at
FROM urls u
JOIN url_tags ut ON u.id = ut.url_id
JOIN tags t ON ut.tag_id = t.id
WHERE t.name = ?1
ORDER BY u.created_at DESC LIMIT ?2 OFFSET ?3;"
)?;
let rows = stmt.query_map(params![tag_name, limit, offset], |row| {
let url_id: String = row.get(0)?;
Ok((url_id, row.get(1)?, row.get(2)?, row.get(3)?, row.get(4)?, row.get(5)?, row.get(6)?, row.get(7)?))
})?;
for r in rows {
let (url_id, code, destination, title, description, status, created_at, updated_at) = r?;
let tags = get_tags_for_url(conn, &url_id)?;
urls.push(Url {
id: url_id,
code,
destination,
title,
description,
status,
created_at,
updated_at,
tags,
});
}
} else {
let mut stmt = conn.prepare(
"SELECT id, code, destination, title, description, status, created_at, updated_at
FROM urls ORDER BY created_at DESC LIMIT ?1 OFFSET ?2;"
)?;
let rows = stmt.query_map(params![limit, offset], |row| {
let url_id: String = row.get(0)?;
Ok((url_id, row.get(1)?, row.get(2)?, row.get(3)?, row.get(4)?, row.get(5)?, row.get(6)?, row.get(7)?))
})?;
for r in rows {
let (url_id, code, destination, title, description, status, created_at, updated_at) = r?;
let tags = get_tags_for_url(conn, &url_id)?;
urls.push(Url {
id: url_id,
code,
destination,
title,
description,
status,
created_at,
updated_at,
tags,
});
}
}
Ok(urls)
}
pub fn list_urls_for_health_check(conn: &Connection) -> rusqlite::Result<Vec<(String, String)>> {
let mut stmt = conn.prepare("SELECT id, destination FROM urls;")?;
let rows = stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?;
let mut res = Vec::new();
for r in rows {
res.push(r?);
}
Ok(res)
}
pub fn update_url_health(conn: &Connection, id: &str, status: &str) -> rusqlite::Result<()> {
let now = Utc::now().to_rfc3339();
conn.execute(
"UPDATE urls SET status = ?1, updated_at = ?2 WHERE id = ?3;",
params![status, now, id],
)?;
Ok(())
}
pub fn get_url_counts(conn: &Connection) -> rusqlite::Result<(i64, i64, i64)> {
let total: i64 = conn.query_row("SELECT COUNT(*) FROM urls;", [], |row| row.get(0))?;
let active: i64 = conn.query_row("SELECT COUNT(*) FROM urls WHERE status IN ('healthy', 'suspect');", [], |row| row.get(0))?;
let dead: i64 = conn.query_row("SELECT COUNT(*) FROM urls WHERE status = 'dead';", [], |row| row.get(0))?;
Ok((total, active, dead))
}
pub fn create_landing_page(
conn: &Connection,
code: &str,
slug: &str,
title: &str,
html_content: &str,
state: &str,
) -> rusqlite::Result<LandingPage> {
let id = Uuid::new_v4().to_string();
let now = Utc::now().to_rfc3339();
conn.execute(
"INSERT INTO landing_pages (id, code, slug, title, html_content, state, created_at, updated_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8);",
params![id, code, slug, title, html_content, state, now, now],
)?;
Ok(LandingPage {
id,
code: code.to_string(),
slug: slug.to_string(),
title: title.to_string(),
html_content: html_content.to_string(),
state: state.to_string(),
created_at: now.clone(),
updated_at: now,
})
}
pub fn get_landing_page_by_id(conn: &Connection, id: &str) -> rusqlite::Result<Option<LandingPage>> {
let mut stmt = conn.prepare(
"SELECT id, code, slug, title, html_content, state, created_at, updated_at FROM landing_pages WHERE id = ?1;"
)?;
let mut rows = stmt.query(params![id])?;
if let Some(row) = rows.next()? {
Ok(Some(LandingPage {
id: row.get(0)?,
code: row.get(1)?,
slug: row.get(2)?,
title: row.get(3)?,
html_content: row.get(4)?,
state: row.get(5)?,
created_at: row.get(6)?,
updated_at: row.get(7)?,
}))
} else {
Ok(None)
}
}
pub fn get_landing_page_by_code(conn: &Connection, code: &str) -> rusqlite::Result<Option<LandingPage>> {
let mut stmt = conn.prepare(
"SELECT id, code, slug, title, html_content, state, created_at, updated_at FROM landing_pages WHERE code = ?1;"
)?;
let mut rows = stmt.query(params![code])?;
if let Some(row) = rows.next()? {
Ok(Some(LandingPage {
id: row.get(0)?,
code: row.get(1)?,
slug: row.get(2)?,
title: row.get(3)?,
html_content: row.get(4)?,
state: row.get(5)?,
created_at: row.get(6)?,
updated_at: row.get(7)?,
}))
} else {
Ok(None)
}
}
pub fn update_landing_page(
conn: &Connection,
id: &str,
slug: &str,
title: &str,
html_content: &str,
state: &str,
) -> rusqlite::Result<Option<LandingPage>> {
let now = Utc::now().to_rfc3339();
let count = conn.execute(
"UPDATE landing_pages SET slug = ?1, title = ?2, html_content = ?3, state = ?4, updated_at = ?5 WHERE id = ?6;",
params![slug, title, html_content, state, now, id],
)?;
if count == 0 {
return Ok(None);
}
get_landing_page_by_id(conn, id)
}
pub fn delete_landing_page(conn: &Connection, id: &str) -> rusqlite::Result<bool> {
let count = conn.execute("DELETE FROM landing_pages WHERE id = ?1;", params![id])?;
Ok(count > 0)
}
pub fn list_landing_pages(conn: &Connection, limit: i64, offset: i64) -> rusqlite::Result<Vec<LandingPage>> {
let mut stmt = conn.prepare(
"SELECT id, code, slug, title, html_content, state, created_at, updated_at
FROM landing_pages ORDER BY created_at DESC LIMIT ?1 OFFSET ?2;"
)?;
let rows = stmt.query_map(params![limit, offset], |row| {
Ok(LandingPage {
id: row.get(0)?,
code: row.get(1)?,
slug: row.get(2)?,
title: row.get(3)?,
html_content: row.get(4)?,
state: row.get(5)?,
created_at: row.get(6)?,
updated_at: row.get(7)?,
})
})?;
let mut pages = Vec::new();
for page in rows {
pages.push(page?);
}
Ok(pages)
}
pub fn get_landing_page_count(conn: &Connection) -> rusqlite::Result<i64> {
conn.query_row("SELECT COUNT(*) FROM landing_pages;", [], |row| row.get(0))
}