mirror of
https://github.com/Phundrak/georm.git
synced 2025-11-30 19:03:59 +00:00
feat(examples): add PostgreSQL example with user relationship
Adds an example demonstrating user, comment, and follower relationship including: - User management with profiles - Comments (not really useful, just for showcasing) - Follower/follozing relationships - Ineractive CLI interface with CRUD operations - Database migrations for the example schema
This commit is contained in:
14
examples/postgres/users-comments-and-followers/Cargo.toml
Normal file
14
examples/postgres/users-comments-and-followers/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "georm-users-comments-and-followers"
|
||||
workspace = "../../../"
|
||||
publish = false
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
georm = { path = "../../.." }
|
||||
sqlx = { workspace = true }
|
||||
clap = { version = "4.4", features = ["derive"] }
|
||||
inquire = "0.7.5"
|
||||
thiserror = "2.0.11"
|
||||
tokio = { version = "1.43.0", features = ["full"] }
|
||||
@@ -0,0 +1,129 @@
|
||||
use super::{Executable, Result};
|
||||
use crate::{
|
||||
errors::UserInputError,
|
||||
models::{Comment, CommentDefault, User},
|
||||
};
|
||||
use clap::{Args, Subcommand};
|
||||
use georm::{Defaultable, Georm};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Args, Clone)]
|
||||
pub struct CommentArgs {
|
||||
#[command(subcommand)]
|
||||
pub command: CommentCommand,
|
||||
}
|
||||
|
||||
impl Executable for CommentArgs {
|
||||
async fn execute(&self, pool: &sqlx::PgPool) -> Result {
|
||||
self.command.execute(pool).await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Subcommand)]
|
||||
pub enum CommentCommand {
|
||||
Create {
|
||||
text: Option<String>,
|
||||
username: Option<String>,
|
||||
},
|
||||
Remove {
|
||||
id: Option<i32>,
|
||||
},
|
||||
RemoveFromUser {
|
||||
username: Option<String>,
|
||||
},
|
||||
ListFromUser {
|
||||
username: Option<String>,
|
||||
},
|
||||
List,
|
||||
}
|
||||
|
||||
impl Executable for CommentCommand {
|
||||
async fn execute(&self, pool: &sqlx::PgPool) -> Result {
|
||||
match self {
|
||||
CommentCommand::Create { text, username } => {
|
||||
create_comment(username.clone(), text.clone(), pool).await
|
||||
}
|
||||
CommentCommand::Remove { id } => remove_comment(*id, pool).await,
|
||||
CommentCommand::RemoveFromUser { username } => {
|
||||
remove_user_comment(username.clone(), pool).await
|
||||
}
|
||||
CommentCommand::ListFromUser { username } => {
|
||||
list_user_comments(username.clone(), pool).await
|
||||
}
|
||||
CommentCommand::List => list_comments(pool).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_comment(
|
||||
username: Option<String>,
|
||||
text: Option<String>,
|
||||
pool: &sqlx::PgPool,
|
||||
) -> Result {
|
||||
let prompt = "Who is creating the comment?";
|
||||
let user = User::get_user_by_username_or_select(username.as_deref(), prompt, pool).await?;
|
||||
let content = match text {
|
||||
Some(text) => text,
|
||||
None => inquire::Text::new("Content of the comment:")
|
||||
.prompt()
|
||||
.map_err(UserInputError::InquireError)?,
|
||||
};
|
||||
let comment = CommentDefault {
|
||||
author_id: user.id,
|
||||
content,
|
||||
id: None,
|
||||
};
|
||||
let comment = comment.create(pool).await?;
|
||||
println!("Successfuly created comment:\n{comment}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_comment(id: Option<i32>, pool: &sqlx::PgPool) -> Result {
|
||||
let prompt = "Select the comment to remove:";
|
||||
let comment = match id {
|
||||
Some(id) => Comment::find(pool, &id)
|
||||
.await
|
||||
.map_err(UserInputError::DatabaseError)?
|
||||
.ok_or(UserInputError::CommentDoesNotExist)?,
|
||||
None => Comment::select_comment(prompt, pool).await?,
|
||||
};
|
||||
comment.delete(pool).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_user_comment(username: Option<String>, pool: &sqlx::PgPool) -> Result {
|
||||
let prompt = "Select user whose comment you want to delete:";
|
||||
let user = User::get_user_by_username_or_select(username.as_deref(), prompt, pool).await?;
|
||||
let comments: HashMap<String, Comment> = user
|
||||
.get_comments(pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|comment| (comment.content.clone(), comment))
|
||||
.collect();
|
||||
let selected_comment_content =
|
||||
inquire::Select::new(prompt, comments.clone().into_keys().collect())
|
||||
.prompt()
|
||||
.map_err(UserInputError::InquireError)?;
|
||||
let comment: &Comment = comments.get(&selected_comment_content).unwrap();
|
||||
comment.delete(pool).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn list_user_comments(username: Option<String>, pool: &sqlx::PgPool) -> Result {
|
||||
let prompt = "User whose comment you want to list:";
|
||||
let user = User::get_user_by_username_or_select(username.as_deref(), prompt, pool).await?;
|
||||
println!("List of comments from user:\n");
|
||||
for comment in user.get_comments(pool).await? {
|
||||
println!("{comment}\n");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn list_comments(pool: &sqlx::PgPool) -> Result {
|
||||
let comments = Comment::find_all(pool).await?;
|
||||
println!("List of all comments:\n");
|
||||
for comment in comments {
|
||||
println!("{comment}\n")
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
use super::{Executable, Result};
|
||||
use crate::models::{FollowerDefault, User};
|
||||
use clap::{Args, Subcommand};
|
||||
use georm::Defaultable;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Args, Clone)]
|
||||
pub struct FollowersArgs {
|
||||
#[command(subcommand)]
|
||||
pub command: FollowersCommand,
|
||||
}
|
||||
|
||||
impl Executable for FollowersArgs {
|
||||
async fn execute(&self, pool: &sqlx::PgPool) -> Result {
|
||||
self.command.execute(pool).await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Subcommand)]
|
||||
pub enum FollowersCommand {
|
||||
Follow {
|
||||
follower: Option<String>,
|
||||
followed: Option<String>,
|
||||
},
|
||||
Unfollow {
|
||||
follower: Option<String>,
|
||||
},
|
||||
ListFollowers {
|
||||
user: Option<String>,
|
||||
},
|
||||
ListFollowed {
|
||||
user: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Executable for FollowersCommand {
|
||||
async fn execute(&self, pool: &sqlx::PgPool) -> Result {
|
||||
match self {
|
||||
FollowersCommand::Follow { follower, followed } => {
|
||||
follow_user(follower.clone(), followed.clone(), pool).await
|
||||
}
|
||||
FollowersCommand::Unfollow { follower } => unfollow_user(follower.clone(), pool).await,
|
||||
FollowersCommand::ListFollowers { user } => {
|
||||
list_user_followers(user.clone(), pool).await
|
||||
}
|
||||
FollowersCommand::ListFollowed { user } => list_user_followed(user.clone(), pool).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn follow_user(
|
||||
follower: Option<String>,
|
||||
followed: Option<String>,
|
||||
pool: &sqlx::PgPool,
|
||||
) -> Result {
|
||||
let follower = User::get_user_by_username_or_select(
|
||||
follower.as_deref(),
|
||||
"Select who will be following someone:",
|
||||
pool,
|
||||
)
|
||||
.await?;
|
||||
let followed = User::get_user_by_username_or_select(
|
||||
followed.as_deref(),
|
||||
"Select who will be followed:",
|
||||
pool,
|
||||
)
|
||||
.await?;
|
||||
let follow = FollowerDefault {
|
||||
id: None,
|
||||
follower: follower.id,
|
||||
followed: followed.id,
|
||||
};
|
||||
follow.create(pool).await?;
|
||||
println!("User {follower} now follows {followed}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn unfollow_user(follower: Option<String>, pool: &sqlx::PgPool) -> Result {
|
||||
let follower =
|
||||
User::get_user_by_username_or_select(follower.as_deref(), "Select who is following", pool)
|
||||
.await?;
|
||||
let followed_list: HashMap<String, User> = follower
|
||||
.get_followed(pool)
|
||||
.await?
|
||||
.iter()
|
||||
.map(|person| (person.username.clone(), person.clone()))
|
||||
.collect();
|
||||
let followed = inquire::Select::new(
|
||||
"Who to unfollow?",
|
||||
followed_list.clone().into_keys().collect(),
|
||||
)
|
||||
.prompt()
|
||||
.unwrap();
|
||||
let followed = followed_list.get(&followed).unwrap();
|
||||
sqlx::query!(
|
||||
"DELETE FROM Followers WHERE follower = $1 AND followed = $2",
|
||||
follower.id,
|
||||
followed.id
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
println!("User {follower} unfollowed {followed}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn list_user_followers(user: Option<String>, pool: &sqlx::PgPool) -> Result {
|
||||
let user = User::get_user_by_username_or_select(
|
||||
user.as_deref(),
|
||||
"Whose followers do you want to display?",
|
||||
pool,
|
||||
)
|
||||
.await?;
|
||||
println!("List of followers of {user}:\n");
|
||||
user.get_followers(pool)
|
||||
.await?
|
||||
.iter()
|
||||
.for_each(|person| println!("{person}"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn list_user_followed(user: Option<String>, pool: &sqlx::PgPool) -> Result {
|
||||
let user = User::get_user_by_username_or_select(
|
||||
user.as_deref(),
|
||||
"Whose follows do you want to display?",
|
||||
pool,
|
||||
)
|
||||
.await?;
|
||||
println!("List of people followed by {user}:\n");
|
||||
user.get_followed(pool)
|
||||
.await?
|
||||
.iter()
|
||||
.for_each(|person| println!("{person}"));
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
mod comments;
|
||||
mod followers;
|
||||
mod users;
|
||||
|
||||
type Result = crate::Result<()>;
|
||||
|
||||
pub trait Executable {
|
||||
async fn execute(&self, pool: &sqlx::PgPool) -> Result;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Parser)]
|
||||
pub struct Cli {
|
||||
#[command(subcommand)]
|
||||
pub command: Commands,
|
||||
}
|
||||
|
||||
impl Executable for Cli {
|
||||
async fn execute(&self, pool: &sqlx::PgPool) -> Result {
|
||||
self.command.execute(pool).await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Subcommand)]
|
||||
pub enum Commands {
|
||||
Users(users::UserArgs),
|
||||
Followers(followers::FollowersArgs),
|
||||
Comments(comments::CommentArgs),
|
||||
}
|
||||
|
||||
impl Executable for Commands {
|
||||
async fn execute(&self, pool: &sqlx::PgPool) -> Result {
|
||||
match self {
|
||||
Commands::Users(user_args) => user_args.execute(pool).await,
|
||||
Commands::Followers(followers_args) => followers_args.execute(pool).await,
|
||||
Commands::Comments(comment_args) => comment_args.execute(pool).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
113
examples/postgres/users-comments-and-followers/src/cli/users.rs
Normal file
113
examples/postgres/users-comments-and-followers/src/cli/users.rs
Normal file
@@ -0,0 +1,113 @@
|
||||
use super::{Executable, Result};
|
||||
use crate::{errors::UserInputError, models::User};
|
||||
use clap::{Args, Subcommand};
|
||||
use georm::Georm;
|
||||
use inquire::{max_length, min_length, required};
|
||||
|
||||
#[derive(Debug, Args, Clone)]
|
||||
pub struct UserArgs {
|
||||
#[command(subcommand)]
|
||||
pub command: UserCommand,
|
||||
}
|
||||
|
||||
impl Executable for UserArgs {
|
||||
async fn execute(&self, pool: &sqlx::PgPool) -> Result {
|
||||
self.command.execute(pool).await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Subcommand)]
|
||||
pub enum UserCommand {
|
||||
Add { username: Option<String> },
|
||||
Remove { id: Option<i32> },
|
||||
UpdateProfile { id: Option<i32> },
|
||||
List,
|
||||
}
|
||||
|
||||
impl Executable for UserCommand {
|
||||
async fn execute(&self, pool: &sqlx::PgPool) -> Result {
|
||||
match self {
|
||||
UserCommand::Add { username } => add_user(username.clone(), pool).await,
|
||||
UserCommand::Remove { id } => remove_user(*id, pool).await,
|
||||
UserCommand::UpdateProfile { id } => update_profile(*id, pool).await,
|
||||
UserCommand::List => list_all(pool).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn add_user(username: Option<String>, pool: &sqlx::PgPool) -> Result {
|
||||
let username = match username {
|
||||
Some(username) => username,
|
||||
None => inquire::Text::new("Enter a username:")
|
||||
.prompt()
|
||||
.map_err(|_| UserInputError::InputRequired)?,
|
||||
};
|
||||
let user = User::try_new(&username, pool).await?;
|
||||
println!("The user {user} has been created!");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_user(id: Option<i32>, pool: &sqlx::PgPool) -> Result {
|
||||
let user = User::remove_interactive(id, pool).await?;
|
||||
println!("Removed user {user} from database");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_profile(id: Option<i32>, pool: &sqlx::PgPool) -> Result {
|
||||
let (user, mut profile) = User::update_profile(id, pool).await?;
|
||||
let update_display_name = inquire::Confirm::new(
|
||||
format!(
|
||||
"Your current display name is \"{}\", do you want to update it?",
|
||||
profile.get_display_name()
|
||||
)
|
||||
.as_str(),
|
||||
)
|
||||
.with_default(false)
|
||||
.prompt()
|
||||
.map_err(UserInputError::InquireError)?;
|
||||
let display_name = if update_display_name {
|
||||
Some(
|
||||
inquire::Text::new("New display name:")
|
||||
.with_help_message("Your display name should not exceed 100 characters")
|
||||
.with_validator(min_length!(3))
|
||||
.with_validator(max_length!(100))
|
||||
.with_validator(required!())
|
||||
.prompt()
|
||||
.map_err(UserInputError::InquireError)?,
|
||||
)
|
||||
} else {
|
||||
Some(profile.get_display_name())
|
||||
};
|
||||
let update_bio = inquire::Confirm::new(
|
||||
format!(
|
||||
"Your current bio is:\n===\n{}\n===\nDo you want to update it?",
|
||||
profile.get_bio()
|
||||
)
|
||||
.as_str(),
|
||||
)
|
||||
.with_default(false)
|
||||
.prompt()
|
||||
.map_err(UserInputError::InquireError)?;
|
||||
let bio = if update_bio {
|
||||
Some(
|
||||
inquire::Text::new("New bio:")
|
||||
.with_validator(min_length!(0))
|
||||
.prompt()
|
||||
.map_err(UserInputError::InquireError)?,
|
||||
)
|
||||
} else {
|
||||
Some(profile.get_bio())
|
||||
};
|
||||
let profile = profile.update_interactive(display_name, bio, pool).await?;
|
||||
println!("Profile of {user} updated:\n{profile}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn list_all(pool: &sqlx::PgPool) -> Result {
|
||||
let users = User::find_all(pool).await?;
|
||||
println!("List of users:\n");
|
||||
for user in users {
|
||||
println!("{user}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
15
examples/postgres/users-comments-and-followers/src/errors.rs
Normal file
15
examples/postgres/users-comments-and-followers/src/errors.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum UserInputError {
|
||||
#[error("Input required")]
|
||||
InputRequired,
|
||||
#[error("User ID does not exist")]
|
||||
UserDoesNotExist,
|
||||
#[error("Comment does not exist")]
|
||||
CommentDoesNotExist,
|
||||
#[error("Unexpected error, please try again")]
|
||||
InquireError(#[from] inquire::error::InquireError),
|
||||
#[error("Error from database: {0}")]
|
||||
DatabaseError(#[from] sqlx::Error),
|
||||
}
|
||||
20
examples/postgres/users-comments-and-followers/src/main.rs
Normal file
20
examples/postgres/users-comments-and-followers/src/main.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
mod cli;
|
||||
mod errors;
|
||||
mod models;
|
||||
|
||||
use clap::Parser;
|
||||
use cli::{Cli, Executable};
|
||||
|
||||
type Result<T> = std::result::Result<T, errors::UserInputError>;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let args = Cli::parse();
|
||||
let url = std::env::var("DATABASE_URL").expect("Environment variable DATABASE_URL must be set");
|
||||
let pool =
|
||||
sqlx::PgPool::connect_lazy(url.as_str()).expect("Failed to create database connection");
|
||||
match args.command.execute(&pool).await {
|
||||
Ok(_) => {}
|
||||
Err(e) => eprintln!("Error: {e}"),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
use super::User;
|
||||
use crate::{Result, errors::UserInputError};
|
||||
use georm::Georm;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Georm, Clone)]
|
||||
#[georm(table = "Comments")]
|
||||
pub struct Comment {
|
||||
#[georm(id, defaultable)]
|
||||
pub id: i32,
|
||||
#[georm(relation = {
|
||||
entity = User,
|
||||
table = "Users",
|
||||
name = "author"
|
||||
})]
|
||||
pub author_id: i32,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
impl Comment {
|
||||
pub async fn select_comment(prompt: &str, pool: &sqlx::PgPool) -> Result<Self> {
|
||||
let comments: HashMap<String, Self> = Self::find_all(pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|comment| (comment.content.clone(), comment))
|
||||
.collect();
|
||||
let comment_content = inquire::Select::new(prompt, comments.clone().into_keys().collect())
|
||||
.prompt()
|
||||
.map_err(UserInputError::InquireError)?;
|
||||
let comment: &Self = comments.get(&comment_content).unwrap();
|
||||
Ok(comment.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Comment {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"Comment:\nID:\t{}\nAuthor:\t{}\nContent:\t{}",
|
||||
self.id, self.author_id, self.content
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
use super::User;
|
||||
use georm::Georm;
|
||||
|
||||
#[derive(Debug, Clone, Georm)]
|
||||
#[georm(table = "Followers")]
|
||||
pub struct Follower {
|
||||
#[georm(id, defaultable)]
|
||||
pub id: i32,
|
||||
#[georm(relation = {
|
||||
entity = User,
|
||||
table = "Users",
|
||||
name = "followed"
|
||||
})]
|
||||
pub followed: i32,
|
||||
#[georm(relation = {
|
||||
entity = User,
|
||||
table = "Users",
|
||||
name = "follower"
|
||||
})]
|
||||
pub follower: i32,
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
mod users;
|
||||
pub use users::*;
|
||||
mod profiles;
|
||||
pub use profiles::*;
|
||||
mod comments;
|
||||
pub use comments::*;
|
||||
mod followers;
|
||||
pub use followers::*;
|
||||
@@ -0,0 +1,66 @@
|
||||
use super::User;
|
||||
use crate::{Result, errors::UserInputError};
|
||||
use georm::{Defaultable, Georm};
|
||||
|
||||
#[derive(Debug, Georm, Default)]
|
||||
#[georm(table = "Profiles")]
|
||||
pub struct Profile {
|
||||
#[georm(id, defaultable)]
|
||||
pub id: i32,
|
||||
#[georm(relation = {
|
||||
entity = User,
|
||||
table = "Users",
|
||||
name = "user",
|
||||
nullable = false
|
||||
})]
|
||||
pub user_id: i32,
|
||||
pub bio: Option<String>,
|
||||
pub display_name: Option<String>,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Profile {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"Display Name:\t{}\nBiography:\n{}\n",
|
||||
self.get_display_name(),
|
||||
self.get_bio()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Profile {
|
||||
pub fn get_display_name(&self) -> String {
|
||||
self.display_name.clone().unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn get_bio(&self) -> String {
|
||||
self.bio.clone().unwrap_or_default()
|
||||
}
|
||||
|
||||
pub async fn try_new(user_id: i32, pool: &sqlx::PgPool) -> Result<Self> {
|
||||
let profile = ProfileDefault {
|
||||
user_id,
|
||||
id: None,
|
||||
bio: None,
|
||||
display_name: None,
|
||||
};
|
||||
profile
|
||||
.create(pool)
|
||||
.await
|
||||
.map_err(UserInputError::DatabaseError)
|
||||
}
|
||||
|
||||
pub async fn update_interactive(
|
||||
&mut self,
|
||||
display_name: Option<String>,
|
||||
bio: Option<String>,
|
||||
pool: &sqlx::PgPool,
|
||||
) -> Result<Self> {
|
||||
self.display_name = display_name;
|
||||
self.bio = bio;
|
||||
self.update(pool)
|
||||
.await
|
||||
.map_err(UserInputError::DatabaseError)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{Result, errors::UserInputError};
|
||||
use georm::{Defaultable, Georm};
|
||||
|
||||
use super::{Comment, Profile};
|
||||
|
||||
#[derive(Debug, Georm, Clone)]
|
||||
#[georm(
|
||||
table = "Users",
|
||||
one_to_one = [{
|
||||
name = "profile", remote_id = "user_id", table = "Profiles", entity = Profile
|
||||
}],
|
||||
one_to_many = [{
|
||||
name = "comments", remote_id = "author_id", table = "Comments", entity = Comment
|
||||
}],
|
||||
many_to_many = [{
|
||||
name = "followers",
|
||||
table = "Users",
|
||||
entity = User,
|
||||
link = { table = "Followers", from = "followed", to = "follower" }
|
||||
},
|
||||
{
|
||||
name = "followed",
|
||||
table = "Users",
|
||||
entity = User,
|
||||
link = { table = "Followers", from = "follower", to = "followed" }
|
||||
}
|
||||
]
|
||||
)]
|
||||
pub struct User {
|
||||
#[georm(id, defaultable)]
|
||||
pub id: i32,
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for User {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{} (ID: {})", self.username, self.id)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for UserDefault {
|
||||
fn from(value: &str) -> Self {
|
||||
Self {
|
||||
id: None,
|
||||
username: value.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl User {
|
||||
async fn select_user(prompt: &str, pool: &sqlx::PgPool) -> Result<Self> {
|
||||
let users: HashMap<String, Self> = Self::find_all(pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|user| (user.username.clone(), user))
|
||||
.collect();
|
||||
let username = inquire::Select::new(prompt, users.clone().into_keys().collect())
|
||||
.prompt()
|
||||
.map_err(UserInputError::InquireError)?;
|
||||
let user: &Self = users.get(&username).unwrap();
|
||||
Ok(user.clone())
|
||||
}
|
||||
|
||||
pub async fn get_user_by_id_or_select(
|
||||
id: Option<i32>,
|
||||
prompt: &str,
|
||||
pool: &sqlx::PgPool,
|
||||
) -> Result<Self> {
|
||||
let user = match id {
|
||||
Some(id) => Self::find(pool, &id)
|
||||
.await?
|
||||
.ok_or(UserInputError::UserDoesNotExist)?,
|
||||
None => Self::select_user(prompt, pool).await?,
|
||||
};
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
pub async fn get_user_by_username_or_select(
|
||||
username: Option<&str>,
|
||||
prompt: &str,
|
||||
pool: &sqlx::PgPool,
|
||||
) -> Result<Self> {
|
||||
let user = match username {
|
||||
Some(username) => Self::find_by_username(username, pool)
|
||||
.await?
|
||||
.ok_or(UserInputError::UserDoesNotExist)?,
|
||||
None => Self::select_user(prompt, pool).await?,
|
||||
};
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
pub async fn find_by_username(username: &str, pool: &sqlx::PgPool) -> Result<Option<Self>> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"SELECT * FROM Users u WHERE u.username = $1",
|
||||
username
|
||||
)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
.map_err(UserInputError::DatabaseError)
|
||||
}
|
||||
|
||||
pub async fn try_new(username: &str, pool: &sqlx::PgPool) -> Result<Self> {
|
||||
let user = UserDefault::from(username);
|
||||
user.create(pool)
|
||||
.await
|
||||
.map_err(UserInputError::DatabaseError)
|
||||
}
|
||||
|
||||
pub async fn remove_interactive(id: Option<i32>, pool: &sqlx::PgPool) -> Result<Self> {
|
||||
let prompt = "Select a user to delete:";
|
||||
let user = Self::get_user_by_id_or_select(id, prompt, pool).await?;
|
||||
let _ = user.clone().delete(pool).await?;
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
pub async fn update_profile(id: Option<i32>, pool: &sqlx::PgPool) -> Result<(User, Profile)> {
|
||||
let prompt = "Select the user whose profile you want to update";
|
||||
let user = Self::get_user_by_id_or_select(id, prompt, pool).await?;
|
||||
let profile = match user.get_profile(pool).await? {
|
||||
Some(profile) => profile,
|
||||
None => Profile::try_new(user.id, pool).await?,
|
||||
};
|
||||
Ok((user, profile))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user