use juniper::{ EmptyMutation, EmptySubscription, ID, RootNode, graphql_object, GraphQLObject, GraphQLEnum, FieldResult, FieldError, }; use sqlx::{ 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; 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) } } 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) } } #[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, display_name: Option, activated: bool, created: DateTime, last_online: Option>, preferences: UserPreferences, } #[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, security_preferences: SecurityPreferences, 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, discovery_user_limit: Option>, discovery_server_limit: Option>, last_seen: RestrictionPolicy, last_seen_user_limit: Option>, last_seen_server_limit: Option>, last_seen_course: bool, info: RestrictionPolicy, info_user_limit: Option>, info_server_limit: Option>, } 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, Decode, Encode)] pub struct SecurityPreferences { account_tokens: Vec, password_hash: String, } 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>, } 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")?, }) } } #[derive(Clone, Copy, Debug, GraphQLEnum, Type)] pub enum RestrictionPolicy { Everyone = 0, Excluding = 1, Including = 2, None = 3, } #[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()) }