examples: ergonomic improvements to `session_auth_axum` (#2057)

I've always hated the get_todos function and I
wanted to change it badly. Added a .env file
containing the db url for sqlx-cli, and cleaned
up with leptosfmt

Co-authored-by: j0lol <me@j0.lol>
This commit is contained in:
jo! 2023-12-10 19:28:42 +00:00 committed by GitHub
parent fcc9242a63
commit 50432e2651
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 170 additions and 159 deletions

View File

@ -0,0 +1 @@
DATABASE_URL=sqlite://Todos.db

View File

@ -39,22 +39,21 @@ pub fn ErrorTemplate(
}
view! {
<h1>"Errors"</h1>
<For
// a function that returns the items we're iterating over; a signal is fine
each= move || {errors.clone().into_iter().enumerate()}
// a unique key for each item as a reference
key=|(index, _error)| *index
// renders each item to a view
children=move |error| {
let error_string = error.1.to_string();
let error_code= error.1.status_code();
view! {
<h2>{error_code.to_string()}</h2>
<p>"Error: " {error_string}</p>
}
}
/>
<h1>"Errors"</h1>
<For
// a function that returns the items we're iterating over; a signal is fine
each=move || { errors.clone().into_iter().enumerate() }
// a unique key for each item as a reference
key=|(index, _error)| *index
// renders each item to a view
children=move |error| {
let error_string = error.1.to_string();
let error_code = error.1.status_code();
view! {
<h2>{error_code.to_string()}</h2>
<p>"Error: " {error_string}</p>
}
}
/>
}
}

View File

@ -15,70 +15,56 @@ pub struct Todo {
}
cfg_if! {
if #[cfg(feature = "ssr")] {
if #[cfg(feature = "ssr")] {
use sqlx::SqlitePool;
use sqlx::SqlitePool;
use futures::future::join_all;
pub fn pool() -> Result<SqlitePool, ServerFnError> {
use_context::<SqlitePool>()
.ok_or_else(|| ServerFnError::ServerError("Pool missing.".into()))
}
pub fn pool() -> Result<SqlitePool, ServerFnError> {
use_context::<SqlitePool>()
.ok_or_else(|| ServerFnError::ServerError("Pool missing.".into()))
}
pub fn auth() -> Result<AuthSession, ServerFnError> {
use_context::<AuthSession>()
.ok_or_else(|| ServerFnError::ServerError("Auth session missing.".into()))
}
pub fn auth() -> Result<AuthSession, ServerFnError> {
use_context::<AuthSession>()
.ok_or_else(|| ServerFnError::ServerError("Auth session missing.".into()))
}
#[derive(sqlx::FromRow, Clone)]
pub struct SqlTodo {
id: u32,
user_id: i64,
title: String,
created_at: String,
completed: bool,
}
#[derive(sqlx::FromRow, Clone)]
pub struct SqlTodo {
id: u32,
user_id: i64,
title: String,
created_at: String,
completed: bool,
}
impl SqlTodo {
pub async fn into_todo(self, pool: &SqlitePool) -> Todo {
Todo {
id: self.id,
user: User::get(self.user_id, pool).await,
title: self.title,
created_at: self.created_at,
completed: self.completed,
impl SqlTodo {
pub async fn into_todo(self, pool: &SqlitePool) -> Todo {
Todo {
id: self.id,
user: User::get(self.user_id, pool).await,
title: self.title,
created_at: self.created_at,
completed: self.completed,
}
}
}
}
}
}
#[server(GetTodos, "/api")]
pub async fn get_todos() -> Result<Vec<Todo>, ServerFnError> {
use futures::TryStreamExt;
let pool = pool()?;
let mut todos = Vec::new();
let mut rows =
sqlx::query_as::<_, SqlTodo>("SELECT * FROM todos").fetch(&pool);
while let Some(row) = rows.try_next().await? {
todos.push(row);
}
// why can't we just have async closures?
// let mut rows: Vec<Todo> = rows.iter().map(|t| async { t }).collect();
let mut converted_todos = Vec::with_capacity(todos.len());
for t in todos {
let todo = t.into_todo(&pool).await;
converted_todos.push(todo);
}
let todos: Vec<Todo> = converted_todos;
Ok(todos)
Ok(join_all(
sqlx::query_as::<_, SqlTodo>("SELECT * FROM todos")
.fetch_all(&pool)
.await?
.iter()
.map(|todo: &SqlTodo| todo.clone().into_todo(&pool)),
)
.await)
}
#[server(AddTodo, "/api")]
@ -94,17 +80,14 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
// fake API delay
std::thread::sleep(std::time::Duration::from_millis(1250));
match sqlx::query(
Ok(sqlx::query(
"INSERT INTO todos (title, user_id, completed) VALUES (?, ?, false)",
)
.bind(title)
.bind(id)
.execute(&pool)
.await
{
Ok(_row) => Ok(()),
Err(e) => Err(ServerFnError::ServerError(e.to_string())),
}
.map(|_| ())?)
}
// The struct name and path prefix arguments are optional.
@ -138,51 +121,71 @@ pub fn TodoApp() -> impl IntoView {
provide_meta_context();
view! {
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
<Stylesheet id="leptos" href="/pkg/session_auth_axum.css"/>
<Router>
<header>
<A href="/"><h1>"My Tasks"</h1></A>
<Transition
fallback=move || view! {<span>"Loading..."</span>}
>
{move || {
user.get().map(|user| match user {
Err(e) => view! {
<A href="/signup">"Signup"</A>", "
<A href="/login">"Login"</A>", "
<span>{format!("Login error: {}", e)}</span>
}.into_view(),
Ok(None) => view! {
<A href="/signup">"Signup"</A>", "
<A href="/login">"Login"</A>", "
<span>"Logged out."</span>
}.into_view(),
Ok(Some(user)) => view! {
<A href="/settings">"Settings"</A>", "
<span>{format!("Logged in as: {} ({})", user.username, user.id)}</span>
}.into_view()
})
}}
<A href="/">
<h1>"My Tasks"</h1>
</A>
<Transition fallback=move || {
view! { <span>"Loading..."</span> }
}>
{move || {
user.get()
.map(|user| match user {
Err(e) => {
view! {
<A href="/signup">"Signup"</A>
", "
<A href="/login">"Login"</A>
", "
<span>{format!("Login error: {}", e)}</span>
}
.into_view()
}
Ok(None) => {
view! {
<A href="/signup">"Signup"</A>
", "
<A href="/login">"Login"</A>
", "
<span>"Logged out."</span>
}
.into_view()
}
Ok(Some(user)) => {
view! {
<A href="/settings">"Settings"</A>
", "
<span>
{format!("Logged in as: {} ({})", user.username, user.id)}
</span>
}
.into_view()
}
})
}}
</Transition>
</header>
<hr/>
<main>
<Routes>
<Route path="" view=Todos/> //Route
<Route path="signup" view=move || view! {
<Signup action=signup/>
}/>
<Route path="login" view=move || view! {
// Route
<Route path="" view=Todos/>
<Route path="signup" view=move || view! { <Signup action=signup/> }/>
<Route path="login" view=move || view! { <Login action=login/> }/>
<Route
path="settings"
view=move || {
view! {
<h1>"Settings"</h1>
<Logout action=logout/>
}
}
/>
<Login action=login />
}/>
<Route path="settings" view=move || view! {
<h1>"Settings"</h1>
<Logout action=logout />
}/>
</Routes>
</main>
</Router>
@ -202,24 +205,26 @@ pub fn Todos() -> impl IntoView {
);
view! {
<div>
<MultiActionForm action=add_todo>
<label>
"Add a Todo"
<input type="text" name="title"/>
</label>
<label>"Add a Todo" <input type="text" name="title"/></label>
<input type="submit" value="Add"/>
</MultiActionForm>
<Transition fallback=move || view! {<p>"Loading..."</p> }>
<ErrorBoundary fallback=|errors| view!{ <ErrorTemplate errors=errors/>}>
<Transition fallback=move || view! { <p>"Loading..."</p> }>
<ErrorBoundary fallback=|errors| {
view! { <ErrorTemplate errors=errors/> }
}>
{move || {
let existing_todos = {
move || {
todos.get()
todos
.get()
.map(move |todos| match todos {
Err(e) => {
view! { <pre class="error">"Server Error: " {e.to_string()}</pre>}.into_view()
view! {
<pre class="error">"Server Error: " {e.to_string()}</pre>
}
.into_view()
}
Ok(todos) => {
if todos.is_empty() {
@ -229,17 +234,11 @@ pub fn Todos() -> impl IntoView {
.into_iter()
.map(move |todo| {
view! {
<li>
{todo.title}
": Created at "
{todo.created_at}
" by "
{
todo.user.unwrap_or_default().username
}
{todo.title} ": Created at " {todo.created_at} " by "
{todo.user.unwrap_or_default().username}
<ActionForm action=delete_todo>
<input type="hidden" name="id" value={todo.id}/>
<input type="hidden" name="id" value=todo.id/>
<input type="submit" value="X"/>
</ActionForm>
</li>
@ -252,30 +251,23 @@ pub fn Todos() -> impl IntoView {
.unwrap_or_default()
}
};
let pending_todos = move || {
submissions
.get()
.into_iter()
.filter(|submission| submission.pending().get())
.map(|submission| {
view! {
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
}
})
.collect_view()
.get()
.into_iter()
.filter(|submission| submission.pending().get())
.map(|submission| {
view! {
<li class="pending">
{move || submission.input.get().map(|data| data.title)}
</li>
}
})
.collect_view()
};
view! { <ul>{existing_todos} {pending_todos}</ul> }
}}
view! {
<ul>
{existing_todos}
{pending_todos}
</ul>
}
}
}
</ErrorBoundary>
</Transition>
</div>
@ -287,25 +279,32 @@ pub fn Login(
action: Action<Login, Result<(), ServerFnError>>,
) -> impl IntoView {
view! {
<ActionForm action=action>
<h1>"Log In"</h1>
<label>
"User ID:"
<input type="text" placeholder="User ID" maxlength="32" name="username" class="auth-input" />
<input
type="text"
placeholder="User ID"
maxlength="32"
name="username"
class="auth-input"
/>
</label>
<br/>
<label>
"Password:"
<input type="password" placeholder="Password" name="password" class="auth-input" />
<input type="password" placeholder="Password" name="password" class="auth-input"/>
</label>
<br/>
<label>
<input type="checkbox" name="remember" class="auth-input" />
<input type="checkbox" name="remember" class="auth-input"/>
"Remember me?"
</label>
<br/>
<button type="submit" class="button">"Log In"</button>
<button type="submit" class="button">
"Log In"
</button>
</ActionForm>
}
}
@ -315,31 +314,42 @@ pub fn Signup(
action: Action<Signup, Result<(), ServerFnError>>,
) -> impl IntoView {
view! {
<ActionForm action=action>
<h1>"Sign Up"</h1>
<label>
"User ID:"
<input type="text" placeholder="User ID" maxlength="32" name="username" class="auth-input" />
<input
type="text"
placeholder="User ID"
maxlength="32"
name="username"
class="auth-input"
/>
</label>
<br/>
<label>
"Password:"
<input type="password" placeholder="Password" name="password" class="auth-input" />
<input type="password" placeholder="Password" name="password" class="auth-input"/>
</label>
<br/>
<label>
"Confirm Password:"
<input type="password" placeholder="Password again" name="password_confirmation" class="auth-input" />
<input
type="password"
placeholder="Password again"
name="password_confirmation"
class="auth-input"
/>
</label>
<br/>
<label>
"Remember me?"
<input type="checkbox" name="remember" class="auth-input" />
"Remember me?" <input type="checkbox" name="remember" class="auth-input"/>
</label>
<br/>
<button type="submit" class="button">"Sign Up"</button>
<button type="submit" class="button">
"Sign Up"
</button>
</ActionForm>
}
}
@ -349,10 +359,11 @@ pub fn Logout(
action: Action<Logout, Result<(), ServerFnError>>,
) -> impl IntoView {
view! {
<div id="loginbox">
<ActionForm action=action>
<button type="submit" class="button">"Log Out"</button>
<button type="submit" class="button">
"Log Out"
</button>
</ActionForm>
</div>
}