use crate::{
errors::AppError,
graphql::types::{jwt::Authentication, user::find_user},
state::AppState,
};
use async_graphql::{Context, Enum, FieldResult, InputObject, SimpleObject};
use core::fmt;
use serde::{Deserialize, Serialize};
use std::error::Error;
use tokio_postgres::{
types::{to_sql_checked, FromSql, IsNull, ToSql, Type},
Client,
};
#[derive(Enum, Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
pub enum MovingActivity {
InVehicle,
Running,
Walking,
Still,
}
impl fmt::Display for MovingActivity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}",
match *self {
MovingActivity::InVehicle => "InVehicle",
MovingActivity::Running => "Running",
MovingActivity::Walking => "Walking",
MovingActivity::Still => "Still",
}
)
}
}
impl<'a> FromSql<'a> for MovingActivity {
fn from_sql(_ty: &Type, raw: &'a [u8]) -> Result<MovingActivity, Box<dyn Error + Sync + Send>> {
match std::str::from_utf8(raw)? {
"InVehicle" => Ok(MovingActivity::InVehicle),
"Running" => Ok(MovingActivity::Running),
"Walking" => Ok(MovingActivity::Walking),
"Still" => Ok(MovingActivity::Still),
other => Err(format!("Unknown variant: {}", other).into()),
}
}
fn accepts(ty: &Type) -> bool {
ty.name() == "moving_activity"
}
}
impl ToSql for MovingActivity {
fn to_sql(
&self,
_ty: &Type,
out: &mut bytes::BytesMut,
) -> Result<IsNull, Box<dyn Error + Sync + Send>> {
let value = match *self {
MovingActivity::InVehicle => "InVehicle",
MovingActivity::Running => "Running",
MovingActivity::Walking => "Walking",
MovingActivity::Still => "Still",
};
out.extend_from_slice(value.as_bytes());
Ok(IsNull::No)
}
fn accepts(ty: &Type) -> bool {
ty.name() == "moving_activity"
}
to_sql_checked!();
}
#[derive(SimpleObject, Clone, Debug, Serialize, Deserialize)]
pub struct Position {
pub id: i32,
pub user_id: i32,
pub created_at: i64,
pub latitude: f64,
pub longitude: f64,
pub moving_activity: MovingActivity,
}
#[derive(InputObject)]
pub struct PositionInput {
pub latitude: f64,
pub longitude: f64,
pub moving_activity: MovingActivity,
}
pub async fn find_user_position(client: &Client, id: i32) -> Result<Position, AppError> {
let rows = client
.query(
"SELECT id, user_id, extract(epoch from created_at)::double precision as created_at, ST_Y(location::geometry) AS latitude, ST_X(location::geometry) AS longitude, activity
FROM positions
WHERE user_id = $1",
&[&id],
)
.await
.unwrap();
let positions: Vec<Position> = rows
.iter()
.map(|row| Position {
id: row.get("id"),
user_id: row.get("user_id"),
created_at: row.get::<_, f64>("created_at") as i64,
latitude: row.get("latitude"),
longitude: row.get("longitude"),
moving_activity: row.get("activity"),
})
.collect();
if positions.len() == 1 {
Ok(positions[0].clone())
} else {
Err(AppError::NotFound("Position".to_string()))
}
}
pub mod query {
use super::*;
pub async fn get_positions<'ctx>(
ctx: &Context<'ctx>,
moving_activity: Option<Vec<MovingActivity>>,
limit: Option<i64>,
offset: Option<i64>,
) -> Result<Option<Vec<Position>>, AppError> {
let state = ctx.data::<AppState>().expect("Can't connect to db");
let client = &*state.client;
let auth: &Authentication = ctx.data()?;
match auth {
Authentication::NotLogged => Err(AppError::Unauthorized),
Authentication::Logged(claims) => {
let limit = limit.unwrap_or(20);
let offset = offset.unwrap_or(0);
let claim_user = find_user(client, claims.user_id)
.await
.expect("Should not be here");
if !claim_user.is_admin {
return Err(AppError::Unauthorized);
}
let moving_activity_filters: Vec<String> = if moving_activity.is_some() {
moving_activity
.unwrap()
.into_iter()
.map(|i| format!("'{}'", i))
.collect()
} else {
vec![
"'InVehicle'".to_string(),
"'Running'".to_string(),
"'Walking'".to_string(),
"'Still'".to_string(),
]
};
let rows = client
.query(&format!("
SELECT id, user_id, extract(epoch from created_at)::double precision as created_at, ST_Y(location::geometry) AS latitude, ST_X(location::geometry) AS longitude, activity
FROM positions
WHERE activity IN ({})
LIMIT $1
OFFSET $2
", moving_activity_filters.join(", ")),
&[&limit, &offset],
)
.await?;
let positions: Vec<Position> = rows
.iter()
.map(|row| Position {
id: row.get("id"),
user_id: row.get("user_id"),
created_at: row.get::<_, f64>("created_at") as i64,
latitude: row.get("latitude"),
longitude: row.get("longitude"),
moving_activity: row.get("activity"),
})
.collect();
Ok(Some(positions))
}
}
}
}
pub mod mutations {
use super::*;
pub async fn new_position<'ctx>(
ctx: &Context<'ctx>,
input: PositionInput,
) -> FieldResult<Position> {
let state = ctx.data::<AppState>().expect("Can't connect to db");
let client = &*state.client;
let auth: &Authentication = ctx.data()?;
match auth {
Authentication::NotLogged => {
Err(AppError::NotFound("Can't find the owner".to_string()).into())
}
Authentication::Logged(claims) => {
let rows = if find_user_position(client, claims.user_id).await.is_ok() {
client.query(
"UPDATE positions SET
location = ST_SetSRID(ST_MakePoint($1, $2), 4326),
activity = $3,
created_at = now()
WHERE user_id = $4
RETURNING id, user_id, extract(epoch from created_at)::double precision as created_at, ST_Y(location::geometry) AS latitude, ST_X(location::geometry) AS longitude, activity
",
&[
&input.longitude,
&input.latitude,
&input.moving_activity,
&claims.user_id,
],
)
.await?
} else {
client.query(
"INSERT INTO positions (user_id, location, activity)
VALUES (
$1,
ST_SetSRID(ST_MakePoint($2, $3), 4326),
$4
)
RETURNING id, user_id, extract(epoch from created_at)::double precision as created_at, ST_Y(location::geometry) AS latitude, ST_X(location::geometry) AS longitude, activity
",
&[
&claims.user_id,
&input.longitude,
&input.latitude,
&input.moving_activity,
],
)
.await?
};
let positions: Vec<Position> = rows
.iter()
.map(|row| Position {
id: row.get("id"),
user_id: row.get("user_id"),
created_at: row.get::<_, f64>("created_at") as i64,
latitude: row.get("latitude"),
longitude: row.get("longitude"),
moving_activity: row.get("activity"),
})
.collect();
Ok(positions[0].clone())
}
}
}
}