1
0

Separate ports & adaptors

This commit is contained in:
2025-01-30 13:48:25 +02:00
parent d1d91581f5
commit bb1ff0c281
14 changed files with 621 additions and 220 deletions

View File

@ -70,7 +70,7 @@ INSERT INTO DependencyTypes (id, name) VALUES
-- Track which dependencies a package has -- Track which dependencies a package has
CREATE TABLE PackageDependencies ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, CREATE TABLE PackageDependencies ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
arch VARCHAR(63) NULL, arch VARCHAR(63) NULL,
condition VARCHAR(255) NULL, requirement VARCHAR(255) NULL,
description VARCHAR(127) NULL, description VARCHAR(127) NULL,
package INT UNSIGNED NOT NULL, package INT UNSIGNED NOT NULL,
@ -93,7 +93,7 @@ INSERT INTO RelationTypes (id, name) VALUES
-- Track which conflicts, provides and replaces a package has -- Track which conflicts, provides and replaces a package has
CREATE TABLE PackageRelations ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, CREATE TABLE PackageRelations ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
arch VARCHAR(63) NULL, arch VARCHAR(63) NULL,
condition VARCHAR(255) NULL, requiremen VARCHAR(255) NULL,
package INT UNSIGNED NOT NULL, package INT UNSIGNED NOT NULL,
relation_type TINYINT UNSIGNED NOT NULL, relation_type TINYINT UNSIGNED NOT NULL,

View File

@ -0,0 +1 @@
pub mod mysql;

View File

@ -0,0 +1,2 @@
pub mod user;
pub mod base;

View File

@ -0,0 +1,98 @@
pub use crate::port::base::*;
use sqlx::{Executor, MySql};
pub struct BaseAdapter;
struct DatabaseBase {
id: u64,
name: String,
description: Option<String>,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
}
impl From<DatabaseBase> for Base {
fn from(value: DatabaseBase) -> Self {
Self {
id: Id(value.id),
name: value.name.into(),
description: value.description.into(),
created_at: value.created_at,
updated_at: value.updated_at,
}
}
}
impl<E> BaseRepository<E> for BaseAdapter where for<'a> &'a E: Executor<'a, Database = MySql> {}
impl<E> crate::port::CRUD<E> for BaseAdapter
where
for<'a> &'a E: Executor<'a, Database = MySql>,
{
type Create = New;
type Read = Id;
type Update = Field;
type Delete = Id;
type Existing = Base;
type Id = Id;
async fn create(connection: &mut E, data: Self::Create) -> Result<Self::Id> {
Ok(Id(sqlx::query!(
"INSERT INTO PackageBases (name, description) VALUES (?, ?)",
data.name.0,
data.description.0,
)
.execute(&*connection)
.await?
.last_insert_id()))
}
async fn read(connection: &E, data: Self::Read) -> Result<Option<Self::Existing>> {
Ok(sqlx::query_as!(
DatabaseBase,
"SELECT * FROM PackageBases WHERE id = ?",
data.0
)
.fetch_optional(connection)
.await?
.map(Into::into))
}
async fn update(connection: &mut E, id: Self::Id, data: Self::Update) -> Result {
match data {
Field::Name(valid) => {
sqlx::query!(
"UPDATE PackageBases SET name = ? WHERE id = ?",
valid.0,
id.0
)
}
Field::Description(valid) => {
sqlx::query!(
"UPDATE PackageBases SET description = ? WHERE id = ?",
valid.0,
id.0
)
}
Field::CreatedAt(date_time) => sqlx::query!(
"UPDATE PackageBases SET created_at = ? WHERE id = ?",
date_time,
id.0
),
Field::UpdatedAt(date_time) => sqlx::query!(
"UPDATE PackageBases SET updated_at = ? WHERE id = ?",
date_time,
id.0
),
}
.execute(&*connection)
.await?;
Ok(())
}
async fn delete(connection: &mut E, data: Self::Delete) -> Result {
sqlx::query!("DELETE FROM PackageBases WHERE id = ?", data.0)
.execute(&*connection)
.await?;
Ok(())
}
}

View File

@ -0,0 +1,125 @@
pub use crate::port::user::*;
use sqlx::{Executor, MySql};
pub struct UserAdapter;
struct DatabaseUser {
id: u64,
name: String,
email: String,
password: String,
last_used: Option<DateTime<Utc>>,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
}
impl From<DatabaseUser> for User {
fn from(value: DatabaseUser) -> Self {
Self {
id: Id(value.id),
name: value.name.into(),
email: value.email.into(),
password: value.password.into(),
last_used: value.last_used,
created_at: value.created_at,
updated_at: value.updated_at,
}
}
}
impl<E> UserRepository<E> for UserAdapter where for<'a> &'a E: Executor<'a, Database = MySql> {}
impl<E> crate::port::CRUD<E> for UserAdapter
where
for<'a> &'a E: Executor<'a, Database = MySql>,
{
type Create = New;
type Read = Unique;
type Update = Field;
type Delete = Unique;
type Existing = User;
type Id = Id;
async fn create(connection: &mut E, data: Self::Create) -> Result<Self::Id> {
Ok(Id(sqlx::query!(
"INSERT INTO Users (name, email, password, last_used) VALUES (?, ?, ?, ?)",
data.name.0,
data.email.0,
data.password.0,
data.last_used,
)
.execute(&*connection)
.await?
.last_insert_id()))
}
async fn read(connection: &E, data: Self::Read) -> Result<Option<Self::Existing>> {
Ok(match data {
Unique::Id(id) => {
sqlx::query_as!(DatabaseUser, "SELECT * FROM Users WHERE id = ?", id.0)
.fetch_optional(connection)
.await
}
Unique::Name(name) => {
sqlx::query_as!(DatabaseUser, "SELECT * FROM Users WHERE name = ?", name.0)
.fetch_optional(connection)
.await
}
Unique::Email(email) => {
sqlx::query_as!(DatabaseUser, "SELECT * FROM Users WHERE email = ?", email.0)
.fetch_optional(connection)
.await
}
}?
.map(Into::into))
}
async fn update(connection: &mut E, id: Self::Id, data: Self::Update) -> Result {
match data {
Field::Name(valid) => {
sqlx::query!("UPDATE Users SET name = ? WHERE id = ?", valid.0, id.0)
}
Field::Email(valid) => {
sqlx::query!("UPDATE Users SET email = ? WHERE id = ?", valid.0, id.0)
}
Field::Password(valid) => {
sqlx::query!("UPDATE Users SET password = ? WHERE id = ?", valid.0, id.0)
}
Field::LastUsed(date_time) => {
sqlx::query!(
"UPDATE Users SET last_used = ? WHERE id = ?",
date_time,
id.0
)
}
Field::CreatedAt(date_time) => sqlx::query!(
"UPDATE Users SET created_at = ? WHERE id = ?",
date_time,
id.0
),
Field::UpdatedAt(date_time) => sqlx::query!(
"UPDATE Users SET updated_at = ? WHERE id = ?",
date_time,
id.0
),
}
.execute(&*connection)
.await?;
Ok(())
}
async fn delete(connection: &mut E, data: Self::Delete) -> Result {
match data {
Unique::Id(id) => sqlx::query!("DELETE FROM Users WHERE id = ?", id.0),
Unique::Name(name) => {
sqlx::query!("DELETE FROM Users WHERE name = ?", name.0)
}
Unique::Email(email) => {
sqlx::query!("DELETE FROM Users WHERE email = ?", email.0)
}
}
.execute(&*connection)
.await?;
Ok(())
}
}

View File

@ -13,4 +13,5 @@ impl<T: garde::Validate> IntoValid for T {}
pub mod atomic; pub mod atomic;
pub mod connect; pub mod connect;
pub mod repository; pub mod port;
pub mod adapter;

18
src/database/src/main.rs Normal file
View File

@ -0,0 +1,18 @@
use garde::{Valid, Validate};
#[derive(Validate)]
enum Data {
Struct {
#[garde(range(min=-10, max=10))]
field: i32,
},
Tuple(#[garde(ascii)] String),
}
fn main() {
let data = Data::Struct { field: 100 };
let data = Data::Tuple("🧱".into());
if let Err(e) = data.validate() {
println!("invalid data: {e}");
}
}

21
src/database/src/port.rs Normal file
View File

@ -0,0 +1,21 @@
pub type Result<T = ()> = std::result::Result<T, Box<dyn std::error::Error>>;
#[allow(async_fn_in_trait)]
pub trait CRUD<C> {
type Create;
type Read;
type Update;
type Delete;
type Existing;
type Id;
async fn create(connection: &mut C, data: Self::Create) -> Result<Self::Id>;
async fn read(connection: &C, data: Self::Read) -> Result<Option<Self::Existing>>;
async fn update(connection: &mut C, id: Self::Id, data: Self::Update) -> Result;
async fn delete(connection: &mut C, data: Self::Delete) -> Result;
}
pub mod user;
pub mod base;
// pub mod package;
// pub mod session;

View File

@ -0,0 +1,79 @@
pub use super::{CRUD, Result};
pub use chrono::{DateTime, Utc};
use derive_more::{Deref, From, Into};
use garde::{Valid, Validate};
#[allow(async_fn_in_trait)]
pub trait BaseRepository<C>:
super::CRUD<
C,
Create = New,
Read = Id,
Update = Field,
Delete = Id,
Existing = Base,
Id = Id,
>
{
async fn update_base(connection: &mut C, base: &mut Base, data: Self::Update) -> Result {
Self::update(connection, base.id, data.clone()).await?;
match data {
Field::Name(valid) => base.name = valid.into_inner(),
Field::Description(valid) => base.description = valid.into_inner(),
Field::CreatedAt(date_time) => base.created_at = date_time,
Field::UpdatedAt(date_time) => base.updated_at = date_time,
}
Ok(())
}
}
#[derive(Deref, Into, Clone, Copy)]
pub struct Id(pub(crate) u64);
#[derive(Validate, Deref, From, Clone)]
#[garde(transparent)]
pub struct Name(#[garde(alphanumeric, length(min = 2, max = 127))] pub String);
#[derive(Validate, Deref, From, Clone)]
#[garde(transparent)]
pub struct Description(#[garde(length(max = 510))] pub Option<String>);
pub struct New {
pub name: Name,
pub description: Description,
}
#[derive(Clone)]
pub enum Field {
Name(Valid<Name>),
Description(Valid<Description>),
CreatedAt(DateTime<Utc>),
UpdatedAt(DateTime<Utc>),
}
pub struct Base {
pub(crate) id: Id,
pub(crate) name: Name,
pub(crate) description: Description,
pub(crate) created_at: DateTime<Utc>,
pub(crate) updated_at: DateTime<Utc>,
}
impl Base {
pub fn id(&self) -> Id {
self.id
}
pub fn name(&self) -> &Name {
&self.name
}
pub fn description(&self) -> &Description {
&self.description
}
pub fn created_at(&self) -> DateTime<Utc> {
self.created_at
}
pub fn updated_at(&self) -> DateTime<Utc> {
self.updated_at
}
}

View File

@ -0,0 +1,168 @@
use super::{Result, package_base::Base};
use derive_more::{Deref, From, Into};
use garde::{Valid, Validate};
use sqlx::{Executor, MySql};
// pub enum GetBy
#[allow(async_fn_in_trait)]
pub trait PackageRepository<C> {
async fn get_by_id(connection: &C, id: Id) -> Result<Option<Package>>;
async fn get_by_name(connection: &C, name: &Valid<Name>) -> Result<Option<Package>>;
async fn change_name(connection: &mut C, package: &mut Package, name: Valid<Name>) -> Result;
async fn change_base(connection: &mut C, package: &mut Package, base: &Base) -> Result;
async fn change_version(connection: &mut C, package: &mut Package, version: Valid<Version>) -> Result;
async fn create(connection: &mut C, data: Valid<PackageData>) -> Result<Package>;
}
#[derive(Deref, Into, Clone, Copy)]
pub struct Id(u32);
pub type BaseId = super::package_base::Id;
#[derive(Validate, Deref)]
#[garde(transparent)]
pub struct Name(#[garde(alphanumeric, length(min = 2, max = 127))] pub String);
#[derive(Validate, Deref)]
#[garde(transparent)]
pub struct Version(#[garde(alphanumeric, length(min = 1, max = 127))] pub String);
#[derive(Validate, Deref)]
#[garde(transparent)]
pub struct Description(#[garde(ascii, length(max = 255))] pub Option<String>);
#[derive(Validate)]
#[garde(transparent)]
pub struct URL(#[garde(url, length(max = 510))] pub Option<String>);
#[derive(Validate)]
pub struct PackageData {
#[garde(dive)]
pub name: Name,
#[garde(dive)]
pub description: Description,
}
#[derive(Deref)]
pub struct Package {
id: Id,
#[deref]
data: PackageData,
}
impl Package {
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 Package {
// fn from(value: QueryUser) -> Self {
// Self {
// id: Id(value.id),
// data: PackageData {
// name: Name(value.name),
// description: Description(value.email),
// },
// }
// }
// }
//
// impl<E> PackageRepository<E> for UserAdapter
// where
// for<'a> &'a E: Executor<'a, Database = MySql>,
// {
// async fn get_by_id(connection: &E, id: Id) -> Result<Option<Package>> {
// 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<Name>) -> Result<Option<Package>> {
// 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<Description>) -> Result<Option<Package>> {
// 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 Package, name: Valid<Name>) -> 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 Package,
// email: Valid<Description>,
// ) -> 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 Package,
// 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, data: Valid<PackageData>) -> Result<Package> {
// let id = sqlx::query!(
// "INSERT INTO Users (name, email, password) VALUES (?, ?, ?)",
// data.name.0,
// data.description.0,
// data.password.0
// )
// .execute(&*connection)
// .await?
// .last_insert_id() as u32;
//
// Ok(Package {
// id: Id(id),
// data: data.into_inner(),
// })
// }
// }

View File

@ -0,0 +1,105 @@
pub use super::{CRUD, Result};
pub use chrono::{DateTime, Utc};
use derive_more::{Deref, From, Into};
use garde::{Valid, Validate};
#[allow(async_fn_in_trait)]
pub trait UserRepository<C>:
super::CRUD<
C,
Create = New,
Read = Unique,
Update = Field,
Delete = Unique,
Existing = User,
Id = Id,
>
{
async fn update_user(connection: &mut C, user: &mut User, data: Self::Update) -> Result {
Self::update(connection, user.id, data.clone()).await?;
match data {
Field::Name(valid) => user.name = valid.into_inner(),
Field::Email(valid) => user.email = valid.into_inner(),
Field::Password(valid) => user.password = valid.into_inner(),
Field::LastUsed(date_time) => user.last_used = date_time,
Field::CreatedAt(date_time) => user.created_at = date_time,
Field::UpdatedAt(date_time) => user.updated_at = date_time,
}
Ok(())
}
}
#[derive(Deref, Into, Clone, Copy)]
pub struct Id(pub(crate) u64);
// TODO: is this the right layer for requirements (email) validatoin?
#[derive(Validate, Deref, From, Clone)]
#[garde(transparent)]
pub struct Name(#[garde(alphanumeric, length(min = 2, max = 31))] pub String);
#[derive(Validate, Deref, From, Clone)]
#[garde(transparent)]
pub struct Email(#[garde(email, length(max = 255))] pub String);
#[derive(Validate, Deref, From, Clone)]
#[garde(transparent)]
pub struct Password(#[garde(ascii, length(max = 255))] pub String);
pub struct New {
pub name: Valid<Name>,
pub email: Valid<Email>,
pub password: Valid<Password>,
pub last_used: Option<DateTime<Utc>>,
}
pub enum Unique {
Id(Id),
Name(Valid<Name>),
Email(Valid<Email>),
}
#[derive(Clone)]
pub enum Field {
Name(Valid<Name>),
Email(Valid<Email>),
Password(Valid<Password>),
LastUsed(Option<DateTime<Utc>>),
CreatedAt(DateTime<Utc>),
UpdatedAt(DateTime<Utc>),
}
pub struct User {
pub(crate) id: Id,
pub(crate) name: Name,
pub(crate) email: Email,
pub(crate) password: Password,
pub(crate) last_used: Option<DateTime<Utc>>,
pub(crate) created_at: DateTime<Utc>,
pub(crate) updated_at: DateTime<Utc>,
}
impl User {
pub const fn id(&self) -> Id {
self.id
}
pub const fn name(&self) -> &Name {
&self.name
}
pub const fn email(&self) -> &Email {
&self.email
}
pub const fn password(&self) -> &Password {
&self.password
}
pub const fn last_used(&self) -> Option<DateTime<Utc>> {
self.last_used
}
pub const fn created_at(&self) -> DateTime<Utc> {
self.created_at
}
pub const fn updated_at(&self) -> DateTime<Utc> {
self.updated_at
}
}

View File

@ -1,2 +0,0 @@
pub mod session;
pub mod user;

View File

@ -1,51 +0,0 @@
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)
}
}

View File

@ -1,164 +0,0 @@
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(),
})
}
}