From 38dbf10130fe8b4f70983dae0ad1988c1f0f06d8 Mon Sep 17 00:00:00 2001 From: trisua Date: Sun, 23 Mar 2025 21:19:16 -0400 Subject: [PATCH] add: redis cache support --- Cargo.lock | 40 +++++ crates/app/Cargo.toml | 2 +- crates/core/Cargo.toml | 5 +- crates/core/src/cache/mod.rs | 77 ++++++++ crates/core/src/cache/no_cache.rs | 62 +++++++ crates/core/src/cache/redis.rs | 123 +++++++++++++ crates/core/src/database/auth.rs | 10 +- crates/core/src/database/common.rs | 177 +++++++++++++++++++ crates/core/src/database/drivers/postgres.rs | 21 ++- crates/core/src/database/drivers/sqlite.rs | 23 ++- crates/core/src/database/pages.rs | 13 +- crates/core/src/lib.rs | 1 + crates/shared/src/time.rs | 4 +- 13 files changed, 541 insertions(+), 17 deletions(-) create mode 100644 crates/core/src/cache/mod.rs create mode 100644 crates/core/src/cache/no_cache.rs create mode 100644 crates/core/src/cache/redis.rs diff --git a/Cargo.lock b/Cargo.lock index 6ee6475..c1ec6c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -59,6 +59,12 @@ version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + [[package]] name = "arg_enum_proc_macro" version = "0.3.4" @@ -403,6 +409,16 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "cookie" version = "0.18.1" @@ -2015,6 +2031,23 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "redis" +version = "0.29.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b110459d6e323b7cda23980c46c77157601199c9da6241552b284cd565a7a133" +dependencies = [ + "arc-swap", + "combine", + "itoa", + "num-bigint", + "percent-encoding", + "ryu", + "sha1_smol", + "socket2", + "url", +] + [[package]] name = "redox_syscall" version = "0.5.10" @@ -2326,6 +2359,12 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + [[package]] name = "sha2" version = "0.10.8" @@ -2571,6 +2610,7 @@ dependencies = [ "bb8-postgres", "bitflags 2.9.0", "pathbufd", + "redis", "rusqlite", "serde", "serde_json", diff --git a/crates/app/Cargo.toml b/crates/app/Cargo.toml index 83b9312..9c7d5db 100644 --- a/crates/app/Cargo.toml +++ b/crates/app/Cargo.toml @@ -19,7 +19,7 @@ axum = { version = "0.8.1", features = ["macros"] } tokio = { version = "1.44.1", features = ["macros", "rt-multi-thread"] } axum-extra = { version = "0.10.0", features = ["cookie", "multipart"] } tetratto-shared = { path = "../shared" } -tetratto-core = { path = "../core", default-features = false } +tetratto-core = { path = "../core", features = ["redis"], default-features = false } tetratto-l10n = { path = "../l10n" } image = "0.25.5" diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 795efac..109ab22 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -6,7 +6,8 @@ edition = "2024" [features] postgres = ["dep:tokio-postgres", "dep:bb8-postgres"] sqlite = ["dep:rusqlite"] -default = ["sqlite"] +redis = ["dep:redis"] +default = ["sqlite", "redis"] [dependencies] pathbufd = "0.1.4" @@ -16,6 +17,8 @@ tetratto-shared = { path = "../shared" } tetratto-l10n = { path = "../l10n" } serde_json = "1.0.140" +redis = { version = "0.29.2", optional = true } + rusqlite = { version = "0.34.0", optional = true } tokio-postgres = { version = "0.7.13", optional = true } diff --git a/crates/core/src/cache/mod.rs b/crates/core/src/cache/mod.rs new file mode 100644 index 0000000..acc006a --- /dev/null +++ b/crates/core/src/cache/mod.rs @@ -0,0 +1,77 @@ +#![allow(async_fn_in_trait)] +use serde::{Serialize, de::DeserializeOwned}; + +pub const EXPIRE_AT: i64 = 3_600_000; + +#[allow(type_alias_bounds)] +pub type TimedObject = (i64, T); + +#[cfg(feature = "redis")] +pub mod redis; + +#[cfg(not(feature = "redis"))] +pub mod no_cache; + +/// A simple cache "database". +pub trait Cache { + type Item; + type Client; + + /// Create a new [`Cache`]. + async fn new() -> Self; + /// Get a connection to the cache. + async fn get_con(&self) -> Self::Client; + + /// Get a cache object by its identifier + /// + /// # Arguments + /// * `id` - `String` of the object's id + async fn get(&self, id: Self::Item) -> Option; + /// Set a cache object by its identifier and content + /// + /// # Arguments + /// * `id` - `String` of the object's id + /// * `content` - `String` of the object's content + async fn set(&self, id: Self::Item, content: Self::Item) -> bool; + /// Update a cache object by its identifier and content + /// + /// # Arguments + /// * `id` - `String` of the object's id + /// * `content` - `String` of the object's content + async fn update(&self, id: Self::Item, content: Self::Item) -> bool; + /// Remove a cache object by its identifier + /// + /// # Arguments + /// * `id` - `String` of the object's id + async fn remove(&self, id: Self::Item) -> bool; + /// Remove a cache object by its identifier('s start) + /// + /// # Arguments + /// * `id` - `String` of the object's id('s start) + async fn remove_starting_with(&self, id: Self::Item) -> bool; + /// Increment a cache object by its identifier + /// + /// # Arguments + /// * `id` - `String` of the object's id + async fn incr(&self, id: Self::Item) -> bool; + /// Decrement a cache object by its identifier + /// + /// # Arguments + /// * `id` - `String` of the object's id + async fn decr(&self, id: Self::Item) -> bool; + + /// Get a cache object by its identifier + /// + /// # Arguments + /// * `id` - `String` of the object's id + async fn get_timed( + &self, + id: Self::Item, + ) -> Option>; + /// Set a cache object by its identifier and content + /// + /// # Arguments + /// * `id` - `String` of the object's id + /// * `content` - `String` of the object's content + async fn set_timed(&self, id: Self::Item, content: T) -> bool; +} diff --git a/crates/core/src/cache/no_cache.rs b/crates/core/src/cache/no_cache.rs new file mode 100644 index 0000000..c6022c6 --- /dev/null +++ b/crates/core/src/cache/no_cache.rs @@ -0,0 +1,62 @@ +use serde::{Serialize, de::DeserializeOwned}; + +use super::{Cache, EXPIRE_AT, TimedObject}; + +pub const EPOCH_YEAR: u32 = 2025; + +#[derive(Clone)] +pub struct NoCache { + pub client: Option, +} + +impl Cache for NoCache { + type Item = String; + type Client = Option; + + async fn new() -> Self { + Self { client: None } + } + + async fn get_con(&self) -> Self::Client { + None + } + + async fn get(&self, id: Self::Item) -> Option { + None + } + + async fn set(&self, id: Self::Item, content: Self::Item) -> bool { + true + } + + async fn update(&self, id: Self::Item, content: Self::Item) -> bool { + true + } + + async fn remove(&self, id: Self::Item) -> bool { + true + } + + async fn remove_starting_with(&self, id: Self::Item) -> bool { + true + } + + async fn incr(&self, id: Self::Item) -> bool { + true + } + + async fn decr(&self, id: Self::Item) -> bool { + true + } + + async fn get_timed( + &self, + id: Self::Item, + ) -> Option> { + None + } + + async fn set_timed(&self, id: Self::Item, content: T) -> bool { + None + } +} diff --git a/crates/core/src/cache/redis.rs b/crates/core/src/cache/redis.rs new file mode 100644 index 0000000..335c5e4 --- /dev/null +++ b/crates/core/src/cache/redis.rs @@ -0,0 +1,123 @@ +use redis::Commands; +use serde::{Serialize, de::DeserializeOwned}; + +use super::{Cache, EXPIRE_AT, TimedObject}; + +pub const EPOCH_YEAR: u32 = 2025; + +#[derive(Clone)] +pub struct RedisCache { + pub client: redis::Client, +} + +impl Cache for RedisCache { + type Item = String; + type Client = redis::Connection; + + async fn new() -> Self { + Self { + client: redis::Client::open("redis://127.0.0.1:6379").unwrap(), + } + } + + async fn get_con(&self) -> Self::Client { + self.client.get_connection().unwrap() + } + + async fn get(&self, id: Self::Item) -> Option { + self.get_con().await.get(id).ok() + } + + async fn set(&self, id: Self::Item, content: Self::Item) -> bool { + let mut c = self.get_con().await; + let res: Result = c.set(id, content); + + res.is_ok() + } + + async fn update(&self, id: Self::Item, content: Self::Item) -> bool { + self.set(id, content).await + } + + async fn remove(&self, id: Self::Item) -> bool { + let mut c = self.get_con().await; + let res: Result = c.del(id); + + res.is_ok() + } + + async fn remove_starting_with(&self, id: Self::Item) -> bool { + let mut c = self.get_con().await; + + // get keys + let mut cmd = redis::cmd("DEL"); + let keys: Result, redis::RedisError> = c.keys(id); + + for key in keys.unwrap() { + cmd.arg(key); + } + + // remove + let res: Result = cmd.query(&mut c); + + res.is_ok() + } + + async fn incr(&self, id: Self::Item) -> bool { + let mut c = self.get_con().await; + let res: Result = c.incr(id, 1); + + res.is_ok() + } + + async fn decr(&self, id: Self::Item) -> bool { + let mut c = self.get_con().await; + let res: Result = c.decr(id, 1); + + res.is_ok() + } + + async fn get_timed( + &self, + id: Self::Item, + ) -> Option> { + let mut c = self.get_con().await; + let res: Result = c.get(&id); + + match res { + Ok(d) => match serde_json::from_str::>(&d) { + Ok(d) => { + // check time + let now = tetratto_shared::epoch_timestamp(EPOCH_YEAR); + + if now - d.0 >= EXPIRE_AT { + // expired key, remove and return None + self.remove(id).await; + return None; + } + + // return + Some(d) + } + Err(_) => None, + }, + Err(_) => None, + } + } + + async fn set_timed(&self, id: Self::Item, content: T) -> bool { + let mut c = self.get_con().await; + let res: Result = c.set( + id, + match serde_json::to_string::>(&( + tetratto_shared::epoch_timestamp(EPOCH_YEAR), + content, + )) { + Ok(s) => s, + Err(_) => return false, + }, + ); + + res.is_ok() + } +} diff --git a/crates/core/src/database/auth.rs b/crates/core/src/database/auth.rs index bd58f91..cee3713 100644 --- a/crates/core/src/database/auth.rs +++ b/crates/core/src/database/auth.rs @@ -1,4 +1,5 @@ use super::*; +use crate::cache::Cache; use crate::model::{ Error, Result, auth::{Token, User}, @@ -31,8 +32,8 @@ impl DataManager { } } - auto_method!(get_user_by_id(&str)@get_user_from_row -> "SELECT * FROM users WHERE id = $1" --name="user" --returns=User); - auto_method!(get_user_by_username(&str)@get_user_from_row -> "SELECT * FROM users WHERE username = $1" --name="user" --returns=User); + auto_method!(get_user_by_id(&str)@get_user_from_row -> "SELECT * FROM users WHERE id = $1" --name="user" --returns=User --cache-key-tmpl="atto.user:{}"); + auto_method!(get_user_by_username(&str)@get_user_from_row -> "SELECT * FROM users WHERE username = $1" --name="user" --returns=User --cache-key-tmpl="atto.user:{}"); /// Get a user given just their auth token. /// @@ -130,8 +131,11 @@ impl DataManager { return Err(Error::DatabaseError(e.to_string())); } + self.2.remove(format!("atto.user:{}", id)).await; + self.2.remove(format!("atto.user:{}", user.username)).await; + Ok(()) } - auto_method!(update_user_tokens(Vec) -> "UPDATE users SET tokens = $1 WHERE id = $2" --serde); + auto_method!(update_user_tokens(Vec) -> "UPDATE users SET tokens = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.user:{}"); } diff --git a/crates/core/src/database/common.rs b/crates/core/src/database/common.rs index da6ced3..76188e8 100644 --- a/crates/core/src/database/common.rs +++ b/crates/core/src/database/common.rs @@ -39,6 +39,31 @@ macro_rules! auto_method { } }; + ($name:ident()@$select_fn:ident -> $query:literal --name=$name_:literal --returns=$returns_:tt --cache-key-tmpl=$cache_key_tmpl:literal) => { + pub async fn $name(&self, id: usize) -> Result<$returns_> { + let conn = match self.connect().await { + Ok(c) => c, + Err(e) => return Err(Error::DatabaseConnection(e.to_string())), + }; + + let res = query_row!(&conn, $query, &[&id], |x| { Ok(Self::$select_fn(x)) }); + + if res.is_err() { + return Err(Error::GeneralNotFound($name_.to_string())); + } + + let x = res.unwrap(); + self.2 + .set( + format!($cache_key_tmpl, id), + serde_json::to_string(&x).unwrap(), + ) + .await; + + Ok(x) + } + }; + ($name:ident($selector_t:ty)@$select_fn:ident -> $query:literal --name=$name_:literal --returns=$returns_:tt) => { pub async fn $name(&self, selector: $selector_t) -> Result<$returns_> { let conn = match self.connect().await { @@ -56,6 +81,31 @@ macro_rules! auto_method { } }; + ($name:ident($selector_t:ty)@$select_fn:ident -> $query:literal --name=$name_:literal --returns=$returns_:tt --cache-key-tmpl=$cache_key_tmpl:literal) => { + pub async fn $name(&self, selector: $selector_t) -> Result<$returns_> { + let conn = match self.connect().await { + Ok(c) => c, + Err(e) => return Err(Error::DatabaseConnection(e.to_string())), + }; + + let res = query_row!(&conn, $query, &[&selector], |x| { Ok(Self::$select_fn(x)) }); + + if res.is_err() { + return Err(Error::GeneralNotFound($name_.to_string())); + } + + let x = res.unwrap(); + self.2 + .set( + format!($cache_key_tmpl, selector), + serde_json::to_string(&x).unwrap(), + ) + .await; + + Ok(x) + } + }; + ($name:ident()@$select_fn:ident:$permission:ident -> $query:literal) => { pub async fn $name(&self, id: usize, user: User) -> Result<()> { let page = self.$select_fn(id).await?; @@ -81,6 +131,33 @@ macro_rules! auto_method { } }; + ($name:ident()@$select_fn:ident:$permission:ident -> $query:literal --cache-key-tmpl=$cache_key_tmpl:literal) => { + pub async fn $name(&self, id: usize, user: User) -> Result<()> { + let page = self.$select_fn(id).await?; + + if user.id != page.owner { + if !user.permissions.check(FinePermission::$permission) { + return Err(Error::NotAllowed); + } + } + + let conn = match self.connect().await { + Ok(c) => c, + Err(e) => return Err(Error::DatabaseConnection(e.to_string())), + }; + + let res = execute!(&conn, $query, &[&id.to_string()]); + + if let Err(e) = res { + return Err(Error::DatabaseError(e.to_string())); + } + + self.2.remove(format!($cache_key_tmpl, id)).await; + + Ok(()) + } + }; + ($name:ident($x:ty)@$select_fn:ident:$permission:ident -> $query:literal) => { pub async fn $name(&self, id: usize, user: User, x: $x) -> Result<()> { let y = self.$select_fn(id).await?; @@ -106,6 +183,33 @@ macro_rules! auto_method { } }; + ($name:ident($x:ty)@$select_fn:ident:$permission:ident -> $query:literal --cache-key-tmpl=$cache_key_tmpl:literal) => { + pub async fn $name(&self, id: usize, user: User, x: $x) -> Result<()> { + let y = self.$select_fn(id).await?; + + if user.id != y.owner { + if !user.permissions.check(FinePermission::$permission) { + return Err(Error::NotAllowed); + } + } + + let conn = match self.connect().await { + Ok(c) => c, + Err(e) => return Err(Error::DatabaseConnection(e.to_string())), + }; + + let res = execute!(&conn, $query, &[&x, &id.to_string()]); + + if let Err(e) = res { + return Err(Error::DatabaseError(e.to_string())); + } + + self.2.remove(format!($cache_key_tmpl, id)).await; + + Ok(()) + } + }; + ($name:ident($x:ty)@$select_fn:ident:$permission:ident -> $query:literal --serde) => { pub async fn $name(&self, id: usize, user: User, x: $x) -> Result<()> { let y = self.$select_fn(id).await?; @@ -135,6 +239,37 @@ macro_rules! auto_method { } }; + ($name:ident($x:ty)@$select_fn:ident:$permission:ident -> $query:literal --serde --cache-key-tmpl=$cache_key_tmpl:literal) => { + pub async fn $name(&self, id: usize, user: User, x: $x) -> Result<()> { + let y = self.$select_fn(id).await?; + + if user.id != y.owner { + if !user.permissions.check(FinePermission::$permission) { + return Err(Error::NotAllowed); + } + } + + let conn = match self.connect().await { + Ok(c) => c, + Err(e) => return Err(Error::DatabaseConnection(e.to_string())), + }; + + let res = execute!( + &conn, + $query, + &[&serde_json::to_string(&x).unwrap(), &id.to_string()] + ); + + if let Err(e) = res { + return Err(Error::DatabaseError(e.to_string())); + } + + self.2.remove(format!($cache_key_tmpl, id)).await; + + Ok(()) + } + }; + ($name:ident($x:ty) -> $query:literal) => { pub async fn $name(&self, id: usize, x: $x) -> Result<()> { let conn = match self.connect().await { @@ -152,6 +287,25 @@ macro_rules! auto_method { } }; + ($name:ident($x:ty) -> $query:literal --cache-key-tmpl=$cache_key_tmpl:literal) => { + pub async fn $name(&self, id: usize, x: $x) -> Result<()> { + let conn = match self.connect().await { + Ok(c) => c, + Err(e) => return Err(Error::DatabaseConnection(e.to_string())), + }; + + let res = execute!(&conn, $query, &[&x, &id.to_string()]); + + if let Err(e) = res { + return Err(Error::DatabaseError(e.to_string())); + } + + self.2.remove(format!($cache_key_tmpl, id)).await; + + Ok(()) + } + }; + ($name:ident($x:ty) -> $query:literal --serde) => { pub async fn $name(&self, id: usize, x: $x) -> Result<()> { let conn = match self.connect().await { @@ -172,4 +326,27 @@ macro_rules! auto_method { Ok(()) } }; + + ($name:ident($x:ty) -> $query:literal --serde --cache-key-tmpl=$cache_key_tmpl:literal) => { + pub async fn $name(&self, id: usize, x: $x) -> Result<()> { + let conn = match self.connect().await { + Ok(c) => c, + Err(e) => return Err(Error::DatabaseConnection(e.to_string())), + }; + + let res = execute!( + &conn, + $query, + &[&serde_json::to_string(&x).unwrap(), &id.to_string()] + ); + + if let Err(e) = res { + return Err(Error::DatabaseError(e.to_string())); + } + + self.2.remove(format!($cache_key_tmpl, id)).await; + + Ok(()) + } + }; } diff --git a/crates/core/src/database/drivers/postgres.rs b/crates/core/src/database/drivers/postgres.rs index ed1a6f1..bdd0e00 100644 --- a/crates/core/src/database/drivers/postgres.rs +++ b/crates/core/src/database/drivers/postgres.rs @@ -1,3 +1,10 @@ +#[cfg(not(feature = "redis"))] +use crate::cache::no_cache::NoCache; +#[cfg(feature = "redis")] +use crate::cache::redis::RedisCache; + +use crate::cache::Cache; + use crate::config::Config; use bb8_postgres::{ PostgresConnectionManager, @@ -13,13 +20,15 @@ pub type Connection<'a> = PooledConnection<'a, PostgresConnectionManager> pub struct DataManager( pub Config, pub HashMap, + #[cfg(feature = "redis")] pub RedisCache, + #[cfg(not(feature = "redis"))] pub NoCache, pub Pool>, ); impl DataManager { /// Obtain a connection to the staging database. pub(crate) async fn connect(&self) -> Result { - Ok(self.2.get().await.unwrap()) + Ok(self.3.get().await.unwrap()) } /// Create a new [`DataManager`] (and init database). @@ -36,7 +45,15 @@ impl DataManager { ); let pool = Pool::builder().max_size(15).build(manager).await.unwrap(); - Ok(Self(config.clone(), read_langs(), pool)) + Ok(Self( + config.clone(), + read_langs(), + #[cfg(feature = "redis")] + RedisCache::new().await, + #[cfg(not(feature = "redis"))] + NoCache::new().await, + pool, + )) } } diff --git a/crates/core/src/database/drivers/sqlite.rs b/crates/core/src/database/drivers/sqlite.rs index cd07d80..b117387 100644 --- a/crates/core/src/database/drivers/sqlite.rs +++ b/crates/core/src/database/drivers/sqlite.rs @@ -1,10 +1,22 @@ +#[cfg(not(feature = "redis"))] +use crate::cache::no_cache::NoCache; +#[cfg(feature = "redis")] +use crate::cache::redis::RedisCache; + +use crate::cache::Cache; + use crate::config::Config; use rusqlite::{Connection, Result}; use std::collections::HashMap; use tetratto_l10n::{LangFile, read_langs}; #[derive(Clone)] -pub struct DataManager(pub Config, pub HashMap); +pub struct DataManager( + pub Config, + pub HashMap, + #[cfg(feature = "redis")] pub RedisCache, + #[cfg(not(feature = "redis"))] pub NoCache, +); impl DataManager { /// Obtain a connection to the staging database. @@ -14,7 +26,14 @@ impl DataManager { /// Create a new [`DataManager`] (and init database). pub async fn new(config: Config) -> Result { - let this = Self(config.clone(), read_langs()); + let this = Self( + config.clone(), + read_langs(), + #[cfg(feature = "redis")] + RedisCache::new().await, + #[cfg(not(feature = "redis"))] + NoCache::new().await, + ); let conn = this.connect().await?; conn.pragma_update(None, "journal_mode", "WAL").unwrap(); diff --git a/crates/core/src/database/pages.rs b/crates/core/src/database/pages.rs index b258d81..d57023a 100644 --- a/crates/core/src/database/pages.rs +++ b/crates/core/src/database/pages.rs @@ -1,4 +1,5 @@ use super::*; +use crate::cache::Cache; use crate::model::auth::User; use crate::model::{Error, Result, journal::JournalPage, permissions::FinePermission}; use crate::{auto_method, execute, get, query_row}; @@ -26,7 +27,7 @@ impl DataManager { } } - auto_method!(get_page_by_id()@get_page_from_row -> "SELECT * FROM pages WHERE id = $1" --name="journal page" --returns=JournalPage); + auto_method!(get_page_by_id()@get_page_from_row -> "SELECT * FROM pages WHERE id = $1" --name="journal page" --returns=JournalPage --cache-key-tmpl="atto.page:{}"); /// Create a new journal page in the database. /// @@ -77,9 +78,9 @@ impl DataManager { Ok(()) } - auto_method!(delete_page()@get_page_by_id:MANAGE_JOURNAL_PAGES -> "DELETE FROM pages WHERE id = $1"); - auto_method!(update_page_title(String)@get_page_by_id:MANAGE_JOURNAL_PAGES -> "UPDATE pages SET title = $1 WHERE id = $2"); - auto_method!(update_page_prompt(String)@get_page_by_id:MANAGE_JOURNAL_PAGES -> "UPDATE pages SET prompt = $1 WHERE id = $2"); - auto_method!(update_page_read_access(String)@get_page_by_id:MANAGE_JOURNAL_PAGES -> "UPDATE pages SET read_access = $1 WHERE id = $2" --serde); - auto_method!(update_page_write_access(String)@get_page_by_id:MANAGE_JOURNAL_PAGES -> "UPDATE pages SET write_access = $1 WHERE id = $2" --serde); + auto_method!(delete_page()@get_page_by_id:MANAGE_JOURNAL_PAGES -> "DELETE FROM pages WHERE id = $1" --cache-key-tmpl="atto.page:{}"); + auto_method!(update_page_title(String)@get_page_by_id:MANAGE_JOURNAL_PAGES -> "UPDATE pages SET title = $1 WHERE id = $2" --cache-key-tmpl="atto.page:{}"); + auto_method!(update_page_prompt(String)@get_page_by_id:MANAGE_JOURNAL_PAGES -> "UPDATE pages SET prompt = $1 WHERE id = $2" --cache-key-tmpl="atto.page:{}"); + auto_method!(update_page_read_access(String)@get_page_by_id:MANAGE_JOURNAL_PAGES -> "UPDATE pages SET read_access = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.page:{}"); + auto_method!(update_page_write_access(String)@get_page_by_id:MANAGE_JOURNAL_PAGES -> "UPDATE pages SET write_access = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.page:{}"); } diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index b3a148a..b94890d 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -1,3 +1,4 @@ +pub mod cache; pub mod config; pub mod database; pub mod model; diff --git a/crates/shared/src/time.rs b/crates/shared/src/time.rs index a2ad900..970b5ea 100644 --- a/crates/shared/src/time.rs +++ b/crates/shared/src/time.rs @@ -12,10 +12,10 @@ pub fn unix_epoch_timestamp() -> u128 { } /// Get a [`i64`] timestamp from the given `year` epoch -pub fn epoch_timestamp(year: i32) -> i64 { +pub fn epoch_timestamp(year: u32) -> i64 { let now = Utc::now().timestamp_millis(); let then = Utc - .with_ymd_and_hms(year, 1, 1, 0, 0, 0) + .with_ymd_and_hms(year as i32, 1, 1, 0, 0, 0) .unwrap() .timestamp_millis();