Implement migration and entity for client

This commit is contained in:
fluo10 2025-05-10 17:55:15 +09:00
parent c88605e5d6
commit 47da8152e6
21 changed files with 215 additions and 355 deletions

22
Cargo.lock generated
View file

@ -2257,14 +2257,19 @@ dependencies = [
name = "progress-pile-client"
version = "0.1.0"
dependencies = [
"async-graphql",
"chrono",
"chrono-tz",
"clap",
"dirs",
"progress-pile-core",
"progress-pile-migration",
"sea-orm",
"serde",
"thiserror 2.0.12",
"tokio",
"toml",
"uuid",
]
[[package]]
@ -2630,6 +2635,7 @@ dependencies = [
"thiserror 2.0.12",
"tracing",
"url",
"uuid",
]
[[package]]
@ -2689,6 +2695,7 @@ dependencies = [
"inherent",
"ordered-float",
"sea-query-derive",
"uuid",
]
[[package]]
@ -2700,6 +2707,7 @@ dependencies = [
"chrono",
"sea-query",
"sqlx",
"uuid",
]
[[package]]
@ -2973,6 +2981,7 @@ dependencies = [
"tokio-stream",
"tracing",
"url",
"uuid",
"webpki-roots",
]
@ -3055,6 +3064,7 @@ dependencies = [
"stringprep",
"thiserror 2.0.12",
"tracing",
"uuid",
"whoami",
]
@ -3093,6 +3103,7 @@ dependencies = [
"stringprep",
"thiserror 2.0.12",
"tracing",
"uuid",
"whoami",
]
@ -3119,6 +3130,7 @@ dependencies = [
"thiserror 2.0.12",
"tracing",
"url",
"uuid",
]
[[package]]
@ -3594,6 +3606,16 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
dependencies = [
"getrandom 0.3.2",
"serde",
]
[[package]]
name = "value-bag"
version = "1.11.1"

View file

@ -17,6 +17,7 @@ serde = { version = "1.0", features = ["derive"] }
thiserror = "2.0.12"
tokio = "1.44.2"
toml = "0.8.22"
uuid = { version = "1.16.0", features = [ "serde", "v4" ] }
[workspace.dependencies.sea-orm]
version = "1.1"
@ -26,6 +27,7 @@ features = [
"runtime-tokio-native-tls",
"sqlx-sqlite",
"with-chrono",
"with-uuid",
]
default-features = false

View file

@ -8,11 +8,16 @@ default = ["clap"]
clap = ["dep:clap"]
[dependencies]
async-graphql.workspace = true
chrono.workspace = true
chrono-tz.workspace = true
clap = { workspace = true, optional = true }
dirs.workspace = true
progress-pile-core.workspace = true
progress-pile-migration = { workspace = true, features = ["client"]}
sea-orm.workspace = true
serde.workspace = true
thiserror.workspace = true
tokio.workspace = true
toml.workspace = true
uuid.workspace = true

View file

@ -0,0 +1,49 @@
mod progress_category;
mod progress_entry;
pub use progress_category::{
ActiveModel as ProgressCategoryActiveModel,
Column as ProgressCategoryColumn,
Entity as ProgressCategoryEntity,
Model as ProgressCategoryModel,
};
pub use progress_entry::{
ActiveModel as ProgressEntryActiveModel,
Column as ProgressEntryColumn,
Entity as ProgressEntryEntity,
Model as ProgressEntryModel,
};
#[cfg(test)]
mod tests {
use super::*;
use chrono::Local;
use sea_orm::entity::*;
use progress_pile_migration::{ClientMigrator, MigratorTrait};
use uuid::Uuid;
use crate::database_connection::DATABASE_CONNECTION;
#[tokio::test]
async fn check_insert_entity() {
let db = DATABASE_CONNECTION.get_or_init("sqlite::memory:").await;
ClientMigrator::up(db, None).await.unwrap();
let local_date_time = Local::now();
let offset_date_time = local_date_time.with_timezone(local_date_time.offset());
let category = ProgressCategoryActiveModel{
name: Set("test_category".to_owned()),
..ProgressCategoryActiveModel::new()
}.insert(db).await.unwrap();
let entry1= ProgressEntryActiveModel {
progress_category_id: Set(category.id),
..ProgressEntryActiveModel::new()
}.insert(db).await.unwrap();
ClientMigrator::reset(db).await.unwrap();
//db.clone().close().await.unwrap();
}
}

View file

@ -0,0 +1,52 @@
use core::time;
use async_graphql::*;
use chrono::Local;
use sea_orm::entity::{
*,
prelude::*
};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "progress_category")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: Uuid,
#[sea_orm(indexed)]
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)]
pub enum Relation {
#[sea_orm(has_many = "super::ProgressEntryEntity")]
ProgressEntry,
}
impl Related<super::ProgressEntryEntity> for Entity {
fn to() -> RelationDef {
Relation::ProgressEntry.def()
}
}
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

@ -0,0 +1,62 @@
use async_graphql::*;
use chrono::Local;
use sea_orm::entity::{
*,
prelude::*
};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize,)]
#[sea_orm(table_name = "progress_entry")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: Uuid,
#[sea_orm(indexed)]
pub progress_category_id: Uuid,
#[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)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::ProgressCategoryEntity",
from = "Column::ProgressCategoryId",
to = "super::ProgressCategoryColumn::Id"
)]
ProgressCategory,
}
impl Related<super::progress_category::Entity> for Entity {
fn to() -> RelationDef {
Relation::ProgressCategory.def()
}
}
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

@ -1,7 +1,8 @@
pub mod auth;
pub mod config;
pub mod entity;
pub use progress_pile_core::error;
pub use progress_pile_core::*;

View file

@ -18,7 +18,6 @@ clap = { workspace = true, optional = true }
csv = "1.3.1"
cynic = "3.10.0"
dotenv = {workspace = true}
progress-pile-migration-server.workspace = true
iana-time-zone = "0.1.63"
log = "0.4.27"
sea-orm.workspace = true

View file

@ -6,7 +6,7 @@ use sea_orm::ConnectOptions;
use serde::Deserialize;
use tokio::sync::OnceCell;
use crate::Error;
use crate::error::Error;
#[derive(Clone, Debug, Deserialize, PartialEq)]
pub struct DatabaseConfig {

View file

@ -4,7 +4,7 @@ use clap::Args;
use serde::{Deserialize, Serialize};
use tokio::sync::OnceCell;
use crate::Error;
use crate::error::Error;
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]

View file

@ -1,9 +1,5 @@
use chrono::{DateTime, NaiveDateTime};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::{
entity::RecordDetailModel,
error::Error,
};
#[derive(Debug, Deserialize, PartialEq, Serialize)]
pub struct CsvRecord{
@ -12,10 +8,3 @@ pub struct CsvRecord{
pub tag: String,
pub count: i32,
}
impl TryFrom<RecordDetailModel> for CsvRecord{
type Error = Error;
fn try_from(model: RecordDetailModel) -> Result<Self, Self::Error> {
todo!()
}
}

View file

@ -4,7 +4,6 @@ use crate::config::{
};
use std::time::Duration;
use sea_orm::{entity::*, query::*, ConnectOptions, Database, DatabaseConnection};
use progress_pile_migration_server::{Migrator, MigratorTrait};
use tokio::sync::OnceCell;
@ -21,44 +20,14 @@ impl OnceDatabaseConnection {
pub fn get(&self) -> Option<&DatabaseConnection> {
self.inner.get()
}
pub async fn get_or_init<F, T>(&self, f: F) -> &DatabaseConnection where
F: FnOnce() -> T,
T: Future<Output = DatabaseConnection>
{
self.inner.get_or_init(f).await
}
pub async fn get_or_init_with_server_config(&self, c: &DatabaseConfig) -> &DatabaseConnection {
self.get_or_init( || async {
let db = Database::connect(c).await.unwrap();
Migrator::fresh(&db).await.unwrap();
db
pub async fn get_or_init<T>(&self, c: T) -> &DatabaseConnection where
T: Into<ConnectOptions>
{
self.inner.get_or_init(|| async {
Database::connect(c).await.unwrap()
}).await
}
pub async fn get_or_init_with_static_server_config(&self) -> &DatabaseConnection {
self.get_or_init_with_server_config(DATABASE_CONFIG.get().unwrap()).await
}
#[cfg(test)]
pub async fn init_test(&self) {
self.get_or_init( || async {
let mut opt = ConnectOptions::new("sqlite::memory:");
opt.max_connections(100)
.min_connections(5)
.connect_timeout(Duration::from_secs(8))
.acquire_timeout(Duration::from_secs(8))
.idle_timeout(Duration::from_secs(8))
.max_lifetime(Duration::from_secs(8))
.sqlx_logging(true)
.sqlx_logging_level(log::LevelFilter::Info);
//.set_schema_search_path("my_schema"); // Setting default PostgreSQL schema
let db = Database::connect(opt).await.unwrap();
Migrator::fresh(&db).await.unwrap();
db
}).await;
}
}
@ -68,71 +37,10 @@ pub static DATABASE_CONNECTION: OnceDatabaseConnection = OnceDatabaseConnection:
mod tests {
use super::*;
use std::time::Duration;
use chrono::{offset, FixedOffset, Local, TimeZone};
use sea_orm::{entity::*, query::*, ConnectOptions, Database};
use progress_pile_migration_server::{Migrator, MigratorTrait};
use crate::entity::*;
#[tokio::test]
//#[tokio::test]
async fn check_database_connection() {
DATABASE_CONNECTION.init_test().await;
DATABASE_CONNECTION.get_or_init("sqlite::memory:").await;
let db = DATABASE_CONNECTION.get().unwrap();
assert!(db.ping().await.is_ok());
}
#[tokio::test]
async fn check_insert_entity() {
DATABASE_CONNECTION.init_test().await;
let db = DATABASE_CONNECTION.get().unwrap();
let local_date_time = Local::now();
let offset_date_time = local_date_time.with_timezone(local_date_time.offset());
let user = UserActiveModel{
login_name: Set("admin".to_owned()),
password_hash: Set("admin".to_owned()),
created_at: Set(offset_date_time),
updated_at: Set(offset_date_time),
..Default::default()
}.insert(db)
.await.unwrap();
let record_tag = RecordTagActiveModel{
user_id: Set(user.id),
name: Set("test".to_owned()),
..Default::default()
}.insert(db)
.await.unwrap();
let record_header = RecordHeaderActiveModel{
user_id: Set(user.id),
created_at: Set(offset_date_time),
updated_at: Set(offset_date_time),
recorded_at: Set(offset_date_time),
comment: Set("".to_owned()),
..Default::default()
}.insert(db)
.await.unwrap();
RecordDetailActiveModel {
record_header_id: Set(record_header.id),
record_tag_id: Set(record_tag.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()
}.insert(db)
.await.unwrap();
Migrator::reset(db).await.unwrap();
db.clone().close().await.unwrap();
}
}

View file

@ -0,0 +1,7 @@
pub trait ProgressCategoryModelTrait {
}
pub trait ProgressEntityModelTrait {
}

View file

@ -1,29 +0,0 @@
mod record_detail;
mod record_header;
mod record_tag;
mod user;
pub use user::{
ActiveModel as UserActiveModel,
Column as UserColumn,
Entity as UserEntity,
Model as UserModel,
};
pub use record_detail::{
ActiveModel as RecordDetailActiveModel,
Entity as RecordDetailEntity,
Model as RecordDetailModel,
};
pub use record_header::{
ActiveModel as RecordHeaderActiveModel,
Entity as RecordHeaderEntity,
Model as RecordHeaderModel,
};
pub use record_tag::{
ActiveModel as RecordTagActiveModel,
Entity as RecordTagEntity,
Model as RecordTagModel,
};

View file

@ -1,46 +0,0 @@
use async_graphql::*;
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize, SimpleObject)]
#[sea_orm(table_name = "record_detail")]
#[graphql(concrete(name = "RecordDetail", params()))]
pub struct Model {
#[sea_orm(primary_key)]
#[serde(skip_deserializing)]
pub record_header_id: i32,
#[sea_orm(primary_key)]
#[serde(skip_deserializing)]
pub record_tag_id: i32,
pub count: i32,
}
#[derive(Copy, Clone, Debug, DeriveRelation, EnumIter)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::record_header::Entity",
from = "Column::RecordHeaderId",
to = "super::record_header::Column::Id"
)]
RecordHeader,
#[sea_orm(
belongs_to = "super::record_tag::Entity",
from = "Column::RecordTagId",
to = "super::record_tag::Column::Id"
)]
RecordTag,
}
impl Related<super::record_header::Entity> for Entity {
fn to() -> RelationDef {
Relation::RecordHeader.def()
}
}
impl Related<super::record_tag::Entity> for Entity {
fn to() -> RelationDef {
Relation::RecordTag.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View file

@ -1,46 +0,0 @@
use async_graphql::*;
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize, SimpleObject)]
#[sea_orm(table_name = "record_header")]
#[graphql(concrete(name = "RecordHeader", params()))]
pub struct Model {
#[sea_orm(primary_key)]
#[serde(skip_deserializing)]
pub id: i32,
#[sea_orm(indexed)]
#[serde(skip_deserializing)]
pub user_id: i32,
pub comment: String,
pub created_at: DateTimeWithTimeZone,
pub updated_at: DateTimeWithTimeZone,
#[sea_orm(indexed)]
pub recorded_at: DateTimeWithTimeZone,
}
#[derive(Copy, Clone, Debug, DeriveRelation, EnumIter)]
pub enum Relation {
#[sea_orm(has_many = "super::record_detail::Entity")]
RecordDetail,
#[sea_orm(
belongs_to = "super::user::Entity",
from = "Column::UserId",
to = "super::user::Column::Id"
)]
User,
}
impl Related<super::record_detail::Entity> for Entity {
fn to() -> RelationDef {
Relation::RecordDetail.def()
}
}
impl Related<super::user::Entity> for Entity {
fn to() -> RelationDef {
Relation::User.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View file

@ -1,43 +0,0 @@
use async_graphql::*;
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize, SimpleObject)]
#[sea_orm(table_name = "record_tag")]
#[graphql(concrete(name = "RecordTag", params()))]
pub struct Model {
#[sea_orm(primary_key)]
#[serde(skip_deserializing)]
pub id: i32,
#[sea_orm(indexed)]
#[serde(skip_deserializing)]
pub user_id: i32,
#[sea_orm(indexed)]
pub name: String,
}
#[derive(Copy, Clone, Debug, DeriveRelation, EnumIter)]
pub enum Relation {
#[sea_orm(has_many = "super::record_detail::Entity")]
RecordDetail,
#[sea_orm(
belongs_to = "super::user::Entity",
from = "Column::UserId",
to = "super::user::Column::Id"
)]
User,
}
impl Related<super::record_detail::Entity> for Entity {
fn to() -> RelationDef {
Relation::RecordDetail.def()
}
}
impl Related<super::user::Entity> for Entity {
fn to() -> RelationDef {
Relation::User.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View file

@ -1,64 +0,0 @@
use async_graphql::SimpleObject;
use chrono::{DateTime, FixedOffset,};
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
use crate::error::Error;
use crate::DATABASE_CONNECTION;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, SimpleObject, Deserialize)]
#[sea_orm(table_name = "user")]
#[graphql(concrete(name = "User", params()))]
pub struct Model {
#[sea_orm(primary_key)]
#[serde(skip_deserializing)]
pub id: i32,
#[sea_orm(unique, indexed)]
pub login_name: String,
pub password_hash: String,
pub created_at: DateTimeWithTimeZone,
pub updated_at: DateTimeWithTimeZone,
}
impl Entity {
pub async fn find_by_name(user_name: &str) -> Result<Option<Model>, Error> {
Ok(Entity::find()
.filter(Column::LoginName.contains(user_name))
.one(DATABASE_CONNECTION.get().unwrap())
.await?
)
}
pub async fn find_all() -> Result<Vec<Model>, Error> {
Ok(Entity::find()
.all(DATABASE_CONNECTION.get().unwrap())
.await?
)
}
}
#[derive(Copy, Clone, Debug, DeriveRelation, EnumIter)]
pub enum Relation {
#[sea_orm(has_many = "super::record_header::Entity")]
RecordHeader,
#[sea_orm(has_many = "super::record_tag::Entity")]
RecordTag,
}
impl Related<super::record_header::Entity> for Model {
fn to() -> RelationDef {
Relation::RecordHeader.def()
}
}
impl Related<super::record_tag::Entity> for Model {
fn to() -> RelationDef {
Relation::RecordTag.def()
}
}
impl ActiveModel {
pub async fn create_user(login_name: &str, password: &str) -> Result<Model, Error>{
todo!()
}
}
impl ActiveModelBehavior for ActiveModel {}

View file

@ -1,9 +1,6 @@
use async_graphql::*;
use chrono::{DateTime, FixedOffset};
use crate::entity::UserModel;
#[derive(SimpleObject)]
pub struct PartialUser {
pub id: Option<i32>,
@ -12,11 +9,6 @@ pub struct PartialUser {
pub updated_at: Option<DateTime<FixedOffset>>,
}
#[derive(SimpleObject)]
pub struct Users{
users: Vec<UserModel>
}
pub struct CreateUser {
login_name: String,
password: String,

View file

@ -5,9 +5,6 @@ pub mod entity;
pub mod error;
pub mod graphql;
pub use database_connection::*;
pub use error::Error;
#[cfg(test)]
mod tests {
}

View file

@ -1,8 +1,10 @@
use sea_orm_migration::{prelude::*, schema::*};
#[cfg(feature="client")]
#[derive(DeriveMigrationName)]
pub struct ClientMigration;
#[cfg(feature="client")]
#[async_trait::async_trait]
impl MigrationTrait for ClientMigration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
@ -290,7 +292,6 @@ impl MigrationTableDefault for ProgressCategory {
Table::create()
.table(Self::Table)
.if_not_exists()
.col(uuid(Self::Id))
.col(string(Self::Name))
.col(timestamp_with_time_zone(Self::CreatedAt))
.col(timestamp_with_time_zone(Self::UpdatedAt))
@ -326,7 +327,7 @@ impl MigrationTableDefault for ProgressCategory {
impl MigrationTableClient for ProgressCategory {
fn table_create_statement_client() -> TableCreateStatement{
let mut tcs = Self::table_create_statement_default();
tcs.primary_key(Index::create().name(PK_PROGRESS_CATEGORY).col(Self::Id));
tcs.col(pk_uuid(Self::Id));
tcs
}
fn index_create_statements_client() -> Vec<IndexCreateStatement> {
@ -342,6 +343,8 @@ impl MigrationTableServer for ProgressCategory {
fn table_create_statement_server() -> TableCreateStatement{
let mut tcs = Self::table_create_statement_default();
tcs.col(uuid(Self::Id));
tcs.col(integer(Self::UserId));
tcs.foreign_key(ForeignKey::create().name(FK_PROGRESS_CATEGORY_USER).from(Self::Table, Self::UserId)
.to(User::Table, User::Id));