Add admin APIs for interacting and viewing feature flags

This commit is contained in:
Mikayla 2023-09-10 23:26:33 -07:00
parent 7cc05c99c2
commit b93df56170
No known key found for this signature in database
5 changed files with 112 additions and 10 deletions

View file

@ -1,6 +1,6 @@
use crate::{
auth,
db::{Invite, NewSignup, NewUserParams, User, UserId, WaitlistSummary},
db::{FeatureFlag, FlagId, Invite, NewSignup, NewUserParams, User, UserId, WaitlistSummary},
rpc::{self, ResultExt},
AppState, Error, Result,
};
@ -26,6 +26,7 @@ pub fn routes(rpc_server: Arc<rpc::Server>, state: Arc<AppState>) -> Router<Body
.route("/users", get(get_users).post(create_user))
.route("/users/:id", put(update_user).delete(destroy_user))
.route("/users/:id/access_tokens", post(create_access_token))
.route("/users/:id/feature_flags", post(add_user_flag))
.route("/users_with_no_invites", get(get_users_with_no_invites))
.route("/invite_codes/:code", get(get_user_for_invite_code))
.route("/panic", post(trace_panic))
@ -35,6 +36,11 @@ pub fn routes(rpc_server: Arc<rpc::Server>, state: Arc<AppState>) -> Router<Body
.route("/user_invites", post(create_invite_from_code))
.route("/unsent_invites", get(get_unsent_invites))
.route("/sent_invites", post(record_sent_invites))
.route(
"/feature_flags",
get(feature_flags).post(create_feature_flag),
)
.route("/feature_flags/:id", get(users_for_feature_flag))
.layer(
ServiceBuilder::new()
.layer(Extension(state))
@ -328,6 +334,55 @@ async fn create_access_token(
}))
}
#[derive(Serialize, Deserialize)]
struct FlagIdField {
flag_id: FlagId,
}
async fn add_user_flag(
Path(user_id): Path<UserId>,
Json(params): Json<FlagIdField>,
Extension(app): Extension<Arc<AppState>>,
) -> Result<()> {
let user = app
.db
.get_user_by_id(user_id)
.await?
.ok_or_else(|| anyhow!("user not found"))?;
let user_id = user.id;
app.db.add_user_flag(user_id, params.flag_id).await?;
Ok(())
}
async fn feature_flags(Extension(app): Extension<Arc<AppState>>) -> Result<Json<Vec<FeatureFlag>>> {
Ok(Json(app.db.get_feature_flags().await?))
}
#[derive(Deserialize)]
struct CreateFeatureFlagParam {
flag: String,
}
async fn create_feature_flag(
Json(params): Json<CreateFeatureFlagParam>,
Extension(app): Extension<Arc<AppState>>,
) -> Result<Json<FlagIdField>> {
let id = app.db.create_feature_flag(&params.flag).await?;
Ok(Json(FlagIdField { flag_id: id }))
}
async fn users_for_feature_flag(
Query(params): Query<FlagIdField>,
Extension(app): Extension<Arc<AppState>>,
) -> Result<Json<Vec<User>>> {
let users = app.db.get_flag_users(params.flag_id).await?;
Ok(Json(users))
}
async fn get_user_for_invite_code(
Path(code): Path<String>,
Extension(app): Extension<Arc<AppState>>,

View file

@ -41,6 +41,7 @@ use tokio::sync::{Mutex, OwnedMutexGuard};
pub use ids::*;
pub use sea_orm::ConnectOptions;
pub use tables::feature_flag::Model as FeatureFlag;
pub use tables::user::Model as User;
pub struct Database {

View file

@ -241,7 +241,7 @@ impl Database {
result
}
pub async fn create_user_flag(&self, flag: &str) -> Result<FlagId> {
pub async fn create_feature_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()),
@ -256,6 +256,11 @@ impl Database {
.await
}
pub async fn get_feature_flags(&self) -> Result<Vec<FeatureFlag>> {
self.transaction(|tx| async move { Ok(feature_flag::Entity::find().all(&*tx).await?) })
.await
}
pub async fn add_user_flag(&self, user: UserId, flag: FlagId) -> Result<()> {
self.transaction(|tx| async move {
user_feature::Entity::insert(user_feature::ActiveModel {
@ -270,6 +275,21 @@ impl Database {
.await
}
pub async fn get_flag_users(&self, id: FlagId) -> Result<Vec<User>> {
self.transaction(|tx| async move {
let users = FeatureFlag {
id,
..Default::default()
}
.find_linked(feature_flag::FlaggedUsers)
.all(&*tx)
.await?;
Ok(users)
})
.await
}
pub async fn get_user_flags(&self, user: UserId) -> Result<Vec<String>> {
self.transaction(|tx| async move {
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]

View file

@ -1,8 +1,9 @@
use sea_orm::entity::prelude::*;
use serde_derive::Serialize;
use crate::db::FlagId;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel, Serialize)]
#[sea_orm(table_name = "feature_flags")]
pub struct Model {
#[sea_orm(primary_key)]

View file

@ -1,16 +1,16 @@
use crate::{
db::{Database, NewUserParams},
db::{Database, FeatureFlag, 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
test_feature_flags,
test_feature_flags_postgres,
test_feature_flags_sqlite
);
async fn test_get_user_flags(db: &Arc<Database>) {
async fn test_feature_flags(db: &Arc<Database>) {
let user_1 = db
.create_user(
&format!("user1@example.com"),
@ -42,8 +42,8 @@ async fn test_get_user_flags(db: &Arc<Database>) {
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();
let channels_flag = db.create_feature_flag(CHANNELS_ALPHA).await.unwrap();
let search_flag = db.create_feature_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();
@ -57,4 +57,29 @@ async fn test_get_user_flags(db: &Arc<Database>) {
let mut user_2_flags = db.get_user_flags(user_2).await.unwrap();
user_2_flags.sort();
assert_eq!(user_2_flags, &[CHANNELS_ALPHA]);
let flags = db.get_feature_flags().await.unwrap();
assert_eq!(
flags,
vec![
FeatureFlag {
id: channels_flag,
flag: CHANNELS_ALPHA.to_string(),
},
FeatureFlag {
id: search_flag,
flag: NEW_SEARCH.to_string(),
},
]
);
let users_for_channels_alpha = db
.get_flag_users(channels_flag)
.await
.unwrap()
.into_iter()
.map(|user| user.id)
.collect::<Vec<_>>();
assert_eq!(users_for_channels_alpha, vec![user_1, user_2])
}