1
0

Working Login & Register functionality

This commit is contained in:
2025-02-08 16:35:07 +02:00
parent f65312209c
commit a7a474743c
37 changed files with 1208 additions and 802 deletions

View File

@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "INSERT INTO Packages (base, name, version, description, url, flagged_at, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 8
},
"nullable": []
},
"hash": "3ffe9168c1eb30bb54c65055314655c21f03d04973e4291e6d4b6c7847364659"
}

View File

@ -14,7 +14,7 @@
},
{
"ordinal": 1,
"name": "package_base",
"name": "base",
"type_info": {
"type": "Long",
"flags": "NOT_NULL | MULTIPLE_KEY | UNSIGNED | NO_DEFAULT_VALUE",

View File

@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "UPDATE Packages SET base = ? WHERE id = ?",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "6b6f842d169e2a9628474edcd6dad10878bb9aaa612f62080d40ba24682b0c3b"
}

View File

@ -14,7 +14,7 @@
},
{
"ordinal": 1,
"name": "package_base",
"name": "base",
"type_info": {
"type": "Long",
"flags": "NOT_NULL | MULTIPLE_KEY | UNSIGNED | NO_DEFAULT_VALUE",

View File

@ -1,12 +0,0 @@
{
"db_name": "MySQL",
"query": "UPDATE Packages SET package_base = ? WHERE id = ?",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "c8de918b432ce82bc7bf1d09a378f9b092d74c2298ac126f7edbb7d59c536910"
}

View File

@ -1,12 +0,0 @@
{
"db_name": "MySQL",
"query": "INSERT INTO Packages (package_base, name, version, description, url, flagged_at, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 8
},
"nullable": []
},
"hash": "dfc2574c39f3f5a9afc1bdb642d8bf8a5ce85e7f84fa0dadb53c88cce63e5634"
}

View File

@ -4,18 +4,14 @@ version = "0.1.0"
edition = "2024"
[dependencies]
derive_more = { version = "1.0.0", features = ["deref", "into"] }
derive_more = { version = "2.0.1", features = ["deref", "into"] }
futures = "0.3.31"
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",
] }
sqlx = { version = "0.8.3", default-features = false, features = ["mysql", "macros", "chrono", "runtime-tokio"] }
# thiserror = "2.0.11"
# garde = { version = "0.22.0", features = ["email", "url", "derive"] }

View File

@ -1,4 +1,5 @@
//! `MySQL` adapters.
pub mod base;
pub mod package;
pub mod search;
pub mod user;

View File

@ -12,7 +12,7 @@ where
for<'a> &'a E: Executor<'a, Database = MySql>,
{
}
impl<E> crate::port::CRUD<E> for BaseAdapter
impl<E> crate::port::Crud<E> for BaseAdapter
where
E: Send,
for<'a> &'a E: Executor<'a, Database = MySql>,

View File

@ -12,7 +12,7 @@ where
for<'a> &'a E: Executor<'a, Database = MySql>,
{
}
impl<E> crate::port::CRUD<E> for PackageAdapter
impl<E> crate::port::Crud<E> for PackageAdapter
where
E: Send,
for<'a> &'a E: Executor<'a, Database = MySql>,
@ -26,7 +26,7 @@ where
let created_at = Utc::now();
let id = sqlx::query!(
"INSERT INTO Packages \
(package_base, name, version, description, url, flagged_at, created_at, updated_at) \
(base, name, version, description, url, flagged_at, created_at, updated_at) \
VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
data.package_base.id,
data.name.as_str(),
@ -43,7 +43,7 @@ where
Ok(Self::Existing {
id,
package_base: data.package_base.id,
base: data.package_base.id,
name: data.name.into(),
version: data.version.into(),
description: data.description.into(),
@ -88,7 +88,7 @@ where
}
Field::PackageBase(package_base) => {
sqlx::query!(
"UPDATE Packages SET package_base = ? WHERE id = ?",
"UPDATE Packages SET base = ? WHERE id = ?",
package_base.id,
existing.id
)
@ -107,7 +107,7 @@ where
existing.id
)
}
Field::URL(url) => {
Field::Url(url) => {
sqlx::query!(
"UPDATE Packages SET url = ? WHERE id = ?",
url.as_ref(),
@ -135,10 +135,10 @@ where
match data {
Field::Name(s) => existing.name = s.into(),
Field::PackageBase(s) => existing.package_base = s.id,
Field::PackageBase(s) => existing.base = s.id,
Field::Version(s) => existing.version = s.into(),
Field::Description(o) => existing.description = o.into(),
Field::URL(o) => existing.url = o.into(),
Field::Url(o) => existing.url = o.into(),
Field::FlaggedAt(date_time) => existing.flagged_at = date_time,
Field::CreatedAt(date_time) => existing.created_at = date_time,
Field::UpdatedAt(date_time) => existing.updated_at = date_time,

View File

@ -0,0 +1,152 @@
use crate::port::search::{Data, Entry, Mode, Order, SearchRepository};
use crate::{Result, adapter::mysql::search};
// use chrono::Utc;
use futures::TryStreamExt;
use sqlx::{Executor, MySql, QueryBuilder, Row};
pub struct UserAdapter;
impl<E> SearchRepository<E> for UserAdapter
where
E: Send,
for<'a> &'a E: Executor<'a, Database = MySql>,
{
async fn search(connection: &E, data: Data) -> Result<Vec<Entry>> {
let mut builder = QueryBuilder::new(
"SELECT \
p.id, p.name, p.version, p.url, p.description, \
p.updated_at, p.created_at, \
pb.id AS base_id, pb.name AS base_name, \
( \
SELECT COUNT(DISTINCT pbur.user) \
FROM PackageBaseUserRoles pbur \
WHERE pbur.base = pb.id AND pbur.role = 3 \
) AS maintainers_num \
FROM \
Packages p \
JOIN \
PackageBases pb ON p.base = pb.id ",
);
let mut push_search = |cond, param| {
builder.push(format_args!(
" {cond} {param} {} ",
if data.exact { "=" } else { "LIKE" }
));
builder.push_bind(if data.exact {
data.search.to_string()
} else {
format!("%{}%", data.search.as_str())
});
};
let join_user = " JOIN PackageBaseUserRoles pbur ON pb.id = pbur.base \
JOIN Users u ON pbur.user = u.id WHERE ";
match data.mode {
Mode::Url => push_search("WHERE", "p.url"),
Mode::Name => push_search("WHERE", "p.name"),
Mode::PackageBase => push_search("WHERE", "pb.name"),
Mode::Description => push_search("WHERE", "p.description"),
Mode::BaseDescription => push_search("WHERE", "pb.description"),
Mode::NameAndDescription => {
// WHERE (p.name LIKE '%search_term%' OR p.description LIKE '%search_term%')
builder.push(" WHERE p.name LIKE ");
builder.push_bind(format!("%{}%", data.search.as_str()));
builder.push(" OR p.description LIKE ");
builder.push_bind(format!("%{}%", data.search.as_str()));
}
Mode::User => {
push_search(
"WHERE EXISTS ( \
SELECT 1 \
FROM PackageBaseUserRoles pbur \
JOIN Users u ON pbur.user = u.id \
WHERE pbur.base = pb.id AND",
"u.name",
);
builder.push(" ) ");
}
Mode::Flagger => {
push_search(join_user, "u.name");
builder.push(" AND pbur.role = 4 ");
} // 4
Mode::Packager => {
push_search(join_user, "u.name");
builder.push(" AND pbur.role = 2 ");
} // 2
Mode::Submitter => {
push_search(join_user, "u.name");
builder.push(" AND pbur.role = 1 ");
} // 1
Mode::Maintainer => {
push_search(join_user, "u.name");
builder.push(" AND pbur.role = 3 ");
} // 3
}
builder.push(format_args!(
" ORDER BY {} {} LIMIT {};",
match data.order {
Order::Name => "p.name",
Order::Version => "p.version",
Order::BaseName => "pb.name",
Order::UpdatedAt => "p.updated_at",
Order::CreatedAt => "p.created_at",
},
if data.ascending { "ASC" } else { "DESC" },
data.limit
));
let mut entries = Vec::new();
let mut rows = builder.build().fetch(connection);
while let Some(row) = rows.try_next().await? {
entries.push(Entry {
id: row.try_get("id")?,
name: row.try_get("name")?,
version: row.try_get("version")?,
base_id: row.try_get("base_id")?,
base_name: row.try_get("base_name")?,
url: row.try_get("url")?,
description: row.try_get("description")?,
submitter_id: row.try_get("submitter_id")?,
submitter_name: row.try_get("submitter_name")?,
updated_at: row.try_get("updated_at")?,
created_at: row.try_get("created_at")?,
});
}
Ok(entries)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Validation;
use crate::port::search::Search;
use sqlx::MySqlPool;
#[sqlx::test]
async fn search() -> crate::Result {
let pool = MySqlPool::connect_lazy(
&std::env::var("DATABASE_URL")
.expect("environment variable `DATABASE_URL` should be set"),
)?;
let data = Data {
mode: Mode::NameAndDescription,
order: Order::UpdatedAt,
search: Search::new("f")?,
limit: 50,
exact: true,
ascending: false,
};
UserAdapter::search(&pool, data).await?;
Ok(())
}
}

View File

@ -12,7 +12,7 @@ where
for<'a> &'a E: Executor<'a, Database = MySql>,
{
}
impl<E> crate::port::CRUD<E> for UserAdapter
impl<E> crate::port::Crud<E> for UserAdapter
where
E: Send,
for<'a> &'a E: Executor<'a, Database = MySql>,

View File

@ -10,12 +10,13 @@ pub type Result<T = (), E = BoxDynError> = std::result::Result<T, E>;
pub use chrono::Utc;
pub use atomic::Atomic;
pub use connect::*;
pub use adapter::mysql::base::BaseAdapter as MySqlBaseAdapter;
pub use adapter::mysql::package::PackageAdapter as MySqlPackageAdapter;
pub use adapter::mysql::user::UserAdapter as MySqlUserAdapter;
pub use port::base::{self, Base, BaseRepository};
pub use port::package::{self, Package, PackageRepository};
pub use port::user::{self, User, UserRepository};
pub use atomic::Atomic;
pub use connect::*;
pub use port::base::{Base, BaseRepository};
pub use port::package::{Package, PackageRepository};
pub use port::user::{User, UserRepository};
pub use port::*;

View File

@ -1,13 +1,14 @@
//! Low-level repository traits for unified data access.
//!
//! No data validation besides very basic one like length violation.
use crate::Result;
//! Very mild argument validation.
use crate::{BoxDynError, Result};
pub mod base;
pub mod package;
pub mod search;
pub mod user;
pub trait CRUD<C> {
pub trait Crud<C> {
type New;
type Unique;
type Update;
@ -16,23 +17,27 @@ pub trait CRUD<C> {
fn create(
connection: &mut C,
data: Self::New,
) -> impl Future<Output = crate::Result<Self::Existing>> + Send;
) -> impl Future<Output = Result<Self::Existing>> + Send;
fn read(
connection: &C,
data: Self::Unique,
) -> impl Future<Output = crate::Result<Option<Self::Existing>>> + Send;
) -> impl Future<Output = Result<Option<Self::Existing>>> + Send;
fn update(
connection: &mut C,
existing: &mut Self::Existing,
data: Self::Update,
) -> impl Future<Output = crate::Result> + Send;
fn delete(connection: &mut C, data: Self::Unique)
-> impl Future<Output = crate::Result> + Send;
) -> impl Future<Output = Result> + Send;
fn delete(connection: &mut C, data: Self::Unique) -> impl Future<Output = Result> + Send;
}
trait CharLength {
pub trait CharLength {
fn length(&self) -> usize;
}
impl CharLength for &str {
fn length(&self) -> usize {
self.chars().count()
}
}
impl CharLength for String {
fn length(&self) -> usize {
self.chars().count()
@ -44,15 +49,43 @@ impl CharLength for Option<String> {
}
}
trait MaxLength {
trait Validatable {
type Inner: CharLength;
const MAX_LENGTH: usize;
fn encapsulate(value: Self::Inner) -> Self;
}
fn validate(value: &Self::Inner) -> Result<(), &'static str> {
#[allow(private_bounds)] // don't expose the impl details
pub trait Validation<T>: Validatable
where
T: CharLength + Into<Self::Inner>,
{
fn valid(value: &T) -> Result<(), String> {
if value.length() > Self::MAX_LENGTH {
Err("too long")
Err(format!(
"too long (length: {}, max length: {})",
value.length(),
Self::MAX_LENGTH
))
} else {
Ok(())
}
}
fn new(value: T) -> Result<Self, (T, BoxDynError)>
where
Self: Sized,
{
match Self::valid(&value) {
Ok(()) => Ok(Self::encapsulate(value.into())),
Err(e) => Err((value, e.into())),
}
}
}
impl<T, U> Validation<U> for T
where
T: Validatable,
U: CharLength + Into<T::Inner>,
{
}

View File

@ -1,10 +1,10 @@
use super::MaxLength;
use super::Validatable;
use chrono::{DateTime, Utc};
use derive_more::{Deref, Into};
pub trait BaseRepository<C>:
super::CRUD<C, New = New, Unique = u64, Update = Field, Existing = Base>
super::Crud<C, New = New, Unique = u64, Update = Field, Existing = Base>
{
}
@ -13,33 +13,21 @@ pub trait BaseRepository<C>:
#[derive(Clone, Deref, Into)]
pub struct Name(String);
impl MaxLength for Name {
impl Validatable for Name {
type Inner = String;
const MAX_LENGTH: usize = 127;
}
impl TryFrom<String> for Name {
type Error = &'static str;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::validate(&value)?;
Ok(Self(value))
fn encapsulate(value: Self::Inner) -> Self {
Self(value)
}
}
#[derive(Clone, Deref, Into)]
pub struct Description(Option<String>);
impl MaxLength for Description {
impl Validatable for Description {
type Inner = Option<String>;
const MAX_LENGTH: usize = 510;
}
impl TryFrom<Option<String>> for Description {
type Error = (Option<String>, &'static str);
fn try_from(value: Option<String>) -> Result<Self, Self::Error> {
match Self::validate(&value) {
Ok(()) => Ok(Self(value)),
Err(e) => Err((value, e)),
}
fn encapsulate(value: Self::Inner) -> Self {
Self(value)
}
}

View File

@ -1,79 +1,51 @@
use super::MaxLength;
use super::Validatable;
use crate::Base;
use chrono::{DateTime, Utc};
use derive_more::{Deref, Into};
pub trait PackageRepository<C>:
super::CRUD<C, New = New, Update = Field, Unique = Unique, Existing = Package>
super::Crud<C, New = New, Update = Field, Unique = Unique, Existing = Package>
{
}
#[derive(Clone, Deref, Into)]
pub struct Name(String);
impl MaxLength for Name {
impl Validatable for Name {
type Inner = String;
const MAX_LENGTH: usize = 127;
}
impl TryFrom<String> for Name {
type Error = (String, &'static str);
fn try_from(value: String) -> Result<Self, Self::Error> {
match Self::validate(&value) {
Ok(()) => Ok(Self(value)),
Err(e) => Err((value, e)),
}
fn encapsulate(value: Self::Inner) -> Self {
Self(value)
}
}
#[derive(Clone, Deref, Into)]
pub struct Version(String);
impl MaxLength for Version {
impl Validatable for Version {
type Inner = String;
const MAX_LENGTH: usize = 127;
}
impl TryFrom<String> for Version {
type Error = (String, &'static str);
fn try_from(value: String) -> Result<Self, Self::Error> {
match Self::validate(&value) {
Ok(()) => Ok(Self(value)),
Err(e) => Err((value, e)),
}
fn encapsulate(value: Self::Inner) -> Self {
Self(value)
}
}
#[derive(Clone, Deref, Into)]
pub struct Description(Option<String>);
impl MaxLength for Description {
impl Validatable for Description {
type Inner = Option<String>;
const MAX_LENGTH: usize = 255;
}
impl TryFrom<Option<String>> for Description {
type Error = (Option<String>, &'static str);
fn try_from(value: Option<String>) -> Result<Self, Self::Error> {
match Self::validate(&value) {
Ok(()) => Ok(Self(value)),
Err(e) => Err((value, e)),
}
fn encapsulate(value: Self::Inner) -> Self {
Self(value)
}
}
#[derive(Clone, Deref, Into)]
pub struct URL(Option<String>);
impl MaxLength for URL {
pub struct Url(Option<String>);
impl Validatable for Url {
type Inner = Option<String>;
const MAX_LENGTH: usize = 510;
}
impl TryFrom<Option<String>> for URL {
type Error = (Option<String>, &'static str);
fn try_from(value: Option<String>) -> Result<Self, Self::Error> {
match Self::validate(&value) {
Ok(()) => Ok(Self(value)),
Err(e) => Err((value, e)),
}
fn encapsulate(value: Self::Inner) -> Self {
Self(value)
}
}
@ -87,7 +59,7 @@ pub enum Field {
Name(Name),
Version(Version),
Description(Description),
URL(URL),
Url(Url),
FlaggedAt(Option<DateTime<Utc>>),
CreatedAt(DateTime<Utc>),
UpdatedAt(DateTime<Utc>),
@ -98,13 +70,13 @@ pub struct New {
pub name: Name,
pub version: Version,
pub description: Description,
pub url: URL,
pub url: Url,
pub flagged_at: Option<DateTime<Utc>>,
}
pub struct Package {
pub(crate) id: u64,
pub(crate) package_base: u64,
pub(crate) base: u64,
pub(crate) name: String,
pub(crate) version: String,
pub(crate) description: Option<String>,
@ -119,7 +91,7 @@ impl Package {
self.id
}
pub const fn package_base(&self) -> u64 {
self.package_base
self.base
}
pub const fn name(&self) -> &String {
&self.name

View File

@ -0,0 +1,69 @@
use super::Validatable;
use crate::Result;
use chrono::{DateTime, Utc};
use derive_more::{Deref, Into};
pub trait SearchRepository<C> {
fn search(connection: &C, data: Data) -> impl Future<Output = Result<Vec<Entry>>> + Send;
}
#[derive(Clone, Deref, Into)]
pub struct Search(String);
impl Validatable for Search {
type Inner = String;
const MAX_LENGTH: usize = 255;
fn encapsulate(value: Self::Inner) -> Self {
Self(value)
}
}
pub struct Data {
pub mode: Mode,
pub order: Order,
pub search: Search,
pub limit: u8,
pub exact: bool,
pub ascending: bool,
}
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct Entry {
pub id: u64,
pub name: Box<str>,
pub version: Box<str>,
pub base_id: u64,
pub base_name: Box<str>,
pub url: Option<Box<str>>,
pub description: Box<str>,
pub submitter_id: u64,
pub submitter_name: Box<str>,
pub updated_at: DateTime<Utc>,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mode {
Url,
Name,
PackageBase,
Description,
BaseDescription,
NameAndDescription,
User,
Flagger,
Packager,
Submitter,
Maintainer,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Order {
Name,
Version,
BaseName,
// Submitter,
UpdatedAt,
CreatedAt,
}

View File

@ -1,61 +1,40 @@
use super::MaxLength;
use super::Validatable;
use chrono::{DateTime, Utc};
use derive_more::{Deref, Into};
pub trait UserRepository<C>:
super::CRUD<C, New = New, Update = Field, Unique = Unique, Existing = User>
super::Crud<C, New = New, Update = Field, Unique = Unique, Existing = User>
{
}
#[derive(Clone, Deref, Into)]
pub struct Name(String);
impl MaxLength for Name {
impl Validatable for Name {
type Inner = String;
const MAX_LENGTH: usize = 31;
}
impl TryFrom<String> for Name {
type Error = (String, &'static str);
fn try_from(value: String) -> Result<Self, Self::Error> {
match Self::validate(&value) {
Ok(()) => Ok(Self(value)),
Err(e) => Err((value, e)),
}
fn encapsulate(value: Self::Inner) -> Self {
Self(value)
}
}
#[derive(Clone, Deref, Into)]
pub struct Email(String);
impl MaxLength for Email {
impl Validatable for Email {
type Inner = String;
const MAX_LENGTH: usize = 255;
}
impl TryFrom<String> for Email {
type Error = (String, &'static str);
fn try_from(value: String) -> Result<Self, Self::Error> {
match Self::validate(&value) {
Ok(()) => Ok(Self(value)),
Err(e) => Err((value, e)),
}
fn encapsulate(value: Self::Inner) -> Self {
Self(value)
}
}
#[derive(Clone, Deref, Into)]
pub struct Password(String);
impl MaxLength for Password {
impl Validatable for Password {
type Inner = String;
const MAX_LENGTH: usize = 255;
}
impl TryFrom<String> for Password {
type Error = (String, &'static str);
fn try_from(value: String) -> Result<Self, Self::Error> {
match Self::validate(&value) {
Ok(()) => Ok(Self(value)),
Err(e) => Err((value, e)),
}
fn encapsulate(value: Self::Inner) -> Self {
Self(value)
}
}
@ -81,7 +60,7 @@ pub struct New {
pub last_used: Option<DateTime<Utc>>,
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct User {
pub(crate) id: u64,
pub(crate) name: String,