From 6fdf101745d18c16d54423d8b6131d4c8b31679c Mon Sep 17 00:00:00 2001 From: Mikayla Date: Fri, 25 Aug 2023 14:34:19 -0700 Subject: [PATCH] Update database and RPC to provide configured feature flags --- crates/client/src/test.rs | 1 + .../20221109000000_test_schema.sql | 19 ++++++ ...0230825190322_add_server_feature_flags.sql | 16 +++++ crates/collab/src/db/ids.rs | 1 + crates/collab/src/db/queries/users.rs | 54 +++++++++++++++++ crates/collab/src/db/tables.rs | 2 + crates/collab/src/db/tables/feature_flag.rs | 40 +++++++++++++ crates/collab/src/db/tables/user.rs | 23 +++++++ crates/collab/src/db/tables/user_feature.rs | 42 +++++++++++++ crates/collab/src/db/tests.rs | 1 + .../collab/src/db/tests/feature_flag_tests.rs | 60 +++++++++++++++++++ crates/collab/src/rpc.rs | 15 +++-- crates/rpc/proto/zed.proto | 1 + crates/rpc/src/rpc.rs | 2 +- 14 files changed, 268 insertions(+), 9 deletions(-) create mode 100644 crates/collab/migrations/20230825190322_add_server_feature_flags.sql create mode 100644 crates/collab/src/db/tables/feature_flag.rs create mode 100644 crates/collab/src/db/tables/user_feature.rs create mode 100644 crates/collab/src/db/tests/feature_flag_tests.rs diff --git a/crates/client/src/test.rs b/crates/client/src/test.rs index 4c12a20566..00e7cd1508 100644 --- a/crates/client/src/test.rs +++ b/crates/client/src/test.rs @@ -168,6 +168,7 @@ impl FakeServer { GetPrivateUserInfoResponse { metrics_id: "the-metrics-id".into(), staff: false, + flags: Default::default(), }, ) .await; diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 7a4cd9fd23..80477dcb3c 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -249,3 +249,22 @@ CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_channel_id_and_replic CREATE INDEX "index_channel_buffer_collaborators_on_connection_server_id" ON "channel_buffer_collaborators" ("connection_server_id"); CREATE INDEX "index_channel_buffer_collaborators_on_connection_id" ON "channel_buffer_collaborators" ("connection_id"); CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_channel_id_connection_id_and_server_id" ON "channel_buffer_collaborators" ("channel_id", "connection_id", "connection_server_id"); + + +CREATE TABLE "feature_flags" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "flag" TEXT NOT NULL UNIQUE +); + +CREATE INDEX "index_feature_flags" ON "feature_flags" ("id"); + + +CREATE TABLE "user_features" ( + "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, + "feature_id" INTEGER NOT NULL REFERENCES feature_flags (id) ON DELETE CASCADE, + PRIMARY KEY (user_id, feature_id) +); + +CREATE UNIQUE INDEX "index_user_features_user_id_and_feature_id" ON "user_features" ("user_id", "feature_id"); +CREATE INDEX "index_user_features_on_user_id" ON "user_features" ("user_id"); +CREATE INDEX "index_user_features_on_feature_id" ON "user_features" ("feature_id"); diff --git a/crates/collab/migrations/20230825190322_add_server_feature_flags.sql b/crates/collab/migrations/20230825190322_add_server_feature_flags.sql new file mode 100644 index 0000000000..fffde54a20 --- /dev/null +++ b/crates/collab/migrations/20230825190322_add_server_feature_flags.sql @@ -0,0 +1,16 @@ +CREATE TABLE "feature_flags" ( + "id" SERIAL PRIMARY KEY, + "flag" VARCHAR(255) NOT NULL UNIQUE +); + +CREATE UNIQUE INDEX "index_feature_flags" ON "feature_flags" ("id"); + +CREATE TABLE "user_features" ( + "user_id" INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + "feature_id" INTEGER NOT NULL REFERENCES feature_flags(id) ON DELETE CASCADE, + PRIMARY KEY (user_id, feature_id) +); + +CREATE UNIQUE INDEX "index_user_features_user_id_and_feature_id" ON "user_features" ("user_id", "feature_id"); +CREATE INDEX "index_user_features_on_user_id" ON "user_features" ("user_id"); +CREATE INDEX "index_user_features_on_feature_id" ON "user_features" ("feature_id"); diff --git a/crates/collab/src/db/ids.rs b/crates/collab/src/db/ids.rs index 8501083f83..b33ea57183 100644 --- a/crates/collab/src/db/ids.rs +++ b/crates/collab/src/db/ids.rs @@ -125,3 +125,4 @@ id_type!(ServerId); id_type!(SignupId); id_type!(UserId); id_type!(ChannelBufferCollaboratorId); +id_type!(FlagId); diff --git a/crates/collab/src/db/queries/users.rs b/crates/collab/src/db/queries/users.rs index bac0f14f83..bd7c3e9ffd 100644 --- a/crates/collab/src/db/queries/users.rs +++ b/crates/collab/src/db/queries/users.rs @@ -240,4 +240,58 @@ impl Database { result.push('%'); result } + + #[cfg(debug_assertions)] + pub async fn create_user_flag(&self, flag: &str) -> Result { + self.transaction(|tx| async move { + let flag = feature_flag::Entity::insert(feature_flag::ActiveModel { + flag: ActiveValue::set(flag.to_string()), + ..Default::default() + }) + .exec(&*tx) + .await? + .last_insert_id; + + Ok(flag) + }) + .await + } + + #[cfg(debug_assertions)] + pub async fn add_user_flag(&self, user: UserId, flag: FlagId) -> Result<()> { + self.transaction(|tx| async move { + user_feature::Entity::insert(user_feature::ActiveModel { + user_id: ActiveValue::set(user), + feature_id: ActiveValue::set(flag), + }) + .exec(&*tx) + .await?; + + Ok(()) + }) + .await + } + + pub async fn get_user_flags(&self, user: UserId) -> Result> { + self.transaction(|tx| async move { + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] + enum QueryAs { + Flag, + } + + let flags = user::Model { + id: user, + ..Default::default() + } + .find_linked(user::UserFlags) + .select_only() + .column(feature_flag::Column::Flag) + .into_values::<_, QueryAs>() + .all(&*tx) + .await?; + + Ok(flags) + }) + .await + } } diff --git a/crates/collab/src/db/tables.rs b/crates/collab/src/db/tables.rs index fe747e0d27..1765cee065 100644 --- a/crates/collab/src/db/tables.rs +++ b/crates/collab/src/db/tables.rs @@ -7,6 +7,7 @@ pub mod channel_buffer_collaborator; pub mod channel_member; pub mod channel_path; pub mod contact; +pub mod feature_flag; pub mod follower; pub mod language_server; pub mod project; @@ -16,6 +17,7 @@ pub mod room_participant; pub mod server; pub mod signup; pub mod user; +pub mod user_feature; pub mod worktree; pub mod worktree_diagnostic_summary; pub mod worktree_entry; diff --git a/crates/collab/src/db/tables/feature_flag.rs b/crates/collab/src/db/tables/feature_flag.rs new file mode 100644 index 0000000000..41c1451c64 --- /dev/null +++ b/crates/collab/src/db/tables/feature_flag.rs @@ -0,0 +1,40 @@ +use sea_orm::entity::prelude::*; + +use crate::db::FlagId; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "feature_flags")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: FlagId, + pub flag: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::user_feature::Entity")] + UserFeature, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::UserFeature.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} + +pub struct FlaggedUsers; + +impl Linked for FlaggedUsers { + type FromEntity = Entity; + + type ToEntity = super::user::Entity; + + fn link(&self) -> Vec { + vec![ + super::user_feature::Relation::Flag.def().rev(), + super::user_feature::Relation::User.def(), + ] + } +} diff --git a/crates/collab/src/db/tables/user.rs b/crates/collab/src/db/tables/user.rs index 402b06c2a7..739693527f 100644 --- a/crates/collab/src/db/tables/user.rs +++ b/crates/collab/src/db/tables/user.rs @@ -28,6 +28,8 @@ pub enum Relation { HostedProjects, #[sea_orm(has_many = "super::channel_member::Entity")] ChannelMemberships, + #[sea_orm(has_many = "super::user_feature::Entity")] + UserFeatures, } impl Related for Entity { @@ -54,4 +56,25 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::UserFeatures.def() + } +} + impl ActiveModelBehavior for ActiveModel {} + +pub struct UserFlags; + +impl Linked for UserFlags { + type FromEntity = Entity; + + type ToEntity = super::feature_flag::Entity; + + fn link(&self) -> Vec { + vec![ + super::user_feature::Relation::User.def().rev(), + super::user_feature::Relation::Flag.def(), + ] + } +} diff --git a/crates/collab/src/db/tables/user_feature.rs b/crates/collab/src/db/tables/user_feature.rs new file mode 100644 index 0000000000..cc24b5e796 --- /dev/null +++ b/crates/collab/src/db/tables/user_feature.rs @@ -0,0 +1,42 @@ +use sea_orm::entity::prelude::*; + +use crate::db::{FlagId, UserId}; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "user_features")] +pub struct Model { + #[sea_orm(primary_key)] + pub user_id: UserId, + #[sea_orm(primary_key)] + pub feature_id: FlagId, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::feature_flag::Entity", + from = "Column::FeatureId", + to = "super::feature_flag::Column::Id" + )] + Flag, + #[sea_orm( + belongs_to = "super::user::Entity", + from = "Column::UserId", + to = "super::user::Column::Id" + )] + User, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Flag.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::User.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/tests.rs b/crates/collab/src/db/tests.rs index 36a0888a62..ee961006cb 100644 --- a/crates/collab/src/db/tests.rs +++ b/crates/collab/src/db/tests.rs @@ -1,5 +1,6 @@ mod buffer_tests; mod db_tests; +mod feature_flag_tests; use super::*; use gpui::executor::Background; diff --git a/crates/collab/src/db/tests/feature_flag_tests.rs b/crates/collab/src/db/tests/feature_flag_tests.rs new file mode 100644 index 0000000000..9d5f039747 --- /dev/null +++ b/crates/collab/src/db/tests/feature_flag_tests.rs @@ -0,0 +1,60 @@ +use crate::{ + db::{Database, NewUserParams}, + test_both_dbs, +}; +use std::sync::Arc; + +test_both_dbs!( + test_get_user_flags, + test_get_user_flags_postgres, + test_get_user_flags_sqlite +); + +async fn test_get_user_flags(db: &Arc) { + let user_1 = db + .create_user( + &format!("user1@example.com"), + false, + NewUserParams { + github_login: format!("user1"), + github_user_id: 1, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + + let user_2 = db + .create_user( + &format!("user2@example.com"), + false, + NewUserParams { + github_login: format!("user2"), + github_user_id: 2, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + + const CHANNELS_ALPHA: &'static str = "channels-alpha"; + const NEW_SEARCH: &'static str = "new-search"; + + let channels_flag = db.create_user_flag(CHANNELS_ALPHA).await.unwrap(); + let search_flag = db.create_user_flag(NEW_SEARCH).await.unwrap(); + + db.add_user_flag(user_1, channels_flag).await.unwrap(); + db.add_user_flag(user_1, search_flag).await.unwrap(); + + db.add_user_flag(user_2, channels_flag).await.unwrap(); + + let mut user_1_flags = db.get_user_flags(user_1).await.unwrap(); + user_1_flags.sort(); + assert_eq!(user_1_flags, &[CHANNELS_ALPHA, NEW_SEARCH]); + + let mut user_2_flags = db.get_user_flags(user_2).await.unwrap(); + user_2_flags.sort(); + assert_eq!(user_2_flags, &[CHANNELS_ALPHA]); +} diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 18587c2ba8..6b44711c42 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2609,20 +2609,19 @@ async fn get_private_user_info( response: Response, session: Session, ) -> Result<()> { - let metrics_id = session - .db() - .await - .get_user_metrics_id(session.user_id) - .await?; - let user = session - .db() - .await + let db = session.db().await; + + let metrics_id = db.get_user_metrics_id(session.user_id).await?; + let user = db .get_user_by_id(session.user_id) .await? .ok_or_else(|| anyhow!("user not found"))?; + let flags = db.get_user_flags(session.user_id).await?; + response.send(proto::GetPrivateUserInfoResponse { metrics_id, staff: user.admin, + flags, })?; Ok(()) } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index ce47830af2..e0356ebb9a 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -1111,6 +1111,7 @@ message GetPrivateUserInfo {} message GetPrivateUserInfoResponse { string metrics_id = 1; bool staff = 2; + repeated string flags = 3; } // Entities diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index bc9dd6f80b..d64cbae929 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -6,4 +6,4 @@ pub use conn::Connection; pub use peer::*; mod macros; -pub const PROTOCOL_VERSION: u32 = 61; +pub const PROTOCOL_VERSION: u32 = 62;