Update database and RPC to provide configured feature flags

Mikayla created

Change summary

crates/client/src/test.rs                                            |  1 
crates/collab/migrations.sqlite/20221109000000_test_schema.sql       | 19 
crates/collab/migrations/20230825190322_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 
crates/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(-)

Detailed changes

crates/client/src/test.rs 🔗

@@ -168,6 +168,7 @@ impl FakeServer {
                     GetPrivateUserInfoResponse {
                         metrics_id: "the-metrics-id".into(),
                         staff: false,
+                        flags: Default::default(),
                     },
                 )
                 .await;

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");

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");

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);

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<FlagId> {
+        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<Vec<String>> {
+        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
+    }
 }

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;

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<super::user_feature::Entity> 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<RelationDef> {
+        vec![
+            super::user_feature::Relation::Flag.def().rev(),
+            super::user_feature::Relation::User.def(),
+        ]
+    }
+}

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<super::access_token::Entity> for Entity {
@@ -54,4 +56,25 @@ impl Related<super::channel_member::Entity> for Entity {
     }
 }
 
+impl Related<super::user_feature::Entity> 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<RelationDef> {
+        vec![
+            super::user_feature::Relation::User.def().rev(),
+            super::user_feature::Relation::Flag.def(),
+        ]
+    }
+}

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<super::feature_flag::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::Flag.def()
+    }
+}
+
+impl Related<super::user::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::User.def()
+    }
+}
+
+impl ActiveModelBehavior for ActiveModel {}

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<Database>) {
+    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]);
+}

crates/collab/src/rpc.rs 🔗

@@ -2609,20 +2609,19 @@ async fn get_private_user_info(
     response: Response<proto::GetPrivateUserInfo>,
     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(())
 }

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

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;