Implement server entity

This commit is contained in:
fluo10 2025-05-13 08:54:15 +09:00
parent 56d839c1d2
commit 43f36b81a9
16 changed files with 189 additions and 103 deletions

3
Cargo.lock generated
View file

@ -2314,12 +2314,15 @@ dependencies = [
"chrono", "chrono",
"clap", "clap",
"progress-pile-core", "progress-pile-core",
"progress-pile-migration",
"rand 0.9.1", "rand 0.9.1",
"sea-orm", "sea-orm",
"serde", "serde",
"tempfile",
"thiserror 2.0.12", "thiserror 2.0.12",
"tokio", "tokio",
"toml", "toml",
"uuid",
] ]
[[package]] [[package]]

View file

@ -14,6 +14,7 @@ progress-pile-core = { path = "progress-pile-core" }
progress-pile-migration = { path = "progress-pile-migration", default-features = false } progress-pile-migration = { path = "progress-pile-migration", default-features = false }
progress-pile-server.path = "progress-pile-server" progress-pile-server.path = "progress-pile-server"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
tempfile = "3.20.0"
thiserror = "2.0.12" thiserror = "2.0.12"
tokio = "1.44.2" tokio = "1.44.2"
toml = "0.8.22" toml = "0.8.22"

View file

@ -23,4 +23,4 @@ toml.workspace = true
uuid.workspace = true uuid.workspace = true
[dev-dependencies] [dev-dependencies]
tempfile = "3.20.0" tempfile.workspace = true

View file

@ -4,6 +4,8 @@ pub enum Error {
Io(#[from] std::io::Error), Io(#[from] std::io::Error),
#[error("Parse int error")] #[error("Parse int error")]
ParseInt(#[from] std::num::ParseIntError), ParseInt(#[from] std::num::ParseIntError),
#[error("Password hash error")]
PasswordHash(String),
#[error("Deserialize toml error")] #[error("Deserialize toml error")]
TomlDe(#[from] toml::de::Error), TomlDe(#[from] toml::de::Error),
#[error("Serialize toml error")] #[error("Serialize toml error")]

View file

@ -11,11 +11,16 @@ async-graphql.workspace = true
axum = "0.8.4" axum = "0.8.4"
clap = {workspace = true, features = ["derive"]} clap = {workspace = true, features = ["derive"]}
progress-pile-core = {workspace = true} progress-pile-core = {workspace = true}
progress-pile-migration = { workspace = true, features = ["server"] }
chrono = {workspace = true} chrono = {workspace = true}
sea-orm = { workspace = true, features = ["sqlx-postgres"] } sea-orm = { workspace = true, features = ["sqlx-postgres"] }
serde.workspace = true serde.workspace = true
thiserror.workspace = true thiserror.workspace = true
tokio.workspace = true tokio.workspace = true
toml.workspace = true toml.workspace = true
uuid.workspace = true
rand = "0.9.1" rand = "0.9.1"
async-graphql-axum = "7.0.16" async-graphql-axum = "7.0.16"
[dev-dependencies]
tempfile.workspace = true

View file

@ -8,7 +8,7 @@ use sea_orm::entity::{
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize, SimpleObject)] #[derive(Clone, Debug, PartialEq, DeriveEntityModel,)]
#[sea_orm(table_name = "access_token")] #[sea_orm(table_name = "access_token")]
pub struct Model { pub struct Model {
#[sea_orm(primary_key)] #[sea_orm(primary_key)]

View file

@ -45,68 +45,48 @@ mod tests {
use std::time::Duration; use std::time::Duration;
use chrono::{offset, FixedOffset, Local, TimeZone}; use chrono::{offset, FixedOffset, Local, TimeZone};
use sea_orm::{entity::*, query::*, ConnectOptions, Database}; use sea_orm::{entity::*, query::*, ConnectOptions, Database};
use progress_pile_migration_server::{Migrator, MigratorTrait}; use progress_pile_migration::{ServerMigrator, MigratorTrait};
use crate::entity::*; use crate::{entity::*, global::GLOBAL};
#[tokio::test] #[tokio::test]
async fn check_database_connection() { async fn check_database_connection() {
DATABASE_CONNECTION.init_test().await;
let db = DATABASE_CONNECTION.get().unwrap(); let db = GLOBAL.get_or_init_temporary_database().await;
assert!(db.ping().await.is_ok()); assert!(db.ping().await.is_ok());
} }
#[tokio::test] #[tokio::test]
async fn check_insert_entity() { async fn check_insert_entity() {
DATABASE_CONNECTION.init_test().await; let db = GLOBAL.get_or_init_temporary_database().await;
let db = DATABASE_CONNECTION.get().unwrap();
let timestamp = Local::now().fixed_offset();
let local_date_time = Local::now();
let offset_date_time = local_date_time.with_timezone(local_date_time.offset());
let user = UserActiveModel{ let user = UserActiveModel{
login_name: Set("admin".to_owned()), login_name: Set("admin".to_owned()),
password_hash: Set("admin".to_owned()), password_hash: Set("admin".to_owned()),
created_at: Set(offset_date_time), created_at: Set(timestamp),
updated_at: Set(offset_date_time), updated_at: Set(timestamp),
..Default::default() ..UserActiveModel::new()
}.insert(db) }.insert(db).await.unwrap();
.await.unwrap();
let record_tag = RecordTagActiveModel{ let access_token = AccessTokenActiveModel{
user_id: Set(user.id), user_id: Set(user.id),
name: Set("test".to_owned()),
..Default::default() ..Default::default()
}.insert(db) }.insert(db).await.unwrap();
.await.unwrap();
let record_header = RecordHeaderActiveModel{ let progress_category = ProgressCategoryActiveModel{
user_id: Set(user.id), user_id: Set(user.id),
created_at: Set(offset_date_time), name: Set("test_category".to_string()),
updated_at: Set(offset_date_time),
recorded_at: Set(offset_date_time),
comment: Set("".to_owned()),
..Default::default() ..Default::default()
}.insert(db) }.insert(db)
.await.unwrap(); .await.unwrap();
RecordDetailActiveModel { ProgressEntryActiveModel {
record_header_id: Set(record_header.id), user_id: Set(user.id),
record_tag_id: Set(record_tag.id), progress_category_id: Set(progress_category.id),
count: Set(1),
..Default::default()
}.insert(db)
.await.unwrap();
RecordDetailActiveModel {
record_header_id: Set(record_header.id),
record_tag_id: Set(record_tag.id),
count: Set(2),
..Default::default() ..Default::default()
}.insert(db) }.insert(db)
.await.unwrap(); .await.unwrap();
Migrator::reset(db).await.unwrap();
db.clone().close().await.unwrap();
} }
} }

View file

@ -1,25 +1,29 @@
use async_graphql::*; use async_graphql::*;
use sea_orm::entity::prelude::*; use chrono::Local;
use sea_orm::{entity::prelude::*, ActiveValue::Set};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize, SimpleObject)] #[derive(Clone, Debug, PartialEq, DeriveEntityModel,)]
#[sea_orm(table_name = "record_tag")] #[sea_orm(table_name = "progress_category")]
#[graphql(concrete(name = "RecordTag", params()))]
pub struct Model { pub struct Model {
#[sea_orm(primary_key)] #[sea_orm(primary_key, auto_increment = false)]
#[serde(skip_deserializing)] pub id: Uuid,
pub id: i32, #[sea_orm(primary_key, auto_increment = false)]
#[sea_orm(indexed)]
#[serde(skip_deserializing)]
pub user_id: i32, pub user_id: i32,
#[sea_orm(indexed)] #[sea_orm(indexed)]
pub name: String, pub name: String,
#[sea_orm(indexed)]
pub created_at: DateTimeWithTimeZone,
#[sea_orm(indexed)]
pub updated_at: DateTimeWithTimeZone,
#[sea_orm(indexed)]
pub deleted_at: Option<DateTimeWithTimeZone>,
} }
#[derive(Copy, Clone, Debug, DeriveRelation, EnumIter)] #[derive(Copy, Clone, Debug, DeriveRelation, EnumIter)]
pub enum Relation { pub enum Relation {
#[sea_orm(has_many = "super::record_detail::Entity")] #[sea_orm(has_many = "super::progress_entry::Entity")]
RecordDetail, ProgressEntry,
#[sea_orm( #[sea_orm(
belongs_to = "super::user::Entity", belongs_to = "super::user::Entity",
from = "Column::UserId", from = "Column::UserId",
@ -28,9 +32,9 @@ pub enum Relation {
User, User,
} }
impl Related<super::record_detail::Entity> for Entity { impl Related<super::progress_entry::Entity> for Entity {
fn to() -> RelationDef { fn to() -> RelationDef {
Relation::RecordDetail.def() Relation::ProgressEntry.def()
} }
} }
@ -39,5 +43,16 @@ impl Related<super::user::Entity> for Entity {
Relation::User.def() Relation::User.def()
} }
} }
impl ActiveModelBehavior for ActiveModel {} impl ActiveModelBehavior for ActiveModel {}
impl ActiveModel {
pub fn new() -> Self {
let timestamp: DateTimeWithTimeZone = Local::now().fixed_offset();
Self{
id: Set(Uuid::new_v4()),
created_at: Set(timestamp),
updated_at: Set(timestamp),
..Default::default()
}
}
}

View file

@ -1,46 +1,71 @@
use async_graphql::*; use async_graphql::*;
use sea_orm::entity::prelude::*; use chrono::Local;
use sea_orm::{entity::prelude::*, ActiveValue::Set};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize, SimpleObject)] #[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "record_detail")] #[sea_orm(table_name = "progress_entry")]
#[graphql(concrete(name = "RecordDetail", params()))]
pub struct Model { pub struct Model {
#[sea_orm(primary_key)] #[sea_orm(primary_key, auto_increment = false)]
#[serde(skip_deserializing)] pub id: Uuid,
pub record_header_id: i32, #[sea_orm(primary_key, auto_increment = false)]
#[sea_orm(primary_key)] pub user_id: i32,
#[serde(skip_deserializing)] #[sea_orm(indexed)]
pub record_tag_id: i32, pub progress_category_id: Uuid,
pub count: i32, #[sea_orm(indexed)]
pub created_at: DateTimeWithTimeZone,
#[sea_orm(indexed)]
pub updated_at: DateTimeWithTimeZone,
#[sea_orm(indexed)]
pub deleted_at: Option<DateTimeWithTimeZone>,
#[sea_orm(indexed)]
pub progressed_at: DateTimeWithTimeZone,
pub quantity: i32,
pub note: String,
} }
#[derive(Copy, Clone, Debug, DeriveRelation, EnumIter)] #[derive(Copy, Clone, Debug, DeriveRelation, EnumIter)]
pub enum Relation { pub enum Relation {
#[sea_orm( #[sea_orm(
belongs_to = "super::record_header::Entity", belongs_to = "super::progress_category::Entity",
from = "Column::RecordHeaderId", from = "Column::ProgressCategoryId",
to = "super::record_header::Column::Id" to = "super::progress_category::Column::Id"
)] )]
RecordHeader, ProgressCategory,
#[sea_orm( #[sea_orm(
belongs_to = "super::record_tag::Entity", belongs_to = "super::user::Entity",
from = "Column::RecordTagId", from = "Column::UserId",
to = "super::record_tag::Column::Id" to = "super::user::Column::Id"
)] )]
RecordTag, User,
} }
impl Related<super::record_header::Entity> for Entity {
impl Related<super::progress_category::Entity> for Entity {
fn to() -> RelationDef { fn to() -> RelationDef {
Relation::RecordHeader.def() Relation::ProgressCategory.def()
} }
} }
impl Related<super::record_tag::Entity> for Entity { impl Related<super::user::Entity> for Entity {
fn to() -> RelationDef { fn to() -> RelationDef {
Relation::RecordTag.def() Relation::User.def()
} }
} }
impl ActiveModelBehavior for ActiveModel {} impl ActiveModelBehavior for ActiveModel {}
impl ActiveModel {
pub fn new() -> Self {
let timestamp: DateTimeWithTimeZone = Local::now().fixed_offset();
Self{
id: Set(Uuid::new_v4()),
created_at: Set(timestamp),
updated_at: Set(timestamp),
progressed_at: Set(timestamp),
quantity: Set(1),
note: Set("".to_string()),
..Default::default()
}
}
}

View file

@ -4,12 +4,10 @@ use sea_orm::{entity::prelude::*, ActiveValue::Set};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::error::Error; use crate::error::Error;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, SimpleObject, Deserialize)] #[derive(Clone, Debug, PartialEq, DeriveEntityModel,)]
#[sea_orm(table_name = "user")] #[sea_orm(table_name = "user")]
#[graphql(concrete(name = "User", params()))]
pub struct Model { pub struct Model {
#[sea_orm(primary_key)] #[sea_orm(primary_key)]
#[serde(skip_deserializing)]
pub id: i32, pub id: i32,
#[sea_orm(unique, indexed)] #[sea_orm(unique, indexed)]
pub login_name: String, pub login_name: String,

View file

@ -1,14 +0,0 @@
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Uninitialized OnceCell: {0}")]
UninitializedOnceCell(String),
#[error("Parse int error")]
ParseInt(#[from] std::num::ParseIntError),
#[error("Argon2 Password hash error: {0}")]
PasswordHash(String),
#[error("Parse toml error")]
TomlDe(#[from] toml::de::Error),
#[error("Missing config value: ({0})")]
MissingConfig(String)
}

View file

@ -0,0 +1,57 @@
use progress_pile_migration::{ServerMigrator, MigratorTrait};
use sea_orm::{ConnectOptions, Database, DatabaseConnection};
use crate::error::Error;
use tokio::sync::OnceCell;
use progress_pile_core::global::GlobalDatabase;
use super::Global;
impl GlobalDatabase for Global {
fn get_database(&self) -> Option<&DatabaseConnection> {
self.database.get()
}
async fn get_or_try_init_database(&self) -> Result<&DatabaseConnection, Error> {
todo!()
}
async fn get_or_try_init_database_with_connect_options<T>(&self, options: T) -> Result<&DatabaseConnection, Error> where
T: Into<ConnectOptions>,
{
Ok(self.database.get_or_try_init(|| async {
let db = Database::connect(options).await?;
ServerMigrator::up(&db, None).await?;
Ok::<DatabaseConnection, Error>(db)
}).await?)
}
}
#[cfg(test)]
pub mod tests {
use std::sync::LazyLock;
use crate::global::GLOBAL;
use super::*;
pub static TEST_DATABASE_URL: LazyLock<String> = LazyLock::new(|| {
let mut temp_path = tempfile::NamedTempFile::new().unwrap().into_temp_path();
temp_path.disable_cleanup(true);
let url = "sqlite://".to_string() + temp_path.as_os_str().to_str().unwrap() + "?mode=rwc";
println!("{}", &url);
url
});
impl Global {
pub async fn get_or_init_temporary_database(&self) -> &DatabaseConnection {
self.get_or_try_init_database_with_connect_options(&*TEST_DATABASE_URL).await.unwrap()
}
}
#[tokio::test]
async fn connect_database () {
let db = GLOBAL.get_or_init_temporary_database().await;
assert!(db.ping().await.is_ok());
}
}

View file

@ -0,0 +1,15 @@
use crate::config::ServerConfig;
use sea_orm::DatabaseConnection;
use tokio::sync::OnceCell;
mod database;
pub static GLOBAL: Global = Global{
config: OnceCell::const_new(),
database: OnceCell::const_new(),
};
pub struct Global {
config: OnceCell<ServerConfig>,
database: OnceCell<DatabaseConnection>,
}

View file

@ -1,5 +1,4 @@
use async_graphql::*; use async_graphql::*;
use progress_pile_core::entity::UserModel;
use crate::{auth::try_hash_password, entity::UserActiveModel}; use crate::{auth::try_hash_password, entity::UserActiveModel};
@ -10,7 +9,7 @@ impl Mutation {
async fn login(&self, username:String, password: String) -> Result<String> { async fn login(&self, username:String, password: String) -> Result<String> {
todo!() todo!()
} }
async fn create_user(&self, username:String, password: String) -> Result<UserModel> { async fn create_user(&self, username:String, password: String) -> Result<String> {
todo!() todo!()
} }
} }

View file

@ -13,10 +13,10 @@ pub struct Query;
#[Object] #[Object]
impl Query { impl Query {
pub async fn user(&self, user_name: String) -> Result<Option<UserModel>> { pub async fn user(&self, user_name: String) -> Result<Option<String>> {
Ok(UserEntity::find_by_name(&user_name).await?) todo!()
} }
pub async fn users(&self) -> Result<Vec<UserModel>> { pub async fn users(&self) -> Result<Vec<String>> {
Ok(UserEntity::find_all().await?) todo!()
} }
} }

View file

@ -2,8 +2,9 @@ mod args;
mod auth; mod auth;
mod config; mod config;
pub mod entity; pub mod entity;
pub mod error; pub mod global;
pub mod graphql; pub mod graphql;
pub use progress_pile_core::error;
pub use args::Args; pub use args::Args;
use async_graphql::{EmptySubscription, Schema}; use async_graphql::{EmptySubscription, Schema};
@ -13,7 +14,6 @@ use async_graphql_axum::{
}; };
use axum::{routing::get, Router}; use axum::{routing::get, Router};
use crate::graphql::build_service; use crate::graphql::build_service;
use progress_pile_core::entity as entity;
pub fn build_app() -> Router { pub fn build_app() -> Router {