summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorOwen Jacobson <owen@grimoire.ca>2024-10-05 20:32:02 -0400
committerOwen Jacobson <owen@grimoire.ca>2024-10-05 22:47:12 -0400
commit1fb26ad31d385ddc628e1b73d6a8764981ca6885 (patch)
treeda226cfc7e054ce93bf37da943a395dee226baa6 /src
parent8edd5625ad5dde0ef1637d5c89e9901b3ee65d73 (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.rs89
-rw-r--r--src/channel/routes/test/list.rs65
-rw-r--r--src/channel/routes/test/mod.rs1
-rw-r--r--src/login/routes.rs65
-rw-r--r--src/message/app.rs27
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?;