From 40abea11f0162a3ebc19ba0f264d9f982862dd99 Mon Sep 17 00:00:00 2001 From: EliasSchriefer Date: Tue, 27 Jul 2021 19:11:40 +0000 Subject: [PATCH] Implement GraphQL logic for SQLite --- src/graphql.rs | 297 +++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 264 insertions(+), 33 deletions(-) diff --git a/src/graphql.rs b/src/graphql.rs index 3dbe984..a5ea9cf 100644 --- a/src/graphql.rs +++ b/src/graphql.rs @@ -1,4 +1,3 @@ -use std::marker::PhantomData; use juniper::{ EmptyMutation, EmptySubscription, @@ -7,24 +6,102 @@ use juniper::{ graphql_object, GraphQLObject, GraphQLEnum, + FieldResult, + FieldError, }; use sqlx::{ - SqlitePool, types::chrono::{ DateTime, Utc, }, + FromRow, + Encode, + Decode, + Type, + Row, + Result as SqlxResult, + Error as SqlxError, +}; +#[cfg(feature = "sqlite")] +use sqlx::{ + SqlitePool, + sqlite::SqliteRow, }; use url::Url; -#[derive(Clone, Copy, Debug)] -pub struct Context<'db> { - db: &'db SqlitePool, +fn into_user_limit( + #[cfg(feature = "sqlite")] + row: &SqliteRow, + index: &str +) -> SqlxResult>> { + let row_value: Option<&str> = row.try_get(index)?; + if let Some(s) = row_value { + into_toml_iter(s, index).map(|i| Some(i.collect())) + } else { + Ok(None) + } } -impl<'db> juniper::Context for Context<'db> {} +fn into_server_limit( + #[cfg(feature = "sqlite")] + row: &SqliteRow, + index: &str +) -> SqlxResult>> { + let row_value: Option<&str> = row.try_get(index)?; + if let Some(s) = row_value { + let mut url_vector = Vec::new(); + for s in into_toml_iter(s, index)? { + url_vector.push(Url::parse(s).map_err(into_column_decode_err(index))?); + } + Ok(Some(url_vector)) + } else { + Ok(None) + } +} -#[derive(Clone, Debug, GraphQLObject)] +#[inline] +fn into_column_decode_err(index: &str) -> impl FnOnce(E) -> SqlxError + '_ +where + E: Into> +{ + move |err: E| SqlxError::ColumnDecode { + index: index.to_owned(), + source: err.into(), + } +} + +#[inline] +fn into_toml_iter<'d, T>(raw: &'d str, index: &str) -> SqlxResult> +where + T: serde::de::Deserialize<'d> +{ + Ok(toml::from_str::>(raw) + .map_err(into_column_decode_err(index))? + .into_iter() + ) +} + +#[inline] +fn into_toml_iter_from_row<'d, T>( + #[cfg(feature = "sqlite")] + row: &'d SqliteRow, + index: &str +) -> SqlxResult> +where + T: serde::de::Deserialize<'d> +{ + into_toml_iter(row.try_get(index)?, index) +} + +#[derive(Clone, Debug)] +pub struct Context { + #[cfg(feature = "sqlite")] + db: SqlitePool, +} + +impl juniper::Context for Context {} + +#[derive(Clone, Debug, Decode, Type)] pub struct User { id: ID, user_name: String, @@ -35,7 +112,58 @@ pub struct User { preferences: UserPreferences, } -#[derive(Clone, Debug, GraphQLObject)] +#[graphql_object(context = Context)] +impl User { + pub const fn id(&self) -> &ID { + &self.id + } + + pub const fn user_name(&self) -> &String { + &self.user_name + } + + pub const fn display_name(&self) -> Option<&String> { + self.display_name.as_ref() + } + + pub const fn activated(&self) -> bool { + self.activated + } + + pub const fn created(&self) -> &DateTime { + &self.created + } + + pub const fn last_online(&self) -> Option<&DateTime> { + self.last_online.as_ref() + } + + pub async fn preferences(&self, context: &Context) -> FieldResult { + UserPreferences::try_get(&self.id, &context.db).await.map_err(FieldError::from) + } +} + +impl User { + async fn from_row_with_db( + #[cfg(feature = "sqlite")] + row: &SqliteRow, + #[cfg(feature = "sqlite")] + db: &SqlitePool, + ) -> Result { + let id = row.try_get::("id")?.into(); + Ok(User { + user_name: row.try_get("user_name")?, + display_name: row.try_get("display_name")?, + activated: row.try_get("activated")?, + created: row.try_get("created")?, + last_online: row.try_get("last_online")?, + preferences: UserPreferences::try_get(&id, db).await?, + id, + }) + } +} + +#[derive(Clone, Debug, GraphQLObject, Decode, Encode, FromRow)] pub struct UserPreferences { privacy_preferences: PrivacyPreferences, notification_preferences: NotificationPreferences, @@ -43,6 +171,32 @@ pub struct UserPreferences { external_servers_preferences: ExternalServersPreferences, } +impl UserPreferences { + async fn try_get(id: &ID, db: &SqlitePool) -> SqlxResult { + let privacy_preferences: PrivacyPreferences = + sqlx::query_as("SELECT * FROM privacy_preferences WHERE users.id = privacy_preferences.id") + .fetch_one(db).await?; + let notification_preferences: NotificationPreferences = + sqlx::query_as("SELECT * FROM notification_preferences WHERE users.id = privacy_preferences.id") + .fetch_one(db).await?; + let security_preferences: SecurityPreferences = + sqlx::query_as("SELECT * FROM security_preferences WHERE users.id = privacy_preferences.id") + .fetch_one(db).await?; + let external_servers_preferences = ExternalServersPreferences::from_row_with_db( + sqlx::query(&*format!("SELECT * FROM external_server_preferences WHERE id = {}", id)) + .fetch_one(db).await?, + db, + ).await?; + + Ok(UserPreferences { + privacy_preferences, + notification_preferences, + security_preferences, + external_servers_preferences, + }) + } +} + #[derive(Clone, Debug, GraphQLObject)] pub struct PrivacyPreferences { discovery: RestrictionPolicy, @@ -57,49 +211,126 @@ pub struct PrivacyPreferences { info_server_limit: Option>, } -#[derive(Clone, Copy, Debug, GraphQLObject)] +impl PrivacyPreferences { + fn from_row_cfg( + #[cfg(feature = "sqlite")] + row: &SqliteRow, + ) -> SqlxResult { + Ok(PrivacyPreferences { + discovery: row.try_get("discovery")?, + discovery_user_limit: into_user_limit(row, "discovery_user_limit")?, + discovery_server_limit: into_server_limit(row, "discovery_server_limit")?, + last_seen: row.try_get("last_seen")?, + last_seen_user_limit: into_user_limit(row, "last_seen_user_limit")?, + last_seen_server_limit: into_server_limit(row, "last_seen_server_limit")?, + last_seen_course: row.try_get("last_seen_course")?, + info: row.try_get("info")?, + info_user_limit: into_user_limit(row, "info_user_limit")?, + info_server_limit: into_server_limit(row, "info_server_limit")?, + }) + } +} + +#[cfg(feature = "sqlite")] +impl FromRow<'_, SqliteRow> for PrivacyPreferences { + fn from_row(row: &SqliteRow) -> SqlxResult { + Self::from_row_cfg(row) + } +} + +#[derive(Clone, Copy, Debug, GraphQLObject, Decode, Encode, FromRow)] pub struct NotificationPreferences { lock_details: bool, do_not_disturb: bool, } -#[derive(Clone, Debug, GraphQLObject)] +#[derive(Clone, Debug, GraphQLObject, Decode, Encode)] pub struct SecurityPreferences { account_tokens: Vec, password_hash: String, } -#[derive(Clone, Debug, GraphQLObject)] +impl SecurityPreferences { + fn from_row_cfg( + #[cfg(feature = "sqlite")] + row: &SqliteRow, + ) -> SqlxResult { + Ok(SecurityPreferences { + account_tokens: into_toml_iter_from_row::<&str>(row, "account_tokens")? + .map(ID::new) + .collect(), + password_hash: row.try_get("password_hash")?, + }) + } +} + +#[cfg(feature = "sqlite")] +impl FromRow<'_, SqliteRow> for SecurityPreferences { + fn from_row(row: &SqliteRow) -> SqlxResult { + Self::from_row_cfg(row) + } +} + +#[derive(Clone, Debug, GraphQLObject, Decode, Encode, FromRow)] pub struct ExternalServersPreferences { privacy_preferences: PrivacyPreferences, external_servers: RestrictionPolicy, external_servers_limit: Option>, } -#[derive(Clone, Copy, Debug, GraphQLEnum)] -pub enum RestrictionPolicy { - Everyone, - Excluding, - Including, - None, -} - -#[derive(Clone, Copy, Debug)] -pub struct Query<'db> { - phantom: PhantomData<&'db ()>, -} - -#[graphql_object(context = Context<'db>)] -impl<'db> Query<'db> { - async fn users(context: &Context<'db>) -> Vec { - unimplemented!() +impl ExternalServersPreferences { + async fn from_row_with_db( + #[cfg(feature = "sqlite")] + row: SqliteRow, + #[cfg(feature = "sqlite")] + db: &SqlitePool, + ) -> SqlxResult { + Ok(Self { + privacy_preferences: + sqlx::query_as(&*format!("SELECT * FROM external_server_privacy_preferences WHERE external_servers_preferences.id = external_servers_privacy_preferences.id && id = {}", row.try_get::("id")?)) + .fetch_one(db).await?, + external_servers: row.try_get("external_servers")?, + external_servers_limit: into_server_limit(&row, "external_server_limit")?, + }) } } -pub type Schema<'root_node, 'db> = RootNode<'root_node, Query<'db>, EmptyMutation>, EmptySubscription>>; +#[derive(Clone, Copy, Debug, GraphQLEnum, Type)] +pub enum RestrictionPolicy { + Everyone = 0, + Excluding = 1, + Including = 2, + None = 3, +} -pub fn schema<'root_node, 'db>() -> Schema<'root_node, 'db> { - Schema::new(Query { - phantom: PhantomData, - }, EmptyMutation::new(), EmptySubscription::new()) +#[derive(Clone, Copy, Debug)] +pub struct Query; + +impl Query { + async fn users_sqlx_result(context: &Context) -> SqlxResult> { + let rows = sqlx::query("SELECT * FROM users") + .fetch_all(&context.db).await?; + let mut users = Vec::with_capacity(rows.capacity()); + for row in rows { + users.push(User::from_row_with_db(&row, &context.db).await?); + } + Ok(users) + } +} + +#[graphql_object(context = Context)] +impl Query { + async fn users(context: &Context) -> FieldResult> { + Self::users_sqlx_result(context).await.map_err(FieldError::from) + } + + async fn user_preferences(context: &Context, id: ID) -> FieldResult { + UserPreferences::try_get(&id, &context.db).await.map_err(FieldError::from) + } +} + +pub type Schema<'root_node> = RootNode<'root_node, Query, EmptyMutation, EmptySubscription>; + +pub fn schema<'root_node>() -> Schema<'root_node> { + Schema::new(Query, EmptyMutation::new(), EmptySubscription::new()) } \ No newline at end of file