add: notifications table
add: query_rows macro fix: postgres driver
This commit is contained in:
parent
0ea6b25138
commit
81005a6e1c
14 changed files with 258 additions and 33 deletions
|
@ -21,8 +21,8 @@ impl DataManager {
|
|||
#[cfg(feature = "postgres")] x: &Row,
|
||||
) -> User {
|
||||
User {
|
||||
id: get!(x->0(u64)) as usize,
|
||||
created: get!(x->1(u64)) as usize,
|
||||
id: get!(x->0(i64)) as usize,
|
||||
created: get!(x->1(i64)) as usize,
|
||||
username: get!(x->2(String)),
|
||||
password: get!(x->3(String)),
|
||||
salt: get!(x->4(String)),
|
||||
|
@ -87,7 +87,7 @@ impl DataManager {
|
|||
|
||||
let res = execute!(
|
||||
&conn,
|
||||
"INSERT INTO users VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
|
||||
"INSERT INTO users VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)",
|
||||
&[
|
||||
&data.id.to_string().as_str(),
|
||||
&data.created.to_string().as_str(),
|
||||
|
@ -96,7 +96,8 @@ impl DataManager {
|
|||
&data.salt.as_str(),
|
||||
&serde_json::to_string(&data.settings).unwrap().as_str(),
|
||||
&serde_json::to_string(&data.tokens).unwrap().as_str(),
|
||||
&(FinePermission::DEFAULT.bits()).to_string().as_str()
|
||||
&(FinePermission::DEFAULT.bits()).to_string().as_str(),
|
||||
&0.to_string().as_str()
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -138,4 +139,7 @@ impl DataManager {
|
|||
}
|
||||
|
||||
auto_method!(update_user_tokens(Vec<Token>) -> "UPDATE users SET tokens = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.user:{}");
|
||||
|
||||
auto_method!(incr_user_notifications() -> "UPDATE users SET notification_count = notification_count + 1 WHERE id = $1" --cache-key-tmpl="atto.user:{}" --reactions-key-tmpl="atto.user.notification_count:{}" --incr);
|
||||
auto_method!(decr_user_notifications() -> "UPDATE users SET notification_count = notification_count - 1 WHERE id = $1" --cache-key-tmpl="atto.user:{}" --reactions-key-tmpl="atto.user.notification_count:{}" --decr);
|
||||
}
|
||||
|
|
|
@ -13,11 +13,12 @@ impl DataManager {
|
|||
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||
};
|
||||
|
||||
execute!(&conn, common::CREATE_TABLE_USERS, []).unwrap();
|
||||
execute!(&conn, common::CREATE_TABLE_PAGES, []).unwrap();
|
||||
execute!(&conn, common::CREATE_TABLE_ENTRIES, []).unwrap();
|
||||
execute!(&conn, common::CREATE_TABLE_MEMBERSHIPS, []).unwrap();
|
||||
execute!(&conn, common::CREATE_TABLE_REACTIONS, []).unwrap();
|
||||
execute!(&conn, common::CREATE_TABLE_USERS).unwrap();
|
||||
execute!(&conn, common::CREATE_TABLE_PAGES).unwrap();
|
||||
execute!(&conn, common::CREATE_TABLE_ENTRIES).unwrap();
|
||||
execute!(&conn, common::CREATE_TABLE_MEMBERSHIPS).unwrap();
|
||||
execute!(&conn, common::CREATE_TABLE_REACTIONS).unwrap();
|
||||
execute!(&conn, common::CREATE_TABLE_NOTIFICATIONS).unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -32,7 +33,9 @@ macro_rules! auto_method {
|
|||
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||
};
|
||||
|
||||
let res = query_row!(&conn, $query, &[&id], |x| { Ok(Self::$select_fn(x)) });
|
||||
let res = query_row!(&conn, $query, &[&(id as i64)], |x| {
|
||||
Ok(Self::$select_fn(x))
|
||||
});
|
||||
|
||||
if res.is_err() {
|
||||
return Err(Error::GeneralNotFound($name_.to_string()));
|
||||
|
@ -49,7 +52,9 @@ macro_rules! auto_method {
|
|||
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||
};
|
||||
|
||||
let res = query_row!(&conn, $query, &[&id], |x| { Ok(Self::$select_fn(x)) });
|
||||
let res = query_row!(&conn, $query, &[&(id as i64)], |x| {
|
||||
Ok(Self::$select_fn(x))
|
||||
});
|
||||
|
||||
if res.is_err() {
|
||||
return Err(Error::GeneralNotFound($name_.to_string()));
|
||||
|
|
|
@ -3,3 +3,4 @@ pub const CREATE_TABLE_PAGES: &str = include_str!("./sql/create_pages.sql");
|
|||
pub const CREATE_TABLE_ENTRIES: &str = include_str!("./sql/create_entries.sql");
|
||||
pub const CREATE_TABLE_MEMBERSHIPS: &str = include_str!("./sql/create_memberships.sql");
|
||||
pub const CREATE_TABLE_REACTIONS: &str = include_str!("./sql/create_reactions.sql");
|
||||
pub const CREATE_TABLE_NOTIFICATIONS: &str = include_str!("./sql/create_notifications.sql");
|
||||
|
|
|
@ -10,6 +10,7 @@ use bb8_postgres::{
|
|||
PostgresConnectionManager,
|
||||
bb8::{Pool, PooledConnection},
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use tetratto_l10n::{LangFile, read_langs};
|
||||
use tokio_postgres::{Config as PgConfig, NoTls, Row, types::ToSql};
|
||||
|
||||
|
@ -91,6 +92,38 @@ macro_rules! query_row {
|
|||
};
|
||||
}
|
||||
|
||||
pub async fn query_rows_helper<T, F>(
|
||||
conn: &Connection<'_>,
|
||||
sql: &str,
|
||||
params: &[&(dyn ToSql + Sync)],
|
||||
mut f: F,
|
||||
) -> Result<Vec<T>>
|
||||
where
|
||||
F: FnMut(&Row) -> T,
|
||||
{
|
||||
let query = conn.prepare(sql).await.unwrap();
|
||||
let res = conn.query(&query, params).await;
|
||||
|
||||
if let Ok(rows) = res {
|
||||
let mut out = Vec::new();
|
||||
|
||||
for row in rows {
|
||||
out.push(f(&row));
|
||||
}
|
||||
|
||||
return Ok(out);
|
||||
} else {
|
||||
Err(res.unwrap_err())
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! query_rows {
|
||||
($conn:expr, $sql:expr, $params:expr, $f:expr) => {
|
||||
crate::database::query_rows_helper($conn, $sql, $params, $f).await
|
||||
};
|
||||
}
|
||||
|
||||
pub async fn execute_helper(
|
||||
conn: &Connection<'_>,
|
||||
sql: &str,
|
||||
|
@ -106,4 +139,8 @@ macro_rules! execute {
|
|||
($conn:expr, $sql:expr, $params:expr) => {
|
||||
crate::database::execute_helper($conn, $sql, $params).await
|
||||
};
|
||||
|
||||
($conn:expr, $sql:expr) => {
|
||||
crate::database::execute_helper($conn, $sql, &[]).await
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
CREATE TABLE IF NOT EXISTS notifications (
|
||||
id INTEGER NOT NULL PRIMARY KEY,
|
||||
created INTEGER NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
owner INTEGER NOT NULL
|
||||
)
|
|
@ -6,5 +6,7 @@ CREATE TABLE IF NOT EXISTS users (
|
|||
salt TEXT NOT NULL,
|
||||
settings TEXT NOT NULL,
|
||||
tokens TEXT NOT NULL,
|
||||
permissions INTEGER NOT NULL
|
||||
permissions INTEGER NOT NULL,
|
||||
-- counts
|
||||
notification_count INTEGER NOT NULL
|
||||
)
|
||||
|
|
|
@ -42,7 +42,6 @@ impl DataManager {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "sqlite")]
|
||||
#[macro_export]
|
||||
macro_rules! get {
|
||||
($row:ident->$idx:literal($t:tt)) => {
|
||||
|
@ -58,9 +57,32 @@ macro_rules! query_row {
|
|||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! query_rows {
|
||||
($conn:expr, $sql:expr, $params:expr, $f:expr) => {{
|
||||
let mut query = $conn.prepare($sql).unwrap();
|
||||
|
||||
if let Ok(mut rows) = query.query($params) {
|
||||
let mut out = Vec::new();
|
||||
|
||||
while let Some(row) = rows.next().unwrap() {
|
||||
out.push($f(&row));
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
} else {
|
||||
Err(Error::Unknown)
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! execute {
|
||||
($conn:expr, $sql:expr, $params:expr) => {
|
||||
$conn.prepare($sql).unwrap().execute($params)
|
||||
};
|
||||
|
||||
($conn:expr, $sql:expr) => {
|
||||
$conn.prepare($sql).unwrap().execute(())
|
||||
};
|
||||
}
|
||||
|
|
|
@ -20,11 +20,11 @@ impl DataManager {
|
|||
#[cfg(feature = "postgres")] x: &Row,
|
||||
) -> JournalEntry {
|
||||
JournalEntry {
|
||||
id: get!(x->0(u64)) as usize,
|
||||
created: get!(x->1(u64)) as usize,
|
||||
id: get!(x->0(i64)) as usize,
|
||||
created: get!(x->1(i64)) as usize,
|
||||
content: get!(x->2(String)),
|
||||
owner: get!(x->3(u64)) as usize,
|
||||
journal: get!(x->4(u64)) as usize,
|
||||
owner: get!(x->3(i64)) as usize,
|
||||
journal: get!(x->4(i64)) as usize,
|
||||
context: serde_json::from_str(&get!(x->5(String))).unwrap(),
|
||||
// likes
|
||||
likes: get!(x->6(i64)) as isize,
|
||||
|
|
|
@ -19,10 +19,10 @@ impl DataManager {
|
|||
#[cfg(feature = "postgres")] x: &Row,
|
||||
) -> JournalPageMembership {
|
||||
JournalPageMembership {
|
||||
id: get!(x->0(u64)) as usize,
|
||||
created: get!(x->1(u64)) as usize,
|
||||
owner: get!(x->2(u64)) as usize,
|
||||
journal: get!(x->3(u64)) as usize,
|
||||
id: get!(x->0(i64)) as usize,
|
||||
created: get!(x->1(i64)) as usize,
|
||||
owner: get!(x->2(i64)) as usize,
|
||||
journal: get!(x->3(i64)) as usize,
|
||||
role: JournalPermission::from_bits(get!(x->4(u32))).unwrap(),
|
||||
}
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ impl DataManager {
|
|||
let res = query_row!(
|
||||
&conn,
|
||||
"SELECT * FROM memberships WHERE owner = $1 AND journal = $2",
|
||||
&[&owner, &journal],
|
||||
&[&(owner as i64), &(journal as i64)],
|
||||
|x| { Ok(Self::get_membership_from_row(x)) }
|
||||
);
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ mod common;
|
|||
mod drivers;
|
||||
mod entries;
|
||||
mod memberships;
|
||||
mod notifications;
|
||||
mod pages;
|
||||
mod reactions;
|
||||
|
||||
|
|
117
crates/core/src/database/notifications.rs
Normal file
117
crates/core/src/database/notifications.rs
Normal file
|
@ -0,0 +1,117 @@
|
|||
use super::*;
|
||||
use crate::cache::Cache;
|
||||
use crate::model::{Error, Result, auth::Notification, auth::User, permissions::FinePermission};
|
||||
use crate::{auto_method, execute, get, query_row, query_rows};
|
||||
|
||||
#[cfg(feature = "sqlite")]
|
||||
use rusqlite::Row;
|
||||
|
||||
#[cfg(feature = "postgres")]
|
||||
use tokio_postgres::Row;
|
||||
|
||||
impl DataManager {
|
||||
/// Get a [`Reaction`] from an SQL row.
|
||||
pub(crate) fn get_notification_from_row(
|
||||
#[cfg(feature = "sqlite")] x: &Row<'_>,
|
||||
#[cfg(feature = "postgres")] x: &Row,
|
||||
) -> Notification {
|
||||
Notification {
|
||||
id: get!(x->0(i64)) as usize,
|
||||
created: get!(x->1(i64)) as usize,
|
||||
title: get!(x->2(String)),
|
||||
content: get!(x->3(String)),
|
||||
owner: get!(x->4(i64)) as usize,
|
||||
}
|
||||
}
|
||||
|
||||
auto_method!(get_notification_by_id()@get_notification_from_row -> "SELECT * FROM notifications WHERE id = $1" --name="notification" --returns=Notification --cache-key-tmpl="atto.notification:{}");
|
||||
|
||||
/// Get a reaction by `owner` and `asset`.
|
||||
pub async fn get_notifications_by_owner(&self, owner: usize) -> Result<Vec<Notification>> {
|
||||
let conn = match self.connect().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||
};
|
||||
|
||||
let res = query_rows!(
|
||||
&conn,
|
||||
"SELECT * FROM notifications WHERE owner = $1",
|
||||
&[&(owner as i64)],
|
||||
|x| { Self::get_notification_from_row(x) }
|
||||
);
|
||||
|
||||
if res.is_err() {
|
||||
return Err(Error::GeneralNotFound("reactions".to_string()));
|
||||
}
|
||||
|
||||
Ok(res.unwrap())
|
||||
}
|
||||
|
||||
/// Create a new notification in the database.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `data` - a mock [`Reaction`] object to insert
|
||||
pub async fn create_notification(&self, data: Notification) -> Result<()> {
|
||||
let conn = match self.connect().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||
};
|
||||
|
||||
let res = execute!(
|
||||
&conn,
|
||||
"INSERT INTO reactions VALUES ($1, $2, $3, $4, $5)",
|
||||
&[
|
||||
&data.id.to_string().as_str(),
|
||||
&data.created.to_string().as_str(),
|
||||
&data.title.to_string().as_str(),
|
||||
&data.content.to_string().as_str(),
|
||||
&data.owner.to_string().as_str()
|
||||
]
|
||||
);
|
||||
|
||||
if let Err(e) = res {
|
||||
return Err(Error::DatabaseError(e.to_string()));
|
||||
}
|
||||
|
||||
// incr notification count
|
||||
self.incr_user_notifications(data.owner).await.unwrap();
|
||||
|
||||
// return
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_notification(&self, id: usize, user: User) -> Result<()> {
|
||||
let notification = self.get_notification_by_id(id).await?;
|
||||
|
||||
if user.id != notification.owner {
|
||||
if !user.permissions.check(FinePermission::MANAGE_NOTIFICATIONS) {
|
||||
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,
|
||||
"DELETE FROM notification WHERE id = $1",
|
||||
&[&id.to_string()]
|
||||
);
|
||||
|
||||
if let Err(e) = res {
|
||||
return Err(Error::DatabaseError(e.to_string()));
|
||||
}
|
||||
|
||||
self.2.remove(format!("atto.notification:{}", id)).await;
|
||||
|
||||
// decr notification count
|
||||
// self.decr_user_notifications(notification.owner)
|
||||
// .await
|
||||
// .unwrap();
|
||||
|
||||
// return
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -24,11 +24,11 @@ impl DataManager {
|
|||
#[cfg(feature = "postgres")] x: &Row,
|
||||
) -> JournalPage {
|
||||
JournalPage {
|
||||
id: get!(x->0(u64)) as usize,
|
||||
created: get!(x->1(u64)) as usize,
|
||||
id: get!(x->0(i64)) as usize,
|
||||
created: get!(x->1(i64)) as usize,
|
||||
title: get!(x->2(String)),
|
||||
prompt: get!(x->3(String)),
|
||||
owner: get!(x->4(u64)) as usize,
|
||||
owner: get!(x->4(i64)) as usize,
|
||||
read_access: serde_json::from_str(&get!(x->5(String)).to_string()).unwrap(),
|
||||
write_access: serde_json::from_str(&get!(x->6(String)).to_string()).unwrap(),
|
||||
// likes
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
use super::*;
|
||||
use crate::cache::Cache;
|
||||
use crate::model::reactions::AssetType;
|
||||
use crate::model::{Error, Result, auth::User, permissions::FinePermission, reactions::Reaction};
|
||||
use crate::model::{
|
||||
Error, Result,
|
||||
auth::User,
|
||||
permissions::FinePermission,
|
||||
reactions::{AssetType, Reaction},
|
||||
};
|
||||
use crate::{auto_method, execute, get, query_row};
|
||||
|
||||
#[cfg(feature = "sqlite")]
|
||||
|
@ -17,12 +21,12 @@ impl DataManager {
|
|||
#[cfg(feature = "postgres")] x: &Row,
|
||||
) -> Reaction {
|
||||
Reaction {
|
||||
id: get!(x->0(u64)) as usize,
|
||||
created: get!(x->1(u64)) as usize,
|
||||
owner: get!(x->2(u64)) as usize,
|
||||
asset: get!(x->3(u64)) as usize,
|
||||
id: get!(x->0(i64)) as usize,
|
||||
created: get!(x->1(i64)) as usize,
|
||||
owner: get!(x->2(i64)) as usize,
|
||||
asset: get!(x->3(i64)) as usize,
|
||||
asset_type: serde_json::from_str(&get!(x->4(String))).unwrap(),
|
||||
is_like: if get!(x->5(u8)) == 1 { true } else { false },
|
||||
is_like: if get!(x->5(i8)) == 1 { true } else { false },
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -42,7 +46,7 @@ impl DataManager {
|
|||
let res = query_row!(
|
||||
&conn,
|
||||
"SELECT * FROM reactions WHERE owner = $1 AND asset = $2",
|
||||
&[&owner, &asset],
|
||||
&[&(owner as i64), &(asset as i64)],
|
||||
|x| { Ok(Self::get_reaction_from_row(x)) }
|
||||
);
|
||||
|
||||
|
|
|
@ -72,3 +72,28 @@ impl User {
|
|||
self.password == hash_salted(against, self.salt.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Notification {
|
||||
pub id: usize,
|
||||
pub created: usize,
|
||||
pub title: String,
|
||||
pub content: String,
|
||||
pub owner: usize,
|
||||
}
|
||||
|
||||
impl Notification {
|
||||
/// Returns a new [`Notification`].
|
||||
pub fn new(title: String, content: String, owner: usize) -> Self {
|
||||
Self {
|
||||
id: AlmostSnowflake::new(1234567890)
|
||||
.to_string()
|
||||
.parse::<usize>()
|
||||
.unwrap(),
|
||||
created: unix_epoch_timestamp() as usize,
|
||||
title,
|
||||
content,
|
||||
owner,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue