cas/graphql/types/
position.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
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)]
/// Enumeration which refers to the kind of moving activity
pub enum MovingActivity {
    // "Car" of the doc
    InVehicle,

    // Running
    Running,

    // Walking
    Walking,

    // Device is not moving
    Still,
}

/// Implement `Display` trait for `MovingActivity` since we have to use it on format! function
/// call.
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)]
/// Position struct
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)]
/// Position input struct
pub struct PositionInput {
    pub latitude: f64,
    pub longitude: f64,
    pub moving_activity: MovingActivity,
}

/// Find a position with user_id = `id` using the PostgreSQL `client`
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::*;

    /// Get positions from the database for each user.
    /// It is restricted to only admin users.
    pub async fn get_positions<'ctx>(
        ctx: &Context<'ctx>,

        // Optional filter by list of moving activity
        moving_activity: Option<Vec<MovingActivity>>,

        // Optional limit results
        limit: Option<i64>,

        // Optional offset results. It should be used with limit field.
        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);
                }

                // Create filter for `movingActivity` field. If not passed by the user, just
                // returns all kinda activities.
                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::*;

    /// Create a new position for a logged user. If a position already exists, just edit that
    /// position.
    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())
            }
        }
    }
}