diff --git a/Cargo.lock b/Cargo.lock index 3f041b7acd..fdb5a30352 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2736,6 +2736,16 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "lipsum" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ee7271c76a89032dcc7e595c0a739a9c5514eab483deb0e82981fe2098c56a" +dependencies = [ + "rand 0.8.3", + "rand_chacha 0.3.0", +] + [[package]] name = "lock_api" version = "0.4.2" @@ -5872,6 +5882,7 @@ dependencies = [ "http-auth-basic", "jwt-simple", "lazy_static", + "lipsum", "oauth2", "oauth2-surf", "parking_lot", diff --git a/script/seed-db b/script/seed-db index 0bdfc0a5e3..8ccac320a5 100755 --- a/script/seed-db +++ b/script/seed-db @@ -2,4 +2,4 @@ set -e cd server -cargo run --bin seed +cargo run --features seed-dependencies --bin seed diff --git a/server/Cargo.toml b/server/Cargo.toml index aad43e5b6e..fec5bec74d 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -5,6 +5,10 @@ edition = "2018" name = "zed-server" version = "0.1.0" +[[bin]] +name = "seed" +required-features = ["seed-support"] + [dependencies] anyhow = "1.0.40" async-std = { version = "1.8.0", features = ["attributes"] } @@ -19,6 +23,7 @@ futures = "0.3" handlebars = "3.5" http-auth-basic = "0.1.3" jwt-simple = "0.10.0" +lipsum = { version = "0.8", optional = true } oauth2 = { version = "4.0.0", default_features = false } oauth2-surf = "0.1.1" parking_lot = "0.11.1" @@ -46,6 +51,9 @@ features = ["runtime-async-std-rustls", "postgres", "time"] [dev-dependencies] gpui = { path = "../gpui" } -zed = { path = "../zed", features = ["test-support"] } lazy_static = "1.4" serde_json = { version = "1.0.64", features = ["preserve_order"] } +zed = { path = "../zed", features = ["test-support"] } + +[features] +seed-support = ["lipsum"] diff --git a/server/src/bin/seed.rs b/server/src/bin/seed.rs index 61af3bcd88..b259dc4c14 100644 --- a/server/src/bin/seed.rs +++ b/server/src/bin/seed.rs @@ -1,6 +1,11 @@ -use sqlx::postgres::PgPoolOptions; +use db::{Db, UserId}; +use rand::prelude::*; use tide::log; +use time::{Duration, OffsetDateTime}; +#[allow(unused)] +#[path = "../db.rs"] +mod db; #[path = "../env.rs"] mod env; @@ -13,95 +18,74 @@ async fn main() { ); } + let mut rng = StdRng::from_entropy(); let database_url = std::env::var("DATABASE_URL").expect("missing DATABASE_URL env var"); - let db = PgPoolOptions::new() - .max_connections(5) - .connect(&database_url) + let db = Db::new(&database_url, 5) .await .expect("failed to connect to postgres database"); let zed_users = ["nathansobo", "maxbrunsfeld", "as-cii", "iamnbutler"]; - let mut zed_user_ids = Vec::::new(); + let mut zed_user_ids = Vec::::new(); for zed_user in zed_users { - zed_user_ids.push( - sqlx::query_scalar( - r#" - INSERT INTO users - (github_login, admin) - VALUES - ($1, true) - ON CONFLICT (github_login) DO UPDATE SET - github_login=EXCLUDED.github_login - RETURNING id - "#, - ) - .bind(zed_user) - .fetch_one(&db) - .await - .expect("failed to insert user"), - ) + if let Some(user_id) = db.get_user(zed_user).await.expect("failed to fetch user") { + zed_user_ids.push(user_id); + } else { + zed_user_ids.push( + db.create_user(zed_user, true) + .await + .expect("failed to insert user"), + ); + } } - let zed_org_id: i32 = sqlx::query_scalar( - r#" - INSERT INTO orgs - (name, slug) - VALUES - ('Zed', 'zed') - ON CONFLICT (slug) DO UPDATE SET - slug=EXCLUDED.slug - RETURNING id - "#, - ) - .fetch_one(&db) - .await - .expect("failed to insert org"); + let zed_org_id = if let Some(org) = db + .find_org_by_slug("zed") + .await + .expect("failed to fetch org") + { + org.id + } else { + db.create_org("Zed", "zed") + .await + .expect("failed to insert org") + }; - let general_channel_id: i32 = sqlx::query_scalar( - r#" - INSERT INTO channels - (owner_is_user, owner_id, name) - VALUES - (false, $1, 'General') - ON CONFLICT (owner_is_user, owner_id, name) DO UPDATE SET - name=EXCLUDED.name - RETURNING id - "#, - ) - .bind(zed_org_id) - .fetch_one(&db) - .await - .expect("failed to insert channel"); + let general_channel_id = if let Some(channel) = db + .get_org_channels(zed_org_id) + .await + .expect("failed to fetch channels") + .iter() + .find(|c| c.name == "General") + { + channel.id + } else { + let channel_id = db + .create_org_channel(zed_org_id, "General") + .await + .expect("failed to insert channel"); + + let now = OffsetDateTime::now_utc(); + let max_seconds = Duration::days(100).as_seconds_f64(); + let mut timestamps = (0..1000) + .map(|_| now - Duration::seconds_f64(rng.gen_range(0_f64..=max_seconds))) + .collect::>(); + timestamps.sort(); + for timestamp in timestamps { + let sender_id = *zed_user_ids.choose(&mut rng).unwrap(); + let body = lipsum::lipsum_words(rng.gen_range(1..=50)); + db.create_channel_message(channel_id, sender_id, &body, timestamp) + .await + .expect("failed to insert message"); + } + channel_id + }; for user_id in zed_user_ids { - sqlx::query( - r#" - INSERT INTO org_memberships - (org_id, user_id, admin) - VALUES - ($1, $2, true) - ON CONFLICT DO NOTHING - "#, - ) - .bind(zed_org_id) - .bind(user_id) - .execute(&db) - .await - .expect("failed to insert org membership"); - - sqlx::query( - r#" - INSERT INTO channel_memberships - (channel_id, user_id, admin) - VALUES - ($1, $2, true) - ON CONFLICT DO NOTHING - "#, - ) - .bind(general_channel_id) - .bind(user_id) - .execute(&db) - .await - .expect("failed to insert channel membership"); + db.add_org_member(zed_org_id, user_id, true) + .await + .expect("failed to insert org membership"); + db.add_channel_member(general_channel_id, user_id, true) + .await + .expect("failed to insert channel membership"); } } diff --git a/server/src/db.rs b/server/src/db.rs index 374761a73c..62797f6b81 100644 --- a/server/src/db.rs +++ b/server/src/db.rs @@ -27,35 +27,6 @@ pub struct Db { test_mode: bool, } -#[derive(Debug, FromRow, Serialize)] -pub struct User { - pub id: UserId, - pub github_login: String, - pub admin: bool, -} - -#[derive(Debug, FromRow, Serialize)] -pub struct Signup { - pub id: SignupId, - pub github_login: String, - pub email_address: String, - pub about: String, -} - -#[derive(Debug, FromRow, Serialize)] -pub struct Channel { - pub id: ChannelId, - pub name: String, -} - -#[derive(Debug, FromRow)] -pub struct ChannelMessage { - pub id: MessageId, - pub sender_id: UserId, - pub body: String, - pub sent_at: OffsetDateTime, -} - impl Db { pub async fn new(url: &str, max_connections: u32) -> tide::Result { let pool = DbOptions::new() @@ -113,6 +84,22 @@ impl Db { // users + #[allow(unused)] // Help rust-analyzer + #[cfg(any(test, feature = "seed-support"))] + pub async fn get_user(&self, github_login: &str) -> Result> { + test_support!(self, { + let query = " + SELECT id + FROM users + WHERE github_login = $1 + "; + sqlx::query_scalar(query) + .bind(github_login) + .fetch_optional(&self.pool) + .await + }) + } + pub async fn create_user(&self, github_login: &str, admin: bool) -> Result { test_support!(self, { let query = " @@ -231,7 +218,23 @@ impl Db { // orgs - #[cfg(test)] + #[allow(unused)] // Help rust-analyzer + #[cfg(any(test, feature = "seed-support"))] + pub async fn find_org_by_slug(&self, slug: &str) -> Result> { + test_support!(self, { + let query = " + SELECT * + FROM orgs + WHERE slug = $1 + "; + sqlx::query_as(query) + .bind(slug) + .fetch_optional(&self.pool) + .await + }) + } + + #[cfg(any(test, feature = "seed-support"))] pub async fn create_org(&self, name: &str, slug: &str) -> Result { test_support!(self, { let query = " @@ -248,7 +251,7 @@ impl Db { }) } - #[cfg(test)] + #[cfg(any(test, feature = "seed-support"))] pub async fn add_org_member( &self, org_id: OrgId, @@ -259,6 +262,7 @@ impl Db { let query = " INSERT INTO org_memberships (org_id, user_id, admin) VALUES ($1, $2, $3) + ON CONFLICT DO NOTHING "; sqlx::query(query) .bind(org_id.0) @@ -272,7 +276,7 @@ impl Db { // channels - #[cfg(test)] + #[cfg(any(test, feature = "seed-support"))] pub async fn create_org_channel(&self, org_id: OrgId, name: &str) -> Result { test_support!(self, { let query = " @@ -289,7 +293,25 @@ impl Db { }) } - pub async fn get_channels_for_user(&self, user_id: UserId) -> Result> { + #[allow(unused)] // Help rust-analyzer + #[cfg(any(test, feature = "seed-support"))] + pub async fn get_org_channels(&self, org_id: OrgId) -> Result> { + test_support!(self, { + let query = " + SELECT * + FROM channels + WHERE + channels.owner_is_user = false AND + channels.owner_id = $1 + "; + sqlx::query_as(query) + .bind(org_id.0) + .fetch_all(&self.pool) + .await + }) + } + + pub async fn get_accessible_channels(&self, user_id: UserId) -> Result> { test_support!(self, { let query = " SELECT @@ -328,7 +350,7 @@ impl Db { }) } - #[cfg(test)] + #[cfg(any(test, feature = "seed-support"))] pub async fn add_channel_member( &self, channel_id: ChannelId, @@ -339,6 +361,7 @@ impl Db { let query = " INSERT INTO channel_memberships (channel_id, user_id, admin) VALUES ($1, $2, $3) + ON CONFLICT DO NOTHING "; sqlx::query(query) .bind(channel_id.0) @@ -432,10 +455,45 @@ macro_rules! id_type { } id_type!(UserId); +#[derive(Debug, FromRow, Serialize)] +pub struct User { + pub id: UserId, + pub github_login: String, + pub admin: bool, +} + id_type!(OrgId); -id_type!(ChannelId); +#[derive(FromRow)] +pub struct Org { + pub id: OrgId, + pub name: String, + pub slug: String, +} + id_type!(SignupId); +#[derive(Debug, FromRow, Serialize)] +pub struct Signup { + pub id: SignupId, + pub github_login: String, + pub email_address: String, + pub about: String, +} + +id_type!(ChannelId); +#[derive(Debug, FromRow, Serialize)] +pub struct Channel { + pub id: ChannelId, + pub name: String, +} + id_type!(MessageId); +#[derive(Debug, FromRow)] +pub struct ChannelMessage { + pub id: MessageId, + pub sender_id: UserId, + pub body: String, + pub sent_at: OffsetDateTime, +} #[cfg(test)] pub mod tests { diff --git a/server/src/rpc.rs b/server/src/rpc.rs index 8a7f2d43af..2328f58871 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -77,12 +77,7 @@ struct Channel { connection_ids: HashSet, } -#[cfg(debug_assertions)] -const MESSAGE_COUNT_PER_PAGE: usize = 10; - -#[cfg(not(debug_assertions))] -const MESSAGE_COUNT_PER_PAGE: usize = 50; - +const MESSAGE_COUNT_PER_PAGE: usize = 100; const MAX_MESSAGE_LEN: usize = 1024; impl Server { @@ -528,7 +523,7 @@ impl Server { .read() .await .user_id_for_connection(request.sender_id)?; - let channels = self.app_state.db.get_channels_for_user(user_id).await?; + let channels = self.app_state.db.get_accessible_channels(user_id).await?; self.peer .respond( request.receipt(), diff --git a/zed/src/chat_panel.rs b/zed/src/chat_panel.rs index 1b757d3ea4..6edac94b2e 100644 --- a/zed/src/chat_panel.rs +++ b/zed/src/chat_panel.rs @@ -16,6 +16,8 @@ use gpui::{ use postage::watch; use time::{OffsetDateTime, UtcOffset}; +const MESSAGE_LOADING_THRESHOLD: usize = 50; + pub struct ChatPanel { channel_list: ModelHandle, active_channel: Option<(ModelHandle, Subscription)>, @@ -80,7 +82,7 @@ impl ChatPanel { } }); message_list.set_scroll_handler(|visible_range, cx| { - if visible_range.start < 5 { + if visible_range.start < MESSAGE_LOADING_THRESHOLD { cx.dispatch_action(LoadMoreMessages); } });