diff options
| author | Owen Jacobson <owen@grimoire.ca> | 2024-10-05 20:32:02 -0400 |
|---|---|---|
| committer | Owen Jacobson <owen@grimoire.ca> | 2024-10-05 22:47:12 -0400 |
| commit | 1fb26ad31d385ddc628e1b73d6a8764981ca6885 (patch) | |
| tree | da226cfc7e054ce93bf37da943a395dee226baa6 /src | |
| parent | 8edd5625ad5dde0ef1637d5c89e9901b3ee65d73 (diff) | |
Use `/api/boot` to bootstrap the client.
The client now takes an initial snapshot from the response to `/api/boot`, then picks up the event stream at the immediately-successive event to the moment the snapshot was taken.
This commit removes the following unused endpoints:
* `/api/channels` (GET)
* `/api/channels/:channel/messages` (GET)
The information therein is now part of the boot response. We can always add 'em back, but I wanted to clear the deck for designing something more capable, for dealing with client needs.
Diffstat (limited to 'src')
| -rw-r--r-- | src/channel/routes.rs | 89 | ||||
| -rw-r--r-- | src/channel/routes/test/list.rs | 65 | ||||
| -rw-r--r-- | src/channel/routes/test/mod.rs | 1 | ||||
| -rw-r--r-- | src/login/routes.rs | 65 | ||||
| -rw-r--r-- | src/message/app.rs | 27 |
5 files changed, 94 insertions, 153 deletions
diff --git a/src/channel/routes.rs b/src/channel/routes.rs index 23c0602..5d67af8 100644 --- a/src/channel/routes.rs +++ b/src/channel/routes.rs @@ -2,56 +2,21 @@ use axum::{ extract::{Json, Path, State}, http::StatusCode, response::{IntoResponse, Response}, - routing::{delete, get, post}, + routing::{delete, post}, Router, }; -use axum_extra::extract::Query; use super::{app, Channel, Id}; -use crate::{ - app::App, - clock::RequestedAt, - error::Internal, - event::{Instant, Sequence}, - login::Login, - message::{self, app::SendError}, -}; +use crate::{app::App, clock::RequestedAt, error::Internal, login::Login, message::app::SendError}; #[cfg(test)] mod test; pub fn router() -> Router<App> { Router::new() - .route("/api/channels", get(list)) .route("/api/channels", post(on_create)) .route("/api/channels/:channel", post(on_send)) .route("/api/channels/:channel", delete(on_delete)) - .route("/api/channels/:channel/messages", get(messages)) -} - -#[derive(Default, serde::Deserialize)] -struct ResumeQuery { - resume_point: Option<Sequence>, -} - -async fn list( - State(app): State<App>, - _: Login, - Query(query): Query<ResumeQuery>, -) -> Result<Channels, Internal> { - let channels = app.channels().all(query.resume_point).await?; - let response = Channels(channels); - - Ok(response) -} - -struct Channels(Vec<Channel>); - -impl IntoResponse for Channels { - fn into_response(self) -> Response { - let Self(channels) = self; - Json(channels).into_response() - } } #[derive(Clone, serde::Deserialize)] @@ -150,53 +115,3 @@ impl IntoResponse for ErrorResponse { } } } - -async fn messages( - State(app): State<App>, - Path(channel): Path<Id>, - _: Login, - Query(query): Query<ResumeQuery>, -) -> Result<Messages, ErrorResponse> { - let messages = app - .channels() - .messages(&channel, query.resume_point) - .await?; - let response = Messages( - messages - .into_iter() - .map(|message| MessageView { - sent: message.sent, - sender: message.sender, - message: MessageInner { - id: message.id, - body: message.body, - }, - }) - .collect(), - ); - - Ok(response) -} - -struct Messages(Vec<MessageView>); - -#[derive(serde::Serialize)] -struct MessageView { - #[serde(flatten)] - sent: Instant, - sender: Login, - message: MessageInner, -} - -#[derive(serde::Serialize)] -struct MessageInner { - id: message::Id, - body: String, -} - -impl IntoResponse for Messages { - fn into_response(self) -> Response { - let Self(messages) = self; - Json(messages).into_response() - } -} diff --git a/src/channel/routes/test/list.rs b/src/channel/routes/test/list.rs deleted file mode 100644 index f15a53c..0000000 --- a/src/channel/routes/test/list.rs +++ /dev/null @@ -1,65 +0,0 @@ -use axum::extract::State; -use axum_extra::extract::Query; - -use crate::{channel::routes, test::fixtures}; - -#[tokio::test] -async fn empty_list() { - // Set up the environment - - let app = fixtures::scratch_app().await; - let viewer = fixtures::login::create(&app).await; - - // Call the endpoint - - let routes::Channels(channels) = routes::list(State(app), viewer, Query::default()) - .await - .expect("always succeeds"); - - // Verify the semantics - - assert!(channels.is_empty()); -} - -#[tokio::test] -async fn one_channel() { - // Set up the environment - - let app = fixtures::scratch_app().await; - let viewer = fixtures::login::create(&app).await; - let channel = fixtures::channel::create(&app, &fixtures::now()).await; - - // Call the endpoint - - let routes::Channels(channels) = routes::list(State(app), viewer, Query::default()) - .await - .expect("always succeeds"); - - // Verify the semantics - - assert!(channels.contains(&channel)); -} - -#[tokio::test] -async fn multiple_channels() { - // Set up the environment - - let app = fixtures::scratch_app().await; - let viewer = fixtures::login::create(&app).await; - let channels = vec![ - fixtures::channel::create(&app, &fixtures::now()).await, - fixtures::channel::create(&app, &fixtures::now()).await, - ]; - - // Call the endpoint - - let routes::Channels(response_channels) = routes::list(State(app), viewer, Query::default()) - .await - .expect("always succeeds"); - - // Verify the semantics - - assert!(channels - .into_iter() - .all(|channel| response_channels.contains(&channel))); -} diff --git a/src/channel/routes/test/mod.rs b/src/channel/routes/test/mod.rs index ab663eb..3e5aa17 100644 --- a/src/channel/routes/test/mod.rs +++ b/src/channel/routes/test/mod.rs @@ -1,3 +1,2 @@ -mod list; mod on_create; mod on_send; diff --git a/src/login/routes.rs b/src/login/routes.rs index 0874cc3..b0e3fee 100644 --- a/src/login/routes.rs +++ b/src/login/routes.rs @@ -5,12 +5,16 @@ use axum::{ routing::{get, post}, Router, }; +use futures::stream::{self, StreamExt as _, TryStreamExt as _}; use crate::{ app::App, + channel::Channel, clock::RequestedAt, error::{Internal, Unauthorized}, + event::Instant, login::{Login, Password}, + message::{self, Message}, token::{app, extract::IdentityToken}, }; @@ -26,9 +30,21 @@ pub fn router() -> Router<App> { async fn boot(State(app): State<App>, login: Login) -> Result<Boot, Internal> { let resume_point = app.logins().boot_point().await?; + let channels = app.channels().all(resume_point.into()).await?; + let channels = stream::iter(channels) + .then(|channel| async { + app.messages() + .in_channel(&channel.id, resume_point.into()) + .await + .map(|messages| BootChannel::new(channel, messages)) + }) + .try_collect() + .await?; + Ok(Boot { login, resume_point: resume_point.to_string(), + channels, }) } @@ -36,6 +52,55 @@ async fn boot(State(app): State<App>, login: Login) -> Result<Boot, Internal> { struct Boot { login: Login, resume_point: String, + channels: Vec<BootChannel>, +} + +#[derive(serde::Serialize)] +struct BootChannel { + #[serde(flatten)] + channel: Channel, + messages: Vec<BootMessage>, +} + +impl BootChannel { + fn new(channel: Channel, messages: impl IntoIterator<Item = Message>) -> Self { + Self { + channel, + messages: messages.into_iter().map(BootMessage::from).collect(), + } + } +} + +#[derive(serde::Serialize)] +struct BootMessage { + #[serde(flatten)] + sent: Instant, + sender: Login, + message: BootMessageBody, +} + +impl From<Message> for BootMessage { + fn from(message: Message) -> Self { + let Message { + sent, + channel: _, + sender, + id, + body, + } = message; + + Self { + sent, + sender, + message: BootMessageBody { id, body }, + } + } +} + +#[derive(serde::Serialize)] +struct BootMessageBody { + id: message::Id, + body: String, } impl IntoResponse for Boot { diff --git a/src/message/app.rs b/src/message/app.rs index 385c92e..1e50a65 100644 --- a/src/message/app.rs +++ b/src/message/app.rs @@ -44,6 +44,33 @@ impl<'a> Messages<'a> { Ok(message.as_sent()) } + pub async fn in_channel( + &self, + channel: &channel::Id, + resume_point: Option<Sequence>, + ) -> Result<Vec<Message>, DeleteError> { + let mut tx = self.db.begin().await?; + let channel = tx + .channels() + .by_id(channel) + .await + .not_found(|| DeleteError::ChannelNotFound(channel.clone()))?; + let messages = tx.messages().in_channel(&channel, resume_point).await?; + tx.commit().await?; + + let messages = messages + .into_iter() + .filter_map(|message| { + message + .events() + .filter(Sequence::up_to(resume_point)) + .collect() + }) + .collect(); + + Ok(messages) + } + pub async fn delete(&self, message: &Id, deleted_at: &DateTime) -> Result<(), DeleteError> { let mut tx = self.db.begin().await?; let deleted = tx.sequence().next(deleted_at).await?; |
