Database User & Session repository
This commit is contained in:
3
src/.gitignore
vendored
Normal file
3
src/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
/target/
|
||||||
|
/**/scrapyard/
|
||||||
|
/database/data/
|
5354
src/Cargo.lock
generated
Normal file
5354
src/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
3
src/Cargo.toml
Normal file
3
src/Cargo.toml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[workspace]
|
||||||
|
resolver = "2"
|
||||||
|
members = ["app/*", "libs/*", "database"]
|
19
src/README.md
Normal file
19
src/README.md
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# Stuff that helped:
|
||||||
|
|
||||||
|
* Architecture:
|
||||||
|
- [How to apply hexagonal architecture to Rust](https://www.barrage.net/blog/technology/how-to-apply-hexagonal-architecture-to-rust)
|
||||||
|
- [Implementing onion architecture using Rust](https://mathias-vandaele.dev/implementing-onion-architecture-using-rust)
|
||||||
|
* Design:
|
||||||
|
- [Rust Data Modelling Without Classes](https://www.youtube.com/watch?v=z-0-bbc80JM)
|
||||||
|
- ["Making Impossible States Impossible" by Richard Feldman](https://www.youtube.com/watch?v=IcgmSRJHu_8)
|
||||||
|
- [Pretty State Machine Patterns in Rust](https://hoverbear.org/blog/rust-state-machine-pattern/)
|
||||||
|
* How to Iced:
|
||||||
|
- [Building a simple text editor with iced, a cross-platform GUI library for Rust](https://www.youtube.com/watch?v=gcBJ7cPSALo)
|
||||||
|
- [Unofficial Iced Guide](https://jl710.github.io/iced-guide/)
|
||||||
|
- [icebreaker](https://github.com/hecrj/icebreaker)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> _The purpose of abstraction is not to be vague, but to create a new semantic level in which one can be absolutely precise._
|
||||||
|
|
||||||
|
— Edsger W. Dijkstra
|
20
src/database/Cargo.toml
Normal file
20
src/database/Cargo.toml
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
[package]
|
||||||
|
name = "database"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
thiserror = "2.0.11"
|
||||||
|
derive_more = { version = "1.0.0", features = ["deref", "deref_mut", "from", "into"] }
|
||||||
|
garde = { version = "0.22.0", features = ["email", "url", "derive"] }
|
||||||
|
|
||||||
|
chrono = { version = "0.4.39", default-features = false, features = [
|
||||||
|
"std",
|
||||||
|
"now",
|
||||||
|
] }
|
||||||
|
sqlx = { version = "0.8.3", default-features = false, features = [
|
||||||
|
"mysql",
|
||||||
|
"macros",
|
||||||
|
"chrono",
|
||||||
|
"runtime-tokio",
|
||||||
|
] }
|
11
src/database/compose.yaml
Normal file
11
src/database/compose.yaml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
services:
|
||||||
|
database:
|
||||||
|
image: mysql:9.0
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:3306:3306"
|
||||||
|
volumes:
|
||||||
|
- ./data:/var/lib/mysql
|
||||||
|
- ./init:/docker-entrypoint-initdb.d/:ro
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: password # yes, I know
|
177
src/database/init/init.sql
Normal file
177
src/database/init/init.sql
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
-- DROP DATABASE IF EXISTS repository;
|
||||||
|
CREATE DATABASE repository;
|
||||||
|
USE repository;
|
||||||
|
|
||||||
|
-- Required info for an account
|
||||||
|
CREATE TABLE Users ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(31) UNIQUE NOT NULL,
|
||||||
|
email VARCHAR(255) UNIQUE NOT NULL,
|
||||||
|
password VARCHAR(255) NOT NULL,
|
||||||
|
|
||||||
|
last_used TIMESTAMP NULL,
|
||||||
|
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Enables multiple packages to have the same base yet different components
|
||||||
|
CREATE TABLE PackageBases ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(127) UNIQUE NOT NULL,
|
||||||
|
description VARCHAR(510) NULL,
|
||||||
|
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- User roles for working on packages: flagger, packager, submitter, maintainer, etc.
|
||||||
|
CREATE TABLE PackageBaseRoles ( id TINYINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(31) UNIQUE NOT NULL,
|
||||||
|
description VARCHAR(255) NULL
|
||||||
|
);
|
||||||
|
-- Roles that a user has for a package
|
||||||
|
CREATE TABLE PackageBaseUserRoles (
|
||||||
|
base_id INT UNSIGNED,
|
||||||
|
user_id INT UNSIGNED,
|
||||||
|
role_id TINYINT UNSIGNED,
|
||||||
|
|
||||||
|
comment VARCHAR(255) NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (base_id, user_id, role_id), -- composite key
|
||||||
|
FOREIGN KEY (base_id) REFERENCES PackageBases(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES Users(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (role_id) REFERENCES PackageBaseRoles(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Information about the actual packages
|
||||||
|
CREATE TABLE Packages ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
package_base INT UNSIGNED NOT NULL,
|
||||||
|
name VARCHAR(127) UNIQUE NOT NULL,
|
||||||
|
version VARCHAR(127) NOT NULL,
|
||||||
|
description VARCHAR(255) NULL,
|
||||||
|
url VARCHAR(510) NULL,
|
||||||
|
|
||||||
|
flagged_at TIMESTAMP NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
|
||||||
|
FOREIGN KEY (package_base) REFERENCES PackageBases (id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- depends, makedepends, optdepends, etc.
|
||||||
|
CREATE TABLE DependencyTypes ( id TINYINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(31) UNIQUE NOT NULL
|
||||||
|
);
|
||||||
|
INSERT INTO DependencyTypes (id, name) VALUES
|
||||||
|
(1, 'depends'),
|
||||||
|
(2, 'makedepends'),
|
||||||
|
(3, 'checkdepends'),
|
||||||
|
(4, 'optdepends');
|
||||||
|
|
||||||
|
-- Track which dependencies a package has
|
||||||
|
CREATE TABLE PackageDependencies ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
arch VARCHAR(63) NULL,
|
||||||
|
condition VARCHAR(255) NULL,
|
||||||
|
description VARCHAR(127) NULL,
|
||||||
|
|
||||||
|
package INT UNSIGNED NOT NULL,
|
||||||
|
dependency_type TINYINT UNSIGNED NOT NULL,
|
||||||
|
dependency_package_name VARCHAR(127) NOT NULL, -- Not an actual package, but an an alias. Allows for package substitution.
|
||||||
|
|
||||||
|
FOREIGN KEY (package) REFERENCES Packages (id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (dependency_type) REFERENCES DependencyTypes (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- conflicts, provides, replaces, etc.
|
||||||
|
CREATE TABLE RelationTypes ( id TINYINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(31) UNIQUE NOT NULL
|
||||||
|
);
|
||||||
|
INSERT INTO RelationTypes (id, name) VALUES
|
||||||
|
(1, 'conflicts'),
|
||||||
|
(2, 'provides'),
|
||||||
|
(3, 'replaces');
|
||||||
|
|
||||||
|
-- Track which conflicts, provides and replaces a package has
|
||||||
|
CREATE TABLE PackageRelations ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
arch VARCHAR(63) NULL,
|
||||||
|
condition VARCHAR(255) NULL,
|
||||||
|
|
||||||
|
package INT UNSIGNED NOT NULL,
|
||||||
|
relation_type TINYINT UNSIGNED NOT NULL,
|
||||||
|
relation_package_name VARCHAR(127) NOT NULL,
|
||||||
|
|
||||||
|
FOREIGN KEY (package) REFERENCES Packages (id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (relation_type) REFERENCES RelationTypes (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Public user profile
|
||||||
|
/* CREATE TABLE UserProfiles ( user_id INT UNSIGNED PRIMARY KEY,
|
||||||
|
real_name VARCHAR(63) NULL,
|
||||||
|
homepage TEXT NULL, -- bio / description / whatever
|
||||||
|
irc_nick VARCHAR(31) NULL,
|
||||||
|
pgp_key CHAR(40) NULL,
|
||||||
|
language VARCHAR(31) NULL, -- only for display
|
||||||
|
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
|
||||||
|
FOREIGN KEY (user_id) REFERENCES Users(id) ON DELETE CASCADE
|
||||||
|
); */
|
||||||
|
|
||||||
|
-- Settings for the User
|
||||||
|
/* CREATE TABLE UserPreferences ( user_id INT UNSIGNED PRIMARY KEY,
|
||||||
|
inactive BOOLEAN DEFAULT 0 NOT NULL, -- user is no longer active
|
||||||
|
show_email BOOLEAN DEFAULT 0 NOT NULL, -- on public profile page
|
||||||
|
utc_timezone TINYINT DEFAULT 0 NOT NULL, -- adjust timestamps shown
|
||||||
|
backup_email VARCHAR(127) NULL, -- to restore the account
|
||||||
|
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
|
||||||
|
FOREIGN KEY (user_id) REFERENCES Users(id) ON DELETE CASCADE
|
||||||
|
); */
|
||||||
|
|
||||||
|
-- Levels of access to the repository
|
||||||
|
/* CREATE TABLE AccessRoles ( id TINYINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(31) UNIQUE NOT NULL,
|
||||||
|
description VARCHAR(255) NULL
|
||||||
|
); */
|
||||||
|
-- Roles that a user has
|
||||||
|
/* CREATE TABLE UserAccessRoles (
|
||||||
|
user_id INT UNSIGNED,
|
||||||
|
role_id TINYINT UNSIGNED,
|
||||||
|
|
||||||
|
PRIMARY KEY (user_id, role_id), -- composite key
|
||||||
|
FOREIGN KEY (user_id) REFERENCES Users(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (role_id) REFERENCES AccessRoles(id) ON DELETE CASCADE
|
||||||
|
); */
|
||||||
|
|
||||||
|
-- Votes
|
||||||
|
/* CREATE TABLE PackageBaseUserVotes (
|
||||||
|
package_base INT UNSIGNED,
|
||||||
|
user INT UNSIGNED,
|
||||||
|
score TINYINT UNSIGNED DEFAULT 0 NOT NULL CHECK (score <= 10),
|
||||||
|
|
||||||
|
comment VARCHAR(255) NULL,
|
||||||
|
log TEXT NULL, -- error logs, etc.
|
||||||
|
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (package_base, user), -- composite key
|
||||||
|
FOREIGN KEY (package_base) REFERENCES PackageBases (id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (user) REFERENCES Users (id) ON DELETE CASCADE
|
||||||
|
); */
|
||||||
|
|
||||||
|
-- Information about licenses
|
||||||
|
/* CREATE TABLE Licenses ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(127) UNIQUE NOT NULL,
|
||||||
|
description TEXT NULL
|
||||||
|
); */
|
||||||
|
-- Information about licenses
|
||||||
|
/* CREATE TABLE PackageLicenses (
|
||||||
|
package INT UNSIGNED,
|
||||||
|
license INT UNSIGNED,
|
||||||
|
|
||||||
|
PRIMARY KEY (package, license), -- composite key
|
||||||
|
FOREIGN KEY (package) REFERENCES Packages (id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (license) REFERENCES Licenses (id) ON DELETE CASCADE
|
||||||
|
); */
|
40
src/database/src/atomic.rs
Normal file
40
src/database/src/atomic.rs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
type Result<T = ()> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||||
|
|
||||||
|
#[allow(async_fn_in_trait)]
|
||||||
|
pub trait Atomic {
|
||||||
|
type Transaction<'a>;
|
||||||
|
|
||||||
|
async fn start_transaction(&mut self) -> Result<Self::Transaction<'_>>;
|
||||||
|
async fn abort_transaction(transaction: Self::Transaction<'_>) -> Result;
|
||||||
|
async fn commit_transaction(transaction: Self::Transaction<'_>) -> Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
use sqlx::Connection;
|
||||||
|
|
||||||
|
impl Atomic for sqlx::MySqlPool {
|
||||||
|
type Transaction<'a> = sqlx::MySqlTransaction<'a>;
|
||||||
|
|
||||||
|
async fn start_transaction(&mut self) -> Result<Self::Transaction<'_>> {
|
||||||
|
self.begin().await.map_err(Box::from)
|
||||||
|
}
|
||||||
|
async fn abort_transaction(transaction: Self::Transaction<'_>) -> Result {
|
||||||
|
transaction.rollback().await.map_err(Box::from)
|
||||||
|
}
|
||||||
|
async fn commit_transaction(transaction: Self::Transaction<'_>) -> Result {
|
||||||
|
transaction.commit().await.map_err(Box::from)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Atomic for sqlx::MySqlConnection {
|
||||||
|
type Transaction<'a> = sqlx::MySqlTransaction<'a>;
|
||||||
|
|
||||||
|
async fn start_transaction(&mut self) -> Result<Self::Transaction<'_>> {
|
||||||
|
self.begin().await.map_err(Box::from)
|
||||||
|
}
|
||||||
|
async fn abort_transaction(transaction: Self::Transaction<'_>) -> Result {
|
||||||
|
transaction.rollback().await.map_err(Box::from)
|
||||||
|
}
|
||||||
|
async fn commit_transaction(transaction: Self::Transaction<'_>) -> Result {
|
||||||
|
transaction.commit().await.map_err(Box::from)
|
||||||
|
}
|
||||||
|
}
|
44
src/database/src/connect.rs
Normal file
44
src/database/src/connect.rs
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
type Result<T = ()> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||||
|
|
||||||
|
#[allow(async_fn_in_trait)]
|
||||||
|
pub trait Connect {
|
||||||
|
type Connection;
|
||||||
|
|
||||||
|
async fn open_connection(&self) -> Result<Self::Connection>;
|
||||||
|
async fn close_connection(connection: Self::Connection) -> Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
use sqlx::Connection;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct MySqlPool {
|
||||||
|
pool: sqlx::MySqlPool,
|
||||||
|
}
|
||||||
|
impl Connect for MySqlPool {
|
||||||
|
type Connection = sqlx::MySqlPool;
|
||||||
|
|
||||||
|
async fn open_connection(&self) -> Result<Self::Connection> {
|
||||||
|
Ok(self.pool.clone())
|
||||||
|
}
|
||||||
|
async fn close_connection(_: Self::Connection) -> Result {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MySqlConnection {
|
||||||
|
link: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Connect for MySqlConnection {
|
||||||
|
type Connection = sqlx::MySqlConnection;
|
||||||
|
|
||||||
|
async fn open_connection(&self) -> Result<Self::Connection> {
|
||||||
|
sqlx::MySqlConnection::connect(&self.link)
|
||||||
|
.await
|
||||||
|
.map_err(Box::from)
|
||||||
|
}
|
||||||
|
async fn close_connection(connection: Self::Connection) -> Result {
|
||||||
|
connection.close().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
16
src/database/src/lib.rs
Normal file
16
src/database/src/lib.rs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
use garde::{Report, Unvalidated, Valid, Validate};
|
||||||
|
|
||||||
|
pub trait IntoValid: Validate {
|
||||||
|
fn into_valid(self) -> Result<Valid<Self>, Report>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
<Self as Validate>::Context: Default,
|
||||||
|
{
|
||||||
|
Unvalidated::new(self).validate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<T: garde::Validate> IntoValid for T {}
|
||||||
|
|
||||||
|
pub mod atomic;
|
||||||
|
pub mod connect;
|
||||||
|
pub mod repository;
|
2
src/database/src/repository.rs
Normal file
2
src/database/src/repository.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pub mod session;
|
||||||
|
pub mod user;
|
51
src/database/src/repository/session.rs
Normal file
51
src/database/src/repository/session.rs
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
use super::user::User;
|
||||||
|
pub type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||||
|
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use derive_more::{Deref, DerefMut};
|
||||||
|
use sqlx::{Executor, MySql};
|
||||||
|
|
||||||
|
#[allow(async_fn_in_trait)]
|
||||||
|
pub trait SessionRepository<C> {
|
||||||
|
async fn start(connection: &mut C, user: User) -> Result<Session>;
|
||||||
|
async fn end(connection: &mut C, session: Session) -> Result<User>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(DerefMut, Deref)]
|
||||||
|
pub struct Session {
|
||||||
|
#[deref]
|
||||||
|
#[deref_mut]
|
||||||
|
user: User,
|
||||||
|
|
||||||
|
start: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Session {
|
||||||
|
pub const fn start(&self) -> DateTime<Utc> {
|
||||||
|
self.start
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SessionAdapter;
|
||||||
|
|
||||||
|
impl<E> SessionRepository<E> for SessionAdapter
|
||||||
|
where
|
||||||
|
for<'a> &'a E: Executor<'a, Database = MySql>,
|
||||||
|
{
|
||||||
|
async fn start(connection: &mut E, user: User) -> Result<Session> {
|
||||||
|
let start = Utc::now();
|
||||||
|
sqlx::query!(
|
||||||
|
"UPDATE Users SET last_used = ? WHERE id = ?",
|
||||||
|
start,
|
||||||
|
*user.id()
|
||||||
|
)
|
||||||
|
.execute(&*connection)
|
||||||
|
.await?;
|
||||||
|
Ok(Session { user, start })
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn end(_: &mut E, session: Session) -> Result<User> {
|
||||||
|
Ok(session.user)
|
||||||
|
}
|
||||||
|
}
|
164
src/database/src/repository/user.rs
Normal file
164
src/database/src/repository/user.rs
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
use derive_more::{Deref, From, Into};
|
||||||
|
use garde::{Valid, Validate};
|
||||||
|
use sqlx::{Executor, MySql};
|
||||||
|
|
||||||
|
pub type Result<T = ()> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||||
|
|
||||||
|
#[allow(async_fn_in_trait)]
|
||||||
|
pub trait UserRepository<C> {
|
||||||
|
async fn get_by_id(connection: &C, id: Id) -> Result<Option<User>>;
|
||||||
|
async fn get_by_name(connection: &C, name: &Valid<Username>) -> Result<Option<User>>;
|
||||||
|
async fn get_by_email(connection: &C, email: &Valid<Email>) -> Result<Option<User>>;
|
||||||
|
|
||||||
|
async fn change_name(connection: &mut C, user: &mut User, name: Valid<Username>) -> Result;
|
||||||
|
async fn change_email(connection: &mut C, user: &mut User, email: Valid<Email>) -> Result;
|
||||||
|
async fn change_password(
|
||||||
|
connection: &mut C,
|
||||||
|
user: &mut User,
|
||||||
|
password: Valid<Password>,
|
||||||
|
) -> Result;
|
||||||
|
|
||||||
|
async fn create(connection: &mut C, user: Valid<UserData>) -> Result<User>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deref, Into, Clone, Copy)]
|
||||||
|
pub struct Id(u32);
|
||||||
|
|
||||||
|
#[derive(Validate, Deref)]
|
||||||
|
#[garde(transparent)]
|
||||||
|
pub struct Username(#[garde(alphanumeric, length(min = 2, max = 31))] pub String);
|
||||||
|
|
||||||
|
#[derive(Validate, Deref)]
|
||||||
|
#[garde(transparent)]
|
||||||
|
pub struct Email(#[garde(email, length(max = 255))] pub String);
|
||||||
|
|
||||||
|
#[derive(Validate, Deref)]
|
||||||
|
#[garde(transparent)]
|
||||||
|
pub struct Password(#[garde(ascii, length(max = 255))] pub String);
|
||||||
|
|
||||||
|
#[derive(Validate)]
|
||||||
|
pub struct UserData {
|
||||||
|
#[garde(dive)]
|
||||||
|
pub name: Username,
|
||||||
|
#[garde(dive)]
|
||||||
|
pub email: Email,
|
||||||
|
#[garde(dive)]
|
||||||
|
pub password: Password,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deref)]
|
||||||
|
pub struct User {
|
||||||
|
id: Id,
|
||||||
|
#[deref]
|
||||||
|
data: UserData,
|
||||||
|
}
|
||||||
|
impl User {
|
||||||
|
pub const fn id(&self) -> Id {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct UserAdapter;
|
||||||
|
|
||||||
|
struct QueryUser {
|
||||||
|
id: u32,
|
||||||
|
name: String,
|
||||||
|
email: String,
|
||||||
|
password: String,
|
||||||
|
}
|
||||||
|
impl From<QueryUser> for User {
|
||||||
|
fn from(value: QueryUser) -> Self {
|
||||||
|
Self {
|
||||||
|
id: Id(value.id),
|
||||||
|
data: UserData {
|
||||||
|
name: Username(value.name),
|
||||||
|
email: Email(value.email),
|
||||||
|
password: Password(value.password),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E> UserRepository<E> for UserAdapter
|
||||||
|
where
|
||||||
|
for<'a> &'a E: Executor<'a, Database = MySql>,
|
||||||
|
{
|
||||||
|
async fn get_by_id(connection: &E, id: Id) -> Result<Option<User>> {
|
||||||
|
Ok(sqlx::query_as!(
|
||||||
|
QueryUser,
|
||||||
|
"SELECT id, name, email, password FROM Users WHERE id = ?",
|
||||||
|
id.0
|
||||||
|
)
|
||||||
|
.fetch_optional(connection)
|
||||||
|
.await?
|
||||||
|
.map(Into::into))
|
||||||
|
}
|
||||||
|
async fn get_by_name(connection: &E, name: &Valid<Username>) -> Result<Option<User>> {
|
||||||
|
Ok(sqlx::query_as!(
|
||||||
|
QueryUser,
|
||||||
|
"SELECT id, name, email, password FROM Users WHERE name = ?",
|
||||||
|
name.0
|
||||||
|
)
|
||||||
|
.fetch_optional(connection)
|
||||||
|
.await?
|
||||||
|
.map(Into::into))
|
||||||
|
}
|
||||||
|
async fn get_by_email(connection: &E, email: &Valid<Email>) -> Result<Option<User>> {
|
||||||
|
Ok(sqlx::query_as!(
|
||||||
|
QueryUser,
|
||||||
|
"SELECT id, name, email, password FROM Users WHERE email = ?",
|
||||||
|
email.0
|
||||||
|
)
|
||||||
|
.fetch_optional(connection)
|
||||||
|
.await?
|
||||||
|
.map(Into::into))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn change_name(connection: &mut E, user: &mut User, name: Valid<Username>) -> Result {
|
||||||
|
sqlx::query!("UPDATE Users SET name = ? WHERE id = ?", name.0, user.id.0)
|
||||||
|
.execute(&*connection)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
async fn change_email(connection: &mut E, user: &mut User, email: Valid<Email>) -> Result {
|
||||||
|
sqlx::query!(
|
||||||
|
"UPDATE Users SET email = ? WHERE id = ?",
|
||||||
|
email.0,
|
||||||
|
user.id.0
|
||||||
|
)
|
||||||
|
.execute(&*connection)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
async fn change_password(
|
||||||
|
connection: &mut E,
|
||||||
|
user: &mut User,
|
||||||
|
password: Valid<Password>,
|
||||||
|
) -> Result {
|
||||||
|
sqlx::query!(
|
||||||
|
"UPDATE Users SET password = ? WHERE id = ?",
|
||||||
|
password.0,
|
||||||
|
user.id.0
|
||||||
|
)
|
||||||
|
.execute(&*connection)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create(connection: &mut E, user: Valid<UserData>) -> Result<User> {
|
||||||
|
let id = sqlx::query!(
|
||||||
|
"INSERT INTO Users (name, email, password) VALUES (?, ?, ?)",
|
||||||
|
user.name.0,
|
||||||
|
user.email.0,
|
||||||
|
user.password.0
|
||||||
|
)
|
||||||
|
.execute(&*connection)
|
||||||
|
.await?
|
||||||
|
.last_insert_id() as u32;
|
||||||
|
|
||||||
|
Ok(User {
|
||||||
|
id: Id(id),
|
||||||
|
data: user.into_inner(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user