1
0

Reorganize & implement Authentication view api

This commit is contained in:
2025-02-02 22:41:32 +02:00
parent 038dec0197
commit f65312209c
69 changed files with 1089 additions and 343 deletions

View File

@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "INSERT INTO PackageBases (name, description, created_at, updated_at) VALUES (?, ?, ?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 4
},
"nullable": []
},
"hash": "014cf2ec55142a17047ad7c469685df75ae8e3c95a1a7c6c21be7b5624a82ae1"
}

View File

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

View File

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

View File

@ -0,0 +1,84 @@
{
"db_name": "MySQL",
"query": "SELECT * FROM Users WHERE email = ?",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": {
"type": "Long",
"flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT",
"max_size": 10
}
},
{
"ordinal": 1,
"name": "name",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE",
"max_size": 124
}
},
{
"ordinal": 2,
"name": "email",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE",
"max_size": 1020
}
},
{
"ordinal": 3,
"name": "password",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | NO_DEFAULT_VALUE",
"max_size": 1020
}
},
{
"ordinal": 4,
"name": "last_used",
"type_info": {
"type": "Timestamp",
"flags": "BINARY",
"max_size": 19
}
},
{
"ordinal": 5,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP",
"max_size": 19
}
},
{
"ordinal": 6,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP",
"max_size": 19
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
false,
true,
false,
false
]
},
"hash": "0bb7353d64231dc12416f5504d94513493670e3f2ae017d87a2f0c3eca045f60"
}

View File

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

View File

@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "DELETE FROM PackageBases WHERE id = ?",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
},
"hash": "389e38e7e0a0b7d9ba667ac148a0a468da889a3455c47325938b819ab41ef4c8"
}

View File

@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "DELETE FROM Users WHERE name = ?",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
},
"hash": "404747d44ee859e8c967695c29963594bb8273e66c053934ec20d5fc3db9d41e"
}

View File

@ -0,0 +1,84 @@
{
"db_name": "MySQL",
"query": "SELECT * FROM Users WHERE name = ?",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": {
"type": "Long",
"flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT",
"max_size": 10
}
},
{
"ordinal": 1,
"name": "name",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE",
"max_size": 124
}
},
{
"ordinal": 2,
"name": "email",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE",
"max_size": 1020
}
},
{
"ordinal": 3,
"name": "password",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | NO_DEFAULT_VALUE",
"max_size": 1020
}
},
{
"ordinal": 4,
"name": "last_used",
"type_info": {
"type": "Timestamp",
"flags": "BINARY",
"max_size": 19
}
},
{
"ordinal": 5,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP",
"max_size": 19
}
},
{
"ordinal": 6,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP",
"max_size": 19
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
false,
true,
false,
false
]
},
"hash": "68ed36ae997fff190b4b15b80bf24b553d8ac922da251d9e8b8f4e897bab46b0"
}

View File

@ -0,0 +1,104 @@
{
"db_name": "MySQL",
"query": "SELECT * FROM Packages WHERE id = ?",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": {
"type": "Long",
"flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT",
"max_size": 10
}
},
{
"ordinal": 1,
"name": "package_base",
"type_info": {
"type": "Long",
"flags": "NOT_NULL | MULTIPLE_KEY | UNSIGNED | NO_DEFAULT_VALUE",
"max_size": 10
}
},
{
"ordinal": 2,
"name": "name",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE",
"max_size": 508
}
},
{
"ordinal": 3,
"name": "version",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | NO_DEFAULT_VALUE",
"max_size": 508
}
},
{
"ordinal": 4,
"name": "description",
"type_info": {
"type": "VarString",
"flags": "",
"max_size": 1020
}
},
{
"ordinal": 5,
"name": "url",
"type_info": {
"type": "VarString",
"flags": "",
"max_size": 2040
}
},
{
"ordinal": 6,
"name": "flagged_at",
"type_info": {
"type": "Timestamp",
"flags": "BINARY",
"max_size": 19
}
},
{
"ordinal": 7,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP",
"max_size": 19
}
},
{
"ordinal": 8,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP | ON_UPDATE_NOW",
"max_size": 19
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
false,
true,
true,
true,
false,
false
]
},
"hash": "695f4b0a4286cf625dc60dc3dfc4a9cd92aaea3ea58ef8702903983cfc32ab47"
}

View File

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

View File

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

View File

@ -0,0 +1,64 @@
{
"db_name": "MySQL",
"query": "SELECT * FROM PackageBases WHERE id = ?",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": {
"type": "Long",
"flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT",
"max_size": 10
}
},
{
"ordinal": 1,
"name": "name",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE",
"max_size": 508
}
},
{
"ordinal": 2,
"name": "description",
"type_info": {
"type": "VarString",
"flags": "",
"max_size": 2040
}
},
{
"ordinal": 3,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP",
"max_size": 19
}
},
{
"ordinal": 4,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP | ON_UPDATE_NOW",
"max_size": 19
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
true,
false,
false
]
},
"hash": "839cea68f9de889f35a0d0ad0b48b4a0dc1af49f0f0e7bb12238d22a9c37fbbc"
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,84 @@
{
"db_name": "MySQL",
"query": "SELECT * FROM Users WHERE id = ?",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": {
"type": "Long",
"flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT",
"max_size": 10
}
},
{
"ordinal": 1,
"name": "name",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE",
"max_size": 124
}
},
{
"ordinal": 2,
"name": "email",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE",
"max_size": 1020
}
},
{
"ordinal": 3,
"name": "password",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | NO_DEFAULT_VALUE",
"max_size": 1020
}
},
{
"ordinal": 4,
"name": "last_used",
"type_info": {
"type": "Timestamp",
"flags": "BINARY",
"max_size": 19
}
},
{
"ordinal": 5,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP",
"max_size": 19
}
},
{
"ordinal": 6,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP",
"max_size": 19
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
false,
true,
false,
false
]
},
"hash": "8e3ffe0d11d3eb38cd805771cd133588c0679404a68a8041f414553226abeeb2"
}

View File

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

View File

@ -0,0 +1,104 @@
{
"db_name": "MySQL",
"query": "SELECT * FROM Packages WHERE name = ?",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": {
"type": "Long",
"flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT",
"max_size": 10
}
},
{
"ordinal": 1,
"name": "package_base",
"type_info": {
"type": "Long",
"flags": "NOT_NULL | MULTIPLE_KEY | UNSIGNED | NO_DEFAULT_VALUE",
"max_size": 10
}
},
{
"ordinal": 2,
"name": "name",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE",
"max_size": 508
}
},
{
"ordinal": 3,
"name": "version",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | NO_DEFAULT_VALUE",
"max_size": 508
}
},
{
"ordinal": 4,
"name": "description",
"type_info": {
"type": "VarString",
"flags": "",
"max_size": 1020
}
},
{
"ordinal": 5,
"name": "url",
"type_info": {
"type": "VarString",
"flags": "",
"max_size": 2040
}
},
{
"ordinal": 6,
"name": "flagged_at",
"type_info": {
"type": "Timestamp",
"flags": "BINARY",
"max_size": 19
}
},
{
"ordinal": 7,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP",
"max_size": 19
}
},
{
"ordinal": 8,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP | ON_UPDATE_NOW",
"max_size": 19
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
false,
true,
true,
true,
false,
false
]
},
"hash": "944eb40633e943a75244dee639fe6efb16919aff7172189c81240cb12462ae58"
}

View File

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

View File

@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "DELETE FROM Users WHERE id = ?",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
},
"hash": "9fa86328c40ce0469f755efb4876010092b7bc9f240a5d43dc69f9d0b1b5b7ce"
}

View File

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

View File

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

View File

@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "DELETE FROM Users WHERE email = ?",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
},
"hash": "c2b00adbcb3c35a6ffa6b2bce08a738a9b3cd1ca4aa4c843909c7e14f7ef3e06"
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "DELETE FROM Packages WHERE id = ?",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
},
"hash": "d474dd848d0ef8832afd4d1302fa562a3c4a4569032e8636d664043b5dc96661"
}

View File

@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "INSERT INTO Users (name, email, password, last_used, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 6
},
"nullable": []
},
"hash": "daf98e6f1013c4993f7329f6fa690e92bccd89d1ff90131719c40626088dabd1"
}

View File

@ -0,0 +1,12 @@
{
"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

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

View File

@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "DELETE FROM Packages WHERE name = ?",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
},
"hash": "f4963ad77bcbc0af4fc929f1f66b7ee842c26c44da32ae9bbbc06c466a908ccf"
}

View File

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

21
src/data/Cargo.toml Normal file
View File

@ -0,0 +1,21 @@
[package]
name = "data"
version = "0.1.0"
edition = "2024"
[dependencies]
derive_more = { version = "1.0.0", features = ["deref", "into"] }
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",
] }
# thiserror = "2.0.11"
# garde = { version = "0.22.0", features = ["email", "url", "derive"] }

2
src/data/src/adapter.rs Normal file
View File

@ -0,0 +1,2 @@
//! Specific implementations of [`crate::port`]s to plug into other parts of the application.
pub mod mysql;

View File

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

View File

@ -0,0 +1,105 @@
use crate::Result;
use crate::port::base::{Base, BaseRepository, Field, New};
use chrono::Utc;
use sqlx::{Executor, MySql};
pub struct BaseAdapter;
impl<E> BaseRepository<E> for BaseAdapter
where
E: Send,
for<'a> &'a E: Executor<'a, Database = MySql>,
{
}
impl<E> crate::port::CRUD<E> for BaseAdapter
where
E: Send,
for<'a> &'a E: Executor<'a, Database = MySql>,
{
type New = New;
type Unique = u64;
type Update = Field;
type Existing = Base;
async fn create(connection: &mut E, data: Self::New) -> Result<Self::Existing> {
let created_at = Utc::now();
let id = sqlx::query!(
"INSERT INTO PackageBases (name, description, created_at, updated_at) VALUES (?, ?, ?, ?)",
data.name.as_str(),
data.description.as_ref(),
created_at, created_at,
)
.execute(&*connection)
.await?
.last_insert_id();
Ok(Self::Existing {
id,
name: data.name.into(),
description: data.description.into(),
created_at,
updated_at: created_at,
})
}
async fn read(connection: &E, data: Self::Unique) -> Result<Option<Self::Existing>> {
Ok(
sqlx::query_as!(Base, "SELECT * FROM PackageBases WHERE id = ?", data)
.fetch_optional(connection)
.await?,
)
}
async fn update(
connection: &mut E,
existing: &mut Self::Existing,
data: Self::Update,
) -> Result {
match &data {
Field::Name(name) => {
sqlx::query!(
"UPDATE PackageBases SET name = ? WHERE id = ?",
name.as_str(),
existing.id
)
}
Field::Description(description) => {
sqlx::query!(
"UPDATE PackageBases SET description = ? WHERE id = ?",
description.as_ref(),
existing.id
)
}
Field::CreatedAt(date_time) => sqlx::query!(
"UPDATE PackageBases SET created_at = ? WHERE id = ?",
date_time,
existing.id
),
Field::UpdatedAt(date_time) => sqlx::query!(
"UPDATE PackageBases SET updated_at = ? WHERE id = ?",
date_time,
existing.id
),
}
.execute(&*connection)
.await?;
match data {
Field::Name(s) => existing.name = s.into(),
Field::Description(o) => existing.description = o.into(),
Field::CreatedAt(date_time) => existing.created_at = date_time,
Field::UpdatedAt(date_time) => existing.updated_at = date_time,
}
Ok(())
}
async fn delete(connection: &mut E, data: Self::Unique) -> Result {
sqlx::query!("DELETE FROM PackageBases WHERE id = ?", data)
.execute(&*connection)
.await?;
Ok(())
}
}

View File

@ -0,0 +1,162 @@
use crate::Result;
use crate::port::package::{Field, New, Package, PackageRepository, Unique};
use chrono::Utc;
use sqlx::{Executor, MySql};
pub struct PackageAdapter;
impl<E> PackageRepository<E> for PackageAdapter
where
E: Send,
for<'a> &'a E: Executor<'a, Database = MySql>,
{
}
impl<E> crate::port::CRUD<E> for PackageAdapter
where
E: Send,
for<'a> &'a E: Executor<'a, Database = MySql>,
{
type New = New;
type Update = Field;
type Unique = Unique;
type Existing = Package;
async fn create(connection: &mut E, data: Self::New) -> Result<Self::Existing> {
let created_at = Utc::now();
let id = sqlx::query!(
"INSERT INTO Packages \
(package_base, name, version, description, url, flagged_at, created_at, updated_at) \
VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
data.package_base.id,
data.name.as_str(),
data.version.as_str(),
data.description.as_ref(),
data.url.as_ref(),
data.flagged_at,
created_at,
created_at,
)
.execute(&*connection)
.await?
.last_insert_id();
Ok(Self::Existing {
id,
package_base: data.package_base.id,
name: data.name.into(),
version: data.version.into(),
description: data.description.into(),
url: data.url.into(),
flagged_at: data.flagged_at,
created_at,
updated_at: created_at,
})
}
async fn read(connection: &E, data: Self::Unique) -> Result<Option<Self::Existing>> {
Ok(match data {
Unique::Id(id) => {
sqlx::query_as!(Package, "SELECT * FROM Packages WHERE id = ?", id)
.fetch_optional(connection)
.await
}
Unique::Name(name) => {
sqlx::query_as!(
Package,
"SELECT * FROM Packages WHERE name = ?",
name.as_str()
)
.fetch_optional(connection)
.await
}
}?)
}
async fn update(
connection: &mut E,
existing: &mut Self::Existing,
data: Self::Update,
) -> Result {
match &data {
Field::Name(name) => {
sqlx::query!(
"UPDATE Packages SET name = ? WHERE id = ?",
name.as_str(),
existing.id
)
}
Field::PackageBase(package_base) => {
sqlx::query!(
"UPDATE Packages SET package_base = ? WHERE id = ?",
package_base.id,
existing.id
)
}
Field::Version(version) => {
sqlx::query!(
"UPDATE Packages SET version = ? WHERE id = ?",
version.as_str(),
existing.id
)
}
Field::Description(description) => {
sqlx::query!(
"UPDATE Packages SET description = ? WHERE id = ?",
description.as_ref(),
existing.id
)
}
Field::URL(url) => {
sqlx::query!(
"UPDATE Packages SET url = ? WHERE id = ?",
url.as_ref(),
existing.id
)
}
Field::FlaggedAt(date_time) => sqlx::query!(
"UPDATE Packages SET flagged_at = ? WHERE id = ?",
date_time,
existing.id
),
Field::CreatedAt(date_time) => sqlx::query!(
"UPDATE Packages SET created_at = ? WHERE id = ?",
date_time,
existing.id
),
Field::UpdatedAt(date_time) => sqlx::query!(
"UPDATE Packages SET updated_at = ? WHERE id = ?",
date_time,
existing.id
),
}
.execute(&*connection)
.await?;
match data {
Field::Name(s) => existing.name = s.into(),
Field::PackageBase(s) => existing.package_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::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,
}
Ok(())
}
async fn delete(connection: &mut E, data: Self::Unique) -> Result {
match data {
Unique::Id(id) => sqlx::query!("DELETE FROM Packages WHERE id = ?", id),
Unique::Name(name) => {
sqlx::query!("DELETE FROM Packages WHERE name = ?", name.as_str())
}
}
.execute(&*connection)
.await?;
Ok(())
}
}

View File

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

43
src/data/src/atomic.rs Normal file
View File

@ -0,0 +1,43 @@
//! Unify transaction management for established connections.
use crate::Result;
pub trait Atomic {
type Transaction<'a>;
fn start_transaction(&mut self) -> impl Future<Output = Result<Self::Transaction<'_>>> + Send;
fn abort_transaction(transaction: Self::Transaction<'_>)
-> impl Future<Output = Result> + Send;
fn commit_transaction(
transaction: Self::Transaction<'_>,
) -> impl Future<Output = Result> + Send;
}
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)
}
}

53
src/data/src/connect.rs Normal file
View File

@ -0,0 +1,53 @@
//! Driver to manage a connection which is passed to adapters.
use crate::Result;
pub trait Connect {
type Connection;
fn open_connection(&self) -> impl Future<Output = Result<Self::Connection>> + Send;
fn close_connection(connection: Self::Connection) -> impl Future<Output = Result> + Send;
}
use sqlx::Connection;
pub use sqlx::MySqlConnection as SqlxConnection;
pub use sqlx::MySqlPool as SqlxPool;
#[derive(Clone)]
pub struct MySqlPool {
pool: SqlxPool,
}
impl MySqlPool {
pub const fn new(pool: SqlxPool) -> Self {
Self { pool }
}
}
impl Connect for MySqlPool {
type Connection = SqlxPool;
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 MySqlConnection {
pub const fn new(link: String) -> Self {
Self { link }
}
}
impl Connect for MySqlConnection {
type Connection = SqlxConnection;
async fn open_connection(&self) -> Result<Self::Connection> {
SqlxConnection::connect(&self.link).await.map_err(Box::from)
}
async fn close_connection(connection: Self::Connection) -> Result {
connection.close().await?;
Ok(())
}
}

21
src/data/src/lib.rs Normal file
View File

@ -0,0 +1,21 @@
//! Data access for the application.
pub mod adapter;
pub mod atomic;
pub mod connect;
pub mod port;
// Don't want to handle errors for dynamic mess.
pub type BoxDynError = Box<dyn std::error::Error + Send + Sync + 'static>;
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};

58
src/data/src/port.rs Normal file
View File

@ -0,0 +1,58 @@
//! Low-level repository traits for unified data access.
//!
//! No data validation besides very basic one like length violation.
use crate::Result;
pub mod base;
pub mod package;
pub mod user;
pub trait CRUD<C> {
type New;
type Unique;
type Update;
type Existing;
fn create(
connection: &mut C,
data: Self::New,
) -> impl Future<Output = crate::Result<Self::Existing>> + Send;
fn read(
connection: &C,
data: Self::Unique,
) -> impl Future<Output = crate::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;
}
trait CharLength {
fn length(&self) -> usize;
}
impl CharLength for String {
fn length(&self) -> usize {
self.chars().count()
}
}
impl CharLength for Option<String> {
fn length(&self) -> usize {
self.as_ref().map_or(0, CharLength::length)
}
}
trait MaxLength {
type Inner: CharLength;
const MAX_LENGTH: usize;
fn validate(value: &Self::Inner) -> Result<(), &'static str> {
if value.length() > Self::MAX_LENGTH {
Err("too long")
} else {
Ok(())
}
}
}

82
src/data/src/port/base.rs Normal file
View File

@ -0,0 +1,82 @@
use super::MaxLength;
use chrono::{DateTime, Utc};
use derive_more::{Deref, Into};
pub trait BaseRepository<C>:
super::CRUD<C, New = New, Unique = u64, Update = Field, Existing = Base>
{
}
// #[derive(Deref, Into, Clone, Copy)]
// pub struct Id(pub(crate) u64);
#[derive(Clone, Deref, Into)]
pub struct Name(String);
impl MaxLength 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))
}
}
#[derive(Clone, Deref, Into)]
pub struct Description(Option<String>);
impl MaxLength 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)),
}
}
}
pub enum Field {
Name(Name),
Description(Description),
CreatedAt(DateTime<Utc>),
UpdatedAt(DateTime<Utc>),
}
pub struct New {
pub name: Name,
pub description: Description,
}
pub struct Base {
pub(crate) id: u64,
pub(crate) name: String,
pub(crate) description: Option<String>,
pub(crate) created_at: DateTime<Utc>,
pub(crate) updated_at: DateTime<Utc>,
}
impl Base {
pub const fn id(&self) -> u64 {
self.id
}
pub const fn name(&self) -> &String {
&self.name
}
pub const fn description(&self) -> Option<&String> {
self.description.as_ref()
}
pub const fn created_at(&self) -> DateTime<Utc> {
self.created_at
}
pub const fn updated_at(&self) -> DateTime<Utc> {
self.updated_at
}
}

View File

@ -0,0 +1,145 @@
use super::MaxLength;
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>
{
}
#[derive(Clone, Deref, Into)]
pub struct Name(String);
impl MaxLength 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)),
}
}
}
#[derive(Clone, Deref, Into)]
pub struct Version(String);
impl MaxLength 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)),
}
}
}
#[derive(Clone, Deref, Into)]
pub struct Description(Option<String>);
impl MaxLength 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)),
}
}
}
#[derive(Clone, Deref, Into)]
pub struct URL(Option<String>);
impl MaxLength 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)),
}
}
}
pub enum Unique {
Id(u64),
Name(Name),
}
pub enum Field {
PackageBase(Base),
Name(Name),
Version(Version),
Description(Description),
URL(URL),
FlaggedAt(Option<DateTime<Utc>>),
CreatedAt(DateTime<Utc>),
UpdatedAt(DateTime<Utc>),
}
pub struct New {
pub package_base: Base,
pub name: Name,
pub version: Version,
pub description: Description,
pub url: URL,
pub flagged_at: Option<DateTime<Utc>>,
}
pub struct Package {
pub(crate) id: u64,
pub(crate) package_base: u64,
pub(crate) name: String,
pub(crate) version: String,
pub(crate) description: Option<String>,
pub(crate) url: Option<String>,
pub(crate) flagged_at: Option<DateTime<Utc>>,
pub(crate) created_at: DateTime<Utc>,
pub(crate) updated_at: DateTime<Utc>,
}
impl Package {
pub const fn id(&self) -> u64 {
self.id
}
pub const fn package_base(&self) -> u64 {
self.package_base
}
pub const fn name(&self) -> &String {
&self.name
}
pub const fn version(&self) -> &String {
&self.version
}
pub const fn description(&self) -> Option<&String> {
self.description.as_ref()
}
pub const fn url(&self) -> Option<&String> {
self.url.as_ref()
}
pub const fn flagged_at(&self) -> Option<DateTime<Utc>> {
self.flagged_at
}
pub const fn created_at(&self) -> DateTime<Utc> {
self.created_at
}
pub const fn updated_at(&self) -> DateTime<Utc> {
self.updated_at
}
}

117
src/data/src/port/user.rs Normal file
View File

@ -0,0 +1,117 @@
use super::MaxLength;
use chrono::{DateTime, Utc};
use derive_more::{Deref, Into};
pub trait UserRepository<C>:
super::CRUD<C, New = New, Update = Field, Unique = Unique, Existing = User>
{
}
#[derive(Clone, Deref, Into)]
pub struct Name(String);
impl MaxLength 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)),
}
}
}
#[derive(Clone, Deref, Into)]
pub struct Email(String);
impl MaxLength 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)),
}
}
}
#[derive(Clone, Deref, Into)]
pub struct Password(String);
impl MaxLength 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)),
}
}
}
pub enum Unique {
Id(u64),
Name(Name),
Email(Email),
}
pub enum Field {
Name(Name),
Email(Email),
Password(Password),
LastUsed(Option<DateTime<Utc>>),
CreatedAt(DateTime<Utc>),
UpdatedAt(DateTime<Utc>),
}
pub struct New {
pub name: Name,
pub email: Email,
pub password: Password,
pub last_used: Option<DateTime<Utc>>,
}
#[derive(Debug)]
pub struct User {
pub(crate) id: u64,
pub(crate) name: String,
pub(crate) email: String,
pub(crate) password: String,
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) -> u64 {
self.id
}
pub const fn name(&self) -> &String {
&self.name
}
pub const fn email(&self) -> &String {
&self.email
}
pub const fn password(&self) -> &String {
&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
}
}