Replace literal SQL with Diesel abstractions
This commit is contained in:
@@ -1,62 +1,74 @@
|
||||
use rusqlite::{Connection, params};
|
||||
use diesel::prelude::*;
|
||||
|
||||
use crate::db::from_row::{GENERATED_FILE_HASH_COLUMNS, generated_file_hash_from_row};
|
||||
use crate::db::DbConnection;
|
||||
use crate::db::from_row::GeneratedFileHashRecord;
|
||||
use crate::db::schema::generated_file_hashes;
|
||||
use crate::model::GeneratedFileHash;
|
||||
|
||||
pub fn get_generated_file_hash(
|
||||
conn: &Connection,
|
||||
conn: &DbConnection,
|
||||
project_id: &str,
|
||||
relative_path: &str,
|
||||
) -> rusqlite::Result<GeneratedFileHash> {
|
||||
conn.query_row(
|
||||
&format!(
|
||||
"SELECT {GENERATED_FILE_HASH_COLUMNS} FROM generated_file_hashes WHERE project_id = ?1 AND relative_path = ?2"
|
||||
),
|
||||
params![project_id, relative_path],
|
||||
generated_file_hash_from_row,
|
||||
)
|
||||
) -> QueryResult<GeneratedFileHash> {
|
||||
conn.with(|c| {
|
||||
generated_file_hashes::table
|
||||
.filter(generated_file_hashes::project_id.eq(project_id))
|
||||
.filter(generated_file_hashes::relative_path.eq(relative_path))
|
||||
.select(GeneratedFileHashRecord::as_select())
|
||||
.first(c)
|
||||
.map(Into::into)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn upsert_generated_file_hash(
|
||||
conn: &Connection,
|
||||
conn: &DbConnection,
|
||||
hash: &GeneratedFileHash,
|
||||
) -> rusqlite::Result<()> {
|
||||
conn.execute(
|
||||
"INSERT INTO generated_file_hashes (project_id, relative_path, content_hash, updated_at)
|
||||
VALUES (?1, ?2, ?3, ?4)
|
||||
ON CONFLICT(project_id, relative_path)
|
||||
DO UPDATE SET content_hash = excluded.content_hash, updated_at = excluded.updated_at",
|
||||
params![
|
||||
hash.project_id,
|
||||
hash.relative_path,
|
||||
hash.content_hash,
|
||||
hash.updated_at
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
) -> QueryResult<()> {
|
||||
conn.with(|c| {
|
||||
diesel::insert_into(generated_file_hashes::table)
|
||||
.values(GeneratedFileHashRecord::from(hash))
|
||||
.on_conflict((
|
||||
generated_file_hashes::project_id,
|
||||
generated_file_hashes::relative_path,
|
||||
))
|
||||
.do_update()
|
||||
.set((
|
||||
generated_file_hashes::content_hash.eq(&hash.content_hash),
|
||||
generated_file_hashes::updated_at.eq(hash.updated_at),
|
||||
))
|
||||
.execute(c)
|
||||
.map(|_| ())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn delete_generated_file_hash(
|
||||
conn: &Connection,
|
||||
conn: &DbConnection,
|
||||
project_id: &str,
|
||||
relative_path: &str,
|
||||
) -> rusqlite::Result<()> {
|
||||
conn.execute(
|
||||
"DELETE FROM generated_file_hashes WHERE project_id = ?1 AND relative_path = ?2",
|
||||
params![project_id, relative_path],
|
||||
)?;
|
||||
Ok(())
|
||||
) -> QueryResult<()> {
|
||||
conn.with(|c| {
|
||||
diesel::delete(
|
||||
generated_file_hashes::table
|
||||
.filter(generated_file_hashes::project_id.eq(project_id))
|
||||
.filter(generated_file_hashes::relative_path.eq(relative_path)),
|
||||
)
|
||||
.execute(c)
|
||||
.map(|_| ())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn list_generated_file_hashes_by_project(
|
||||
conn: &Connection,
|
||||
conn: &DbConnection,
|
||||
project_id: &str,
|
||||
) -> rusqlite::Result<Vec<GeneratedFileHash>> {
|
||||
let mut stmt = conn.prepare(&format!(
|
||||
"SELECT {GENERATED_FILE_HASH_COLUMNS} FROM generated_file_hashes WHERE project_id = ?1 ORDER BY relative_path"
|
||||
))?;
|
||||
let rows = stmt.query_map(params![project_id], generated_file_hash_from_row)?;
|
||||
rows.collect()
|
||||
) -> QueryResult<Vec<GeneratedFileHash>> {
|
||||
conn.with(|c| {
|
||||
generated_file_hashes::table
|
||||
.filter(generated_file_hashes::project_id.eq(project_id))
|
||||
.order(generated_file_hashes::relative_path)
|
||||
.select(GeneratedFileHashRecord::as_select())
|
||||
.load(c)
|
||||
.map(|rows: Vec<GeneratedFileHashRecord>| rows.into_iter().map(Into::into).collect())
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -66,7 +78,7 @@ mod tests {
|
||||
use crate::db::queries::project::{insert_project, make_test_project};
|
||||
|
||||
fn setup() -> Database {
|
||||
let mut db = Database::open_in_memory().unwrap();
|
||||
let db = Database::open_in_memory().unwrap();
|
||||
db.migrate().unwrap();
|
||||
insert_project(db.conn(), &make_test_project("p1", "blog")).unwrap();
|
||||
db
|
||||
|
||||
@@ -1,120 +1,87 @@
|
||||
use rusqlite::{Connection, params};
|
||||
use chrono::{Datelike, TimeZone, Utc};
|
||||
use diesel::prelude::*;
|
||||
use diesel::sql_types::Text;
|
||||
|
||||
use crate::db::from_row::{MEDIA_COLUMNS, media_from_row};
|
||||
use crate::db::DbConnection;
|
||||
use crate::db::from_row::{MediaRecord, convert, convert_all};
|
||||
use crate::db::schema::media;
|
||||
use crate::model::Media;
|
||||
use crate::util::calendar_range_unix_ms;
|
||||
|
||||
fn tags_to_json(tags: &[String]) -> String {
|
||||
serde_json::to_string(tags).unwrap_or_else(|_| "[]".into())
|
||||
diesel::define_sql_function!(fn instr(haystack: Text, needle: Text) -> Integer);
|
||||
|
||||
pub fn insert_media(conn: &DbConnection, m: &Media) -> QueryResult<()> {
|
||||
conn.with(|c| {
|
||||
diesel::insert_into(media::table)
|
||||
.values(MediaRecord::from(m))
|
||||
.execute(c)
|
||||
.map(|_| ())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn insert_media(conn: &Connection, m: &Media) -> rusqlite::Result<()> {
|
||||
conn.execute(
|
||||
"INSERT INTO media (
|
||||
id, project_id, filename, original_name, mime_type, size,
|
||||
width, height, title, alt, caption, author, language,
|
||||
file_path, sidecar_path, checksum, tags, created_at, updated_at
|
||||
) VALUES (
|
||||
?1, ?2, ?3, ?4, ?5, ?6,
|
||||
?7, ?8, ?9, ?10, ?11, ?12, ?13,
|
||||
?14, ?15, ?16, ?17, ?18, ?19
|
||||
)",
|
||||
params![
|
||||
m.id,
|
||||
m.project_id,
|
||||
m.filename,
|
||||
m.original_name,
|
||||
m.mime_type,
|
||||
m.size,
|
||||
m.width,
|
||||
m.height,
|
||||
m.title,
|
||||
m.alt,
|
||||
m.caption,
|
||||
m.author,
|
||||
m.language,
|
||||
m.file_path,
|
||||
m.sidecar_path,
|
||||
m.checksum,
|
||||
tags_to_json(&m.tags),
|
||||
m.created_at,
|
||||
m.updated_at,
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
pub fn get_media_by_id(conn: &DbConnection, id: &str) -> QueryResult<Media> {
|
||||
conn.with(|c| {
|
||||
media::table
|
||||
.filter(media::id.eq(id))
|
||||
.select(MediaRecord::as_select())
|
||||
.first(c)
|
||||
.and_then(convert)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_media_by_id(conn: &Connection, id: &str) -> rusqlite::Result<Media> {
|
||||
conn.query_row(
|
||||
&format!("SELECT {MEDIA_COLUMNS} FROM media WHERE id = ?1"),
|
||||
params![id],
|
||||
media_from_row,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn list_media_by_project(conn: &Connection, project_id: &str) -> rusqlite::Result<Vec<Media>> {
|
||||
let mut stmt = conn.prepare(&format!(
|
||||
"SELECT {MEDIA_COLUMNS} FROM media WHERE project_id = ?1 ORDER BY created_at DESC"
|
||||
))?;
|
||||
let rows = stmt.query_map(params![project_id], media_from_row)?;
|
||||
rows.collect()
|
||||
pub fn list_media_by_project(conn: &DbConnection, project_id: &str) -> QueryResult<Vec<Media>> {
|
||||
conn.with(|c| {
|
||||
media::table
|
||||
.filter(media::project_id.eq(project_id))
|
||||
.order(media::created_at.desc())
|
||||
.select(MediaRecord::as_select())
|
||||
.load(c)
|
||||
.and_then(convert_all)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn list_media_by_project_limited(
|
||||
conn: &Connection,
|
||||
conn: &DbConnection,
|
||||
project_id: &str,
|
||||
limit: i64,
|
||||
offset: i64,
|
||||
) -> rusqlite::Result<Vec<Media>> {
|
||||
let mut stmt = conn.prepare(&format!(
|
||||
"SELECT {MEDIA_COLUMNS} FROM media WHERE project_id = ?1 ORDER BY created_at DESC LIMIT ?2 OFFSET ?3"
|
||||
))?;
|
||||
let rows = stmt.query_map(params![project_id, limit, offset], media_from_row)?;
|
||||
rows.collect()
|
||||
) -> QueryResult<Vec<Media>> {
|
||||
conn.with(|c| {
|
||||
media::table
|
||||
.filter(media::project_id.eq(project_id))
|
||||
.order(media::created_at.desc())
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
.select(MediaRecord::as_select())
|
||||
.load(c)
|
||||
.and_then(convert_all)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn count_media_by_project(conn: &Connection, project_id: &str) -> rusqlite::Result<i64> {
|
||||
conn.query_row(
|
||||
"SELECT COUNT(*) FROM media WHERE project_id = ?1",
|
||||
params![project_id],
|
||||
|row| row.get(0),
|
||||
)
|
||||
pub fn count_media_by_project(conn: &DbConnection, project_id: &str) -> QueryResult<i64> {
|
||||
conn.with(|c| {
|
||||
media::table
|
||||
.filter(media::project_id.eq(project_id))
|
||||
.count()
|
||||
.get_result(c)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update_media(conn: &Connection, m: &Media) -> rusqlite::Result<()> {
|
||||
conn.execute(
|
||||
"UPDATE media SET
|
||||
filename = ?1, original_name = ?2, mime_type = ?3, size = ?4,
|
||||
width = ?5, height = ?6, title = ?7, alt = ?8, caption = ?9,
|
||||
author = ?10, language = ?11, file_path = ?12, sidecar_path = ?13,
|
||||
checksum = ?14, tags = ?15, updated_at = ?16
|
||||
WHERE id = ?17",
|
||||
params![
|
||||
m.filename,
|
||||
m.original_name,
|
||||
m.mime_type,
|
||||
m.size,
|
||||
m.width,
|
||||
m.height,
|
||||
m.title,
|
||||
m.alt,
|
||||
m.caption,
|
||||
m.author,
|
||||
m.language,
|
||||
m.file_path,
|
||||
m.sidecar_path,
|
||||
m.checksum,
|
||||
tags_to_json(&m.tags),
|
||||
m.updated_at,
|
||||
m.id,
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
pub fn update_media(conn: &DbConnection, m: &Media) -> QueryResult<()> {
|
||||
conn.with(|c| {
|
||||
diesel::update(media::table.filter(media::id.eq(&m.id)))
|
||||
.set(MediaRecord::from(m))
|
||||
.execute(c)
|
||||
.map(|_| ())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn delete_media(conn: &Connection, id: &str) -> rusqlite::Result<()> {
|
||||
conn.execute("DELETE FROM media WHERE id = ?1", params![id])?;
|
||||
Ok(())
|
||||
pub fn delete_media(conn: &DbConnection, id: &str) -> QueryResult<()> {
|
||||
conn.with(|c| {
|
||||
diesel::delete(media::table.filter(media::id.eq(id)))
|
||||
.execute(c)
|
||||
.map(|_| ())
|
||||
})
|
||||
}
|
||||
|
||||
// ── Filtered queries (per sidebar_views.allium MediaView) ───
|
||||
@@ -140,95 +107,86 @@ impl MediaFilterParams {
|
||||
|
||||
/// List media with optional filters applied.
|
||||
pub fn list_media_filtered(
|
||||
conn: &Connection,
|
||||
conn: &DbConnection,
|
||||
project_id: &str,
|
||||
filters: &MediaFilterParams,
|
||||
limit: i64,
|
||||
offset: i64,
|
||||
) -> rusqlite::Result<Vec<Media>> {
|
||||
let mut conditions = vec!["project_id = ?1".to_string()];
|
||||
let mut param_values: Vec<Box<dyn rusqlite::types::ToSql>> = Vec::new();
|
||||
param_values.push(Box::new(project_id.to_string()));
|
||||
|
||||
if !filters.search_query.is_empty() {
|
||||
let idx = param_values.len() + 1;
|
||||
let pattern = format!("%{}%", filters.search_query.replace('%', "\\%"));
|
||||
conditions.push(format!(
|
||||
"(COALESCE(title, original_name) LIKE ?{idx} ESCAPE '\\')"
|
||||
));
|
||||
param_values.push(Box::new(pattern));
|
||||
}
|
||||
|
||||
if let Some(year) = filters.year {
|
||||
let (start, end) =
|
||||
calendar_range_unix_ms(year, filters.month).ok_or(rusqlite::Error::InvalidQuery)?;
|
||||
let idx1 = param_values.len() + 1;
|
||||
let idx2 = param_values.len() + 2;
|
||||
conditions.push(format!("(created_at >= ?{idx1} AND created_at < ?{idx2})"));
|
||||
param_values.push(Box::new(start));
|
||||
param_values.push(Box::new(end));
|
||||
}
|
||||
|
||||
for tag in &filters.tags {
|
||||
let idx = param_values.len() + 1;
|
||||
let pattern = format!("%\"{}\"%", tag.replace('"', "\\\""));
|
||||
conditions.push(format!("(tags LIKE ?{idx})"));
|
||||
param_values.push(Box::new(pattern));
|
||||
}
|
||||
|
||||
let where_clause = conditions.join(" AND ");
|
||||
let idx_limit = param_values.len() + 1;
|
||||
let idx_offset = param_values.len() + 2;
|
||||
param_values.push(Box::new(limit));
|
||||
param_values.push(Box::new(offset));
|
||||
|
||||
let sql = format!(
|
||||
"SELECT {MEDIA_COLUMNS} FROM media WHERE {where_clause} ORDER BY created_at DESC LIMIT ?{idx_limit} OFFSET ?{idx_offset}"
|
||||
);
|
||||
|
||||
let params_refs: Vec<&dyn rusqlite::types::ToSql> =
|
||||
param_values.iter().map(|b| b.as_ref()).collect();
|
||||
|
||||
let mut stmt = conn.prepare(&sql)?;
|
||||
let rows = stmt.query_map(params_refs.as_slice(), media_from_row)?;
|
||||
rows.collect()
|
||||
) -> QueryResult<Vec<Media>> {
|
||||
conn.with(|c| {
|
||||
let mut query = media::table
|
||||
.filter(media::project_id.eq(project_id))
|
||||
.into_boxed();
|
||||
if !filters.search_query.is_empty() {
|
||||
query = query.filter(
|
||||
media::title
|
||||
.is_not_null()
|
||||
.and(instr(media::title.assume_not_null(), &filters.search_query).gt(0))
|
||||
.or(media::title
|
||||
.is_null()
|
||||
.and(instr(media::original_name, &filters.search_query).gt(0))),
|
||||
);
|
||||
}
|
||||
if let Some(year) = filters.year {
|
||||
let (start, end) = calendar_range_unix_ms(year, filters.month).ok_or_else(|| {
|
||||
diesel::result::Error::SerializationError("invalid calendar range".into())
|
||||
})?;
|
||||
query = query.filter(media::created_at.ge(start).and(media::created_at.lt(end)));
|
||||
}
|
||||
for tag in &filters.tags {
|
||||
query = query.filter(instr(media::tags, serde_json::to_string(tag).unwrap()).gt(0));
|
||||
}
|
||||
query
|
||||
.order(media::created_at.desc())
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
.select(MediaRecord::as_select())
|
||||
.load(c)
|
||||
.and_then(convert_all)
|
||||
})
|
||||
}
|
||||
|
||||
/// Year/month counts for the media calendar archive widget.
|
||||
pub fn media_calendar_counts(
|
||||
conn: &Connection,
|
||||
conn: &DbConnection,
|
||||
project_id: &str,
|
||||
) -> rusqlite::Result<Vec<(i32, u32, usize)>> {
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT
|
||||
CAST(strftime('%Y', created_at / 1000, 'unixepoch') AS INTEGER) AS y,
|
||||
CAST(strftime('%m', created_at / 1000, 'unixepoch') AS INTEGER) AS m,
|
||||
COUNT(*) AS cnt
|
||||
FROM media
|
||||
WHERE project_id = ?1
|
||||
GROUP BY y, m
|
||||
ORDER BY y DESC, m DESC",
|
||||
)?;
|
||||
let rows = stmt.query_map(params![project_id], |row| {
|
||||
Ok((
|
||||
row.get::<_, i32>(0)?,
|
||||
row.get::<_, u32>(1)?,
|
||||
row.get::<_, usize>(2)?,
|
||||
))
|
||||
})?;
|
||||
rows.collect()
|
||||
) -> QueryResult<Vec<(i32, u32, usize)>> {
|
||||
conn.with(|c| {
|
||||
let timestamps = media::table
|
||||
.filter(media::project_id.eq(project_id))
|
||||
.select(media::created_at)
|
||||
.load::<i64>(c)?;
|
||||
let mut counts = std::collections::BTreeMap::new();
|
||||
for timestamp in timestamps {
|
||||
let date = Utc
|
||||
.timestamp_millis_opt(timestamp)
|
||||
.single()
|
||||
.ok_or_else(|| {
|
||||
diesel::result::Error::DeserializationError("invalid timestamp".into())
|
||||
})?;
|
||||
*counts.entry((date.year(), date.month())).or_insert(0) += 1;
|
||||
}
|
||||
Ok(counts
|
||||
.into_iter()
|
||||
.rev()
|
||||
.map(|((year, month), count)| (year, month, count))
|
||||
.collect())
|
||||
})
|
||||
}
|
||||
|
||||
/// Collect all distinct tag values across media for a project.
|
||||
pub fn distinct_media_tags(conn: &Connection, project_id: &str) -> rusqlite::Result<Vec<String>> {
|
||||
let mut stmt =
|
||||
conn.prepare("SELECT DISTINCT tags FROM media WHERE project_id = ?1 AND tags != '[]'")?;
|
||||
let rows = stmt.query_map(params![project_id], |row| row.get::<_, String>(0))?;
|
||||
pub fn distinct_media_tags(conn: &DbConnection, project_id: &str) -> QueryResult<Vec<String>> {
|
||||
let rows = conn.with(|c| {
|
||||
media::table
|
||||
.filter(media::project_id.eq(project_id))
|
||||
.filter(media::tags.ne("[]"))
|
||||
.select(media::tags)
|
||||
.distinct()
|
||||
.load::<String>(c)
|
||||
})?;
|
||||
let mut all_tags = std::collections::BTreeSet::new();
|
||||
for json_str in rows {
|
||||
if let Ok(json_str) = json_str
|
||||
&& let Ok(tags) = serde_json::from_str::<Vec<String>>(&json_str)
|
||||
{
|
||||
if let Ok(tags) = serde_json::from_str::<Vec<String>>(&json_str) {
|
||||
all_tags.extend(tags);
|
||||
}
|
||||
}
|
||||
@@ -268,7 +226,7 @@ mod tests {
|
||||
use crate::db::queries::project::{insert_project, make_test_project};
|
||||
|
||||
fn setup() -> Database {
|
||||
let mut db = Database::open_in_memory().unwrap();
|
||||
let db = Database::open_in_memory().unwrap();
|
||||
db.migrate().unwrap();
|
||||
insert_project(db.conn(), &make_test_project("p1", "blog")).unwrap();
|
||||
db
|
||||
|
||||
@@ -1,92 +1,82 @@
|
||||
use rusqlite::{Connection, params};
|
||||
use diesel::prelude::*;
|
||||
|
||||
use crate::db::from_row::{MEDIA_TRANSLATION_COLUMNS, media_translation_from_row};
|
||||
use crate::db::DbConnection;
|
||||
use crate::db::from_row::MediaTranslationRecord;
|
||||
use crate::db::schema::media_translations;
|
||||
use crate::model::MediaTranslation;
|
||||
|
||||
pub fn insert_media_translation(conn: &Connection, t: &MediaTranslation) -> rusqlite::Result<()> {
|
||||
conn.execute(
|
||||
"INSERT INTO media_translations (
|
||||
id, project_id, translation_for, language, title, alt, caption,
|
||||
created_at, updated_at
|
||||
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
|
||||
params![
|
||||
t.id,
|
||||
t.project_id,
|
||||
t.translation_for,
|
||||
t.language,
|
||||
t.title,
|
||||
t.alt,
|
||||
t.caption,
|
||||
t.created_at,
|
||||
t.updated_at,
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
pub fn insert_media_translation(conn: &DbConnection, t: &MediaTranslation) -> QueryResult<()> {
|
||||
conn.with(|c| {
|
||||
diesel::insert_into(media_translations::table)
|
||||
.values(MediaTranslationRecord::from(t))
|
||||
.execute(c)
|
||||
.map(|_| ())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_media_translation_by_media_and_language(
|
||||
conn: &Connection,
|
||||
conn: &DbConnection,
|
||||
translation_for: &str,
|
||||
language: &str,
|
||||
) -> rusqlite::Result<MediaTranslation> {
|
||||
conn.query_row(
|
||||
&format!(
|
||||
"SELECT {MEDIA_TRANSLATION_COLUMNS} FROM media_translations
|
||||
WHERE translation_for = ?1 AND language = ?2"
|
||||
),
|
||||
params![translation_for, language],
|
||||
media_translation_from_row,
|
||||
)
|
||||
) -> QueryResult<MediaTranslation> {
|
||||
conn.with(|c| {
|
||||
media_translations::table
|
||||
.filter(media_translations::translation_for.eq(translation_for))
|
||||
.filter(media_translations::language.eq(language))
|
||||
.select(MediaTranslationRecord::as_select())
|
||||
.first(c)
|
||||
.map(Into::into)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn list_media_translations_by_media(
|
||||
conn: &Connection,
|
||||
conn: &DbConnection,
|
||||
translation_for: &str,
|
||||
) -> rusqlite::Result<Vec<MediaTranslation>> {
|
||||
let mut stmt = conn.prepare(&format!(
|
||||
"SELECT {MEDIA_TRANSLATION_COLUMNS} FROM media_translations
|
||||
WHERE translation_for = ?1 ORDER BY language"
|
||||
))?;
|
||||
let rows = stmt.query_map(params![translation_for], media_translation_from_row)?;
|
||||
rows.collect()
|
||||
) -> QueryResult<Vec<MediaTranslation>> {
|
||||
conn.with(|c| {
|
||||
media_translations::table
|
||||
.filter(media_translations::translation_for.eq(translation_for))
|
||||
.order(media_translations::language)
|
||||
.select(MediaTranslationRecord::as_select())
|
||||
.load(c)
|
||||
.map(|rows: Vec<MediaTranslationRecord>| rows.into_iter().map(Into::into).collect())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn upsert_media_translation(conn: &Connection, t: &MediaTranslation) -> rusqlite::Result<()> {
|
||||
conn.execute(
|
||||
"INSERT INTO media_translations (
|
||||
id, project_id, translation_for, language, title, alt, caption,
|
||||
created_at, updated_at
|
||||
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)
|
||||
ON CONFLICT(translation_for, language) DO UPDATE SET
|
||||
title = excluded.title,
|
||||
alt = excluded.alt,
|
||||
caption = excluded.caption,
|
||||
updated_at = excluded.updated_at",
|
||||
params![
|
||||
t.id,
|
||||
t.project_id,
|
||||
t.translation_for,
|
||||
t.language,
|
||||
t.title,
|
||||
t.alt,
|
||||
t.caption,
|
||||
t.created_at,
|
||||
t.updated_at,
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
pub fn upsert_media_translation(conn: &DbConnection, t: &MediaTranslation) -> QueryResult<()> {
|
||||
conn.with(|c| {
|
||||
diesel::insert_into(media_translations::table)
|
||||
.values(MediaTranslationRecord::from(t))
|
||||
.on_conflict((
|
||||
media_translations::translation_for,
|
||||
media_translations::language,
|
||||
))
|
||||
.do_update()
|
||||
.set((
|
||||
media_translations::title.eq(&t.title),
|
||||
media_translations::alt.eq(&t.alt),
|
||||
media_translations::caption.eq(&t.caption),
|
||||
media_translations::updated_at.eq(t.updated_at),
|
||||
))
|
||||
.execute(c)
|
||||
.map(|_| ())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn delete_media_translation(
|
||||
conn: &Connection,
|
||||
conn: &DbConnection,
|
||||
translation_for: &str,
|
||||
language: &str,
|
||||
) -> rusqlite::Result<()> {
|
||||
conn.execute(
|
||||
"DELETE FROM media_translations WHERE translation_for = ?1 AND language = ?2",
|
||||
params![translation_for, language],
|
||||
)?;
|
||||
Ok(())
|
||||
) -> QueryResult<()> {
|
||||
conn.with(|c| {
|
||||
diesel::delete(
|
||||
media_translations::table
|
||||
.filter(media_translations::translation_for.eq(translation_for))
|
||||
.filter(media_translations::language.eq(language)),
|
||||
)
|
||||
.execute(c)
|
||||
.map(|_| ())
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -97,7 +87,7 @@ mod tests {
|
||||
use crate::db::queries::project::{insert_project, make_test_project};
|
||||
|
||||
fn setup() -> Database {
|
||||
let mut db = Database::open_in_memory().unwrap();
|
||||
let db = Database::open_in_memory().unwrap();
|
||||
db.migrate().unwrap();
|
||||
insert_project(db.conn(), &make_test_project("p1", "blog")).unwrap();
|
||||
insert_media(db.conn(), &make_test_media("m1", "p1")).unwrap();
|
||||
|
||||
@@ -1,169 +1,142 @@
|
||||
use rusqlite::{Connection, params};
|
||||
use chrono::{Datelike, TimeZone, Utc};
|
||||
use diesel::prelude::*;
|
||||
use diesel::sql_types::Text;
|
||||
use diesel::sqlite::Sqlite;
|
||||
|
||||
use crate::db::from_row::{POST_COLUMNS, post_from_row, post_status_to_str};
|
||||
use crate::db::DbConnection;
|
||||
use crate::db::from_row::{PostRecord, convert, convert_all, post_status_to_str};
|
||||
use crate::db::schema::posts;
|
||||
use crate::model::{Post, PostStatus};
|
||||
use crate::util::calendar_range_unix_ms;
|
||||
|
||||
fn tags_to_json(tags: &[String]) -> String {
|
||||
serde_json::to_string(tags).unwrap_or_else(|_| "[]".into())
|
||||
diesel::define_sql_function!(fn instr(haystack: Text, needle: Text) -> Integer);
|
||||
diesel::define_sql_function!(fn lower(value: Text) -> Text);
|
||||
|
||||
pub fn insert_post(conn: &DbConnection, post: &Post) -> QueryResult<()> {
|
||||
conn.with(|c| {
|
||||
diesel::insert_into(posts::table)
|
||||
.values(PostRecord::from(post))
|
||||
.execute(c)
|
||||
.map(|_| ())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn insert_post(conn: &Connection, post: &Post) -> rusqlite::Result<()> {
|
||||
conn.execute(
|
||||
"INSERT INTO posts (
|
||||
id, project_id, title, slug, excerpt, content, status, author,
|
||||
language, do_not_translate, template_slug, file_path, checksum,
|
||||
tags, categories,
|
||||
published_title, published_content, published_tags,
|
||||
published_categories, published_excerpt,
|
||||
created_at, updated_at, published_at
|
||||
) VALUES (
|
||||
?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8,
|
||||
?9, ?10, ?11, ?12, ?13,
|
||||
?14, ?15,
|
||||
?16, ?17, ?18, ?19, ?20,
|
||||
?21, ?22, ?23
|
||||
)",
|
||||
params![
|
||||
post.id,
|
||||
post.project_id,
|
||||
post.title,
|
||||
post.slug,
|
||||
post.excerpt,
|
||||
post.content,
|
||||
post_status_to_str(&post.status),
|
||||
post.author,
|
||||
post.language,
|
||||
post.do_not_translate as i64,
|
||||
post.template_slug,
|
||||
post.file_path,
|
||||
post.checksum,
|
||||
tags_to_json(&post.tags),
|
||||
tags_to_json(&post.categories),
|
||||
post.published_title,
|
||||
post.published_content,
|
||||
post.published_tags,
|
||||
post.published_categories,
|
||||
post.published_excerpt,
|
||||
post.created_at,
|
||||
post.updated_at,
|
||||
post.published_at,
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_post_by_id(conn: &Connection, id: &str) -> rusqlite::Result<Post> {
|
||||
conn.query_row(
|
||||
&format!("SELECT {POST_COLUMNS} FROM posts WHERE id = ?1"),
|
||||
params![id],
|
||||
post_from_row,
|
||||
)
|
||||
pub fn get_post_by_id(conn: &DbConnection, id: &str) -> QueryResult<Post> {
|
||||
conn.with(|c| {
|
||||
posts::table
|
||||
.filter(posts::id.eq(id))
|
||||
.select(PostRecord::as_select())
|
||||
.first(c)
|
||||
.and_then(convert)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_post_by_project_and_slug(
|
||||
conn: &Connection,
|
||||
conn: &DbConnection,
|
||||
project_id: &str,
|
||||
slug: &str,
|
||||
) -> rusqlite::Result<Post> {
|
||||
conn.query_row(
|
||||
&format!("SELECT {POST_COLUMNS} FROM posts WHERE project_id = ?1 AND slug = ?2"),
|
||||
params![project_id, slug],
|
||||
post_from_row,
|
||||
)
|
||||
) -> QueryResult<Post> {
|
||||
conn.with(|c| {
|
||||
posts::table
|
||||
.filter(posts::project_id.eq(project_id))
|
||||
.filter(posts::slug.eq(slug))
|
||||
.select(PostRecord::as_select())
|
||||
.first(c)
|
||||
.and_then(convert)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn list_posts_by_project(conn: &Connection, project_id: &str) -> rusqlite::Result<Vec<Post>> {
|
||||
let mut stmt = conn.prepare(&format!(
|
||||
"SELECT {POST_COLUMNS} FROM posts WHERE project_id = ?1 ORDER BY created_at DESC"
|
||||
))?;
|
||||
let rows = stmt.query_map(params![project_id], post_from_row)?;
|
||||
rows.collect()
|
||||
pub fn list_posts_by_project(conn: &DbConnection, project_id: &str) -> QueryResult<Vec<Post>> {
|
||||
conn.with(|c| {
|
||||
posts::table
|
||||
.filter(posts::project_id.eq(project_id))
|
||||
.order(posts::created_at.desc())
|
||||
.select(PostRecord::as_select())
|
||||
.load(c)
|
||||
.and_then(convert_all)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn list_posts_by_project_limited(
|
||||
conn: &Connection,
|
||||
conn: &DbConnection,
|
||||
project_id: &str,
|
||||
limit: i64,
|
||||
offset: i64,
|
||||
) -> rusqlite::Result<Vec<Post>> {
|
||||
let mut stmt = conn.prepare(&format!(
|
||||
"SELECT {POST_COLUMNS} FROM posts WHERE project_id = ?1 ORDER BY created_at DESC LIMIT ?2 OFFSET ?3"
|
||||
))?;
|
||||
let rows = stmt.query_map(params![project_id, limit, offset], post_from_row)?;
|
||||
rows.collect()
|
||||
) -> QueryResult<Vec<Post>> {
|
||||
conn.with(|c| {
|
||||
posts::table
|
||||
.filter(posts::project_id.eq(project_id))
|
||||
.order(posts::created_at.desc())
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
.select(PostRecord::as_select())
|
||||
.load(c)
|
||||
.and_then(convert_all)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update_post(conn: &Connection, post: &Post) -> rusqlite::Result<()> {
|
||||
conn.execute(
|
||||
"UPDATE posts SET
|
||||
title = ?1, slug = ?2, excerpt = ?3, content = ?4, status = ?5,
|
||||
author = ?6, language = ?7, do_not_translate = ?8, template_slug = ?9,
|
||||
file_path = ?10, checksum = ?11, tags = ?12, categories = ?13,
|
||||
published_title = ?14, published_content = ?15, published_tags = ?16,
|
||||
published_categories = ?17, published_excerpt = ?18,
|
||||
created_at = ?19, updated_at = ?20, published_at = ?21
|
||||
WHERE id = ?22",
|
||||
params![
|
||||
post.title,
|
||||
post.slug,
|
||||
post.excerpt,
|
||||
post.content,
|
||||
post_status_to_str(&post.status),
|
||||
post.author,
|
||||
post.language,
|
||||
post.do_not_translate as i64,
|
||||
post.template_slug,
|
||||
post.file_path,
|
||||
post.checksum,
|
||||
tags_to_json(&post.tags),
|
||||
tags_to_json(&post.categories),
|
||||
post.published_title,
|
||||
post.published_content,
|
||||
post.published_tags,
|
||||
post.published_categories,
|
||||
post.published_excerpt,
|
||||
post.created_at,
|
||||
post.updated_at,
|
||||
post.published_at,
|
||||
post.id,
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
pub fn update_post(conn: &DbConnection, post: &Post) -> QueryResult<()> {
|
||||
conn.with(|c| {
|
||||
diesel::update(posts::table.filter(posts::id.eq(&post.id)))
|
||||
.set(PostRecord::from(post))
|
||||
.execute(c)
|
||||
.map(|_| ())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update_post_status(
|
||||
conn: &Connection,
|
||||
conn: &DbConnection,
|
||||
id: &str,
|
||||
status: &PostStatus,
|
||||
updated_at: i64,
|
||||
) -> rusqlite::Result<()> {
|
||||
conn.execute(
|
||||
"UPDATE posts SET status = ?1, updated_at = ?2 WHERE id = ?3",
|
||||
params![post_status_to_str(status), updated_at, id],
|
||||
)?;
|
||||
Ok(())
|
||||
) -> QueryResult<()> {
|
||||
conn.with(|c| {
|
||||
diesel::update(posts::table.filter(posts::id.eq(id)))
|
||||
.set((
|
||||
posts::status.eq(post_status_to_str(status)),
|
||||
posts::updated_at.eq(updated_at),
|
||||
))
|
||||
.execute(c)
|
||||
.map(|_| ())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn clear_post_content(conn: &Connection, id: &str, updated_at: i64) -> rusqlite::Result<()> {
|
||||
conn.execute(
|
||||
"UPDATE posts SET content = NULL, updated_at = ?1 WHERE id = ?2",
|
||||
params![updated_at, id],
|
||||
)?;
|
||||
Ok(())
|
||||
pub fn clear_post_content(conn: &DbConnection, id: &str, updated_at: i64) -> QueryResult<()> {
|
||||
conn.with(|c| {
|
||||
diesel::update(posts::table.filter(posts::id.eq(id)))
|
||||
.set((
|
||||
posts::content.eq(None::<String>),
|
||||
posts::updated_at.eq(updated_at),
|
||||
))
|
||||
.execute(c)
|
||||
.map(|_| ())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_post_file_path(
|
||||
conn: &Connection,
|
||||
conn: &DbConnection,
|
||||
id: &str,
|
||||
file_path: &str,
|
||||
updated_at: i64,
|
||||
) -> rusqlite::Result<()> {
|
||||
conn.execute(
|
||||
"UPDATE posts SET file_path = ?1, updated_at = ?2 WHERE id = ?3",
|
||||
params![file_path, updated_at, id],
|
||||
)?;
|
||||
Ok(())
|
||||
) -> QueryResult<()> {
|
||||
conn.with(|c| {
|
||||
diesel::update(posts::table.filter(posts::id.eq(id)))
|
||||
.set((
|
||||
posts::file_path.eq(file_path),
|
||||
posts::updated_at.eq(updated_at),
|
||||
))
|
||||
.execute(c)
|
||||
.map(|_| ())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_post_checksum(conn: &DbConnection, id: &str, checksum: Option<&str>) -> QueryResult<()> {
|
||||
conn.with(|c| {
|
||||
diesel::update(posts::table.filter(posts::id.eq(id)))
|
||||
.set(posts::checksum.eq(checksum))
|
||||
.execute(c)
|
||||
.map(|_| ())
|
||||
})
|
||||
}
|
||||
|
||||
#[expect(
|
||||
@@ -171,7 +144,7 @@ pub fn set_post_file_path(
|
||||
reason = "arguments mirror the published snapshot columns"
|
||||
)]
|
||||
pub fn set_published_snapshot(
|
||||
conn: &Connection,
|
||||
conn: &DbConnection,
|
||||
id: &str,
|
||||
title: &str,
|
||||
content: &str,
|
||||
@@ -180,38 +153,38 @@ pub fn set_published_snapshot(
|
||||
excerpt: Option<&str>,
|
||||
published_at: i64,
|
||||
updated_at: i64,
|
||||
) -> rusqlite::Result<()> {
|
||||
conn.execute(
|
||||
"UPDATE posts SET
|
||||
published_title = ?1, published_content = ?2, published_tags = ?3,
|
||||
published_categories = ?4, published_excerpt = ?5,
|
||||
published_at = ?6, updated_at = ?7
|
||||
WHERE id = ?8",
|
||||
params![
|
||||
title,
|
||||
content,
|
||||
tags,
|
||||
categories,
|
||||
excerpt,
|
||||
published_at,
|
||||
updated_at,
|
||||
id
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
) -> QueryResult<()> {
|
||||
conn.with(|c| {
|
||||
diesel::update(posts::table.filter(posts::id.eq(id)))
|
||||
.set((
|
||||
posts::published_title.eq(title),
|
||||
posts::published_content.eq(content),
|
||||
posts::published_tags.eq(tags),
|
||||
posts::published_categories.eq(categories),
|
||||
posts::published_excerpt.eq(excerpt),
|
||||
posts::published_at.eq(published_at),
|
||||
posts::updated_at.eq(updated_at),
|
||||
))
|
||||
.execute(c)
|
||||
.map(|_| ())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn delete_post(conn: &Connection, id: &str) -> rusqlite::Result<()> {
|
||||
conn.execute("DELETE FROM posts WHERE id = ?1", params![id])?;
|
||||
Ok(())
|
||||
pub fn delete_post(conn: &DbConnection, id: &str) -> QueryResult<()> {
|
||||
conn.with(|c| {
|
||||
diesel::delete(posts::table.filter(posts::id.eq(id)))
|
||||
.execute(c)
|
||||
.map(|_| ())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn count_posts_by_project(conn: &Connection, project_id: &str) -> rusqlite::Result<i64> {
|
||||
conn.query_row(
|
||||
"SELECT COUNT(*) FROM posts WHERE project_id = ?1",
|
||||
params![project_id],
|
||||
|row| row.get(0),
|
||||
)
|
||||
pub fn count_posts_by_project(conn: &DbConnection, project_id: &str) -> QueryResult<i64> {
|
||||
conn.with(|c| {
|
||||
posts::table
|
||||
.filter(posts::project_id.eq(project_id))
|
||||
.count()
|
||||
.get_result(c)
|
||||
})
|
||||
}
|
||||
|
||||
// ── Filtered queries (per sidebar_views.allium PostsView) ───
|
||||
@@ -266,163 +239,155 @@ impl PostFilterParams {
|
||||
///
|
||||
/// Returns all matching posts (up to `limit`), ordered by created_at DESC.
|
||||
/// Caller splits into draft/published/archived sections.
|
||||
fn post_query<'a>(
|
||||
project_id: &'a str,
|
||||
filters: &'a PostFilterParams,
|
||||
apply_filters: bool,
|
||||
exclude_drafts: bool,
|
||||
) -> posts::BoxedQuery<'a, Sqlite> {
|
||||
let mut query = posts::table
|
||||
.filter(posts::project_id.eq(project_id))
|
||||
.into_boxed();
|
||||
|
||||
let page = serde_json::to_string("page").unwrap();
|
||||
if filters.pages_only {
|
||||
query = query.filter(instr(lower(posts::categories), page.clone()).gt(0));
|
||||
} else if filters.exclude_pages {
|
||||
query = query.filter(instr(lower(posts::categories), page).eq(0));
|
||||
}
|
||||
|
||||
if exclude_drafts {
|
||||
query = query.filter(posts::status.ne("draft"));
|
||||
}
|
||||
if !apply_filters {
|
||||
return query;
|
||||
}
|
||||
|
||||
if !filters.search_query.is_empty() {
|
||||
query = query.filter(instr(posts::title, &filters.search_query).gt(0));
|
||||
}
|
||||
if let Some(status) = &filters.status {
|
||||
query = query.filter(posts::status.eq(status));
|
||||
}
|
||||
if let Some(language) = &filters.language {
|
||||
query = query.filter(posts::language.eq(language).or(posts::language.is_null()));
|
||||
}
|
||||
if let Some(year) = filters.year
|
||||
&& let Some((start, end)) = calendar_range_unix_ms(year, filters.month)
|
||||
{
|
||||
query = query.filter(posts::created_at.ge(start).and(posts::created_at.lt(end)));
|
||||
}
|
||||
for tag in &filters.tags {
|
||||
query = query.filter(instr(posts::tags, serde_json::to_string(tag).unwrap()).gt(0));
|
||||
}
|
||||
for category in &filters.categories {
|
||||
query =
|
||||
query.filter(instr(posts::categories, serde_json::to_string(category).unwrap()).gt(0));
|
||||
}
|
||||
if let Some(from) = filters.from {
|
||||
query = query.filter(posts::created_at.ge(from));
|
||||
}
|
||||
if let Some(to) = filters.to {
|
||||
query = query.filter(posts::created_at.le(to));
|
||||
}
|
||||
query
|
||||
}
|
||||
|
||||
pub fn list_posts_filtered(
|
||||
conn: &Connection,
|
||||
conn: &DbConnection,
|
||||
project_id: &str,
|
||||
filters: &PostFilterParams,
|
||||
limit: i64,
|
||||
offset: i64,
|
||||
) -> rusqlite::Result<Vec<Post>> {
|
||||
// Build dynamic WHERE clause
|
||||
let mut conditions = vec!["p.project_id = ?1".to_string()];
|
||||
let mut param_values: Vec<Box<dyn rusqlite::types::ToSql>> = Vec::new();
|
||||
param_values.push(Box::new(project_id.to_string()));
|
||||
|
||||
// pages_only / exclude_pages
|
||||
if filters.pages_only {
|
||||
conditions.push("LOWER(p.categories) LIKE '%\"page\"%'".to_string());
|
||||
} else if filters.exclude_pages {
|
||||
conditions.push("LOWER(p.categories) NOT LIKE '%\"page\"%'".to_string());
|
||||
}
|
||||
|
||||
// For non-draft posts, apply filters. Drafts always pass.
|
||||
// We build this as: (status = 'draft') OR (filter conditions)
|
||||
let mut filter_conditions: Vec<String> = Vec::new();
|
||||
|
||||
if !filters.search_query.is_empty() {
|
||||
let idx = param_values.len() + 1;
|
||||
let pattern = format!("%{}%", filters.search_query.replace('%', "\\%"));
|
||||
filter_conditions.push(format!("(p.title LIKE ?{idx} ESCAPE '\\')"));
|
||||
param_values.push(Box::new(pattern));
|
||||
}
|
||||
|
||||
if let Some(status) = &filters.status {
|
||||
let idx = param_values.len() + 1;
|
||||
filter_conditions.push(format!("(p.status = ?{idx})"));
|
||||
param_values.push(Box::new(status.clone()));
|
||||
}
|
||||
|
||||
if let Some(language) = &filters.language {
|
||||
let idx = param_values.len() + 1;
|
||||
filter_conditions.push(format!("(p.language = ?{idx} OR p.language IS NULL)"));
|
||||
param_values.push(Box::new(language.clone()));
|
||||
}
|
||||
|
||||
if let Some(year) = filters.year {
|
||||
let (start, end) =
|
||||
calendar_range_unix_ms(year, filters.month).ok_or(rusqlite::Error::InvalidQuery)?;
|
||||
let idx1 = param_values.len() + 1;
|
||||
let idx2 = param_values.len() + 2;
|
||||
filter_conditions.push(format!(
|
||||
"(p.created_at >= ?{idx1} AND p.created_at < ?{idx2})"
|
||||
));
|
||||
param_values.push(Box::new(start));
|
||||
param_values.push(Box::new(end));
|
||||
}
|
||||
|
||||
for tag in &filters.tags {
|
||||
let idx = param_values.len() + 1;
|
||||
let pattern = format!("%\"{}\"%", tag.replace('"', "\\\""));
|
||||
filter_conditions.push(format!("(p.tags LIKE ?{idx})"));
|
||||
param_values.push(Box::new(pattern));
|
||||
}
|
||||
|
||||
for cat in &filters.categories {
|
||||
let idx = param_values.len() + 1;
|
||||
let pattern = format!("%\"{}\"%", cat.replace('"', "\\\""));
|
||||
filter_conditions.push(format!("(p.categories LIKE ?{idx})"));
|
||||
param_values.push(Box::new(pattern));
|
||||
}
|
||||
|
||||
if let Some(from) = filters.from {
|
||||
let idx = param_values.len() + 1;
|
||||
filter_conditions.push(format!("(p.created_at >= ?{idx})"));
|
||||
param_values.push(Box::new(from));
|
||||
}
|
||||
|
||||
if let Some(to) = filters.to {
|
||||
let idx = param_values.len() + 1;
|
||||
filter_conditions.push(format!("(p.created_at <= ?{idx})"));
|
||||
param_values.push(Box::new(to));
|
||||
}
|
||||
|
||||
// Without an explicit status filter, drafts remain visible regardless of the
|
||||
// rest of the active filters.
|
||||
if !filter_conditions.is_empty() {
|
||||
let combined = filter_conditions.join(" AND ");
|
||||
if filters.status.is_some() {
|
||||
conditions.push(format!("({combined})"));
|
||||
) -> QueryResult<Vec<Post>> {
|
||||
conn.with(|c| {
|
||||
let content_filters = filters.has_active_filters();
|
||||
let records = if filters.status.is_some() {
|
||||
post_query(project_id, filters, true, false)
|
||||
.order(posts::created_at.desc())
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
.select(PostRecord::as_select())
|
||||
.load(c)?
|
||||
} else if content_filters {
|
||||
let mut records: Vec<PostRecord> = post_query(project_id, filters, false, false)
|
||||
.filter(posts::status.eq("draft"))
|
||||
.select(PostRecord::as_select())
|
||||
.load(c)?;
|
||||
records.extend(
|
||||
post_query(project_id, filters, true, true)
|
||||
.select(PostRecord::as_select())
|
||||
.load::<PostRecord>(c)?,
|
||||
);
|
||||
records.sort_unstable_by_key(|record| std::cmp::Reverse(record.created_at));
|
||||
records
|
||||
.into_iter()
|
||||
.skip(offset.max(0) as usize)
|
||||
.take(limit.max(0) as usize)
|
||||
.collect()
|
||||
} else {
|
||||
conditions.push(format!("(p.status = 'draft' OR ({combined}))"));
|
||||
}
|
||||
}
|
||||
|
||||
let where_clause = conditions.join(" AND ");
|
||||
let idx_limit = param_values.len() + 1;
|
||||
let idx_offset = param_values.len() + 2;
|
||||
param_values.push(Box::new(limit));
|
||||
param_values.push(Box::new(offset));
|
||||
|
||||
let sql = format!(
|
||||
"SELECT {POST_COLUMNS} FROM posts p WHERE {where_clause} ORDER BY p.created_at DESC LIMIT ?{idx_limit} OFFSET ?{idx_offset}"
|
||||
);
|
||||
|
||||
let params_refs: Vec<&dyn rusqlite::types::ToSql> =
|
||||
param_values.iter().map(|b| b.as_ref()).collect();
|
||||
|
||||
let mut stmt = conn.prepare(&sql)?;
|
||||
let rows = stmt.query_map(params_refs.as_slice(), post_from_row)?;
|
||||
rows.collect()
|
||||
post_query(project_id, filters, false, false)
|
||||
.order(posts::created_at.desc())
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
.select(PostRecord::as_select())
|
||||
.load(c)?
|
||||
};
|
||||
convert_all(records)
|
||||
})
|
||||
}
|
||||
|
||||
/// Year/month counts for the calendar archive widget.
|
||||
/// Returns (year, month, count) tuples, ordered by year DESC, month DESC.
|
||||
pub fn post_calendar_counts(
|
||||
conn: &Connection,
|
||||
conn: &DbConnection,
|
||||
project_id: &str,
|
||||
pages_only: bool,
|
||||
exclude_pages: bool,
|
||||
) -> rusqlite::Result<Vec<(i32, u32, usize)>> {
|
||||
let page_filter = if pages_only {
|
||||
" AND LOWER(categories) LIKE '%\"page\"%'"
|
||||
} else if exclude_pages {
|
||||
" AND LOWER(categories) NOT LIKE '%\"page\"%'"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
let sql = format!(
|
||||
"SELECT
|
||||
CAST(strftime('%Y', created_at / 1000, 'unixepoch') AS INTEGER) AS y,
|
||||
CAST(strftime('%m', created_at / 1000, 'unixepoch') AS INTEGER) AS m,
|
||||
COUNT(*) AS cnt
|
||||
FROM posts
|
||||
WHERE project_id = ?1{page_filter}
|
||||
GROUP BY y, m
|
||||
ORDER BY y DESC, m DESC"
|
||||
);
|
||||
|
||||
let mut stmt = conn.prepare(&sql)?;
|
||||
let rows = stmt.query_map(params![project_id], |row| {
|
||||
Ok((
|
||||
row.get::<_, i32>(0)?,
|
||||
row.get::<_, u32>(1)?,
|
||||
row.get::<_, usize>(2)?,
|
||||
))
|
||||
})?;
|
||||
rows.collect()
|
||||
) -> QueryResult<Vec<(i32, u32, usize)>> {
|
||||
conn.with(|c| {
|
||||
let rows = posts::table
|
||||
.filter(posts::project_id.eq(project_id))
|
||||
.select((posts::created_at, posts::categories))
|
||||
.load::<(i64, String)>(c)?;
|
||||
let mut counts = std::collections::BTreeMap::new();
|
||||
for (timestamp, categories) in rows {
|
||||
let categories: Vec<String> = serde_json::from_str(&categories).unwrap_or_default();
|
||||
let is_page = categories
|
||||
.iter()
|
||||
.any(|category| category.eq_ignore_ascii_case("page"));
|
||||
if (pages_only && !is_page) || (exclude_pages && is_page) {
|
||||
continue;
|
||||
}
|
||||
let date = Utc
|
||||
.timestamp_millis_opt(timestamp)
|
||||
.single()
|
||||
.ok_or_else(|| {
|
||||
diesel::result::Error::DeserializationError("invalid timestamp".into())
|
||||
})?;
|
||||
*counts.entry((date.year(), date.month())).or_insert(0) += 1;
|
||||
}
|
||||
Ok(counts
|
||||
.into_iter()
|
||||
.rev()
|
||||
.map(|((year, month), count)| (year, month, count))
|
||||
.collect())
|
||||
})
|
||||
}
|
||||
|
||||
/// Collect all distinct tag values across posts for a project.
|
||||
pub fn distinct_post_tags(conn: &Connection, project_id: &str) -> rusqlite::Result<Vec<String>> {
|
||||
let mut stmt =
|
||||
conn.prepare("SELECT DISTINCT tags FROM posts WHERE project_id = ?1 AND tags != '[]'")?;
|
||||
let rows = stmt.query_map(params![project_id], |row| row.get::<_, String>(0))?;
|
||||
pub fn distinct_post_tags(conn: &DbConnection, project_id: &str) -> QueryResult<Vec<String>> {
|
||||
let rows = conn.with(|c| {
|
||||
posts::table
|
||||
.filter(posts::project_id.eq(project_id))
|
||||
.filter(posts::tags.ne("[]"))
|
||||
.select(posts::tags)
|
||||
.distinct()
|
||||
.load::<String>(c)
|
||||
})?;
|
||||
let mut all_tags = std::collections::BTreeSet::new();
|
||||
for json_str in rows {
|
||||
if let Ok(json_str) = json_str
|
||||
&& let Ok(tags) = serde_json::from_str::<Vec<String>>(&json_str)
|
||||
{
|
||||
if let Ok(tags) = serde_json::from_str::<Vec<String>>(&json_str) {
|
||||
all_tags.extend(tags);
|
||||
}
|
||||
}
|
||||
@@ -430,25 +395,53 @@ pub fn distinct_post_tags(conn: &Connection, project_id: &str) -> rusqlite::Resu
|
||||
}
|
||||
|
||||
/// Collect all distinct category values across posts for a project.
|
||||
pub fn distinct_post_categories(
|
||||
conn: &Connection,
|
||||
project_id: &str,
|
||||
) -> rusqlite::Result<Vec<String>> {
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT DISTINCT categories FROM posts WHERE project_id = ?1 AND categories != '[]'",
|
||||
)?;
|
||||
let rows = stmt.query_map(params![project_id], |row| row.get::<_, String>(0))?;
|
||||
pub fn distinct_post_categories(conn: &DbConnection, project_id: &str) -> QueryResult<Vec<String>> {
|
||||
let rows = conn.with(|c| {
|
||||
posts::table
|
||||
.filter(posts::project_id.eq(project_id))
|
||||
.filter(posts::categories.ne("[]"))
|
||||
.select(posts::categories)
|
||||
.distinct()
|
||||
.load::<String>(c)
|
||||
})?;
|
||||
let mut all_cats = std::collections::BTreeSet::new();
|
||||
for json_str in rows {
|
||||
if let Ok(json_str) = json_str
|
||||
&& let Ok(cats) = serde_json::from_str::<Vec<String>>(&json_str)
|
||||
{
|
||||
if let Ok(cats) = serde_json::from_str::<Vec<String>>(&json_str) {
|
||||
all_cats.extend(cats);
|
||||
}
|
||||
}
|
||||
Ok(all_cats.into_iter().collect())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn make_test_post(id: &str, project_id: &str, slug: &str) -> Post {
|
||||
Post {
|
||||
id: id.into(),
|
||||
project_id: project_id.into(),
|
||||
title: id.into(),
|
||||
slug: slug.into(),
|
||||
excerpt: None,
|
||||
content: None,
|
||||
status: PostStatus::Draft,
|
||||
author: None,
|
||||
language: None,
|
||||
do_not_translate: false,
|
||||
template_slug: None,
|
||||
file_path: String::new(),
|
||||
checksum: None,
|
||||
tags: Vec::new(),
|
||||
categories: Vec::new(),
|
||||
published_title: None,
|
||||
published_content: None,
|
||||
published_tags: None,
|
||||
published_categories: None,
|
||||
published_excerpt: None,
|
||||
created_at: 1000,
|
||||
updated_at: 1000,
|
||||
published_at: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -456,7 +449,7 @@ mod tests {
|
||||
use crate::db::queries::project::{insert_project, make_test_project};
|
||||
|
||||
fn setup() -> Database {
|
||||
let mut db = Database::open_in_memory().unwrap();
|
||||
let db = Database::open_in_memory().unwrap();
|
||||
db.migrate().unwrap();
|
||||
insert_project(db.conn(), &make_test_project("p1", "blog")).unwrap();
|
||||
db
|
||||
|
||||
@@ -1,82 +1,77 @@
|
||||
use rusqlite::{Connection, params};
|
||||
use diesel::prelude::*;
|
||||
|
||||
use crate::db::from_row::{POST_LINK_COLUMNS, post_link_from_row};
|
||||
use crate::db::DbConnection;
|
||||
use crate::db::from_row::PostLinkRecord;
|
||||
use crate::db::schema::post_links;
|
||||
use crate::model::PostLink;
|
||||
|
||||
pub fn insert_post_link(conn: &Connection, link: &PostLink) -> rusqlite::Result<()> {
|
||||
conn.execute(
|
||||
"INSERT INTO post_links (id, source_post_id, target_post_id, link_text, created_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||
params![
|
||||
link.id,
|
||||
link.source_post_id,
|
||||
link.target_post_id,
|
||||
link.link_text,
|
||||
link.created_at,
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
pub fn insert_post_link(conn: &DbConnection, link: &PostLink) -> QueryResult<()> {
|
||||
conn.with(|c| {
|
||||
diesel::insert_into(post_links::table)
|
||||
.values(PostLinkRecord::from(link))
|
||||
.execute(c)
|
||||
.map(|_| ())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn delete_links_by_source(conn: &Connection, source_post_id: &str) -> rusqlite::Result<()> {
|
||||
conn.execute(
|
||||
"DELETE FROM post_links WHERE source_post_id = ?1",
|
||||
params![source_post_id],
|
||||
)?;
|
||||
Ok(())
|
||||
pub fn delete_links_by_source(conn: &DbConnection, source_post_id: &str) -> QueryResult<()> {
|
||||
conn.with(|c| {
|
||||
diesel::delete(post_links::table.filter(post_links::source_post_id.eq(source_post_id)))
|
||||
.execute(c)
|
||||
.map(|_| ())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn delete_links_by_target(conn: &DbConnection, target_post_id: &str) -> QueryResult<()> {
|
||||
conn.with(|c| {
|
||||
diesel::delete(post_links::table.filter(post_links::target_post_id.eq(target_post_id)))
|
||||
.execute(c)
|
||||
.map(|_| ())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn list_links_by_source(
|
||||
conn: &Connection,
|
||||
conn: &DbConnection,
|
||||
source_post_id: &str,
|
||||
) -> rusqlite::Result<Vec<PostLink>> {
|
||||
let mut stmt = conn.prepare(&format!(
|
||||
"SELECT {POST_LINK_COLUMNS} FROM post_links WHERE source_post_id = ?1 ORDER BY created_at"
|
||||
))?;
|
||||
let rows = stmt.query_map(params![source_post_id], post_link_from_row)?;
|
||||
rows.collect()
|
||||
) -> QueryResult<Vec<PostLink>> {
|
||||
conn.with(|c| {
|
||||
post_links::table
|
||||
.filter(post_links::source_post_id.eq(source_post_id))
|
||||
.order(post_links::created_at)
|
||||
.select(PostLinkRecord::as_select())
|
||||
.load(c)
|
||||
.map(|rows: Vec<PostLinkRecord>| rows.into_iter().map(Into::into).collect())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn list_links_by_target(
|
||||
conn: &Connection,
|
||||
conn: &DbConnection,
|
||||
target_post_id: &str,
|
||||
) -> rusqlite::Result<Vec<PostLink>> {
|
||||
let mut stmt = conn.prepare(&format!(
|
||||
"SELECT {POST_LINK_COLUMNS} FROM post_links WHERE target_post_id = ?1 ORDER BY created_at"
|
||||
))?;
|
||||
let rows = stmt.query_map(params![target_post_id], post_link_from_row)?;
|
||||
rows.collect()
|
||||
) -> QueryResult<Vec<PostLink>> {
|
||||
conn.with(|c| {
|
||||
post_links::table
|
||||
.filter(post_links::target_post_id.eq(target_post_id))
|
||||
.order(post_links::created_at)
|
||||
.select(PostLinkRecord::as_select())
|
||||
.load(c)
|
||||
.map(|rows: Vec<PostLinkRecord>| rows.into_iter().map(Into::into).collect())
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::db::Database;
|
||||
use crate::db::queries::post::{insert_post, make_test_post};
|
||||
use crate::db::queries::project::{insert_project, make_test_project};
|
||||
|
||||
fn setup() -> Database {
|
||||
let mut db = Database::open_in_memory().unwrap();
|
||||
let db = Database::open_in_memory().unwrap();
|
||||
db.migrate().unwrap();
|
||||
insert_project(db.conn(), &make_test_project("p1", "blog")).unwrap();
|
||||
let c = db.conn();
|
||||
c.execute(
|
||||
"INSERT INTO posts (id, project_id, title, slug, status, created_at, updated_at)
|
||||
VALUES ('a', 'p1', 'A', 'a', 'draft', 1000, 1000)",
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
c.execute(
|
||||
"INSERT INTO posts (id, project_id, title, slug, status, created_at, updated_at)
|
||||
VALUES ('b', 'p1', 'B', 'b', 'draft', 1000, 1000)",
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
c.execute(
|
||||
"INSERT INTO posts (id, project_id, title, slug, status, created_at, updated_at)
|
||||
VALUES ('c', 'p1', 'C', 'c', 'draft', 1000, 1000)",
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
insert_post(db.conn(), &make_test_post("a", "p1", "a")).unwrap();
|
||||
insert_post(db.conn(), &make_test_post("b", "p1", "b")).unwrap();
|
||||
insert_post(db.conn(), &make_test_post("c", "p1", "c")).unwrap();
|
||||
db
|
||||
}
|
||||
|
||||
|
||||
@@ -1,65 +1,80 @@
|
||||
use rusqlite::{Connection, params};
|
||||
use diesel::prelude::*;
|
||||
|
||||
use crate::db::from_row::{POST_MEDIA_COLUMNS, post_media_from_row};
|
||||
use crate::db::DbConnection;
|
||||
use crate::db::from_row::PostMediaRecord;
|
||||
use crate::db::schema::post_media;
|
||||
use crate::model::PostMedia;
|
||||
|
||||
pub fn link_media(conn: &Connection, pm: &PostMedia) -> rusqlite::Result<()> {
|
||||
conn.execute(
|
||||
"INSERT INTO post_media (id, project_id, post_id, media_id, sort_order, created_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
||||
params![
|
||||
pm.id,
|
||||
pm.project_id,
|
||||
pm.post_id,
|
||||
pm.media_id,
|
||||
pm.sort_order,
|
||||
pm.created_at,
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
pub fn link_media(conn: &DbConnection, pm: &PostMedia) -> QueryResult<()> {
|
||||
conn.with(|c| {
|
||||
diesel::insert_into(post_media::table)
|
||||
.values(PostMediaRecord::from(pm))
|
||||
.execute(c)
|
||||
.map(|_| ())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn unlink_media(conn: &Connection, post_id: &str, media_id: &str) -> rusqlite::Result<()> {
|
||||
conn.execute(
|
||||
"DELETE FROM post_media WHERE post_id = ?1 AND media_id = ?2",
|
||||
params![post_id, media_id],
|
||||
)?;
|
||||
Ok(())
|
||||
pub fn unlink_media(conn: &DbConnection, post_id: &str, media_id: &str) -> QueryResult<()> {
|
||||
conn.with(|c| {
|
||||
diesel::delete(
|
||||
post_media::table
|
||||
.filter(post_media::post_id.eq(post_id))
|
||||
.filter(post_media::media_id.eq(media_id)),
|
||||
)
|
||||
.execute(c)
|
||||
.map(|_| ())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn list_post_media_by_post(
|
||||
conn: &Connection,
|
||||
post_id: &str,
|
||||
) -> rusqlite::Result<Vec<PostMedia>> {
|
||||
let mut stmt = conn.prepare(&format!(
|
||||
"SELECT {POST_MEDIA_COLUMNS} FROM post_media WHERE post_id = ?1 ORDER BY sort_order"
|
||||
))?;
|
||||
let rows = stmt.query_map(params![post_id], post_media_from_row)?;
|
||||
rows.collect()
|
||||
pub fn delete_post_media_by_post(conn: &DbConnection, post_id: &str) -> QueryResult<()> {
|
||||
conn.with(|c| {
|
||||
diesel::delete(post_media::table.filter(post_media::post_id.eq(post_id)))
|
||||
.execute(c)
|
||||
.map(|_| ())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn list_post_media_by_post(conn: &DbConnection, post_id: &str) -> QueryResult<Vec<PostMedia>> {
|
||||
conn.with(|c| {
|
||||
post_media::table
|
||||
.filter(post_media::post_id.eq(post_id))
|
||||
.order(post_media::sort_order)
|
||||
.select(PostMediaRecord::as_select())
|
||||
.load(c)
|
||||
.map(|rows: Vec<PostMediaRecord>| rows.into_iter().map(Into::into).collect())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn list_post_media_by_media(
|
||||
conn: &Connection,
|
||||
conn: &DbConnection,
|
||||
media_id: &str,
|
||||
) -> rusqlite::Result<Vec<PostMedia>> {
|
||||
let mut stmt = conn.prepare(&format!(
|
||||
"SELECT {POST_MEDIA_COLUMNS} FROM post_media WHERE media_id = ?1 ORDER BY created_at"
|
||||
))?;
|
||||
let rows = stmt.query_map(params![media_id], post_media_from_row)?;
|
||||
rows.collect()
|
||||
) -> QueryResult<Vec<PostMedia>> {
|
||||
conn.with(|c| {
|
||||
post_media::table
|
||||
.filter(post_media::media_id.eq(media_id))
|
||||
.order(post_media::created_at)
|
||||
.select(PostMediaRecord::as_select())
|
||||
.load(c)
|
||||
.map(|rows: Vec<PostMediaRecord>| rows.into_iter().map(Into::into).collect())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update_sort_order(
|
||||
conn: &Connection,
|
||||
conn: &DbConnection,
|
||||
post_id: &str,
|
||||
media_id: &str,
|
||||
sort_order: i32,
|
||||
) -> rusqlite::Result<()> {
|
||||
conn.execute(
|
||||
"UPDATE post_media SET sort_order = ?1 WHERE post_id = ?2 AND media_id = ?3",
|
||||
params![sort_order, post_id, media_id],
|
||||
)?;
|
||||
Ok(())
|
||||
) -> QueryResult<()> {
|
||||
conn.with(|c| {
|
||||
diesel::update(
|
||||
post_media::table
|
||||
.filter(post_media::post_id.eq(post_id))
|
||||
.filter(post_media::media_id.eq(media_id)),
|
||||
)
|
||||
.set(post_media::sort_order.eq(sort_order))
|
||||
.execute(c)
|
||||
.map(|_| ())
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -67,19 +82,14 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::db::Database;
|
||||
use crate::db::queries::media::{insert_media, make_test_media};
|
||||
use crate::db::queries::post::{insert_post, make_test_post};
|
||||
use crate::db::queries::project::{insert_project, make_test_project};
|
||||
|
||||
fn setup() -> Database {
|
||||
let mut db = Database::open_in_memory().unwrap();
|
||||
let db = Database::open_in_memory().unwrap();
|
||||
db.migrate().unwrap();
|
||||
insert_project(db.conn(), &make_test_project("p1", "blog")).unwrap();
|
||||
db.conn()
|
||||
.execute(
|
||||
"INSERT INTO posts (id, project_id, title, slug, status, created_at, updated_at)
|
||||
VALUES ('post1', 'p1', 'Hello', 'hello', 'draft', 1000, 1000)",
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
insert_post(db.conn(), &make_test_post("post1", "p1", "hello")).unwrap();
|
||||
insert_media(db.conn(), &make_test_media("m1", "p1")).unwrap();
|
||||
insert_media(db.conn(), &make_test_media("m2", "p1")).unwrap();
|
||||
db
|
||||
|
||||
@@ -1,139 +1,111 @@
|
||||
use rusqlite::{Connection, params};
|
||||
use diesel::prelude::*;
|
||||
|
||||
use crate::db::from_row::{
|
||||
POST_TRANSLATION_COLUMNS, post_status_to_str, post_translation_from_row,
|
||||
};
|
||||
use crate::db::DbConnection;
|
||||
use crate::db::from_row::{PostTranslationRecord, convert, convert_all};
|
||||
use crate::db::schema::post_translations;
|
||||
use crate::model::PostTranslation;
|
||||
|
||||
pub fn insert_post_translation(conn: &Connection, t: &PostTranslation) -> rusqlite::Result<()> {
|
||||
pub fn insert_post_translation(conn: &DbConnection, t: &PostTranslation) -> QueryResult<()> {
|
||||
if !t.status.is_valid_for_translation() {
|
||||
return Err(rusqlite::Error::InvalidParameterName(
|
||||
"translation status must be draft or published".to_string(),
|
||||
return Err(diesel::result::Error::SerializationError(
|
||||
"translation status must be draft or published".into(),
|
||||
));
|
||||
}
|
||||
conn.execute(
|
||||
"INSERT INTO post_translations (
|
||||
id, project_id, translation_for, language, title, excerpt, content,
|
||||
status, file_path, checksum, created_at, updated_at, published_at
|
||||
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)",
|
||||
params![
|
||||
t.id,
|
||||
t.project_id,
|
||||
t.translation_for,
|
||||
t.language,
|
||||
t.title,
|
||||
t.excerpt,
|
||||
t.content,
|
||||
post_status_to_str(&t.status),
|
||||
t.file_path,
|
||||
t.checksum,
|
||||
t.created_at,
|
||||
t.updated_at,
|
||||
t.published_at,
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
conn.with(|c| {
|
||||
diesel::insert_into(post_translations::table)
|
||||
.values(PostTranslationRecord::from(t))
|
||||
.execute(c)
|
||||
.map(|_| ())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_post_translation_by_id(
|
||||
conn: &Connection,
|
||||
id: &str,
|
||||
) -> rusqlite::Result<PostTranslation> {
|
||||
conn.query_row(
|
||||
&format!("SELECT {POST_TRANSLATION_COLUMNS} FROM post_translations WHERE id = ?1"),
|
||||
params![id],
|
||||
post_translation_from_row,
|
||||
)
|
||||
pub fn get_post_translation_by_id(conn: &DbConnection, id: &str) -> QueryResult<PostTranslation> {
|
||||
conn.with(|c| {
|
||||
post_translations::table
|
||||
.filter(post_translations::id.eq(id))
|
||||
.select(PostTranslationRecord::as_select())
|
||||
.first(c)
|
||||
.and_then(convert)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_post_translation_by_post_and_language(
|
||||
conn: &Connection,
|
||||
conn: &DbConnection,
|
||||
translation_for: &str,
|
||||
language: &str,
|
||||
) -> rusqlite::Result<PostTranslation> {
|
||||
conn.query_row(
|
||||
&format!(
|
||||
"SELECT {POST_TRANSLATION_COLUMNS} FROM post_translations
|
||||
WHERE translation_for = ?1 AND language = ?2"
|
||||
),
|
||||
params![translation_for, language],
|
||||
post_translation_from_row,
|
||||
)
|
||||
) -> QueryResult<PostTranslation> {
|
||||
conn.with(|c| {
|
||||
post_translations::table
|
||||
.filter(post_translations::translation_for.eq(translation_for))
|
||||
.filter(post_translations::language.eq(language))
|
||||
.select(PostTranslationRecord::as_select())
|
||||
.first(c)
|
||||
.and_then(convert)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn list_post_translations_by_post(
|
||||
conn: &Connection,
|
||||
conn: &DbConnection,
|
||||
translation_for: &str,
|
||||
) -> rusqlite::Result<Vec<PostTranslation>> {
|
||||
let mut stmt = conn.prepare(&format!(
|
||||
"SELECT {POST_TRANSLATION_COLUMNS} FROM post_translations
|
||||
WHERE translation_for = ?1 ORDER BY language"
|
||||
))?;
|
||||
let rows = stmt.query_map(params![translation_for], post_translation_from_row)?;
|
||||
rows.collect()
|
||||
) -> QueryResult<Vec<PostTranslation>> {
|
||||
conn.with(|c| {
|
||||
post_translations::table
|
||||
.filter(post_translations::translation_for.eq(translation_for))
|
||||
.order(post_translations::language)
|
||||
.select(PostTranslationRecord::as_select())
|
||||
.load(c)
|
||||
.and_then(convert_all)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update_post_translation(conn: &Connection, t: &PostTranslation) -> rusqlite::Result<()> {
|
||||
pub fn update_post_translation(conn: &DbConnection, t: &PostTranslation) -> QueryResult<()> {
|
||||
if !t.status.is_valid_for_translation() {
|
||||
return Err(rusqlite::Error::InvalidParameterName(
|
||||
"translation status must be draft or published".to_string(),
|
||||
return Err(diesel::result::Error::SerializationError(
|
||||
"translation status must be draft or published".into(),
|
||||
));
|
||||
}
|
||||
conn.execute(
|
||||
"UPDATE post_translations SET
|
||||
title = ?1, excerpt = ?2, content = ?3, status = ?4,
|
||||
file_path = ?5, checksum = ?6, updated_at = ?7, published_at = ?8
|
||||
WHERE id = ?9",
|
||||
params![
|
||||
t.title,
|
||||
t.excerpt,
|
||||
t.content,
|
||||
post_status_to_str(&t.status),
|
||||
t.file_path,
|
||||
t.checksum,
|
||||
t.updated_at,
|
||||
t.published_at,
|
||||
t.id,
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
conn.with(|c| {
|
||||
diesel::update(post_translations::table.filter(post_translations::id.eq(&t.id)))
|
||||
.set(PostTranslationRecord::from(t))
|
||||
.execute(c)
|
||||
.map(|_| ())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn delete_post_translation(conn: &Connection, id: &str) -> rusqlite::Result<()> {
|
||||
conn.execute("DELETE FROM post_translations WHERE id = ?1", params![id])?;
|
||||
Ok(())
|
||||
pub fn delete_post_translation(conn: &DbConnection, id: &str) -> QueryResult<()> {
|
||||
conn.with(|c| {
|
||||
diesel::delete(post_translations::table.filter(post_translations::id.eq(id)))
|
||||
.execute(c)
|
||||
.map(|_| ())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn delete_all_translations_for_post(
|
||||
conn: &Connection,
|
||||
conn: &DbConnection,
|
||||
translation_for: &str,
|
||||
) -> rusqlite::Result<()> {
|
||||
conn.execute(
|
||||
"DELETE FROM post_translations WHERE translation_for = ?1",
|
||||
params![translation_for],
|
||||
)?;
|
||||
Ok(())
|
||||
) -> QueryResult<()> {
|
||||
conn.with(|c| {
|
||||
diesel::delete(
|
||||
post_translations::table.filter(post_translations::translation_for.eq(translation_for)),
|
||||
)
|
||||
.execute(c)
|
||||
.map(|_| ())
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::db::Database;
|
||||
use crate::db::queries::post::{insert_post, make_test_post};
|
||||
use crate::db::queries::project::{insert_project, make_test_project};
|
||||
use crate::model::PostStatus;
|
||||
|
||||
fn setup() -> Database {
|
||||
let mut db = Database::open_in_memory().unwrap();
|
||||
let db = Database::open_in_memory().unwrap();
|
||||
db.migrate().unwrap();
|
||||
insert_project(db.conn(), &make_test_project("p1", "blog")).unwrap();
|
||||
// insert a parent post
|
||||
db.conn()
|
||||
.execute(
|
||||
"INSERT INTO posts (id, project_id, title, slug, status, created_at, updated_at)
|
||||
VALUES ('post1', 'p1', 'Hello', 'hello', 'draft', 1000, 1000)",
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
insert_post(db.conn(), &make_test_post("post1", "p1", "hello")).unwrap();
|
||||
db
|
||||
}
|
||||
|
||||
|
||||
@@ -1,90 +1,88 @@
|
||||
use rusqlite::{Connection, params};
|
||||
use diesel::prelude::*;
|
||||
|
||||
use crate::db::from_row::{PROJECT_COLUMNS, project_from_row};
|
||||
use crate::db::DbConnection;
|
||||
use crate::db::from_row::ProjectRecord;
|
||||
use crate::db::schema::projects;
|
||||
use crate::model::Project;
|
||||
|
||||
pub fn insert_project(conn: &Connection, project: &Project) -> rusqlite::Result<()> {
|
||||
conn.execute(
|
||||
"INSERT INTO projects (id, name, slug, description, data_path, is_active, created_at, updated_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
|
||||
params![
|
||||
project.id,
|
||||
project.name,
|
||||
project.slug,
|
||||
project.description,
|
||||
project.data_path,
|
||||
project.is_active as i64,
|
||||
project.created_at,
|
||||
project.updated_at,
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
pub fn insert_project(conn: &DbConnection, project: &Project) -> QueryResult<()> {
|
||||
conn.with(|c| {
|
||||
diesel::insert_into(projects::table)
|
||||
.values(ProjectRecord::from(project))
|
||||
.execute(c)
|
||||
.map(|_| ())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_project_by_id(conn: &Connection, id: &str) -> rusqlite::Result<Project> {
|
||||
conn.query_row(
|
||||
&format!("SELECT {PROJECT_COLUMNS} FROM projects WHERE id = ?1"),
|
||||
params![id],
|
||||
project_from_row,
|
||||
)
|
||||
pub fn get_project_by_id(conn: &DbConnection, id: &str) -> QueryResult<Project> {
|
||||
conn.with(|c| {
|
||||
projects::table
|
||||
.filter(projects::id.eq(id))
|
||||
.select(ProjectRecord::as_select())
|
||||
.first(c)
|
||||
.map(Into::into)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_project_by_slug(conn: &Connection, slug: &str) -> rusqlite::Result<Project> {
|
||||
conn.query_row(
|
||||
&format!("SELECT {PROJECT_COLUMNS} FROM projects WHERE slug = ?1"),
|
||||
params![slug],
|
||||
project_from_row,
|
||||
)
|
||||
pub fn get_project_by_slug(conn: &DbConnection, slug: &str) -> QueryResult<Project> {
|
||||
conn.with(|c| {
|
||||
projects::table
|
||||
.filter(projects::slug.eq(slug))
|
||||
.select(ProjectRecord::as_select())
|
||||
.first(c)
|
||||
.map(Into::into)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_active_project(conn: &Connection) -> rusqlite::Result<Project> {
|
||||
conn.query_row(
|
||||
&format!("SELECT {PROJECT_COLUMNS} FROM projects WHERE is_active = 1 LIMIT 1"),
|
||||
[],
|
||||
project_from_row,
|
||||
)
|
||||
pub fn get_active_project(conn: &DbConnection) -> QueryResult<Project> {
|
||||
conn.with(|c| {
|
||||
projects::table
|
||||
.filter(projects::is_active.eq(1))
|
||||
.select(ProjectRecord::as_select())
|
||||
.first(c)
|
||||
.map(Into::into)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_active_project(conn: &Connection, id: &str) -> rusqlite::Result<()> {
|
||||
let tx = conn.unchecked_transaction()?;
|
||||
tx.execute("UPDATE projects SET is_active = 0 WHERE is_active = 1", [])?;
|
||||
tx.execute(
|
||||
"UPDATE projects SET is_active = 1 WHERE id = ?1",
|
||||
params![id],
|
||||
)?;
|
||||
tx.commit()?;
|
||||
Ok(())
|
||||
pub fn set_active_project(conn: &DbConnection, id: &str) -> QueryResult<()> {
|
||||
conn.with(|c| {
|
||||
c.transaction(|c| {
|
||||
diesel::update(projects::table.filter(projects::is_active.eq(1)))
|
||||
.set(projects::is_active.eq(0))
|
||||
.execute(c)?;
|
||||
diesel::update(projects::table.filter(projects::id.eq(id)))
|
||||
.set(projects::is_active.eq(1))
|
||||
.execute(c)?;
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn list_projects(conn: &Connection) -> rusqlite::Result<Vec<Project>> {
|
||||
let mut stmt = conn.prepare(&format!(
|
||||
"SELECT {PROJECT_COLUMNS} FROM projects ORDER BY name"
|
||||
))?;
|
||||
let rows = stmt.query_map([], project_from_row)?;
|
||||
rows.collect()
|
||||
pub fn list_projects(conn: &DbConnection) -> QueryResult<Vec<Project>> {
|
||||
conn.with(|c| {
|
||||
projects::table
|
||||
.order(projects::name)
|
||||
.select(ProjectRecord::as_select())
|
||||
.load(c)
|
||||
.map(|rows: Vec<ProjectRecord>| rows.into_iter().map(Into::into).collect())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update_project(conn: &Connection, project: &Project) -> rusqlite::Result<()> {
|
||||
conn.execute(
|
||||
"UPDATE projects SET name = ?1, slug = ?2, description = ?3, data_path = ?4,
|
||||
is_active = ?5, updated_at = ?6
|
||||
WHERE id = ?7",
|
||||
params![
|
||||
project.name,
|
||||
project.slug,
|
||||
project.description,
|
||||
project.data_path,
|
||||
project.is_active as i64,
|
||||
project.updated_at,
|
||||
project.id,
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
pub fn update_project(conn: &DbConnection, project: &Project) -> QueryResult<()> {
|
||||
conn.with(|c| {
|
||||
diesel::update(projects::table.filter(projects::id.eq(&project.id)))
|
||||
.set(ProjectRecord::from(project))
|
||||
.execute(c)
|
||||
.map(|_| ())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn delete_project(conn: &Connection, id: &str) -> rusqlite::Result<()> {
|
||||
conn.execute("DELETE FROM projects WHERE id = ?1", params![id])?;
|
||||
Ok(())
|
||||
pub fn delete_project(conn: &DbConnection, id: &str) -> QueryResult<()> {
|
||||
conn.with(|c| {
|
||||
diesel::delete(projects::table.filter(projects::id.eq(id)))
|
||||
.execute(c)
|
||||
.map(|_| ())
|
||||
})
|
||||
}
|
||||
|
||||
/// Test helper: create a minimal Project value (available to sibling test modules).
|
||||
@@ -108,7 +106,7 @@ mod tests {
|
||||
use crate::db::Database;
|
||||
|
||||
fn setup() -> Database {
|
||||
let mut db = Database::open_in_memory().unwrap();
|
||||
let db = Database::open_in_memory().unwrap();
|
||||
db.migrate().unwrap();
|
||||
db
|
||||
}
|
||||
|
||||
@@ -1,92 +1,70 @@
|
||||
use rusqlite::{Connection, params};
|
||||
use diesel::prelude::*;
|
||||
|
||||
use crate::db::from_row::{
|
||||
SCRIPT_COLUMNS, script_from_row, script_kind_to_str, script_status_to_str,
|
||||
};
|
||||
use crate::db::DbConnection;
|
||||
use crate::db::from_row::{ScriptRecord, convert, convert_all};
|
||||
use crate::db::schema::scripts;
|
||||
use crate::model::Script;
|
||||
|
||||
pub fn insert_script(conn: &Connection, s: &Script) -> rusqlite::Result<()> {
|
||||
conn.execute(
|
||||
"INSERT INTO scripts (
|
||||
id, project_id, slug, title, kind, entrypoint, enabled, version,
|
||||
file_path, status, content, created_at, updated_at
|
||||
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)",
|
||||
params![
|
||||
s.id,
|
||||
s.project_id,
|
||||
s.slug,
|
||||
s.title,
|
||||
script_kind_to_str(&s.kind),
|
||||
s.entrypoint,
|
||||
s.enabled as i64,
|
||||
s.version,
|
||||
s.file_path,
|
||||
script_status_to_str(&s.status),
|
||||
s.content,
|
||||
s.created_at,
|
||||
s.updated_at,
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
pub fn insert_script(conn: &DbConnection, s: &Script) -> QueryResult<()> {
|
||||
conn.with(|c| {
|
||||
diesel::insert_into(scripts::table)
|
||||
.values(ScriptRecord::from(s))
|
||||
.execute(c)
|
||||
.map(|_| ())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_script_by_id(conn: &Connection, id: &str) -> rusqlite::Result<Script> {
|
||||
conn.query_row(
|
||||
&format!("SELECT {SCRIPT_COLUMNS} FROM scripts WHERE id = ?1"),
|
||||
params![id],
|
||||
script_from_row,
|
||||
)
|
||||
pub fn get_script_by_id(conn: &DbConnection, id: &str) -> QueryResult<Script> {
|
||||
conn.with(|c| {
|
||||
scripts::table
|
||||
.filter(scripts::id.eq(id))
|
||||
.select(ScriptRecord::as_select())
|
||||
.first(c)
|
||||
.and_then(convert)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_script_by_slug(
|
||||
conn: &Connection,
|
||||
conn: &DbConnection,
|
||||
project_id: &str,
|
||||
slug: &str,
|
||||
) -> rusqlite::Result<Script> {
|
||||
conn.query_row(
|
||||
&format!("SELECT {SCRIPT_COLUMNS} FROM scripts WHERE project_id = ?1 AND slug = ?2"),
|
||||
params![project_id, slug],
|
||||
script_from_row,
|
||||
)
|
||||
) -> QueryResult<Script> {
|
||||
conn.with(|c| {
|
||||
scripts::table
|
||||
.filter(scripts::project_id.eq(project_id))
|
||||
.filter(scripts::slug.eq(slug))
|
||||
.select(ScriptRecord::as_select())
|
||||
.first(c)
|
||||
.and_then(convert)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn list_scripts_by_project(
|
||||
conn: &Connection,
|
||||
project_id: &str,
|
||||
) -> rusqlite::Result<Vec<Script>> {
|
||||
let mut stmt = conn.prepare(&format!(
|
||||
"SELECT {SCRIPT_COLUMNS} FROM scripts WHERE project_id = ?1 ORDER BY title"
|
||||
))?;
|
||||
let rows = stmt.query_map(params![project_id], script_from_row)?;
|
||||
rows.collect()
|
||||
pub fn list_scripts_by_project(conn: &DbConnection, project_id: &str) -> QueryResult<Vec<Script>> {
|
||||
conn.with(|c| {
|
||||
scripts::table
|
||||
.filter(scripts::project_id.eq(project_id))
|
||||
.order(scripts::title)
|
||||
.select(ScriptRecord::as_select())
|
||||
.load(c)
|
||||
.and_then(convert_all)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update_script(conn: &Connection, s: &Script) -> rusqlite::Result<()> {
|
||||
conn.execute(
|
||||
"UPDATE scripts SET
|
||||
slug = ?1, title = ?2, kind = ?3, entrypoint = ?4, enabled = ?5,
|
||||
version = ?6, file_path = ?7, status = ?8, content = ?9, updated_at = ?10
|
||||
WHERE id = ?11",
|
||||
params![
|
||||
s.slug,
|
||||
s.title,
|
||||
script_kind_to_str(&s.kind),
|
||||
s.entrypoint,
|
||||
s.enabled as i64,
|
||||
s.version,
|
||||
s.file_path,
|
||||
script_status_to_str(&s.status),
|
||||
s.content,
|
||||
s.updated_at,
|
||||
s.id,
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
pub fn update_script(conn: &DbConnection, s: &Script) -> QueryResult<()> {
|
||||
conn.with(|c| {
|
||||
diesel::update(scripts::table.filter(scripts::id.eq(&s.id)))
|
||||
.set(ScriptRecord::from(s))
|
||||
.execute(c)
|
||||
.map(|_| ())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn delete_script(conn: &Connection, id: &str) -> rusqlite::Result<()> {
|
||||
conn.execute("DELETE FROM scripts WHERE id = ?1", params![id])?;
|
||||
Ok(())
|
||||
pub fn delete_script(conn: &DbConnection, id: &str) -> QueryResult<()> {
|
||||
conn.with(|c| {
|
||||
diesel::delete(scripts::table.filter(scripts::id.eq(id)))
|
||||
.execute(c)
|
||||
.map(|_| ())
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -97,7 +75,7 @@ mod tests {
|
||||
use crate::model::{ScriptKind, ScriptStatus};
|
||||
|
||||
fn setup() -> Database {
|
||||
let mut db = Database::open_in_memory().unwrap();
|
||||
let db = Database::open_in_memory().unwrap();
|
||||
db.migrate().unwrap();
|
||||
insert_project(db.conn(), &make_test_project("p1", "blog")).unwrap();
|
||||
db
|
||||
|
||||
@@ -1,36 +1,52 @@
|
||||
use rusqlite::{Connection, params};
|
||||
use diesel::prelude::*;
|
||||
|
||||
use crate::db::from_row::{SETTING_COLUMNS, setting_from_row};
|
||||
use crate::db::DbConnection;
|
||||
use crate::db::from_row::SettingRecord;
|
||||
use crate::db::schema::settings;
|
||||
use crate::model::Setting;
|
||||
|
||||
pub fn get_setting_by_key(conn: &Connection, key: &str) -> rusqlite::Result<Setting> {
|
||||
conn.query_row(
|
||||
&format!("SELECT {SETTING_COLUMNS} FROM settings WHERE key = ?1"),
|
||||
params![key],
|
||||
setting_from_row,
|
||||
)
|
||||
pub fn get_setting_by_key(conn: &DbConnection, key: &str) -> QueryResult<Setting> {
|
||||
conn.with(|c| {
|
||||
settings::table
|
||||
.filter(settings::key.eq(key))
|
||||
.select(SettingRecord::as_select())
|
||||
.first(c)
|
||||
.map(Into::into)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_setting_value(
|
||||
conn: &Connection,
|
||||
conn: &DbConnection,
|
||||
key: &str,
|
||||
value: &str,
|
||||
updated_at: i64,
|
||||
) -> rusqlite::Result<()> {
|
||||
conn.execute(
|
||||
"INSERT INTO settings (key, value, updated_at) VALUES (?1, ?2, ?3)
|
||||
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at",
|
||||
params![key, value, updated_at],
|
||||
)?;
|
||||
Ok(())
|
||||
) -> QueryResult<()> {
|
||||
conn.with(|c| {
|
||||
diesel::insert_into(settings::table)
|
||||
.values((
|
||||
settings::key.eq(key),
|
||||
settings::value.eq(value),
|
||||
settings::updated_at.eq(updated_at),
|
||||
))
|
||||
.on_conflict(settings::key)
|
||||
.do_update()
|
||||
.set((
|
||||
settings::value.eq(value),
|
||||
settings::updated_at.eq(updated_at),
|
||||
))
|
||||
.execute(c)
|
||||
.map(|_| ())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn list_all_settings(conn: &Connection) -> rusqlite::Result<Vec<Setting>> {
|
||||
let mut stmt = conn.prepare(&format!(
|
||||
"SELECT {SETTING_COLUMNS} FROM settings ORDER BY key"
|
||||
))?;
|
||||
let rows = stmt.query_map([], setting_from_row)?;
|
||||
rows.collect()
|
||||
pub fn list_all_settings(conn: &DbConnection) -> QueryResult<Vec<Setting>> {
|
||||
conn.with(|c| {
|
||||
settings::table
|
||||
.order(settings::key)
|
||||
.select(SettingRecord::as_select())
|
||||
.load(c)
|
||||
.map(|rows: Vec<SettingRecord>| rows.into_iter().map(Into::into).collect())
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -39,7 +55,7 @@ mod tests {
|
||||
use crate::db::Database;
|
||||
|
||||
fn setup() -> Database {
|
||||
let mut db = Database::open_in_memory().unwrap();
|
||||
let db = Database::open_in_memory().unwrap();
|
||||
db.migrate().unwrap();
|
||||
db
|
||||
}
|
||||
|
||||
@@ -1,73 +1,73 @@
|
||||
use rusqlite::{Connection, params};
|
||||
use diesel::prelude::*;
|
||||
use diesel::sql_types::Text;
|
||||
|
||||
use crate::db::from_row::{TAG_COLUMNS, tag_from_row};
|
||||
use crate::db::DbConnection;
|
||||
use crate::db::from_row::TagRecord;
|
||||
use crate::db::schema::tags;
|
||||
use crate::model::Tag;
|
||||
|
||||
pub fn insert_tag(conn: &Connection, tag: &Tag) -> rusqlite::Result<()> {
|
||||
conn.execute(
|
||||
"INSERT INTO tags (id, project_id, name, color, post_template_slug, created_at, updated_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
|
||||
params![
|
||||
tag.id,
|
||||
tag.project_id,
|
||||
tag.name,
|
||||
tag.color,
|
||||
tag.post_template_slug,
|
||||
tag.created_at,
|
||||
tag.updated_at,
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
diesel::define_sql_function!(fn lower(value: Text) -> Text);
|
||||
|
||||
pub fn insert_tag(conn: &DbConnection, tag: &Tag) -> QueryResult<()> {
|
||||
conn.with(|c| {
|
||||
diesel::insert_into(tags::table)
|
||||
.values(TagRecord::from(tag))
|
||||
.execute(c)
|
||||
.map(|_| ())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_tag_by_id(conn: &Connection, id: &str) -> rusqlite::Result<Tag> {
|
||||
conn.query_row(
|
||||
&format!("SELECT {TAG_COLUMNS} FROM tags WHERE id = ?1"),
|
||||
params![id],
|
||||
tag_from_row,
|
||||
)
|
||||
pub fn get_tag_by_id(conn: &DbConnection, id: &str) -> QueryResult<Tag> {
|
||||
conn.with(|c| {
|
||||
tags::table
|
||||
.filter(tags::id.eq(id))
|
||||
.select(TagRecord::as_select())
|
||||
.first(c)
|
||||
.map(Into::into)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_tag_by_project_and_name(
|
||||
conn: &Connection,
|
||||
conn: &DbConnection,
|
||||
project_id: &str,
|
||||
name: &str,
|
||||
) -> rusqlite::Result<Tag> {
|
||||
conn.query_row(
|
||||
&format!(
|
||||
"SELECT {TAG_COLUMNS} FROM tags WHERE project_id = ?1 AND LOWER(name) = LOWER(?2)"
|
||||
),
|
||||
params![project_id, name],
|
||||
tag_from_row,
|
||||
)
|
||||
) -> QueryResult<Tag> {
|
||||
conn.with(|c| {
|
||||
tags::table
|
||||
.filter(tags::project_id.eq(project_id))
|
||||
.filter(lower(tags::name).eq(name.to_lowercase()))
|
||||
.select(TagRecord::as_select())
|
||||
.first(c)
|
||||
.map(Into::into)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn list_tags_by_project(conn: &Connection, project_id: &str) -> rusqlite::Result<Vec<Tag>> {
|
||||
let mut stmt = conn.prepare(&format!(
|
||||
"SELECT {TAG_COLUMNS} FROM tags WHERE project_id = ?1 ORDER BY name"
|
||||
))?;
|
||||
let rows = stmt.query_map(params![project_id], tag_from_row)?;
|
||||
rows.collect()
|
||||
pub fn list_tags_by_project(conn: &DbConnection, project_id: &str) -> QueryResult<Vec<Tag>> {
|
||||
conn.with(|c| {
|
||||
tags::table
|
||||
.filter(tags::project_id.eq(project_id))
|
||||
.order(tags::name)
|
||||
.select(TagRecord::as_select())
|
||||
.load(c)
|
||||
.map(|rows: Vec<TagRecord>| rows.into_iter().map(Into::into).collect())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update_tag(conn: &Connection, tag: &Tag) -> rusqlite::Result<()> {
|
||||
conn.execute(
|
||||
"UPDATE tags SET name = ?1, color = ?2, post_template_slug = ?3, updated_at = ?4
|
||||
WHERE id = ?5",
|
||||
params![
|
||||
tag.name,
|
||||
tag.color,
|
||||
tag.post_template_slug,
|
||||
tag.updated_at,
|
||||
tag.id,
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
pub fn update_tag(conn: &DbConnection, tag: &Tag) -> QueryResult<()> {
|
||||
conn.with(|c| {
|
||||
diesel::update(tags::table.filter(tags::id.eq(&tag.id)))
|
||||
.set(TagRecord::from(tag))
|
||||
.execute(c)
|
||||
.map(|_| ())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn delete_tag(conn: &Connection, id: &str) -> rusqlite::Result<()> {
|
||||
conn.execute("DELETE FROM tags WHERE id = ?1", params![id])?;
|
||||
Ok(())
|
||||
pub fn delete_tag(conn: &DbConnection, id: &str) -> QueryResult<()> {
|
||||
conn.with(|c| {
|
||||
diesel::delete(tags::table.filter(tags::id.eq(id)))
|
||||
.execute(c)
|
||||
.map(|_| ())
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -77,7 +77,7 @@ mod tests {
|
||||
use crate::db::queries::project::{insert_project, make_test_project};
|
||||
|
||||
fn setup() -> Database {
|
||||
let mut db = Database::open_in_memory().unwrap();
|
||||
let db = Database::open_in_memory().unwrap();
|
||||
db.migrate().unwrap();
|
||||
insert_project(db.conn(), &make_test_project("p1", "blog")).unwrap();
|
||||
db
|
||||
|
||||
@@ -1,90 +1,73 @@
|
||||
use rusqlite::{Connection, params};
|
||||
use diesel::prelude::*;
|
||||
|
||||
use crate::db::from_row::{
|
||||
TEMPLATE_COLUMNS, template_from_row, template_kind_to_str, template_status_to_str,
|
||||
};
|
||||
use crate::db::DbConnection;
|
||||
use crate::db::from_row::{TemplateRecord, convert, convert_all};
|
||||
use crate::db::schema::templates;
|
||||
use crate::model::Template;
|
||||
|
||||
pub fn insert_template(conn: &Connection, t: &Template) -> rusqlite::Result<()> {
|
||||
conn.execute(
|
||||
"INSERT INTO templates (
|
||||
id, project_id, slug, title, kind, enabled, version,
|
||||
file_path, status, content, created_at, updated_at
|
||||
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)",
|
||||
params![
|
||||
t.id,
|
||||
t.project_id,
|
||||
t.slug,
|
||||
t.title,
|
||||
template_kind_to_str(&t.kind),
|
||||
t.enabled as i64,
|
||||
t.version,
|
||||
t.file_path,
|
||||
template_status_to_str(&t.status),
|
||||
t.content,
|
||||
t.created_at,
|
||||
t.updated_at,
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
pub fn insert_template(conn: &DbConnection, t: &Template) -> QueryResult<()> {
|
||||
conn.with(|c| {
|
||||
diesel::insert_into(templates::table)
|
||||
.values(TemplateRecord::from(t))
|
||||
.execute(c)
|
||||
.map(|_| ())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_template_by_id(conn: &Connection, id: &str) -> rusqlite::Result<Template> {
|
||||
conn.query_row(
|
||||
&format!("SELECT {TEMPLATE_COLUMNS} FROM templates WHERE id = ?1"),
|
||||
params![id],
|
||||
template_from_row,
|
||||
)
|
||||
pub fn get_template_by_id(conn: &DbConnection, id: &str) -> QueryResult<Template> {
|
||||
conn.with(|c| {
|
||||
templates::table
|
||||
.filter(templates::id.eq(id))
|
||||
.select(TemplateRecord::as_select())
|
||||
.first(c)
|
||||
.and_then(convert)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_template_by_slug(
|
||||
conn: &Connection,
|
||||
conn: &DbConnection,
|
||||
project_id: &str,
|
||||
slug: &str,
|
||||
) -> rusqlite::Result<Template> {
|
||||
conn.query_row(
|
||||
&format!("SELECT {TEMPLATE_COLUMNS} FROM templates WHERE project_id = ?1 AND slug = ?2"),
|
||||
params![project_id, slug],
|
||||
template_from_row,
|
||||
)
|
||||
) -> QueryResult<Template> {
|
||||
conn.with(|c| {
|
||||
templates::table
|
||||
.filter(templates::project_id.eq(project_id))
|
||||
.filter(templates::slug.eq(slug))
|
||||
.select(TemplateRecord::as_select())
|
||||
.first(c)
|
||||
.and_then(convert)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn list_templates_by_project(
|
||||
conn: &Connection,
|
||||
conn: &DbConnection,
|
||||
project_id: &str,
|
||||
) -> rusqlite::Result<Vec<Template>> {
|
||||
let mut stmt = conn.prepare(&format!(
|
||||
"SELECT {TEMPLATE_COLUMNS} FROM templates WHERE project_id = ?1 ORDER BY title"
|
||||
))?;
|
||||
let rows = stmt.query_map(params![project_id], template_from_row)?;
|
||||
rows.collect()
|
||||
) -> QueryResult<Vec<Template>> {
|
||||
conn.with(|c| {
|
||||
templates::table
|
||||
.filter(templates::project_id.eq(project_id))
|
||||
.order(templates::title)
|
||||
.select(TemplateRecord::as_select())
|
||||
.load(c)
|
||||
.and_then(convert_all)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update_template(conn: &Connection, t: &Template) -> rusqlite::Result<()> {
|
||||
conn.execute(
|
||||
"UPDATE templates SET
|
||||
slug = ?1, title = ?2, kind = ?3, enabled = ?4, version = ?5,
|
||||
file_path = ?6, status = ?7, content = ?8, updated_at = ?9
|
||||
WHERE id = ?10",
|
||||
params![
|
||||
t.slug,
|
||||
t.title,
|
||||
template_kind_to_str(&t.kind),
|
||||
t.enabled as i64,
|
||||
t.version,
|
||||
t.file_path,
|
||||
template_status_to_str(&t.status),
|
||||
t.content,
|
||||
t.updated_at,
|
||||
t.id,
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
pub fn update_template(conn: &DbConnection, t: &Template) -> QueryResult<()> {
|
||||
conn.with(|c| {
|
||||
diesel::update(templates::table.filter(templates::id.eq(&t.id)))
|
||||
.set(TemplateRecord::from(t))
|
||||
.execute(c)
|
||||
.map(|_| ())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn delete_template(conn: &Connection, id: &str) -> rusqlite::Result<()> {
|
||||
conn.execute("DELETE FROM templates WHERE id = ?1", params![id])?;
|
||||
Ok(())
|
||||
pub fn delete_template(conn: &DbConnection, id: &str) -> QueryResult<()> {
|
||||
conn.with(|c| {
|
||||
diesel::delete(templates::table.filter(templates::id.eq(id)))
|
||||
.execute(c)
|
||||
.map(|_| ())
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -95,7 +78,7 @@ mod tests {
|
||||
use crate::model::{TemplateKind, TemplateStatus};
|
||||
|
||||
fn setup() -> Database {
|
||||
let mut db = Database::open_in_memory().unwrap();
|
||||
let db = Database::open_in_memory().unwrap();
|
||||
db.migrate().unwrap();
|
||||
insert_project(db.conn(), &make_test_project("p1", "blog")).unwrap();
|
||||
db
|
||||
|
||||
Reference in New Issue
Block a user