Compare commits
1 commit
main
...
channels-a
Author | SHA1 | Date | |
---|---|---|---|
![]() |
b93df56170 |
5 changed files with 112 additions and 10 deletions
|
@ -1,6 +1,6 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
auth,
|
auth,
|
||||||
db::{Invite, NewSignup, NewUserParams, User, UserId, WaitlistSummary},
|
db::{FeatureFlag, FlagId, Invite, NewSignup, NewUserParams, User, UserId, WaitlistSummary},
|
||||||
rpc::{self, ResultExt},
|
rpc::{self, ResultExt},
|
||||||
AppState, Error, Result,
|
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", get(get_users).post(create_user))
|
||||||
.route("/users/:id", put(update_user).delete(destroy_user))
|
.route("/users/:id", put(update_user).delete(destroy_user))
|
||||||
.route("/users/:id/access_tokens", post(create_access_token))
|
.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("/users_with_no_invites", get(get_users_with_no_invites))
|
||||||
.route("/invite_codes/:code", get(get_user_for_invite_code))
|
.route("/invite_codes/:code", get(get_user_for_invite_code))
|
||||||
.route("/panic", post(trace_panic))
|
.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("/user_invites", post(create_invite_from_code))
|
||||||
.route("/unsent_invites", get(get_unsent_invites))
|
.route("/unsent_invites", get(get_unsent_invites))
|
||||||
.route("/sent_invites", post(record_sent_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(
|
.layer(
|
||||||
ServiceBuilder::new()
|
ServiceBuilder::new()
|
||||||
.layer(Extension(state))
|
.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(
|
async fn get_user_for_invite_code(
|
||||||
Path(code): Path<String>,
|
Path(code): Path<String>,
|
||||||
Extension(app): Extension<Arc<AppState>>,
|
Extension(app): Extension<Arc<AppState>>,
|
||||||
|
|
|
@ -41,6 +41,7 @@ use tokio::sync::{Mutex, OwnedMutexGuard};
|
||||||
|
|
||||||
pub use ids::*;
|
pub use ids::*;
|
||||||
pub use sea_orm::ConnectOptions;
|
pub use sea_orm::ConnectOptions;
|
||||||
|
pub use tables::feature_flag::Model as FeatureFlag;
|
||||||
pub use tables::user::Model as User;
|
pub use tables::user::Model as User;
|
||||||
|
|
||||||
pub struct Database {
|
pub struct Database {
|
||||||
|
|
|
@ -241,7 +241,7 @@ impl Database {
|
||||||
result
|
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 {
|
self.transaction(|tx| async move {
|
||||||
let flag = feature_flag::Entity::insert(feature_flag::ActiveModel {
|
let flag = feature_flag::Entity::insert(feature_flag::ActiveModel {
|
||||||
flag: ActiveValue::set(flag.to_string()),
|
flag: ActiveValue::set(flag.to_string()),
|
||||||
|
@ -256,6 +256,11 @@ impl Database {
|
||||||
.await
|
.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<()> {
|
pub async fn add_user_flag(&self, user: UserId, flag: FlagId) -> Result<()> {
|
||||||
self.transaction(|tx| async move {
|
self.transaction(|tx| async move {
|
||||||
user_feature::Entity::insert(user_feature::ActiveModel {
|
user_feature::Entity::insert(user_feature::ActiveModel {
|
||||||
|
@ -270,6 +275,21 @@ impl Database {
|
||||||
.await
|
.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>> {
|
pub async fn get_user_flags(&self, user: UserId) -> Result<Vec<String>> {
|
||||||
self.transaction(|tx| async move {
|
self.transaction(|tx| async move {
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
|
use serde_derive::Serialize;
|
||||||
|
|
||||||
use crate::db::FlagId;
|
use crate::db::FlagId;
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel, Serialize)]
|
||||||
#[sea_orm(table_name = "feature_flags")]
|
#[sea_orm(table_name = "feature_flags")]
|
||||||
pub struct Model {
|
pub struct Model {
|
||||||
#[sea_orm(primary_key)]
|
#[sea_orm(primary_key)]
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
db::{Database, NewUserParams},
|
db::{Database, FeatureFlag, NewUserParams},
|
||||||
test_both_dbs,
|
test_both_dbs,
|
||||||
};
|
};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
test_both_dbs!(
|
test_both_dbs!(
|
||||||
test_get_user_flags,
|
test_feature_flags,
|
||||||
test_get_user_flags_postgres,
|
test_feature_flags_postgres,
|
||||||
test_get_user_flags_sqlite
|
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
|
let user_1 = db
|
||||||
.create_user(
|
.create_user(
|
||||||
&format!("user1@example.com"),
|
&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 CHANNELS_ALPHA: &'static str = "channels-alpha";
|
||||||
const NEW_SEARCH: &'static str = "new-search";
|
const NEW_SEARCH: &'static str = "new-search";
|
||||||
|
|
||||||
let channels_flag = db.create_user_flag(CHANNELS_ALPHA).await.unwrap();
|
let channels_flag = db.create_feature_flag(CHANNELS_ALPHA).await.unwrap();
|
||||||
let search_flag = db.create_user_flag(NEW_SEARCH).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, channels_flag).await.unwrap();
|
||||||
db.add_user_flag(user_1, search_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();
|
let mut user_2_flags = db.get_user_flags(user_2).await.unwrap();
|
||||||
user_2_flags.sort();
|
user_2_flags.sort();
|
||||||
assert_eq!(user_2_flags, &[CHANNELS_ALPHA]);
|
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