From 1d228ff76e7999ce724b9f40da067e69649557ce Mon Sep 17 00:00:00 2001 From: Paul Makles Date: Wed, 26 Apr 2023 18:32:29 +0100 Subject: [PATCH] feat(core/database): implement server model --- Cargo.lock | 10 + crates/core/database/Cargo.toml | 3 +- crates/core/database/src/drivers/reference.rs | 4 +- crates/core/database/src/models/mod.rs | 3 + .../core/database/src/models/servers/mod.rs | 5 + .../core/database/src/models/servers/model.rs | 451 ++++++++++++++++++ .../core/database/src/models/servers/ops.rs | 46 ++ .../src/models/servers/ops/mongodb.rs | 183 +++++++ .../src/models/servers/ops/reference.rs | 126 +++++ .../database/src/models/users/ops/mongodb.rs | 2 +- crates/core/permissions/src/models/server.rs | 4 +- 11 files changed, 831 insertions(+), 6 deletions(-) create mode 100644 crates/core/database/src/models/servers/mod.rs create mode 100644 crates/core/database/src/models/servers/model.rs create mode 100644 crates/core/database/src/models/servers/ops.rs create mode 100644 crates/core/database/src/models/servers/ops/mongodb.rs create mode 100644 crates/core/database/src/models/servers/ops/reference.rs diff --git a/Cargo.lock b/Cargo.lock index 462af954..1a41f52f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2862,6 +2862,7 @@ dependencies = [ "schemars", "serde", "serde_json", + "ulid 1.0.0", ] [[package]] @@ -4234,6 +4235,15 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "ulid" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13a3aaa69b04e5b66cc27309710a569ea23593612387d67daaf102e73aa974fd" +dependencies = [ + "rand 0.8.5", +] + [[package]] name = "uname" version = "0.1.1" diff --git a/crates/core/database/Cargo.toml b/crates/core/database/Cargo.toml index f2be94ab..a5d6487f 100644 --- a/crates/core/database/Cargo.toml +++ b/crates/core/database/Cargo.toml @@ -22,10 +22,11 @@ default = [ "mongodb", "async-std-runtime" ] [dependencies] # Core revolt-result = { version = "0.0.2", path = "../result" } -revolt-permissions = { version = "0.0.2", path = "../permissions" } +revolt-permissions = { version = "0.0.2", path = "../permissions", features = [ "serde" ] } # Utility log = "0.4" +ulid = "1.0.0" nanoid = "0.4.0" once_cell = "1.17" diff --git a/crates/core/database/src/drivers/reference.rs b/crates/core/database/src/drivers/reference.rs index 4749dc44..6b5c7674 100644 --- a/crates/core/database/src/drivers/reference.rs +++ b/crates/core/database/src/drivers/reference.rs @@ -2,7 +2,7 @@ use std::{collections::HashMap, sync::Arc}; use futures::lock::Mutex; -use crate::{Bot, File, User, UserSettings}; +use crate::{Bot, File, Server, User, UserSettings}; database_derived!( /// Reference implementation @@ -11,9 +11,9 @@ database_derived!( pub bots: Arc>>, pub user_settings: Arc>>, pub users: Arc>>, + pub servers: Arc>>, pub files: Arc>>, - pub servers: Arc>>, pub server_bans: Arc>>, pub server_members: Arc>>, pub safety_reports: Arc>>, diff --git a/crates/core/database/src/models/mod.rs b/crates/core/database/src/models/mod.rs index 67c8e35d..1a981ba6 100644 --- a/crates/core/database/src/models/mod.rs +++ b/crates/core/database/src/models/mod.rs @@ -1,12 +1,14 @@ mod admin_migrations; mod bots; mod files; +mod servers; mod user_settings; mod users; pub use admin_migrations::*; pub use bots::*; pub use files::*; +pub use servers::*; pub use user_settings::*; pub use users::*; @@ -18,6 +20,7 @@ pub trait AbstractDatabase: + admin_migrations::AbstractMigrations + bots::AbstractBots + files::AbstractAttachments + + servers::AbstractServers + user_settings::AbstractUserSettings + users::AbstractUsers { diff --git a/crates/core/database/src/models/servers/mod.rs b/crates/core/database/src/models/servers/mod.rs new file mode 100644 index 00000000..4d801b73 --- /dev/null +++ b/crates/core/database/src/models/servers/mod.rs @@ -0,0 +1,5 @@ +mod model; +mod ops; + +pub use model::*; +pub use ops::*; diff --git a/crates/core/database/src/models/servers/model.rs b/crates/core/database/src/models/servers/model.rs new file mode 100644 index 00000000..43ba859e --- /dev/null +++ b/crates/core/database/src/models/servers/model.rs @@ -0,0 +1,451 @@ +use std::collections::{HashMap, HashSet}; + +use revolt_permissions::OverrideField; +use revolt_result::Result; +use ulid::Ulid; + +use crate::{Database, File}; + +auto_derived_partial!( + /// Server + pub struct Server { + /// Unique Id + #[serde(rename = "_id")] + pub id: String, + /// User id of the owner + pub owner: String, + + /// Name of the server + pub name: String, + /// Description for the server + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + + /// Channels within this server + // ! FIXME: this may be redundant + pub channels: Vec, + /// Categories for this server + #[serde(skip_serializing_if = "Option::is_none")] + pub categories: Option>, + /// Configuration for sending system event messages + #[serde(skip_serializing_if = "Option::is_none")] + pub system_messages: Option, + + /// Roles for this server + #[serde( + default = "HashMap::::new", + skip_serializing_if = "HashMap::::is_empty" + )] + pub roles: HashMap, + /// Default set of server and channel permissions + pub default_permissions: i64, + + /// Icon attachment + #[serde(skip_serializing_if = "Option::is_none")] + pub icon: Option, + /// Banner attachment + #[serde(skip_serializing_if = "Option::is_none")] + pub banner: Option, + + /// Bitfield of server flags + #[serde(skip_serializing_if = "Option::is_none")] + pub flags: Option, + + /// Whether this server is flagged as not safe for work + #[serde(skip_serializing_if = "crate::if_false", default)] + pub nsfw: bool, + /// Whether to enable analytics + #[serde(skip_serializing_if = "crate::if_false", default)] + pub analytics: bool, + /// Whether this server should be publicly discoverable + #[serde(skip_serializing_if = "crate::if_false", default)] + pub discoverable: bool, + }, + "PartialServer" +); + +auto_derived_partial!( + /// Role + pub struct Role { + /// Role name + pub name: String, + /// Permissions available to this role + pub permissions: OverrideField, + /// Colour used for this role + /// + /// This can be any valid CSS colour + #[serde(skip_serializing_if = "Option::is_none")] + pub colour: Option, + /// Whether this role should be shown separately on the member sidebar + #[serde(skip_serializing_if = "crate::if_false", default)] + pub hoist: bool, + /// Ranking of this role + #[serde(default)] + pub rank: i64, + }, + "PartialRole" +); + +auto_derived!( + /// Channel category + pub struct Category { + /// Unique ID for this category + pub id: String, + /// Title for this category + pub title: String, + /// Channels in this category + pub channels: Vec, + } + + /// System message channel assignments + pub struct SystemMessageChannels { + /// ID of channel to send user join messages in + #[serde(skip_serializing_if = "Option::is_none")] + pub user_joined: Option, + /// ID of channel to send user left messages in + #[serde(skip_serializing_if = "Option::is_none")] + pub user_left: Option, + /// ID of channel to send user kicked messages in + #[serde(skip_serializing_if = "Option::is_none")] + pub user_kicked: Option, + /// ID of channel to send user banned messages in + #[serde(skip_serializing_if = "Option::is_none")] + pub user_banned: Option, + } + + /// Optional fields on server object + pub enum FieldsServer { + Description, + Categories, + SystemMessages, + Icon, + Banner, + } + + /// Optional fields on server object + pub enum FieldsRole { + Colour, + } +); + +impl Server { + /// Create a server + pub async fn create(&self, db: &Database) -> Result<()> { + db.insert_server(self).await + } + + /// Update server data + pub async fn update<'a>( + &mut self, + db: &Database, + partial: PartialServer, + remove: Vec, + ) -> Result<()> { + for field in &remove { + self.remove_field(field); + } + + self.apply_options(partial.clone()); + + db.update_server(&self.id, &partial, remove.clone()).await?; + + /* // TODO: EventV1::ServerUpdate { + id: self.id.clone(), + data: partial, + clear: remove, + } + .p(self.id.clone()) + .await; */ + + Ok(()) + } + + /// Delete a server + pub async fn delete(self, db: &Database) -> Result<()> { + /* // TODO: EventV1::ServerDelete { + id: self.id.clone(), + } + .p(self.id.clone()) + .await; */ + + db.delete_server(&self.id).await + } + + /// Remove a field from Server + pub fn remove_field(&mut self, field: &FieldsServer) { + match field { + FieldsServer::Description => self.description = None, + FieldsServer::Categories => self.categories = None, + FieldsServer::SystemMessages => self.system_messages = None, + FieldsServer::Icon => self.icon = None, + FieldsServer::Banner => self.banner = None, + } + } + + /// Set role permission on a server + pub async fn set_role_permission( + &mut self, + db: &Database, + role_id: &str, + permissions: OverrideField, + ) -> Result<()> { + if let Some(role) = self.roles.get_mut(role_id) { + role.update( + db, + &self.id, + role_id, + PartialRole { + permissions: Some(permissions), + ..Default::default() + }, + vec![], + ) + .await?; + + Ok(()) + } else { + Err(create_error!(NotFound)) + } + } + + /* /// Create a new member in a server + pub async fn create_member( + &self, + db: &Database, + user: User, + channels: Option>, + ) -> Result> { + if db.fetch_ban(&self.id, &user.id).await.is_ok() { + return Err(Error::Banned); + } + + let member = Member { + id: MemberCompositeKey { + server: self.id.clone(), + user: user.id.clone(), + }, + joined_at: Timestamp::now_utc(), + nickname: None, + avatar: None, + roles: vec![], + timeout: None, + }; + + db.insert_member(&member).await?; + + let should_fetch = channels.is_none(); + let mut channels = channels.unwrap_or_default(); + + if should_fetch { + let perm = perms(&user).server(self).member(&member); + let existing_channels = db.fetch_channels(&self.channels).await?; + for channel in existing_channels { + if perm + .clone() + .channel(&channel) + .has_permission(db, Permission::ViewChannel) + .await? + { + channels.push(channel); + } + } + } + + /* // TODO: EventV1::ServerMemberJoin { + id: self.id.clone(), + user: user.id.clone(), + } + .p(self.id.clone()) + .await; + + EventV1::ServerCreate { + id: self.id.clone(), + server: self.clone(), + channels: channels.clone(), + } + .private(user.id.clone()) + .await; */ + + if let Some(id) = self + .system_messages + .as_ref() + .and_then(|x| x.user_joined.as_ref()) + { + SystemMessage::UserJoined { + id: user.id.clone(), + } + .into_message(id.to_string()) + .create_no_web_push(db, id, false) + .await + .ok(); + } + + Ok(channels) + } + + /// Remove a member from a server + pub async fn remove_member( + &self, + db: &Database, + member: Member, + intention: RemovalIntention, + silent: bool, + ) -> Result<()> { + db.delete_member(&member.id).await?; + + /* // TODO: EventV1::ServerMemberLeave { + id: self.id.to_string(), + user: member.id.user.clone(), + } + .p(member.id.server) + .await; */ + + if !silent { + if let Some(id) = self.system_messages.as_ref().and_then(|x| match intention { + RemovalIntention::Leave => x.user_left.as_ref(), + RemovalIntention::Kick => x.user_kicked.as_ref(), + RemovalIntention::Ban => x.user_banned.as_ref(), + }) { + match intention { + RemovalIntention::Leave => SystemMessage::UserLeft { id: member.id.user }, + RemovalIntention::Kick => SystemMessage::UserKicked { id: member.id.user }, + RemovalIntention::Ban => SystemMessage::UserBanned { id: member.id.user }, + } + .into_message(id.to_string()) + .create_no_web_push(db, id, false) + .await + .ok(); + } + } + + Ok(()) + } + + /// Create ban + pub async fn ban_user( + self, + db: &Database, + id: MemberCompositeKey, + reason: Option, + ) -> Result { + let ban = ServerBan { id, reason }; + db.insert_ban(&ban).await?; + Ok(ban) + } + + /// Ban a member from a server + pub async fn ban_member( + self, + db: &Database, + member: Member, + reason: Option, + ) -> Result { + self.remove_member(db, member.clone(), RemovalIntention::Ban, false) + .await?; + + self.ban_user(db, member.id, reason).await + } */ +} + +impl Role { + /// Into optional struct + pub fn into_optional(self) -> PartialRole { + PartialRole { + name: Some(self.name), + permissions: Some(self.permissions), + colour: self.colour, + hoist: Some(self.hoist), + rank: Some(self.rank), + } + } + + /// Create a role + pub async fn create(&self, db: &Database, server_id: &str) -> Result { + let role_id = Ulid::new().to_string(); + db.insert_role(server_id, &role_id, self).await?; + + /* // TODO: EventV1::ServerRoleUpdate { + id: server_id.to_string(), + role_id: role_id.to_string(), + data: self.clone().into_optional(), + clear: vec![], + } + .p(server_id.to_string()) + .await; */ + + Ok(role_id) + } + + /// Update server data + pub async fn update<'a>( + &mut self, + db: &Database, + server_id: &str, + role_id: &str, + partial: PartialRole, + remove: Vec, + ) -> Result<()> { + for field in &remove { + self.remove_field(field); + } + + self.apply_options(partial.clone()); + + db.update_role(server_id, role_id, &partial, remove.clone()) + .await?; + + /* // TODO: EventV1::ServerRoleUpdate { + id: server_id.to_string(), + role_id: role_id.to_string(), + data: partial, + clear: vec![], + } + .p(server_id.to_string()) + .await; */ + + Ok(()) + } + + /// Delete a role + pub async fn delete(self, db: &Database, server_id: &str, role_id: &str) -> Result<()> { + /* // TODO: EventV1::ServerRoleDelete { + id: server_id.to_string(), + role_id: role_id.to_string(), + } + .p(server_id.to_string()) + .await; */ + + db.delete_role(server_id, role_id).await + } + + /// Remove field from Role + pub fn remove_field(&mut self, field: &FieldsRole) { + match field { + FieldsRole::Colour => self.colour = None, + } + } +} + +impl SystemMessageChannels { + pub fn into_channel_ids(self) -> HashSet { + let mut ids = HashSet::new(); + + if let Some(id) = self.user_joined { + ids.insert(id); + } + + if let Some(id) = self.user_left { + ids.insert(id); + } + + if let Some(id) = self.user_kicked { + ids.insert(id); + } + + if let Some(id) = self.user_banned { + ids.insert(id); + } + + ids + } +} diff --git a/crates/core/database/src/models/servers/ops.rs b/crates/core/database/src/models/servers/ops.rs new file mode 100644 index 00000000..6c58773a --- /dev/null +++ b/crates/core/database/src/models/servers/ops.rs @@ -0,0 +1,46 @@ +use revolt_result::Result; + +use crate::{FieldsRole, FieldsServer, PartialRole, PartialServer, Role, Server}; + +mod mongodb; +mod reference; + +#[async_trait] +pub trait AbstractServers: Sync + Send { + /// Insert a new server into database + async fn insert_server(&self, server: &Server) -> Result<()>; + + /// Fetch a server by its id + async fn fetch_server(&self, id: &str) -> Result; + + /// Fetch a servers by their ids + async fn fetch_servers<'a>(&self, ids: &'a [String]) -> Result>; + + /// Update a server with new information + async fn update_server( + &self, + id: &str, + partial: &PartialServer, + remove: Vec, + ) -> Result<()>; + + /// Delete a server by its id + async fn delete_server(&self, id: &str) -> Result<()>; + + /// Insert a new role into server object + async fn insert_role(&self, server_id: &str, role_id: &str, role: &Role) -> Result<()>; + + /// Update an existing role on a server + async fn update_role( + &self, + server_id: &str, + role_id: &str, + partial: &PartialRole, + remove: Vec, + ) -> Result<()>; + + /// Delete a role from a server + /// + /// Also updates channels and members. + async fn delete_role(&self, server_id: &str, role_id: &str) -> Result<()>; +} diff --git a/crates/core/database/src/models/servers/ops/mongodb.rs b/crates/core/database/src/models/servers/ops/mongodb.rs new file mode 100644 index 00000000..0c679e5f --- /dev/null +++ b/crates/core/database/src/models/servers/ops/mongodb.rs @@ -0,0 +1,183 @@ +use bson::{to_document, Document}; +use futures::StreamExt; +use revolt_result::Result; + +use crate::{FieldsRole, FieldsServer, PartialRole, PartialServer, Role, Server}; +use crate::{IntoDocumentPath, MongoDb}; + +use super::AbstractServers; + +static COL: &str = "servers"; + +#[async_trait] +impl AbstractServers for MongoDb { + /// Insert a new server into database + async fn insert_server(&self, server: &Server) -> Result<()> { + query!(self, insert_one, COL, &server).map(|_| ()) + } + + /// Fetch a server by its id + async fn fetch_server(&self, id: &str) -> Result { + query!(self, find_one_by_id, COL, id)?.ok_or_else(|| create_error!(NotFound)) + } + + /// Fetch a servers by their ids + async fn fetch_servers<'a>(&self, ids: &'a [String]) -> Result> { + Ok(self + .col::(COL) + .find( + doc! { + "_id": { + "$in": ids + } + }, + None, + ) + .await + .map_err(|_| create_database_error!("find", "servers"))? + .filter_map(|s| async { + if cfg!(debug_assertions) { + Some(s.unwrap()) + } else { + s.ok() + } + }) + .collect() + .await) + } + + /// Update a server with new information + async fn update_server( + &self, + id: &str, + partial: &PartialServer, + remove: Vec, + ) -> Result<()> { + query!( + self, + update_one_by_id, + COL, + id, + partial, + remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(), + None + ) + .map(|_| ()) + } + + /// Delete a server by its id + async fn delete_server(&self, id: &str) -> Result<()> { + query!(self, delete_one_by_id, COL, id).map(|_| ()) + } + + /// Insert a new role into server object + async fn insert_role(&self, server_id: &str, role_id: &str, role: &Role) -> Result<()> { + self.col::(COL) + .update_one( + doc! { + "_id": server_id + }, + doc! { + "$set": { + "roles.".to_owned() + role_id: to_document(role) + .map_err(|_| create_database_error!("to_document", "role"))? + } + }, + None, + ) + .await + .map(|_| ()) + .map_err(|_| create_database_error!("update_one", "server")) + } + + /// Update an existing role on a server + async fn update_role( + &self, + server_id: &str, + role_id: &str, + partial: &PartialRole, + remove: Vec, + ) -> Result<()> { + query!( + self, + update_one_by_id, + COL, + server_id, + partial, + remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(), + "roles.".to_owned() + role_id + "." + ) + .map(|_| ()) + } + + /// Delete a role from a server + /// + /// Also updates channels and members. + async fn delete_role(&self, server_id: &str, role_id: &str) -> Result<()> { + self.col::("server_members") + .update_many( + doc! { + "_id.server": server_id + }, + doc! { + "$pull": { + "roles": &role_id + } + }, + None, + ) + .await + .map_err(|_| create_database_error!("update_many", "server_members"))?; + + self.col::("channels") + .update_one( + doc! { + "server": server_id + }, + doc! { + "$unset": { + "role_permissions.".to_owned() + role_id: 1_i32 + } + }, + None, + ) + .await + .map_err(|_| create_database_error!("update_one", "channels"))?; + + self.col::("servers") + .update_one( + doc! { + "_id": server_id + }, + doc! { + "$unset": { + "roles.".to_owned() + role_id: 1_i32 + } + }, + None, + ) + .await + .map(|_| ()) + .map_err(|_| create_database_error!("update_one", "servers")) + } +} + +impl IntoDocumentPath for FieldsServer { + fn as_path(&self) -> Option<&'static str> { + Some(match self { + FieldsServer::Banner => "banner", + FieldsServer::Categories => "categories", + FieldsServer::Description => "description", + FieldsServer::Icon => "icon", + FieldsServer::SystemMessages => "system_messages", + }) + } +} + +impl IntoDocumentPath for FieldsRole { + fn as_path(&self) -> Option<&'static str> { + Some(match self { + FieldsRole::Colour => "colour", + }) + } +} diff --git a/crates/core/database/src/models/servers/ops/reference.rs b/crates/core/database/src/models/servers/ops/reference.rs new file mode 100644 index 00000000..18ee1b38 --- /dev/null +++ b/crates/core/database/src/models/servers/ops/reference.rs @@ -0,0 +1,126 @@ +use revolt_result::Result; + +use crate::ReferenceDb; +use crate::{FieldsRole, FieldsServer, PartialRole, PartialServer, Role, Server}; + +use super::AbstractServers; + +#[async_trait] +impl AbstractServers for ReferenceDb { + /// Insert a new server into database + async fn insert_server(&self, server: &Server) -> Result<()> { + let mut servers = self.servers.lock().await; + if servers.contains_key(&server.id) { + Err(create_database_error!("insert", "server")) + } else { + servers.insert(server.id.to_string(), server.clone()); + Ok(()) + } + } + + /// Fetch a server by its id + async fn fetch_server(&self, id: &str) -> Result { + let servers = self.servers.lock().await; + servers + .get(id) + .cloned() + .ok_or_else(|| create_error!(NotFound)) + } + + /// Fetch a servers by their ids + async fn fetch_servers<'a>(&self, ids: &'a [String]) -> Result> { + let servers = self.servers.lock().await; + ids.iter() + .map(|id| { + servers + .get(id) + .cloned() + .ok_or_else(|| create_error!(NotFound)) + }) + .collect() + } + + /// Update a server with new information + async fn update_server( + &self, + id: &str, + partial: &PartialServer, + remove: Vec, + ) -> Result<()> { + let mut servers = self.servers.lock().await; + if let Some(server) = servers.get_mut(id) { + for field in remove { + #[allow(clippy::disallowed_methods)] + server.remove_field(&field); + } + + server.apply_options(partial.clone()); + Ok(()) + } else { + Err(create_error!(NotFound)) + } + } + + /// Delete a server by its id + async fn delete_server(&self, id: &str) -> Result<()> { + let mut servers = self.servers.lock().await; + if servers.remove(id).is_some() { + Ok(()) + } else { + Err(create_error!(NotFound)) + } + } + + /// Insert a new role into server object + async fn insert_role(&self, server_id: &str, role_id: &str, role: &Role) -> Result<()> { + let mut servers = self.servers.lock().await; + if let Some(server) = servers.get_mut(server_id) { + server.roles.insert(role_id.to_string(), role.clone()); + Ok(()) + } else { + Err(create_error!(NotFound)) + } + } + + /// Update an existing role on a server + async fn update_role( + &self, + server_id: &str, + role_id: &str, + partial: &PartialRole, + remove: Vec, + ) -> Result<()> { + let mut servers = self.servers.lock().await; + if let Some(server) = servers.get_mut(server_id) { + if let Some(role) = server.roles.get_mut(role_id) { + for field in remove { + #[allow(clippy::disallowed_methods)] + role.remove_field(&field); + } + + role.apply_options(partial.clone()); + Ok(()) + } else { + Err(create_error!(NotFound)) + } + } else { + Err(create_error!(NotFound)) + } + } + + /// Delete a role from a server + /// + /// Also updates channels and members. + async fn delete_role(&self, server_id: &str, role_id: &str) -> Result<()> { + let mut servers = self.servers.lock().await; + if let Some(server) = servers.get_mut(server_id) { + if server.roles.remove(role_id).is_some() { + Ok(()) + } else { + Err(create_error!(NotFound)) + } + } else { + Err(create_error!(NotFound)) + } + } +} diff --git a/crates/core/database/src/models/users/ops/mongodb.rs b/crates/core/database/src/models/users/ops/mongodb.rs index 42cf4af1..cfb27016 100644 --- a/crates/core/database/src/models/users/ops/mongodb.rs +++ b/crates/core/database/src/models/users/ops/mongodb.rs @@ -75,7 +75,7 @@ impl AbstractUsers for MongoDb { None, ) .await - .map_err(|_| create_database_error!("find", "user"))? + .map_err(|_| create_database_error!("find", "users"))? .filter_map(|s| async { if cfg!(debug_assertions) { Some(s.unwrap()) diff --git a/crates/core/permissions/src/models/server.rs b/crates/core/permissions/src/models/server.rs index 08ae8443..7c11e98d 100644 --- a/crates/core/permissions/src/models/server.rs +++ b/crates/core/permissions/src/models/server.rs @@ -1,5 +1,5 @@ /// Representation of a single permission override -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Eq, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Override { /// Allow bit flags @@ -10,7 +10,7 @@ pub struct Override { /// Representation of a single permission override /// as it appears on models and in the database -#[derive(/*JsonSchema, */ Debug, Clone, Copy, Default)] +#[derive(/*JsonSchema, */ Debug, Clone, Copy, Default, Eq, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct OverrideField { /// Allow bit flags