Add admin APIs for interacting and viewing feature flags
This commit is contained in:
parent
7cc05c99c2
commit
b93df56170
5 changed files with 112 additions and 10 deletions
|
@ -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(¶ms.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>>,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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])
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue