summaryrefslogtreecommitdiff
path: root/src/graphql
diff options
context:
space:
mode:
authorSanto Cariotti <santo@dcariotti.me>2024-09-12 11:46:27 +0200
committerSanto Cariotti <santo@dcariotti.me>2024-09-12 11:46:27 +0200
commit105f6831d13ebb473b6ce9b63c5c159b5a6c964d (patch)
tree0e408145276bf8be723710e0a57d872afa9fd49c /src/graphql
parent48a0c0f007bff5df95c69b727e3ee0e6c3b9bfeb (diff)
Each alert has text{1,2,3} for the three possible area
Alert level is moved to the notification struct
Diffstat (limited to 'src/graphql')
-rw-r--r--src/graphql/mutation.rs6
-rw-r--r--src/graphql/query.rs9
-rw-r--r--src/graphql/types/alert.rs306
-rw-r--r--src/graphql/types/notification.rs127
4 files changed, 256 insertions, 192 deletions
diff --git a/src/graphql/mutation.rs b/src/graphql/mutation.rs
index 4a31428..34aa1cc 100644
--- a/src/graphql/mutation.rs
+++ b/src/graphql/mutation.rs
@@ -149,7 +149,7 @@ impl Mutation {
/// -H "Content-Type: application/json" \
/// -H "Authorization: Bearer ****" \
/// -d '{
- /// "query": "mutation NewAlert($input: AlertInput!) { newAlert(input: $input) { id createdAt level } }",
+ /// "query": "mutation NewAlert($input: AlertInput!) { newAlert(input: $input) { id createdAt } }",
/// "variables": {
/// "input": {
/// "points": [
@@ -159,7 +159,9 @@ impl Mutation {
/// { "latitude": 44.498321, "longitude": 11.312145},
/// { "latitude": 44.490025, "longitude": 11.311498}
/// ],
- /// "level": "TWO"
+ /// "text1": "Alert level 1",
+ /// "text2": "Alert level 2",
+ /// "text3": "Alert level 3"
/// }
/// }
/// }
diff --git a/src/graphql/query.rs b/src/graphql/query.rs
index c39d19a..e3750c9 100644
--- a/src/graphql/query.rs
+++ b/src/graphql/query.rs
@@ -92,7 +92,7 @@ impl Query {
/// curl http://localhost:8000/graphql
/// -H 'authorization: Bearer ***'
/// -H 'content-type: application/json'
- /// -d '{"query":"{alerts(id: 12) {id, userId, createdAt, area, extendedArea, level}}"}'
+ /// -d '{"query":"{alerts(id: 12) {id, userId, createdAt, area, areaLevel2, areaLevel3, text1, text2, text3}}"}'
/// ```
async fn alerts<'ctx>(
&self,
@@ -112,7 +112,12 @@ impl Query {
/// -H 'authorization: Bearer ***'
/// -H 'content-type: application/json'
/// -d '{"query":"{notifications(seen: false alertId: 1) {
- /// id, alert { id, userId, createdAt, area, extendedArea, level, reachedUsers }, position {id, userId, createdAt, latitude, longitude, movingActivity}, seen, createdAt
+ /// id,
+ /// alert { id, userId, createdAt, area, areaLevel2, areaLevel3, text1, text2, text3, reachedUsers },
+ /// position {id, userId, createdAt, latitude, longitude, movingActivity},
+ /// seen,
+ /// level,
+ /// createdAt
/// }}"}'
/// ```
async fn notifications<'ctx>(
diff --git a/src/graphql/types/alert.rs b/src/graphql/types/alert.rs
index 2e7f10b..1a480c5 100644
--- a/src/graphql/types/alert.rs
+++ b/src/graphql/types/alert.rs
@@ -1,62 +1,14 @@
use crate::{
expo,
- graphql::types::{jwt::Authentication, notification::Notification, user::find_user},
+ graphql::types::{
+ jwt::Authentication,
+ notification::{LevelAlert, Notification},
+ user::find_user,
+ },
state::AppState,
};
-use async_graphql::{Context, Enum, FieldResult, InputObject, SimpleObject};
+use async_graphql::{Context, FieldResult, InputObject, SimpleObject};
use serde::{Deserialize, Serialize};
-use std::error::Error;
-use tokio_postgres::types::{to_sql_checked, FromSql, IsNull, ToSql, Type};
-
-#[derive(Enum, Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
-/// Enumeration which refers to the level of alert
-pub enum LevelAlert {
- // User in the AREA
- One,
-
- // User in the AREA OR < 1km distance
- Two,
-
- // User in the AREA OR < 2km distance
- Three,
-}
-
-impl<'a> FromSql<'a> for LevelAlert {
- fn from_sql(_ty: &Type, raw: &'a [u8]) -> Result<LevelAlert, Box<dyn Error + Sync + Send>> {
- match std::str::from_utf8(raw)? {
- "One" => Ok(LevelAlert::One),
- "Two" => Ok(LevelAlert::Two),
- "Three" => Ok(LevelAlert::Three),
- other => Err(format!("Unknown variant: {}", other).into()),
- }
- }
-
- fn accepts(ty: &Type) -> bool {
- ty.name() == "level_alert"
- }
-}
-
-impl ToSql for LevelAlert {
- fn to_sql(
- &self,
- _ty: &Type,
- out: &mut bytes::BytesMut,
- ) -> Result<IsNull, Box<dyn Error + Sync + Send>> {
- let value = match *self {
- LevelAlert::One => "One",
- LevelAlert::Two => "Two",
- LevelAlert::Three => "Three",
- };
- out.extend_from_slice(value.as_bytes());
- Ok(IsNull::No)
- }
-
- fn accepts(ty: &Type) -> bool {
- ty.name() == "level_alert"
- }
-
- to_sql_checked!();
-}
#[derive(Serialize, Deserialize)]
pub struct PolygonValid {
@@ -70,8 +22,11 @@ pub struct Alert {
pub user_id: i32,
pub created_at: i64,
pub area: String,
- pub extended_area: String,
- pub level: LevelAlert,
+ pub area_level2: String,
+ pub area_level3: String,
+ pub text1: String,
+ pub text2: String,
+ pub text3: String,
pub reached_users: i32,
}
@@ -85,7 +40,9 @@ pub struct Point {
/// Alert input struct
pub struct AlertInput {
pub points: Vec<Point>,
- pub level: LevelAlert,
+ pub text1: String,
+ pub text2: String,
+ pub text3: String,
}
pub mod query {
@@ -117,18 +74,11 @@ pub mod query {
user_id,
extract(epoch from created_at)::double precision as created_at,
ST_AsText(area) as area,
- ST_AsText(
- ST_Buffer(
- area::geography,
- CASE
- WHEN level = 'One' THEN 0
- WHEN level = 'Two' THEN 1000
- WHEN level = 'Three' THEN 2000
- ELSE 0
- END
- )
- ) as extended_area,
- level,
+ ST_AsText(ST_Buffer(area::geography, 1000)) as area_level2,
+ ST_AsText(ST_Buffer(area::geography, 2000)) as area_level3,
+ text1,
+ text2,
+ text3,
reached_users
FROM alerts
WHERE id = $1",
@@ -142,18 +92,11 @@ pub mod query {
user_id,
extract(epoch from created_at)::double precision as created_at,
ST_AsText(area) as area,
- ST_AsText(
- ST_Buffer(
- area::geography,
- CASE
- WHEN level = 'One' THEN 0
- WHEN level = 'Two' THEN 1000
- WHEN level = 'Three' THEN 2000
- ELSE 0
- END
- )
- ) as extended_area,
- level,
+ ST_AsText(ST_Buffer(area::geography, 1000)) as area_level2,
+ ST_AsText(ST_Buffer(area::geography, 2000)) as area_level3,
+ text1,
+ text2,
+ text3,
reached_users
FROM alerts
ORDER BY id DESC
@@ -172,8 +115,11 @@ pub mod query {
user_id: row.get("user_id"),
created_at: row.get::<_, f64>("created_at") as i64,
area: row.get("area"),
- extended_area: row.get("extended_area"),
- level: row.get("level"),
+ area_level2: row.get("area_level2"),
+ area_level3: row.get("area_level3"),
+ text1: row.get("text1"),
+ text2: row.get("text2"),
+ text3: row.get("text3"),
reached_users: row.get("reached_users"),
})
.collect();
@@ -185,6 +131,10 @@ pub mod query {
}
pub mod mutations {
+ use std::str::FromStr;
+
+ use crate::graphql::types::position;
+
use super::*;
/// Create a new alert
@@ -232,15 +182,23 @@ pub mod mutations {
}
let insert_query = format!(
- "INSERT INTO alerts (user_id, area, level)
- VALUES($1, {}, $2)
- RETURNING id, user_id, extract(epoch from created_at)::double precision as created_at, ST_AsText(area) as area,
- ST_AsText(ST_Buffer(area::geography, CASE WHEN level = 'One' THEN 0 WHEN level = 'Two' THEN 1000 WHEN level = 'Three' THEN 2000 ELSE 0 END)) as extended_area, level, reached_users",
+ "INSERT INTO alerts (user_id, area, text1, text2, text3)
+ VALUES($1, {}, $2, $3, $4)
+ RETURNING
+ id, user_id, extract(epoch from created_at)::double precision as created_at,
+ ST_AsText(area) as area,
+ ST_AsText(ST_Buffer(area::geography, 1000)) as area_level2,
+ ST_AsText(ST_Buffer(area::geography, 2000)) as area_level3,
+ text1, text2, text3,
+ reached_users",
polygon
);
let rows = client
- .query(&insert_query, &[&claims.user_id, &input.level])
+ .query(
+ &insert_query,
+ &[&claims.user_id, &input.text1, &input.text2, &input.text3],
+ )
.await
.unwrap();
let mut alert = rows
@@ -250,8 +208,11 @@ pub mod mutations {
user_id: row.get("user_id"),
created_at: row.get::<_, f64>("created_at") as i64,
area: row.get("area"),
- extended_area: row.get("extended_area"),
- level: row.get("level"),
+ area_level2: row.get("area_level2"),
+ area_level3: row.get("area_level3"),
+ text1: row.get("text1"),
+ text2: row.get("text2"),
+ text3: row.get("text3"),
reached_users: row.get("reached_users"),
})
.collect::<Vec<Alert>>()
@@ -259,43 +220,111 @@ pub mod mutations {
.cloned()
.ok_or_else(|| async_graphql::Error::new("Failed to create alert"))?;
- let distance: f64 = match alert.level {
- LevelAlert::One => 0.0,
- LevelAlert::Two => 1000.0,
- LevelAlert::Three => 2000.0,
- };
+ struct Level<'a> {
+ text: &'a str,
+ distance: f64,
+ }
- let position_ids: Vec<i32> = client
- .query(
- "SELECT id
- FROM positions p
- WHERE ST_DWithin(
- p.location::geography,
- (SELECT area::geography FROM alerts WHERE id = $1),
- $2
- )
- AND id = (
- SELECT MAX(id)
- FROM positions
- WHERE user_id = p.user_id
- )",
- &[&alert.id, &distance],
- )
- .await
- .unwrap()
- .iter()
- .map(|row| row.get(0))
- .collect();
+ let levels = vec![
+ Level {
+ text: "One",
+ distance: 0f64,
+ },
+ Level {
+ text: "Two",
+ distance: 1000f64,
+ },
+ Level {
+ text: "Three",
+ distance: 2000f64,
+ },
+ ];
+
+ let mut positions: Vec<i32> = vec![];
+
+ // Send notifications for each available level
+ for level in levels {
+ let position_ids: Vec<i32> = client
+ .query(
+ "SELECT id
+ FROM positions p
+ WHERE ST_DWithin(
+ p.location::geography,
+ (SELECT area::geography FROM alerts WHERE id = $1),
+ $2
+ )
+ AND id = (
+ SELECT MAX(id)
+ FROM positions
+ WHERE user_id = p.user_id
+ )",
+ &[&alert.id, &level.distance],
+ )
+ .await
+ .unwrap()
+ .iter()
+ .map(|row| row.get(0))
+ .filter(|id| !positions.contains(id))
+ .collect();
+
+ let mut notification_ids = vec![];
+ for id in &position_ids {
+ let notification = Notification::insert_db(
+ client,
+ alert.id,
+ *id,
+ LevelAlert::from_str(level.text).unwrap(),
+ )
+ .await
+ .unwrap();
+ notification_ids.push(notification);
+ }
+
+ alert.reached_users += notification_ids.len() as i32;
+ let placeholders: Vec<String> = (1..=position_ids.len())
+ .map(|i| format!("${}", i))
+ .collect();
- let mut notification_ids = vec![];
- for id in &position_ids {
- let notification = Notification::insert_db(client, alert.id, *id)
+ if placeholders.len() > 0 {
+ let query = format!(
+ "SELECT DISTINCT u.notification_token FROM positions p JOIN users u ON u.id = p.user_id
+ WHERE p.id IN ({}) AND notification_token IS NOT NULL",
+ placeholders.join(", ")
+ );
+
+ let tokens: Vec<String> = client
+ .query(
+ &query,
+ &position_ids
+ .iter()
+ .map(|id| id as &(dyn tokio_postgres::types::ToSql + Sync))
+ .collect::<Vec<&(dyn tokio_postgres::types::ToSql + Sync)>>(),
+ )
+ .await
+ .unwrap()
+ .iter()
+ .map(|row| {
+ format!("ExponentPushToken[{}]", row.get::<usize, String>(0))
+ })
+ .collect();
+
+ expo::send(
+ tokens,
+ "New Alert!".to_string(),
+ match level.text {
+ "One" => alert.text1.clone(),
+ "Two" => alert.text2.clone(),
+ "Three" => alert.text3.clone(),
+ _ => "Check it out in app!".to_string(),
+ },
+ )
.await
.unwrap();
- notification_ids.push(notification);
+ }
+
+ positions.extend(position_ids);
}
- alert.reached_users = notification_ids.len() as i32;
client
.query(
"UPDATE alerts SET reached_users = $1 WHERE id = $2",
@@ -304,41 +333,6 @@ pub mod mutations {
.await
.unwrap();
- let placeholders: Vec<String> = (1..=position_ids.len())
- .map(|i| format!("${}", i))
- .collect();
-
- if placeholders.len() > 0 {
- let query = format!(
- "SELECT DISTINCT u.notification_token FROM positions p JOIN users u ON u.id = p.user_id
- WHERE p.id IN ({}) AND notification_token IS NOT NULL",
- placeholders.join(", ")
- );
-
- println!("{query}");
- let tokens: Vec<String> = client
- .query(
- &query,
- &position_ids
- .iter()
- .map(|id| id as &(dyn tokio_postgres::types::ToSql + Sync))
- .collect::<Vec<&(dyn tokio_postgres::types::ToSql + Sync)>>(),
- )
- .await
- .unwrap()
- .iter()
- .map(|row| format!("ExponentPushToken[{}]", row.get::<usize, String>(0)))
- .collect();
-
- expo::send(
- tokens,
- "New Alert!".to_string(),
- "Keep an eye open".to_string(),
- )
- .await
- .unwrap();
- }
-
Ok(alert)
}
}
diff --git a/src/graphql/types/notification.rs b/src/graphql/types/notification.rs
index 21c6b4e..1ff7532 100644
--- a/src/graphql/types/notification.rs
+++ b/src/graphql/types/notification.rs
@@ -3,10 +3,75 @@ use crate::{
graphql::types::{alert::Alert, jwt::Authentication, position::Position, user::find_user},
state::AppState,
};
-use async_graphql::{Context, FieldResult, InputObject, SimpleObject};
+use async_graphql::{Context, Enum, FieldResult, InputObject, SimpleObject};
use serde::{Deserialize, Serialize};
+use std::{error::Error, str::FromStr};
+use tokio_postgres::types::{to_sql_checked, FromSql, IsNull, ToSql, Type};
use tokio_postgres::Client;
+#[derive(Enum, Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
+/// Enumeration which refers to the level of alert
+pub enum LevelAlert {
+ // User in the AREA
+ One,
+
+ // User in the AREA OR < 1km distance
+ Two,
+
+ // User in the AREA OR < 2km distance
+ Three,
+}
+
+impl FromStr for LevelAlert {
+ type Err = String;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ match s {
+ "One" => Ok(LevelAlert::One),
+ "Two" => Ok(LevelAlert::Two),
+ "Three" => Ok(LevelAlert::Three),
+ _ => Err(String::from("Can't parse this value as Level")),
+ }
+ }
+}
+
+impl<'a> FromSql<'a> for LevelAlert {
+ fn from_sql(_ty: &Type, raw: &'a [u8]) -> Result<LevelAlert, Box<dyn Error + Sync + Send>> {
+ match std::str::from_utf8(raw)? {
+ "One" => Ok(LevelAlert::One),
+ "Two" => Ok(LevelAlert::Two),
+ "Three" => Ok(LevelAlert::Three),
+ other => Err(format!("Unknown variant: {}", other).into()),
+ }
+ }
+
+ fn accepts(ty: &Type) -> bool {
+ ty.name() == "level_alert"
+ }
+}
+
+impl ToSql for LevelAlert {
+ fn to_sql(
+ &self,
+ _ty: &Type,
+ out: &mut bytes::BytesMut,
+ ) -> Result<IsNull, Box<dyn Error + Sync + Send>> {
+ let value = match *self {
+ LevelAlert::One => "One",
+ LevelAlert::Two => "Two",
+ LevelAlert::Three => "Three",
+ };
+ out.extend_from_slice(value.as_bytes());
+ Ok(IsNull::No)
+ }
+
+ fn accepts(ty: &Type) -> bool {
+ ty.name() == "level_alert"
+ }
+
+ to_sql_checked!();
+}
+
#[derive(SimpleObject, Clone, Debug, Serialize, Deserialize)]
/// Notification struct
pub struct Notification {
@@ -14,6 +79,7 @@ pub struct Notification {
pub alert: Alert,
pub position: Position,
pub seen: bool,
+ pub level: LevelAlert,
pub created_at: i64,
}
@@ -31,14 +97,15 @@ impl Notification {
client: &Client,
alert_id: i32,
position_id: i32,
+ level: LevelAlert,
) -> Result<i32, AppError> {
match client
.query(
- "INSERT INTO notifications(alert_id, position_id)
- VALUES($1, $2)
+ "INSERT INTO notifications(alert_id, position_id, level)
+ VALUES($1, $2, $3)
RETURNING id
",
- &[&alert_id, &position_id],
+ &[&alert_id, &position_id, &level],
)
.await
{
@@ -90,23 +157,17 @@ pub mod query {
n.alert_id,
n.position_id,
n.seen,
+ n.level,
extract(epoch from n.created_at)::double precision as created_at,
a.id as alert_id,
a.user_id as alert_user_id,
extract(epoch from a.created_at)::double precision as alert_created_at,
ST_AsText(a.area) as alert_area,
- ST_AsText(
- ST_Buffer(
- a.area::geography,
- CASE
- WHEN level = 'One' THEN 0
- WHEN level = 'Two' THEN 1000
- WHEN level = 'Three' THEN 2000
- ELSE 0
- END
- )
- ) as alert_extended_area,
- a.level as alert_level,
+ ST_AsText(ST_Buffer(a.area::geography, 1000)) as alert_area_level2,
+ ST_AsText(ST_Buffer(a.area::geography, 2000)) as alert_area_level3,
+ a.text1 as alert_text1,
+ a.text2 as alert_text2,
+ a.text3 as alert_text3,
a.reached_users as alert_reached_users,
p.id as position_id,
p.user_id as position_user_id,
@@ -169,8 +230,11 @@ pub mod query {
user_id: row.get("alert_user_id"),
created_at: row.get::<_, f64>("alert_created_at") as i64,
area: row.get("alert_area"),
- extended_area: row.get("alert_extended_area"),
- level: row.get("alert_level"),
+ area_level2: row.get("alert_area_level2"),
+ area_level3: row.get("alert_area_level3"),
+ text1: row.get("alert_text1"),
+ text2: row.get("alert_text2"),
+ text3: row.get("alert_text3"),
reached_users: row.get("alert_reached_users"),
},
position: Position {
@@ -182,6 +246,7 @@ pub mod query {
moving_activity: row.get("position_activity"),
},
seen: row.get("seen"),
+ level: row.get("level"),
created_at: row.get::<_, f64>("created_at") as i64,
})
.collect();
@@ -213,24 +278,18 @@ pub mod mutations {
let notification = client.query("SELECT n.id,
n.alert_id,
n.position_id,
+ n.level,
n.seen,
extract(epoch from n.created_at)::double precision as created_at,
a.id as alert_id,
a.user_id as alert_user_id,
extract(epoch from a.created_at)::double precision as alert_created_at,
ST_AsText(a.area) as alert_area,
- ST_AsText(
- ST_Buffer(
- a.area::geography,
- CASE
- WHEN level = 'One' THEN 0
- WHEN level = 'Two' THEN 1000
- WHEN level = 'Three' THEN 2000
- ELSE 0
- END
- )
- ) as alert_extended_area,
- a.level as alert_level,
+ ST_AsText(ST_Buffer(a.area::geography, 1000)) as alert_area_level2,
+ ST_AsText(ST_Buffer(a.area::geography, 2000)) as alert_area_level3,
+ a.text1 as alert_text1,
+ a.text2 as alert_text2,
+ a.text3 as alert_text3,
a.reached_users as alert_reached_users,
p.id as position_id,
p.user_id as position_user_id,
@@ -254,8 +313,11 @@ pub mod mutations {
user_id: row.get("alert_user_id"),
created_at: row.get::<_, f64>("alert_created_at") as i64,
area: row.get("alert_area"),
- extended_area: row.get("alert_extended_area"),
- level: row.get("alert_level"),
+ area_level2: row.get("alert_area_level2"),
+ area_level3: row.get("alert_area_level3"),
+ text1: row.get("alert_text1"),
+ text2: row.get("alert_text2"),
+ text3: row.get("alert_text3"),
reached_users: row.get("alert_reached_users"),
},
position: Position {
@@ -267,6 +329,7 @@ pub mod mutations {
moving_activity: row.get("position_activity"),
},
seen: row.get("seen"),
+ level: row.get("level"),
created_at: row.get::<_, f64>("created_at") as i64,
})
.collect::<Vec<Notification>>()