feat: mentions in lives_with and text_body fields
Some checks failed
/ integration-test--firefox (push) Failing after 3m7s
Some checks failed
/ integration-test--firefox (push) Failing after 3m7s
This commit is contained in:
parent
fd5f1899c1
commit
d42adbe274
10 changed files with 369 additions and 200 deletions
2
Taskfile
2
Taskfile
|
|
@ -40,7 +40,7 @@ deploy_to_server() {
|
||||||
}
|
}
|
||||||
|
|
||||||
dev() {
|
dev() {
|
||||||
_cargo run -- serve
|
find src static migrations | entr -ccdr ./Taskfile _cargo run -- serve
|
||||||
}
|
}
|
||||||
|
|
||||||
"$@"
|
"$@"
|
||||||
|
|
|
||||||
|
|
@ -30,17 +30,18 @@ insert into names(contact_id, sort, name) values
|
||||||
(3, 0, 'Eleanor Edgeworth'),
|
(3, 0, 'Eleanor Edgeworth'),
|
||||||
(3, 1, 'Eleanor');
|
(3, 1, 'Eleanor');
|
||||||
|
|
||||||
insert into contacts(id, lives_with) values (4, 'Henrietta');
|
insert into contacts(id, lives_with) values (4, '[[Henrietta]]');
|
||||||
insert into names(contact_id, sort, name) values
|
insert into names(contact_id, sort, name) values
|
||||||
(4, 0, 'Felicia Homeowner');
|
(4, 0, 'Felicia Homeowner');
|
||||||
|
|
||||||
insert into contacts(id, lives_with) values (5, 'Henrietta');
|
insert into contacts(id, lives_with) values (5, '[[Henrietta]]');
|
||||||
insert into names(contact_id, sort, name) values
|
insert into names(contact_id, sort, name) values
|
||||||
(5, 0, 'Gregory Homeowner');
|
(5, 0, 'Gregory Homeowner');
|
||||||
|
|
||||||
insert into contacts(id) values (6);
|
insert into contacts(id) values (6);
|
||||||
insert into names(contact_id, sort, name) values
|
insert into names(contact_id, sort, name) values
|
||||||
(6, 0, 'Henrietta Homeowner');
|
(6, 0, 'Henrietta Homeowner'),
|
||||||
|
(6, 1, 'Henrietta');
|
||||||
|
|
||||||
insert into addresses(contact_id, label, value) values
|
insert into addresses(contact_id, label, value) values
|
||||||
(6, null, '123 Main St., Realville, WI 99999');
|
(6, null, '123 Main St., Realville, WI 99999');
|
||||||
|
|
@ -53,13 +54,13 @@ insert into journal_entries(id, date, value) values
|
||||||
(4, '2024-02-17', 'Friendship ended with [[Bazel]]. Now [[Eleanor Edgeworth]] is my best friend.'),
|
(4, '2024-02-17', 'Friendship ended with [[Bazel]]. Now [[Eleanor Edgeworth]] is my best friend.'),
|
||||||
(5, '2024-02-18', 'With [[Bazel]] gone, dissolved [[ABC]] Corp. Start discussions for a potential ACE, Inc. with [[Alexi]] and [[Eleanor]].');
|
(5, '2024-02-18', 'With [[Bazel]] gone, dissolved [[ABC]] Corp. Start discussions for a potential ACE, Inc. with [[Alexi]] and [[Eleanor]].');
|
||||||
|
|
||||||
insert into journal_mentions values
|
insert into mentions (entity_id, entity_type, input_text, byte_range_start, byte_range_end, url) values
|
||||||
(0, 'Bazel Bagend', 11, 27, '/contact/1'),
|
(0, 'journal_entry', 'Bazel Bagend', 11, 27, '/contact/1'),
|
||||||
(1, 'Alexi', 12, 21, '/contact/0'),
|
(1, 'journal_entry', 'Alexi', 12, 21, '/contact/0'),
|
||||||
(3, 'ABC', 24, 31, '/group/ABC'),
|
(3, 'journal_entry', 'ABC', 24, 31, '/group/ABC'),
|
||||||
(4, 'Bazel', 22, 31, '/contact/1'),
|
(4, 'journal_entry', 'Bazel', 22, 31, '/contact/1'),
|
||||||
(4, 'Eleanor Edgeworth', 37, 58, '/contact/3'),
|
(4, 'journal_entry', 'Eleanor Edgeworth', 37, 58, '/contact/3'),
|
||||||
(5, 'Eleanor', 108, 119, '/contact/3'),
|
(5, 'journal_entry', 'Eleanor', 108, 119, '/contact/3'),
|
||||||
(5, 'Alexi', 94, 103, '/contact/0'),
|
(5, 'journal_entry', 'Alexi', 94, 103, '/contact/0'),
|
||||||
(5, 'Bazel', 5, 14, '/contact/1'),
|
(5, 'journal_entry', 'Bazel', 5, 14, '/contact/1'),
|
||||||
(5, 'ABC', 31, 38, '/group/ABC');
|
(5, 'journal_entry', 'ABC', 31, 38, '/group/ABC');
|
||||||
|
|
|
||||||
30
migrations/each_user/0010_more-mentions.sql
Normal file
30
migrations/each_user/0010_more-mentions.sql
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
create table if not exists mentions (
|
||||||
|
entity_id integer not null,
|
||||||
|
entity_type integer not null,
|
||||||
|
url text not null,
|
||||||
|
input_text text not null,
|
||||||
|
byte_range_start integer not null,
|
||||||
|
byte_range_end integer not null
|
||||||
|
);
|
||||||
|
|
||||||
|
insert into mentions (
|
||||||
|
entity_id, url, input_text, byte_range_start, byte_range_end, entity_type)
|
||||||
|
select entry_id, url, input_text, byte_range_start, byte_range_end, 'journal_entry'
|
||||||
|
from journal_mentions;
|
||||||
|
|
||||||
|
drop table journal_mentions;
|
||||||
|
|
||||||
|
-- entity types:
|
||||||
|
-- 0: journal_entry
|
||||||
|
-- 1: contact.text_body
|
||||||
|
-- 2: contact.lives_with
|
||||||
|
create trigger if not exists cascade_delete_journal_mentions
|
||||||
|
after delete on journal_entries for each row begin
|
||||||
|
delete from mentions where entity_type = 0 and entity_id = OLD.id;
|
||||||
|
end;
|
||||||
|
|
||||||
|
create trigger if not exists cascade_delete_contact_text_body_mentions
|
||||||
|
after delete on contacts for each row begin
|
||||||
|
delete from mentions where entity_type = 1 and entity_id = OLD.id;
|
||||||
|
delete from mentions where entity_type = 2 and entity_id = OLD.id;
|
||||||
|
end;
|
||||||
45
src/main.rs
45
src/main.rs
|
|
@ -17,7 +17,6 @@ use tower_sessions_sqlx_store::SqliteStore;
|
||||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||||
|
|
||||||
mod models;
|
mod models;
|
||||||
use models::contact::MentionTrie;
|
|
||||||
use models::user::{Backend, User};
|
use models::user::{Backend, User};
|
||||||
|
|
||||||
mod db;
|
mod db;
|
||||||
|
|
@ -26,10 +25,13 @@ use db::{Database, DbId};
|
||||||
mod web;
|
mod web;
|
||||||
use web::{auth, contact, group, home, ics, journal, settings};
|
use web::{auth, contact, group, home, ics, journal, settings};
|
||||||
|
|
||||||
|
mod switchboard;
|
||||||
|
use switchboard::Switchboard;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct AppStateEntry {
|
struct AppStateEntry {
|
||||||
database: Arc<Database>,
|
database: Arc<Database>,
|
||||||
contact_search: Arc<RwLock<MentionTrie>>,
|
switchboard: Arc<RwLock<Switchboard>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
|
@ -37,10 +39,6 @@ struct AppState {
|
||||||
map: Arc<RwLock<HashMap<DbId, AppStateEntry>>>,
|
map: Arc<RwLock<HashMap<DbId, AppStateEntry>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct NameReference {
|
|
||||||
name: String,
|
|
||||||
contact_id: DbId,
|
|
||||||
}
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
AppState {
|
AppState {
|
||||||
|
|
@ -49,39 +47,14 @@ impl AppState {
|
||||||
}
|
}
|
||||||
pub async fn init(&mut self, user: &User) -> Result<Option<AppStateEntry>, AppError> {
|
pub async fn init(&mut self, user: &User) -> Result<Option<AppStateEntry>, AppError> {
|
||||||
let database = Database::for_user(&user).await?;
|
let database = Database::for_user(&user).await?;
|
||||||
let mut trie = radix_trie::Trie::new();
|
let switchboard = Switchboard::new(&database.pool).await?;
|
||||||
let mentionable_names = sqlx::query_as!(
|
|
||||||
NameReference,
|
|
||||||
"select name, contact_id from (
|
|
||||||
select contact_id, name, count(name) as ct from names group by name
|
|
||||||
) where ct = 1;",
|
|
||||||
)
|
|
||||||
.fetch_all(&database.pool)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
for row in mentionable_names {
|
|
||||||
trie.insert(
|
|
||||||
row.name,
|
|
||||||
format!("/contact/{}", DbId::try_from(row.contact_id)?),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let groups: Vec<(String, String)> =
|
|
||||||
sqlx::query_as("select distinct name, slug from groups")
|
|
||||||
.fetch_all(&database.pool)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
for (group, slug) in groups {
|
|
||||||
// TODO urlencode
|
|
||||||
trie.insert(group, format!("/group/{}", slug));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut map = self.map.write().expect("rwlock poisoned");
|
let mut map = self.map.write().expect("rwlock poisoned");
|
||||||
Ok(map.insert(
|
Ok(map.insert(
|
||||||
user.id(),
|
user.id(),
|
||||||
crate::AppStateEntry {
|
crate::AppStateEntry {
|
||||||
database: Arc::new(database),
|
database: Arc::new(database),
|
||||||
contact_search: Arc::new(RwLock::new(trie)),
|
switchboard: Arc::new(RwLock::new(switchboard)),
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
@ -93,9 +66,9 @@ impl AppState {
|
||||||
let map = self.map.read().expect("rwlock poisoned");
|
let map = self.map.read().expect("rwlock poisoned");
|
||||||
map.get(&user.id()).unwrap().database.clone()
|
map.get(&user.id()).unwrap().database.clone()
|
||||||
}
|
}
|
||||||
pub fn contact_search(&self, user: &impl AuthUser<Id = DbId>) -> Arc<RwLock<MentionTrie>> {
|
pub fn switchboard(&self, user: &impl AuthUser<Id = DbId>) -> Arc<RwLock<Switchboard>> {
|
||||||
let map = self.map.read().expect("rwlock poisoned");
|
let map = self.map.read().expect("rwlock poisoned");
|
||||||
map.get(&user.id()).unwrap().contact_search.clone()
|
map.get(&user.id()).unwrap().switchboard.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -177,7 +150,7 @@ async fn serve(port: &u32) -> Result<(), anyhow::Error> {
|
||||||
.with(
|
.with(
|
||||||
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| {
|
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| {
|
||||||
format!(
|
format!(
|
||||||
"{}=debug,tower_http=debug,axum=trace,sqlx=debug",
|
"{}=debug,tower_http=debug,axum=trace",
|
||||||
env!("CARGO_CRATE_NAME")
|
env!("CARGO_CRATE_NAME")
|
||||||
)
|
)
|
||||||
.into()
|
.into()
|
||||||
|
|
|
||||||
|
|
@ -38,9 +38,6 @@ impl HydratedContact {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* name/group, url */
|
|
||||||
pub type MentionTrie = radix_trie::Trie<String, String>;
|
|
||||||
|
|
||||||
impl FromRow<'_, SqliteRow> for Contact {
|
impl FromRow<'_, SqliteRow> for Contact {
|
||||||
fn from_row(row: &SqliteRow) -> sqlx::Result<Self> {
|
fn from_row(row: &SqliteRow) -> sqlx::Result<Self> {
|
||||||
let id: DbId = row.try_get("id")?;
|
let id: DbId = row.try_get("id")?;
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,12 @@
|
||||||
use chrono::NaiveDate;
|
use chrono::NaiveDate;
|
||||||
use maud::{Markup, PreEscaped, html};
|
use maud::{Markup, html};
|
||||||
use regex::Regex;
|
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use sqlx::sqlite::{SqlitePool, SqliteRow};
|
use sqlx::sqlite::{SqlitePool, SqliteRow};
|
||||||
use sqlx::{FromRow, Row};
|
use sqlx::{FromRow, Row};
|
||||||
use std::collections::HashSet;
|
|
||||||
use std::sync::{Arc, RwLock};
|
|
||||||
|
|
||||||
use super::contact::MentionTrie;
|
|
||||||
use crate::AppError;
|
use crate::AppError;
|
||||||
use crate::db::DbId;
|
use crate::db::DbId;
|
||||||
|
use crate::switchboard::{MentionHost, MentionHostType};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct JournalEntry {
|
pub struct JournalEntry {
|
||||||
|
|
@ -18,84 +15,19 @@ pub struct JournalEntry {
|
||||||
pub date: NaiveDate,
|
pub date: NaiveDate,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Hash, FromRow)]
|
impl<'a> Into<MentionHost<'a>> for &'a JournalEntry {
|
||||||
pub struct Mention {
|
fn into(self) -> MentionHost<'a> {
|
||||||
pub entry_id: DbId,
|
MentionHost {
|
||||||
pub url: String,
|
entity_id: self.id,
|
||||||
pub input_text: String,
|
entity_type: MentionHostType::JournalEntry as DbId,
|
||||||
pub byte_range_start: u32,
|
input: &self.value,
|
||||||
pub byte_range_end: u32,
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl JournalEntry {
|
impl JournalEntry {
|
||||||
pub fn extract_mentions(&self, trie: &MentionTrie) -> HashSet<Mention> {
|
|
||||||
let name_re = Regex::new(r"\[\[(.+?)\]\]").unwrap();
|
|
||||||
name_re
|
|
||||||
.captures_iter(&self.value)
|
|
||||||
.map(|caps| {
|
|
||||||
let range = caps.get_match().range();
|
|
||||||
trie.get(&caps[1]).map(|url| Mention {
|
|
||||||
entry_id: self.id,
|
|
||||||
url: url.to_string(),
|
|
||||||
input_text: caps[1].to_string(),
|
|
||||||
byte_range_start: u32::try_from(range.start).unwrap(),
|
|
||||||
byte_range_end: u32::try_from(range.end).unwrap(),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.filter(|o| o.is_some())
|
|
||||||
.map(|o| o.unwrap())
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn insert_mentions(
|
|
||||||
&self,
|
|
||||||
trie: Arc<RwLock<MentionTrie>>,
|
|
||||||
pool: &SqlitePool,
|
|
||||||
) -> Result<HashSet<Mention>, AppError> {
|
|
||||||
let mentions = {
|
|
||||||
let trie = trie.read().unwrap();
|
|
||||||
self.extract_mentions(&trie)
|
|
||||||
};
|
|
||||||
|
|
||||||
for mention in &mentions {
|
|
||||||
sqlx::query!(
|
|
||||||
"insert into journal_mentions(
|
|
||||||
entry_id, url, input_text,
|
|
||||||
byte_range_start, byte_range_end
|
|
||||||
) values ($1, $2, $3, $4, $5)",
|
|
||||||
mention.entry_id,
|
|
||||||
mention.url,
|
|
||||||
mention.input_text,
|
|
||||||
mention.byte_range_start,
|
|
||||||
mention.byte_range_end
|
|
||||||
)
|
|
||||||
.execute(pool)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(mentions)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn to_html(&self, pool: &SqlitePool) -> Result<Markup, AppError> {
|
pub async fn to_html(&self, pool: &SqlitePool) -> Result<Markup, AppError> {
|
||||||
// important to sort desc so that changing contents early in the string
|
let rendered = Into::<MentionHost>::into(self).format_pool(pool).await?;
|
||||||
// doesn't break inserting mentions at byte offsets further in
|
|
||||||
let mentions: Vec<Mention> = sqlx::query_as(
|
|
||||||
"select * from journal_mentions
|
|
||||||
where entry_id = $1 order by byte_range_start desc",
|
|
||||||
)
|
|
||||||
.bind(self.id)
|
|
||||||
.fetch_all(pool)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let mut value = self.value.clone();
|
|
||||||
for mention in mentions {
|
|
||||||
tracing::debug!("url ({})", mention.url);
|
|
||||||
value.replace_range(
|
|
||||||
(mention.byte_range_start as usize)..(mention.byte_range_end as usize),
|
|
||||||
&format!("[{}]({})", mention.input_text, mention.url),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let entry_url = format!("/journal_entry/{}", self.id);
|
let entry_url = format!("/journal_entry/{}", self.id);
|
||||||
let date = self.date.to_string();
|
let date = self.date.to_string();
|
||||||
|
|
||||||
|
|
@ -103,7 +35,7 @@ impl JournalEntry {
|
||||||
.entry {
|
.entry {
|
||||||
.view ":class"="{ hide: edit }" {
|
.view ":class"="{ hide: edit }" {
|
||||||
.date { (date) }
|
.date { (date) }
|
||||||
.content { (PreEscaped(markdown::to_html(&value))) }
|
.content { (rendered) }
|
||||||
}
|
}
|
||||||
form .edit ":class"="{ hide: !edit }" x-data=(json!({ "date": date, "initial_date": date, "value": self.value, "initial_value": self.value })) {
|
form .edit ":class"="{ hide: !edit }" x-data=(json!({ "date": date, "initial_date": date, "value": self.value, "initial_value": self.value })) {
|
||||||
input name="date" x-model="date";
|
input name="date" x-model="date";
|
||||||
|
|
|
||||||
147
src/switchboard.rs
Normal file
147
src/switchboard.rs
Normal file
|
|
@ -0,0 +1,147 @@
|
||||||
|
use maud::{Markup, PreEscaped};
|
||||||
|
use regex::Regex;
|
||||||
|
use sqlx::QueryBuilder;
|
||||||
|
use sqlx::sqlite::SqlitePool;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use crate::AppError;
|
||||||
|
use crate::db::DbId;
|
||||||
|
|
||||||
|
pub struct Switchboard {
|
||||||
|
trie: radix_trie::Trie<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Mentionable {
|
||||||
|
text: String,
|
||||||
|
uri: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||||
|
pub struct Mention {
|
||||||
|
pub entity_id: DbId,
|
||||||
|
pub entity_type: DbId,
|
||||||
|
pub url: String,
|
||||||
|
pub input_text: String,
|
||||||
|
pub byte_range_start: DbId,
|
||||||
|
pub byte_range_end: DbId,
|
||||||
|
}
|
||||||
|
|
||||||
|
// must match the constants in trigger definitions in
|
||||||
|
// migrations/each_user/0010_more-mentions.sql (or future migrations)
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
pub enum MentionHostType {
|
||||||
|
JournalEntry,
|
||||||
|
ContactTextBody,
|
||||||
|
ContactLivesWith,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
pub struct MentionHost<'a> {
|
||||||
|
pub entity_id: DbId,
|
||||||
|
pub entity_type: DbId,
|
||||||
|
pub input: &'a String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MentionHost<'_> {
|
||||||
|
pub fn format<'a>(
|
||||||
|
self: &Self,
|
||||||
|
mentions: impl IntoIterator<Item = &'a Mention>,
|
||||||
|
) -> Result<Markup, AppError> {
|
||||||
|
let mut out = self.input.clone();
|
||||||
|
for mention in mentions.into_iter() {
|
||||||
|
out.replace_range(
|
||||||
|
(mention.byte_range_start as usize)..(mention.byte_range_end as usize),
|
||||||
|
&format!("[{}]({})", mention.input_text, mention.url),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(PreEscaped(markdown::to_html(&out)))
|
||||||
|
}
|
||||||
|
pub async fn format_pool(self: &Self, pool: &SqlitePool) -> Result<Markup, AppError> {
|
||||||
|
let mentions = sqlx::query_as!(
|
||||||
|
Mention,
|
||||||
|
"select * from mentions
|
||||||
|
where entity_id = $1 and entity_type = $2
|
||||||
|
order by byte_range_start desc",
|
||||||
|
self.entity_id,
|
||||||
|
self.entity_type
|
||||||
|
)
|
||||||
|
.fetch_all(pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
self.format(&mentions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Switchboard {
|
||||||
|
pub async fn new(pool: &SqlitePool) -> Result<Self, AppError> {
|
||||||
|
let mut trie = radix_trie::Trie::new();
|
||||||
|
|
||||||
|
let mentionables = sqlx::query_as!(
|
||||||
|
Mentionable,
|
||||||
|
"select name as text, '/contact/'||contact_id as uri from (
|
||||||
|
select contact_id, name, count(name) as ct from names group by name
|
||||||
|
) where ct = 1
|
||||||
|
union
|
||||||
|
select distinct name as text, '/group/'||slug as uri from groups",
|
||||||
|
)
|
||||||
|
.fetch_all(pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
for mentionable in mentionables {
|
||||||
|
trie.insert(mentionable.text, mentionable.uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Switchboard { trie })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove(self: &mut Self, text: &String) {
|
||||||
|
self.trie.remove(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_mentionable(self: &mut Self, text: String, uri: String) {
|
||||||
|
self.trie.insert(text, uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn extract_mentions<'a>(&self, host: impl Into<MentionHost<'a>>) -> HashSet<Mention> {
|
||||||
|
let host: MentionHost = host.into();
|
||||||
|
let name_re = Regex::new(r"\[\[(.+?)\]\]").unwrap();
|
||||||
|
name_re
|
||||||
|
.captures_iter(host.input)
|
||||||
|
.map(|caps| {
|
||||||
|
let range = caps.get_match().range();
|
||||||
|
self.trie.get(&caps[1]).map(|url| Mention {
|
||||||
|
entity_id: host.entity_id,
|
||||||
|
entity_type: host.entity_type,
|
||||||
|
url: url.to_string(),
|
||||||
|
input_text: caps[1].to_string(),
|
||||||
|
byte_range_start: DbId::try_from(range.start).unwrap(),
|
||||||
|
byte_range_end: DbId::try_from(range.end).unwrap(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.filter(|o| o.is_some())
|
||||||
|
.map(|o| o.unwrap())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn insert_mentions<'a>(
|
||||||
|
mentions: impl IntoIterator<Item = &'a Mention>,
|
||||||
|
pool: &SqlitePool,
|
||||||
|
) -> Result<(), AppError> {
|
||||||
|
let mut qb = QueryBuilder::<sqlx::Sqlite>::new(
|
||||||
|
"insert into mentions (
|
||||||
|
entity_id, entity_type, url, input_text,
|
||||||
|
byte_range_start, byte_range_end) ",
|
||||||
|
);
|
||||||
|
qb.push_values(mentions, |mut b, mention| {
|
||||||
|
b.push_bind(mention.entity_id)
|
||||||
|
.push_bind(mention.entity_type)
|
||||||
|
.push_bind(&mention.url)
|
||||||
|
.push_bind(&mention.input_text)
|
||||||
|
.push_bind(mention.byte_range_start)
|
||||||
|
.push_bind(mention.byte_range_end);
|
||||||
|
});
|
||||||
|
qb.build().execute(pool).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
@ -8,7 +8,7 @@ use axum::{
|
||||||
use axum_extra::extract::Form;
|
use axum_extra::extract::Form;
|
||||||
use cache_bust::asset;
|
use cache_bust::asset;
|
||||||
use chrono::DateTime;
|
use chrono::DateTime;
|
||||||
use maud::{Markup, PreEscaped, html};
|
use maud::{Markup, html};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use slug::slugify;
|
use slug::slugify;
|
||||||
|
|
@ -19,6 +19,7 @@ use super::home::journal_section;
|
||||||
use crate::db::DbId;
|
use crate::db::DbId;
|
||||||
use crate::models::user::AuthSession;
|
use crate::models::user::AuthSession;
|
||||||
use crate::models::{HydratedContact, JournalEntry};
|
use crate::models::{HydratedContact, JournalEntry};
|
||||||
|
use crate::switchboard::{MentionHost, MentionHostType, insert_mentions};
|
||||||
use crate::{AppError, AppState};
|
use crate::{AppError, AppState};
|
||||||
|
|
||||||
#[derive(serde::Serialize, Debug)]
|
#[derive(serde::Serialize, Debug)]
|
||||||
|
|
@ -69,10 +70,12 @@ mod get {
|
||||||
pub async fn contact(
|
pub async fn contact(
|
||||||
auth_session: AuthSession,
|
auth_session: AuthSession,
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path(contact_id): Path<u32>,
|
Path(contact_id): Path<DbId>,
|
||||||
layout: Layout,
|
layout: Layout,
|
||||||
) -> Result<Markup, AppError> {
|
) -> Result<Markup, AppError> {
|
||||||
let pool = &state.db(&auth_session.user.unwrap()).pool;
|
let user = auth_session.user.unwrap();
|
||||||
|
let pool = &state.db(&user).pool;
|
||||||
|
|
||||||
let contact: HydratedContact = sqlx::query_as(
|
let contact: HydratedContact = sqlx::query_as(
|
||||||
"select *, (
|
"select *, (
|
||||||
select string_agg(name,'\x1c' order by sort)
|
select string_agg(name,'\x1c' order by sort)
|
||||||
|
|
@ -87,18 +90,30 @@ mod get {
|
||||||
|
|
||||||
let entries: Vec<JournalEntry> = sqlx::query_as(
|
let entries: Vec<JournalEntry> = sqlx::query_as(
|
||||||
"select distinct j.id, j.value, j.date from journal_entries j
|
"select distinct j.id, j.value, j.date from journal_entries j
|
||||||
join journal_mentions cm on j.id = cm.entry_id
|
join mentions m on j.id = m.entity_id
|
||||||
where cm.url = '/contact/'||$1 or cm.url in (
|
where m.entity_type = $1 and (m.url = '/contact/'||$1 or m.url in (
|
||||||
select '/group/'||slug from groups
|
select '/group/'||slug from groups
|
||||||
where contact_id = $1
|
where contact_id = $2
|
||||||
)
|
))
|
||||||
order by j.date desc
|
order by j.date desc
|
||||||
",
|
",
|
||||||
)
|
)
|
||||||
|
.bind(MentionHostType::JournalEntry as DbId)
|
||||||
.bind(contact_id)
|
.bind(contact_id)
|
||||||
.fetch_all(pool)
|
.fetch_all(pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
let lives_with = if contact.lives_with.len() > 1 {
|
||||||
|
let mention_host = MentionHost {
|
||||||
|
entity_id: contact_id,
|
||||||
|
entity_type: MentionHostType::ContactLivesWith as DbId,
|
||||||
|
input: &contact.lives_with,
|
||||||
|
};
|
||||||
|
Some(mention_host.format_pool(pool).await?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let addresses: Vec<Address> = sqlx::query_as!(
|
let addresses: Vec<Address> = sqlx::query_as!(
|
||||||
Address,
|
Address,
|
||||||
"select * from addresses where contact_id = $1",
|
"select * from addresses where contact_id = $1",
|
||||||
|
|
@ -157,9 +172,9 @@ mod get {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@if contact.lives_with.len() > 0 {
|
@if let Some(lives_with) = lives_with {
|
||||||
label { "lives with" }
|
label { "lives with" }
|
||||||
div { (contact.lives_with) }
|
div { (lives_with) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@if addresses.len() == 1 {
|
@if addresses.len() == 1 {
|
||||||
|
|
@ -196,7 +211,11 @@ mod get {
|
||||||
|
|
||||||
@if let Some(text_body) = text_body {
|
@if let Some(text_body) = text_body {
|
||||||
@if text_body.len() > 0 {
|
@if text_body.len() > 0 {
|
||||||
#text_body { (PreEscaped(markdown::to_html(&text_body))) }
|
#text_body { (MentionHost {
|
||||||
|
entity_id: contact_id,
|
||||||
|
entity_type: MentionHostType::ContactTextBody as DbId,
|
||||||
|
input: &text_body
|
||||||
|
}.format_pool(pool).await?) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -208,7 +227,7 @@ mod get {
|
||||||
pub async fn contact_edit(
|
pub async fn contact_edit(
|
||||||
auth_session: AuthSession,
|
auth_session: AuthSession,
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path(contact_id): Path<u32>,
|
Path(contact_id): Path<DbId>,
|
||||||
layout: Layout,
|
layout: Layout,
|
||||||
) -> Result<Markup, AppError> {
|
) -> Result<Markup, AppError> {
|
||||||
let pool = &state.db(&auth_session.user.unwrap()).pool;
|
let pool = &state.db(&auth_session.user.unwrap()).pool;
|
||||||
|
|
@ -218,17 +237,20 @@ mod get {
|
||||||
from names where contact_id = c.id
|
from names where contact_id = c.id
|
||||||
) as names, (
|
) as names, (
|
||||||
select jes.date from journal_entries jes
|
select jes.date from journal_entries jes
|
||||||
join journal_mentions cms on cms.entry_id = jes.id
|
join mentions m on m.entity_id = jes.id
|
||||||
where cms.url = '/contact/'||c.id
|
where
|
||||||
or cms.url in (
|
m.entity_type = $1 and (
|
||||||
|
m.url = '/contact/'||c.id
|
||||||
|
or m.url in (
|
||||||
select '/group/'||name
|
select '/group/'||name
|
||||||
from groups
|
from groups
|
||||||
where contact_id = c.id
|
where contact_id = c.id
|
||||||
)
|
))
|
||||||
order by jes.date desc limit 1
|
order by jes.date desc limit 1
|
||||||
) as last_mention_date from contacts c
|
) as last_mention_date from contacts c
|
||||||
where c.id = $1",
|
where c.id = $2",
|
||||||
)
|
)
|
||||||
|
.bind(MentionHostType::JournalEntry as DbId)
|
||||||
.bind(contact_id)
|
.bind(contact_id)
|
||||||
.fetch_one(pool)
|
.fetch_one(pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
@ -391,6 +413,7 @@ mod put {
|
||||||
) -> Result<impl IntoResponse, AppError> {
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
let user = auth_session.user.unwrap();
|
let user = auth_session.user.unwrap();
|
||||||
let pool = &state.db(&user).pool;
|
let pool = &state.db(&user).pool;
|
||||||
|
let sw_lock = state.switchboard(&user);
|
||||||
|
|
||||||
let birthday = if payload.birthday.is_empty() {
|
let birthday = if payload.birthday.is_empty() {
|
||||||
None
|
None
|
||||||
|
|
@ -415,8 +438,15 @@ mod put {
|
||||||
Some(payload.text_body)
|
Some(payload.text_body)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let old_contact = sqlx::query!("select * from contacts where id = $1", contact_id)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"update contacts set (birthday, manually_freshened_at, lives_with, text_body) = ($1, $2, $3, $4) where id = $5",
|
"update contacts set
|
||||||
|
(birthday, manually_freshened_at, lives_with, text_body) =
|
||||||
|
($1, $2, $3, $4)
|
||||||
|
where id = $5",
|
||||||
birthday,
|
birthday,
|
||||||
manually_freshened_at,
|
manually_freshened_at,
|
||||||
payload.lives_with,
|
payload.lives_with,
|
||||||
|
|
@ -426,6 +456,52 @@ mod put {
|
||||||
.execute(pool)
|
.execute(pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
if old_contact.lives_with != payload.lives_with {
|
||||||
|
sqlx::query!(
|
||||||
|
"delete from mentions where entity_id = $1 and entity_type = $2",
|
||||||
|
contact_id,
|
||||||
|
MentionHostType::ContactLivesWith as DbId
|
||||||
|
)
|
||||||
|
.execute(pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mention_host = MentionHost {
|
||||||
|
entity_id: contact_id,
|
||||||
|
entity_type: MentionHostType::ContactLivesWith as DbId,
|
||||||
|
input: &payload.lives_with,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mentions = {
|
||||||
|
let switchboard = sw_lock.read().unwrap();
|
||||||
|
switchboard.extract_mentions(mention_host)
|
||||||
|
};
|
||||||
|
insert_mentions(&mentions, pool).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if old_contact.text_body != text_body {
|
||||||
|
sqlx::query!(
|
||||||
|
"delete from mentions where entity_id = $1 and entity_type = $2",
|
||||||
|
contact_id,
|
||||||
|
MentionHostType::ContactTextBody as DbId
|
||||||
|
)
|
||||||
|
.execute(pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if text_body.is_some() {
|
||||||
|
let mention_host = MentionHost {
|
||||||
|
entity_id: contact_id,
|
||||||
|
entity_type: MentionHostType::ContactTextBody as DbId,
|
||||||
|
input: &text_body.unwrap(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mentions = {
|
||||||
|
let switchboard = sw_lock.read().unwrap();
|
||||||
|
switchboard.extract_mentions(mention_host)
|
||||||
|
};
|
||||||
|
insert_mentions(&mentions, pool).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// these blocks are not in functions because payload gets progressively
|
// these blocks are not in functions because payload gets progressively
|
||||||
// partially moved as we handle each field and i don't want to deal with it
|
// partially moved as we handle each field and i don't want to deal with it
|
||||||
|
|
||||||
|
|
@ -485,11 +561,11 @@ mod put {
|
||||||
let old_names: Vec<String> = old_names.into_iter().map(|(s,)| s).collect();
|
let old_names: Vec<String> = old_names.into_iter().map(|(s,)| s).collect();
|
||||||
|
|
||||||
if old_names != new_names {
|
if old_names != new_names {
|
||||||
// delete and regen *all* journal mentions, not just the ones for the
|
// delete and regen *all* mentions, not just the ones for the current
|
||||||
// current user, since changing *this* user's names can change, *globally*,
|
// contact, since changing *this* contact's names can change, *globally*,
|
||||||
// which names have n=1 and thus are eligible for mentioning
|
// which names have n=1 and thus are eligible for mentioning
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"delete from journal_mentions; delete from names where contact_id = $1",
|
"delete from mentions; delete from names where contact_id = $1",
|
||||||
contact_id
|
contact_id
|
||||||
)
|
)
|
||||||
.execute(pool)
|
.execute(pool)
|
||||||
|
|
@ -531,14 +607,13 @@ mod put {
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
{
|
{
|
||||||
let trie_mutex = state.contact_search(&user);
|
let mut switchboard = sw_lock.write().unwrap();
|
||||||
let mut trie = trie_mutex.write().unwrap();
|
|
||||||
for name in &old_names {
|
for name in &old_names {
|
||||||
trie.remove(name);
|
switchboard.remove(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
for name in recalc_names {
|
for name in recalc_names {
|
||||||
trie.insert(name.0, format!("/contact/{}", name.1));
|
switchboard.add_mentionable(name.0, format!("/contact/{}", name.1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -558,7 +633,7 @@ mod put {
|
||||||
|
|
||||||
if new_groups != old_groups {
|
if new_groups != old_groups {
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"delete from journal_mentions; delete from groups where contact_id = $1",
|
"delete from mentions; delete from groups where contact_id = $1",
|
||||||
contact_id
|
contact_id
|
||||||
)
|
)
|
||||||
.execute(pool)
|
.execute(pool)
|
||||||
|
|
@ -576,17 +651,17 @@ mod put {
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
{
|
{
|
||||||
let trie_mutex = state.contact_search(&user);
|
let mut switchboard = sw_lock.write().unwrap();
|
||||||
let mut trie = trie_mutex.write().unwrap();
|
|
||||||
for name in &old_groups {
|
for name in &old_groups {
|
||||||
// TODO i think we care about group name vs contact name counts,
|
// TODO i think we care about group name vs contact name counts,
|
||||||
// otherwise this will cause a problem (or we want to disallow
|
// otherwise this will cause a problem (or we want to disallow
|
||||||
// setting group names that are contact names or vice versa?)
|
// setting group names that are contact names or vice versa?)
|
||||||
trie.remove(name);
|
switchboard.remove(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
for group in &new_groups {
|
for group in &new_groups {
|
||||||
trie.insert(group.clone(), format!("/group/{}", slugify(group)));
|
switchboard
|
||||||
|
.add_mentionable(group.clone(), format!("/group/{}", slugify(group)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -598,9 +673,11 @@ mod put {
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
for entry in journal_entries {
|
for entry in journal_entries {
|
||||||
entry
|
let mentions = {
|
||||||
.insert_mentions(state.contact_search(&user), pool)
|
let switchboard = sw_lock.read().unwrap();
|
||||||
.await?;
|
switchboard.extract_mentions(&entry)
|
||||||
|
};
|
||||||
|
insert_mentions(&mentions, pool).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -620,12 +697,7 @@ mod delete {
|
||||||
let user = auth_session.user.unwrap();
|
let user = auth_session.user.unwrap();
|
||||||
let pool = &state.db(&user).pool;
|
let pool = &state.db(&user).pool;
|
||||||
|
|
||||||
sqlx::query(
|
sqlx::query!("delete from contacts where id = $1", contact_id)
|
||||||
"delete from journal_mentions where contact_id = $1;
|
|
||||||
delete from names where contact_id = $1;
|
|
||||||
delete from contacts where id = $1;",
|
|
||||||
)
|
|
||||||
.bind(contact_id)
|
|
||||||
.execute(pool)
|
.execute(pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ use super::Layout;
|
||||||
use crate::db::DbId;
|
use crate::db::DbId;
|
||||||
use crate::models::user::AuthSession;
|
use crate::models::user::AuthSession;
|
||||||
use crate::models::{Birthday, HydratedContact, JournalEntry};
|
use crate::models::{Birthday, HydratedContact, JournalEntry};
|
||||||
|
use crate::switchboard::{MentionHost, MentionHostType};
|
||||||
use crate::{AppError, AppState};
|
use crate::{AppError, AppState};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
@ -109,7 +110,7 @@ pub async fn journal_section(
|
||||||
|
|
||||||
.entries {
|
.entries {
|
||||||
@for entry in entries {
|
@for entry in entries {
|
||||||
(entry.to_html(pool).await?)
|
(Into::<MentionHost>::into(entry).format_pool(pool).await?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -124,18 +125,22 @@ pub mod get {
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
layout: Layout,
|
layout: Layout,
|
||||||
) -> Result<impl IntoResponse, AppError> {
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
let pool = &state.db(&auth_session.user.unwrap()).pool;
|
let user = auth_session.user.unwrap();
|
||||||
|
let pool = &state.db(&user).pool;
|
||||||
|
|
||||||
let contacts: Vec<HydratedContact> = sqlx::query_as(
|
let contacts: Vec<HydratedContact> = sqlx::query_as(
|
||||||
"select id, birthday, manually_freshened_at, (
|
"select id, birthday, manually_freshened_at, (
|
||||||
select string_agg(name,'\x1c' order by sort)
|
select string_agg(name,'\x1c' order by sort)
|
||||||
from names where contact_id = c.id
|
from names where contact_id = c.id
|
||||||
) as names, (
|
) as names, (
|
||||||
select jes.date from journal_entries jes
|
select jes.date from journal_entries jes
|
||||||
join journal_mentions cms on cms.entry_id = jes.id
|
join mentions ms on ms.entity_id = jes.id
|
||||||
where cms.url = '/contact/'||c.id
|
where ms.entity_type = $1
|
||||||
|
and ms.url = '/contact/'||c.id
|
||||||
order by jes.date desc limit 1
|
order by jes.date desc limit 1
|
||||||
) as last_mention_date from contacts c",
|
) as last_mention_date from contacts c",
|
||||||
)
|
)
|
||||||
|
.bind(MentionHostType::JournalEntry as DbId)
|
||||||
.fetch_all(pool)
|
.fetch_all(pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ use serde::Deserialize;
|
||||||
|
|
||||||
use crate::models::JournalEntry;
|
use crate::models::JournalEntry;
|
||||||
use crate::models::user::AuthSession;
|
use crate::models::user::AuthSession;
|
||||||
|
use crate::switchboard::{MentionHost, insert_mentions};
|
||||||
use crate::{AppError, AppState};
|
use crate::{AppError, AppState};
|
||||||
|
|
||||||
pub fn router() -> Router<AppState> {
|
pub fn router() -> Router<AppState> {
|
||||||
|
|
@ -36,6 +37,8 @@ mod post {
|
||||||
) -> Result<Markup, AppError> {
|
) -> Result<Markup, AppError> {
|
||||||
let user = auth_session.user.unwrap();
|
let user = auth_session.user.unwrap();
|
||||||
let pool = &state.db(&user).pool;
|
let pool = &state.db(&user).pool;
|
||||||
|
let sw_lock = state.switchboard(&user);
|
||||||
|
|
||||||
let now = Local::now().date_naive();
|
let now = Local::now().date_naive();
|
||||||
|
|
||||||
let date = if payload.date.is_empty() {
|
let date = if payload.date.is_empty() {
|
||||||
|
|
@ -73,9 +76,11 @@ mod post {
|
||||||
.fetch_one(pool)
|
.fetch_one(pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
entry
|
let mentions = {
|
||||||
.insert_mentions(state.contact_search(&user), pool)
|
let switchboard = sw_lock.read().unwrap();
|
||||||
.await?;
|
switchboard.extract_mentions(&entry)
|
||||||
|
};
|
||||||
|
insert_mentions(&mentions, pool).await?;
|
||||||
|
|
||||||
Ok(entry.to_html(pool).await?)
|
Ok(entry.to_html(pool).await?)
|
||||||
}
|
}
|
||||||
|
|
@ -84,6 +89,7 @@ mod post {
|
||||||
mod patch {
|
mod patch {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[axum::debug_handler]
|
||||||
pub async fn entry(
|
pub async fn entry(
|
||||||
auth_session: AuthSession,
|
auth_session: AuthSession,
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
|
|
@ -92,8 +98,10 @@ mod patch {
|
||||||
) -> Result<impl IntoResponse, AppError> {
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
let user = auth_session.user.unwrap();
|
let user = auth_session.user.unwrap();
|
||||||
let pool = &state.db(&user).pool;
|
let pool = &state.db(&user).pool;
|
||||||
|
let sw_lock = state.switchboard(&user);
|
||||||
|
|
||||||
// not a macro query, we want to use JournalEntry's custom FromRow
|
// not a macro query, we want to use JournalEntry's custom FromRow
|
||||||
let entry: JournalEntry = sqlx::query_as("select * from journal_entries where id = $1")
|
let old_entry: JournalEntry = sqlx::query_as("select * from journal_entries where id = $1")
|
||||||
.bind(entry_id)
|
.bind(entry_id)
|
||||||
.fetch_one(pool)
|
.fetch_one(pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
@ -107,17 +115,24 @@ mod patch {
|
||||||
.fetch_one(pool)
|
.fetch_one(pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if entry.value != new_entry.value {
|
if old_entry.value != new_entry.value {
|
||||||
sqlx::query!("delete from journal_mentions where entry_id = $1", entry_id)
|
sqlx::query!(
|
||||||
|
"delete from mentions where entity_id = $1 and entity_type = 'journal_entry'",
|
||||||
|
entry_id
|
||||||
|
)
|
||||||
.execute(pool)
|
.execute(pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
new_entry
|
let mentions = {
|
||||||
.insert_mentions(state.contact_search(&user), pool)
|
let switchboard = sw_lock.read().unwrap();
|
||||||
.await?;
|
switchboard.extract_mentions(&new_entry)
|
||||||
|
};
|
||||||
|
insert_mentions(&mentions, pool).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(new_entry.to_html(pool).await?)
|
Ok(Into::<MentionHost>::into(&new_entry)
|
||||||
|
.format_pool(pool)
|
||||||
|
.await?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -132,10 +147,7 @@ mod delete {
|
||||||
let user = auth_session.user.unwrap();
|
let user = auth_session.user.unwrap();
|
||||||
let pool = &state.db(&user).pool;
|
let pool = &state.db(&user).pool;
|
||||||
|
|
||||||
sqlx::query(
|
sqlx::query("delete from journal_entries where id = $2 returning id,date,value")
|
||||||
"delete from journal_mentions where entry_id = $1;
|
|
||||||
delete from journal_entries where id = $2 returning id,date,value",
|
|
||||||
)
|
|
||||||
.bind(entry_id)
|
.bind(entry_id)
|
||||||
.bind(entry_id)
|
.bind(entry_id)
|
||||||
.execute(pool)
|
.execute(pool)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue