Update database and RPC to provide configured feature flags

This commit is contained in:
Mikayla 2023-08-25 14:34:19 -07:00
parent 1c945a7521
commit 6fdf101745
No known key found for this signature in database
14 changed files with 268 additions and 9 deletions

View File

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

View File

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

View File

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

View File

@ -125,3 +125,4 @@ id_type!(ServerId);
id_type!(SignupId);
id_type!(UserId);
id_type!(ChannelBufferCollaboratorId);
id_type!(FlagId);

View File

@ -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
}
}

View File

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

View File

@ -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(),
]
}
}

View File

@ -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(),
]
}
}

View File

@ -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 {}

View File

@ -1,5 +1,6 @@
mod buffer_tests;
mod db_tests;
mod feature_flag_tests;
use super::*;
use gpui::executor::Background;

View File

@ -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]);
}

View File

@ -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(())
}

View File

@ -1111,6 +1111,7 @@ message GetPrivateUserInfo {}
message GetPrivateUserInfoResponse {
string metrics_id = 1;
bool staff = 2;
repeated string flags = 3;
}
// Entities

View File

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